- 培歌行学LangGraph(1):从 LangChain 到 LangGraph 的思维跃迁]
- 培歌行学LangGraph(2):Reducer(归约器)彻底搞懂
- 培歌行学LangGraph(3):编译、执行与可视化
- 培歌行学LangGraph(4):一文搞懂图的状态(State)管理
- 培歌行学LangGraph(5):状态管理与
graph.invoke入参深度解析 - 培歌行学LangGraph(6):Multi Schema多状态管理详解
- 培歌行学LangGraph(7):预定义状态MessagesState与AgentState
- 培歌行学LangGraph(8):控制流详解
- 培歌行学LangGraph(9):控制流详解:defer延迟节点——让收尾工作自动排到最后
- 培歌行学LangGraph(10):多分支汇聚Fan-in——多个分支如何汇聚到一起?
一、什么是 ReAct?
ReAct = Reason(推理) + Act(行动) 。
这是目前最经典的 LLM Agent 运行模式,核心流程是:
- 推理:LLM 看当前对话历史,决定要不要调工具
- 行动:如果要调工具,就返回
tool_calls - 观察:工具执行完,结果以
ToolMessage形式回到消息列表 - 再推理:LLM 看到工具结果,继续决定下一步
- 结束:LLM 不再调工具,直接给出最终回答
画成图就是:
LLM → Tool → LLM → Tool → LLM → ... → 最终回答
LangChain 自带的 Agent 底层就是这套循环,而用 LangGraph 你可以自己从零搭一遍,彻底看懂它。
二、静态实现:用条件边控制循环
2.1 思路
-
节点:
input_node → llm_node → (tool_node | output_node) -
llm_node调用模型 -
用一个独立的
router函数判断:- 如果最后一条消息有
tool_calls→ 去tool_node - 否则 → 去
output_node
- 如果最后一条消息有
-
tool_node执行完再回到llm_node,形成循环
叫"静态",是因为
llm_node的下游候选(tool_node、output_node)在编译时就定好了,运行时只是二选一。
2.2 核心代码片段
def router(state: OverAllState) -> Literal["tool_node", "output_node"]:
last_msg = state["messages"][-1]
if last_msg.tool_calls:
return "tool_node"
return "output_node"
builder.add_conditional_edges("llm_node", router)
builder.add_edge("tool_node", "llm_node")
builder.add_edge("output_node", END)
循环路径就是:
llm_node → tool_node → llm_node → ...
2.3 运行表现
模型先返回:
AIMessage(tool_calls=[get_weather("杭州"), get_news("AI")])
tool_node执行后追加两条 ToolMessage,再回到 llm_node。
如果工具失败(例子里模拟了 60% 网络抖动),系统提示词要求"失败必须重试",模型就会再发 tool_calls,循环继续,直到成功或模型放弃。
2.4 优缺点
✅ 图结构清晰,路由逻辑和节点分离
❌ 多一个 router函数;图里多一条条件边
三、动态实现:用 Command 在节点里自己决定去向
3.1 思路
把"是否继续调工具"的判断直接放进 llm_node,不再写独立的 router。
llm_node返回:
return Command(
update={"messages": [ai_msg]},
goto="tool_node" # 或 "output_node"
)
叫"动态",是因为下一步去哪是节点运行时自己算出来的,不再依赖
add_conditional_edges。
3.2 核心代码片段
def llm_node(state: OverAllState) -> Command[Literal["tool_node", "output_node"]]:
ai_msg = model_with_tools.invoke(state["messages"])
goto = "tool_node" if ai_msg.tool_calls else "output_node"
return Command(update={"messages": [ai_msg]}, goto=goto)
# 注意:不再写 add_conditional_edges
builder.add_edge("tool_node", "llm_node")
builder.add_edge("output_node", END)
循环路径和静态版完全一样,但控制流逻辑内聚在 llm_node内部。
3.3 运行表现
和静态版行为一致:模型调工具 → 执行 → 回 LLM → 再调工具或结束。
区别只是:图里看不到 router节点,条件边也没了,llm_node自己说了算。
3.4 优缺点
✅ 少写路由函数,状态更新和跳转写在一起
✅ 适合"先处理再决定下一步"的节点
❌ 图可视化时看不到显式条件边,新人读图可能有点懵
四、静态 vs 动态:怎么选?
| 对比项 | 静态(add_conditional_edges) | 动态(Command(goto)) |
|---|---|---|
| 路由在哪 | 独立 router 函数 | 节点内部 |
| 图结构 | 显式条件边,可视化清楚 | 无显式条件边 |
| 状态更新 | 节点返回 dict | Command.update 里写 |
| 适合场景 | 纯路由、多节点共享判断 | 节点自己处理完再决定去向 |
经验法则:
- 路由规则简单、多个节点共用 → 静态
- 节点里已经拿到全部信息、顺手就决定下一步 → 动态
五、一个关键细节:循环退出靠谁?
两种实现里,是否继续循环都不是程序强制的,而是:
- 模型返回
tool_calls→ 继续 - 模型不返回
tool_calls→ 结束 - 模型中途摆烂(比如多次失败后直接给最终回答)→ 循环终止,图不拦你
例子里靠这句系统提示词兜底:
SystemMessage("如果工具调用失败,必须重新调用直至成功")
但模型听不听,是另一回事。真要硬控次数,得自己加:
- State 里放
retry_count - 节点里判断超过 N 次直接
goto=output_node
六、面试题
面试题1:ReAct 循环在 LangGraph 里是怎么形成的?
答:靠 llm_node → tool_node → llm_node这条回边。每次 llm_node调模型,若有 tool_calls就去 tool_node,执行完再回 llm_node;若没有 tool_calls就去结束节点。回边 + 条件判断 = 循环。
面试题2:静态条件和 Command(goto) 在实现循环时本质区别是什么?
答:静态条件是编译期声明 llm_node的下游候选,运行时由 router 选;Command 是节点运行时自己返回"下一跳"。前者控制流在边里,后者控制流在节点返回值里。
面试题3:如果模型一直返回 tool_calls,图会无限循环吗?
答:默认会。LangGraph 不内置"最大步数"拦截,必须在代码里自己控:
- 用
config={"recursion_limit": 10}限制超步数 - 或在 State 里加计数器,节点里判断超限强制结束
面试题4:tool_node 为什么要把 ToolMessage 追加进 messages,而不是直接返回新列表?
答:messages字段用了 add_messagesReducer,返回新列表会和旧消息合并。如果直接覆盖,前面 LLM 的推理历史就丢了,下一轮模型看不到上下文。工具结果必须以 ToolMessage形式追加回去。
面试题5:动态实现里 Command[Literal["tool_node","output_node"]]这个类型注解有什么用?
答:三件事:
-
告诉开发者这个节点可能跳这两个目标
-
给 mypy/pyright 做静态检查
-
帮 LangGraph 渲染图时推断
llm_node的潜在出边运行时不会拿它拦你,但写了比不写好。
循环不可怕,可怕的是循环里没有出口。ReAct 的出口永远在模型手里——你要做的,是把"模型说停就停,模型说调就调"这件事,用边和 Command 稳稳接住。