Router 执行一次工具就结束。Agent 把工具结果送回 LLM,让 LLM 继续推理、继续调用工具——直到任务真正完成。
一行代码的改变,成就了通用 Agent。
Router 已经很强大了——它可以让 LLM 决定是否调用工具,然后真正执行那个工具。但 Router 有一个根本限制:工具执行完毕后,直接走向 END。
如果用户说:"把 3 和 4 相加,然后把结果乘以 2,再除以 5"——Router 只会执行第一个工具(加法),然后就结束了。LLM 拿不到加法结果,根本没机会去做乘法和除法。
这个限制来自一条边:
# Router 中的这一行:
builder.add_edge("tools", END) # ← 工具执行完 → 结束,LLM 永远看不到工具结果
解决方案是什么?把这一行替换掉。把工具结果送回给 LLM,让 LLM 继续推理。这就是 Agent。
ReAct 是 2022 年 Google 提出的 Agent 范式,名字来自两个词的组合:Reasoning(推理)+ Acting(行动)。
它描述了一个 LLM Agent 应该如何工作:
这个循环的关键洞察是:工具结果本身也是信息。LLM 看到工具返回了什么,才能决定下一步怎么做。把工具结果追加到 messages 列表,等价于"让 LLM 观察到了执行结果"。
在 LangGraph 里,messages 列表就是 Agent 的"工作记忆"。每次 LLM 调用都能看到完整的对话历史——包括所有工具调用和工具结果。这就是 ReAct 在代码层面的实现:状态(State)携带着完整的观察历史,驱动下一轮推理。
从 Router 到 Agent,代码改动极小。找到这一行的差异,你就理解了 Agent 的本质:
# tools 执行完 → 结束
builder.add_edge(
"tools",
END ← 这里
)
工具结果被写入 messages,但 LLM 永远看不到它——图已经结束了。
# tools 执行完 → 回到 assistant
builder.add_edge(
"tools",
"assistant" ← 这里!
)
工具结果追加到 messages 后,LLM 再次被调用。它看到了结果,可以继续推理。
builder.add_edge("tools", "assistant") 是整个 Agent 的核心。这一行把线性流程变成了循环,把单步工具调用变成了多步推理。从 Router 到 Agent,只改了目标节点:从 END 改为 "assistant"。
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import tools_condition, ToolNode
# ─── 1. 定义工具 ───────────────────────────────────────
def add(a: int, b: int) -> int:
"""Adds a and b."""
return a + b
def multiply(a: int, b: int) -> int:
"""Multiplies a and b."""
return a * b
def divide(a: int, b: int) -> float:
"""Divides a by b."""
return a / b
tools = [add, multiply, divide]
# ─── 2. LLM 绑定工具 ───────────────────────────────────
llm = ChatOpenAI(model="gpt-4o")
llm_with_tools = llm.bind_tools(tools)
# ─── 3. 系统提示(在每次 LLM 调用时注入)─────────────
sys_msg = SystemMessage(content="You are a helpful assistant tasked with performing arithmetic.")
# ─── 4. assistant 节点:把 sys_msg 前置注入 ────────────
def assistant(state: MessagesState):
return {"messages": [llm_with_tools.invoke([sys_msg] + state["messages"])]}
# ─── 5. 构建图 ────────────────────────────────────────
builder = StateGraph(MessagesState)
builder.add_node("assistant", assistant) # LLM 节点
builder.add_node("tools", ToolNode(tools)) # 工具执行节点
builder.add_edge(START, "assistant") # 入口
builder.add_conditional_edges("assistant", tools_condition) # 条件分支
builder.add_edge("tools", "assistant") # ← 关键!循环回去
react_graph = builder.compile()
# ─── 6. 运行 ─────────────────────────────────────────
messages = [HumanMessage(content="Add 3 and 4. Multiply the output by 2. Divide the output by 5")]
result = react_graph.invoke({"messages": messages})
[sys_msg] + state["messages"]:每次调用 LLM 时在对话前面加上 SystemMessage,不管循环了多少次add_edge("tools", "assistant"),这是 Agent 循环的物理实现普通的 DAG(有向无环图)不允许循环。LangGraph 的特殊能力之一就是支持循环边。tools → assistant 这条边让图从线性变成可以反复执行的循环结构,这是 LangGraph 区别于普通 Pipeline 框架的核心特性。
用户输入:"Add 3 and 4. Multiply the output by 2. Divide the output by 5"
这道题需要三步计算:3+4=7,7×2=14,14÷5=2.8。让我们追踪 Agent 的每一步,以及 messages 列表的演变。
| 步骤 | 执行节点 | 发生了什么 | messages 新增内容 |
|---|---|---|---|
| 初始 | — | 用户输入传入图 | HumanMessage("Add 3 and 4...") |
| 第 1 轮 | A assistant | LLM 分析任务,决定先调用 add(3, 4) | AIMessage(tool_calls=[add(a=3,b=4)]) |
| T tools | 执行 add(3, 4) → 返回 7 | ToolMessage(content="7", name="add") | |
| 第 2 轮 | A assistant | LLM 看到结果 7,决定调用 multiply(7, 2) | AIMessage(tool_calls=[multiply(a=7,b=2)]) |
| T tools | 执行 multiply(7, 2) → 返回 14 | ToolMessage(content="14", name="multiply") | |
| 第 3 轮 | A assistant | LLM 看到结果 14,决定调用 divide(14, 5) | AIMessage(tool_calls=[divide(a=14,b=5)]) |
| T tools | 执行 divide(14, 5) → 返回 2.8 | ToolMessage(content="2.8", name="divide") | |
| 第 4 轮 | A assistant | LLM 看到最终结果,组织自然语言答案,不再请求工具 | AIMessage("The result is 2.8") ← 无 tool_calls |
| 结束 | E END | tools_condition 检测到无 tool_calls → 路由到 END | 图执行完毕,返回最终 State |
每次 assistant 节点被调用时,它看到的 messages 是什么:
注意 messages 列表是累积增长的。每次循环都把新的 AIMessage 和 ToolMessage 追加进去。LLM 每次调用都能看到完整的"对话历史",这就是它能正确推理多步任务的原因——工作记忆全在这里。
这是 Agent 和 Router 的另一个小区别:assistant 节点在调用 LLM 时,会在消息列表最前面加一条 SystemMessage:
sys_msg = SystemMessage(content="You are a helpful assistant tasked with performing arithmetic.")
def assistant(state: MessagesState):
return {"messages": [llm_with_tools.invoke(
[sys_msg] + state["messages"] # ← sys_msg 注入在最前面
)]}
SystemMessage 被定义在节点函数外部,每次 assistant 被调用时动态拼接。这样做有两个好处:
messages 列表,不会随着循环重复累积。State 里只保留真正的对话历史| 消息类型 | 存在位置 | LLM 每次能看到 | 用途 |
|---|---|---|---|
SystemMessage |
节点函数局部变量 | 是(每次动态拼接) | 告诉 LLM 它的角色和职责 |
HumanMessage |
State["messages"] | 是(在 State 里) | 用户的原始请求 |
AIMessage |
State["messages"] | 是(在 State 里) | LLM 的历史决策和工具调用请求 |
ToolMessage |
State["messages"] | 是(在 State 里) | 工具执行的结果,是 ReAct 的"Observe"环节 |
Agent 循环是无限的吗?不是。终止条件非常明确,由 tools_condition 控制:
LLM 自己决定什么时候停止。当它判断所有子任务都完成了,它的回复就不再包含 tool_calls,只有纯文字答案。tools_condition 检测到这一点,路由到 END,图执行完毕。
理论上,如果 LLM 永远请求工具,图会无限运行。实践中,你可以通过 react_graph = builder.compile() 时传入 recursion_limit 参数来限制最大迭代次数(默认是 25):
# 限制最大递归深度(每个节点执行一次算一步)
result = react_graph.invoke(
{"messages": messages},
config={"recursion_limit": 10} # 超过则抛出 GraphRecursionError
)
让我们把两个图并排比较。除了那一行边,其他完全相同:
# 节点
builder.add_node("tool_calling_llm", tool_calling_llm)
builder.add_node("tools", ToolNode([multiply]))
# 边
builder.add_edge(START, "tool_calling_llm")
builder.add_conditional_edges(
"tool_calling_llm", tools_condition
)
# ↓ 工具结果 → 结束
builder.add_edge("tools", END)
# 节点
builder.add_node("assistant", assistant)
builder.add_node("tools", ToolNode(tools))
# 边
builder.add_edge(START, "assistant")
builder.add_conditional_edges(
"assistant", tools_condition
)
# ↓ 工具结果 → 回到 LLM(关键!)
builder.add_edge("tools", "assistant")
| 维度 | Router | Agent |
|---|---|---|
| tools 节点的出口 | END |
"assistant"(循环) |
| LLM 能看到工具结果吗 | 不能 | 能(追加到 messages) |
| 支持多步工具调用 | 不支持(只能调一次) | 支持(无限次,直到 LLM 停止) |
| 图的拓扑结构 | DAG(有向无环图) | 有环图(包含循环边) |
| 适合的任务 | 单步工具任务(查一次数据库、查一次天气) | 多步推理任务(复杂计算、搜索+总结+验证) |
| SystemMessage | 可选 | 推荐(每轮重新注入,保持角色稳定) |
我们用三个数学函数演示了 Agent,但这个架构完全通用。把工具换成任何东西,Agent 的工作方式不变:
OpenAI 的 Assistants API、Anthropic 的 tool use、LangChain 的 AgentExecutor——本质上都是同一件事:ReAct 循环。LangGraph 的价值在于用图的语言显式地表达这个循环,让你能够检查、修改、扩展 Agent 的每一步行为。你现在已经掌握了它最核心的结构。
StateGraph → MessagesState → ToolNode → tools_condition → add_edge("tools", "assistant")。把这五个拼在一起,就是一个功能完整的 ReAct Agent。接下来的所有模块,都是在这个基础上加功能,而不是推倒重来。