ReAct(Reason + Act)是目前主流 Agent 架构的基础模式。它的逻辑很直白:LLM 观察当前上下文,进行推理,决定是否调用某个工具;如果调用,就执行工具、把结果交还给 LLM,LLM 再次推理,决定下一步——直到它认为任务完成,不再产生工具调用为止。
用 LangGraph 的语言描述,这个模式的图结构是:
graph TD
START(["START"]) --> agent["agent\n调用 LLM"]
agent -->|"AIMessage.tool_calls 非空"| tools["tools\nToolNode 并发执行工具"]
tools --> agent
agent -->|"AIMessage.tool_calls 为空"| END(["END"])
style agent fill:#dae8fc,stroke:#6c8ebf
style tools fill:#d5e8d4,stroke:#82b366
style START fill:#f5f5f5,stroke:#666
style END fill:#f5f5f5,stroke:#666
两个节点,一条条件边,一个循环。整个图的逻辑可以在一张白纸上画清楚。把这张图翻译成代码,就是 create_react_agent() 的核心。
这个函数在 langgraph.prebuilt 模块里,大约 300 行(含文档、边界处理和可选特性),其中真正描述图结构的核心代码不超过 30 行。这个规模本身就说明了一件事:一个标准的 ReAct Agent 并没有什么神秘的内部魔法,它就是一个极其简洁的图。
阅读源码,去掉可选特性和边界处理,create_react_agent() 的骨干结构是这样的:
workflow = StateGraph(state_schema=AgentState)
# 两个节点
workflow.add_node("agent", call_model) # 调用 LLM
workflow.add_node("tools", tool_node) # 执行工具
# 入口是 agent 节点
workflow.set_entry_point("agent")
# agent 节点的条件路由:有 tool_calls 去 tools,否则结束
workflow.add_conditional_edges("agent", should_continue, ["tools", END])
# tools 节点执行完毕后,无条件回到 agent
workflow.add_edge("tools", "agent")
return workflow.compile(checkpointer=checkpointer, store=store, ...)
call_model 是一个普通函数,它从状态里取出消息列表,调用 LLM,把返回的 AIMessage 追加到 messages 里:
def call_model(state, runtime, config):
response = model.invoke(state["messages"], config)
return {"messages": [response]}
这里没有手动管理消息列表。call_model 只是返回 {"messages": [response]},框架会用 add_messages 这个 reducer 把新消息追加到现有列表上。整个对话历史不需要节点去管理,它靠 reducer 的累积语义自然形成。
这个设计选择很关键:状态的合并逻辑("追加"还是"覆盖")是在状态模式定义时声明的,而不是在每个节点里手动实现的。这样做的好处是,节点代码只需要关心"我要输出什么",不需要关心"现有状态是什么样子、我该如何合并"。
should_continue 是另一个简单函数,它检查消息列表最后一条是否是带有 tool_calls 的 AIMessage:
def should_continue(state):
last_message = state["messages"][-1]
if isinstance(last_message, AIMessage) and last_message.tool_calls:
return "tools" # 有工具调用,继续循环
return END # 没有工具调用,退出
这就是整个 ReAct 循环的控制流逻辑——十行以内。
ToolNode 是执行工具的节点,也是 prebuilt 层里技术含量最高的部分。它不只是"接收工具调用,调用函数,返回结果",它还承担了工具执行的所有横切关注点:注入机制、错误处理、并发执行。