第 9 章 ReAct Agent:预构建实现与手写等价实现
9.1 ReAct 范式
ReAct(Reasoning + Acting,推理与行动) 范式由 Shunyu Yao、Jeffrey Zhao 等人于 2022 年在论文《ReAct: Synergizing Reasoning and Acting in Language Models》中提出,其核心洞见是:大模型的复杂任务解决能力源于"推理(Reasoning,形成并调整思考链)"与"行动(Acting,与外部环境交互获取信息)"的交替协同——纯思维链(Chain-of-Thought)无法获取外部事实(如最新天气、数据库内容),容易产生幻觉;纯行动(无思考地调用工具)无法做深度推理,容易在工具选择上盲目。ReAct 将两者组织为一个"思考(Thought)→行动(Action/tool call)→观察(Observation/tool result)"的循环:模型先基于当前对话历史与观察产生一段内部推理(可隐式也可显式输出),再选择并调用一个工具获得观察结果,观察追加到上下文后再次进入思考,直到模型判断已有足够信息生成最终答案。
在 LangGraph 中,ReAct 循环被建模为一张最小循环图:call_model → (条件路由) → tools → call_model。该循环由 LangGraph 1.2.x 的预构建工厂函数直接生成,也可以完全手写以获得对 prompt、消息裁剪、错误处理、记忆注入等环节的完全控制权。手写等价实现是理解 LangGraph 节点/边/路由协同机制的最佳练习。
9.2 create_react_agent 工厂函数
在 LangGraph 1.0 之前的版本中,create_react_agent 位于 langgraph.prebuilt 包;LangChain 1.x 生态整合后,官方在 langchain.agents 包中提供统一入口 create_agent,其底层在 LangGraph 运行时上编译出与 create_react_agent 行为等价的图。LangGraph 1.2.x 中两条路径均可使用,行为保持一致;后续生态演进将以 langchain.agents.create_agent 为高层推荐路径,langgraph.prebuilt.create_react_agent 保留兼容(长期将标记为 deprecated)。
# 两种导入方式(LangGraph 1.2.x 均可用)
from langgraph.prebuilt import create_react_agent # LangGraph 原生路径,保持兼容
from langchain.agents import create_agent # LangChain 1.x+ 推荐高层路径create_react_agent 签名:
def create_react_agent(
model: BaseChatModel,
tools: Sequence[BaseTool | Callable] = (),
*,
prompt: ChatPromptTemplate | str | None = None,
state_schema: type[BaseModel] | type[TypedDict] | None = None,
checkpointer: BaseCheckpointSaver | None = None,
store: BaseStore | None = None,
interrupt_before: list[str] | None = None,
interrupt_after: list[str] | None = None,
response_format: type[BaseModel] | Literal["content_and_artifact"] | None = None,
name: str = "agent",
) -> CompiledStateGraph: ...| 参数 | 说明 |
|---|---|
| model | 必须支持原生 tool_calls 的聊天模型;不支持 tool_calls 的模型需走 Prompt-based 解析路径 |
| tools | 工具列表,元素可以是 BaseTool 实例或普通 Python 函数(自动转为 StructuredTool) |
| prompt | 系统提示词字符串或 ChatPromptTemplate;None 时使用内置默认系统提示词(包含工具使用规范、格式约束、输出风格) |
| state_schema | 自定义 State Schema;None 时使用预置 AgentState(继承 MessagesState,含 messages 字段) |
| checkpointer | 检查点存储器,透传至 compile() |
| store | 长期记忆 Store,透传至 compile() |
| interrupt_before / interrupt_after | 静态断点,透传至 compile();常用值 interrupt_before=["tools"] 实现工具调用前人工审批 |
| response_format | 结构化输出格式(Pydantic BaseModel 或 "content_and_artifact"),见 9.4 节 |
| name | Agent 名称,影响 LangSmith trace 标题与子图命名空间 |
返回值是已经编译好的 CompiledStateGraph,内部固定结构为:
- agent 节点(默认名 "agent"):将 prompt 与绑定工具后的模型组合为一个 Runnable,调用 LLM 让其根据消息历史决定是直接回复文本还是输出 tool_calls;
- tools 节点:ToolNode 实例,执行 agent 节点产生的 tool_calls;
- 条件路由:等价于 tools_condition 的逻辑,判断最后一条 AIMessage 是否有 tool_calls——有则路由到 "tools",无则路由到 END;
- 回边:tools→agent,构成 ReAct 循环,工具结果追加到 messages 后再次让模型决策。
State 默认使用预定义的 AgentState,等价于继承自 MessagesState,即自动拥有 messages: Annotated[list[BaseMessage], add_messages] 字段。用户可以通过 state_schema 传入扩展的 TypedDict 或 Pydantic 模型,添加业务字段(如 user_id、current_task、retries)。
最小可运行示例:
from langgraph.prebuilt import create_react_agent
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
@tool
def get_weather(city: str) -> str:
"""查询指定城市的当前天气。"""
return f"{city} 晴,气温 25℃,湿度 40%"
@tool
def get_exchange_rate(base: str, target: str) -> float:
"""查询两种货币的汇率。"""
rates = {("USD", "CNY"): 7.24, ("EUR", "CNY"): 7.88}
return rates.get((base, target), 1.0)
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
agent = create_react_agent(model, tools=[get_weather, get_exchange_rate])
# 调用:默认输入为 {"messages": [...]}
result = agent.invoke(
{"messages": [{"role": "user", "content": "北京天气怎么样?100 美元能换多少人民币?"}]},
)
print("最终回答:", result["messages"][-1].content)该示例中模型会在第一轮并行调用 get_weather 与 get_exchange_rate(并行工具调用),ToolNode 并发执行后结果一次性回传,模型基于两个结果生成最终回答。
9.3 手写等价实现
手写等价实现能完全控制 prompt 构造、工具绑定、消息裁剪、错误处理与记忆注入,是深入理解 ReAct Agent 内部机制的最佳路径。以下代码产出与 create_react_agent 行为等价的 CompiledStateGraph:
from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
# 1) 工具
@tool
def get_weather(city: str) -> str:
"""查询指定城市天气。"""
return f"{city} 晴,25℃"
tools = [get_weather]
# 2) Prompt:create_react_agent 默认 prompt 会自动注入工具描述,
# 手写时通过 ChatPromptTemplate 构造并在调用时让 bind_tools 注入工具 schema。
# 注意必须保留 MessagesPlaceholder("messages") 让消息历史展开。
prompt = ChatPromptTemplate.from_messages([
("system",
"你是一个乐于助人的 AI 助手,可以使用提供的工具回答用户问题。"
"请仔细思考,必要时调用工具获取信息,拿到结果后再回答。"),
("placeholder", "{messages}"),
])
# 3) 绑定工具到模型,再与 prompt 组成 Runnable 序列
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm_with_tools = llm.bind_tools(tools)
agent_runnable = prompt | llm_with_tools
# 4) State
class State(TypedDict):
messages: Annotated[list, add_messages]
# 5) agent 节点
def agent_node(state: State) -> dict:
ai_message = agent_runnable.invoke(state)
return {"messages": [ai_message]}
# 6) 构图:与 create_react_agent 相同的拓扑
builder = StateGraph(State)
builder.add_node("agent", agent_node)
builder.add_node("tools", ToolNode(tools, handle_tool_errors=True))
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", tools_condition, {"tools": "tools", END: END})
builder.add_edge("tools", "agent") # 关键回边
agent = builder.compile()关键等价点对照:模型必须 bind_tools 让其感知工具;prompt 必须通过 ChatPromptTemplate 保留消息占位符;ToolNode 处理工具执行并返回 ToolMessage;条件路由使用 tools_condition(或等价逻辑);tools 执行后必须通过回边回到 agent 节点形成循环。
9.4 系统提示词与结构化输出
ChatPromptTemplate 用法。ChatPromptTemplate.from_messages 接受一个消息列表,每个元素是 (role, template) 元组:role 为 "system"/"human"/"ai"/"placeholder";template 是带 {variable} 占位符的字符串或 "placeholder"(展开列表变量)。调用时传入的参数会填充占位符。create_react_agent 传入 prompt 参数时,内部会把模板与模型组合,工具 schema 通过 bind_tools 自动注入。
response_format(结构化输出):传入一个 Pydantic BaseModel 或 "content_and_artifact" 后,Agent 在判断无需再调用工具的"最终回答轮"会强制模型输出符合该 Schema 的结构化对象,而非自由文本。底层机制是:在 Agent 检测到模型不输出 tool_calls 时,将模型替换为 .with_structured_output(response_format) 的版本调用一次,结果存入 State 的 structured_response 字段(或直接作为消息解析)。
from pydantic import BaseModel, Field
class WeatherReport(BaseModel):
city: str = Field(description="查询的城市名称")
temperature: str = Field(description="温度描述,如 '25℃'")
condition: str = Field(description="天气状况,如 '晴'、'雨'")
recommendation: str = Field(description="基于天气的出行建议,50 字以内")
agent = create_react_agent(
model,
tools=[get_weather],
response_format=WeatherReport,
)
result = agent.invoke({"messages": [("user", "北京天气怎么样?给出出行建议。")]})
# result["structured_response"] 是 WeatherReport 实例
report = result["structured_response"]
print(report.city, report.temperature, report.recommendation)使用 response_format 时需注意:该约束只在"最后一轮不再调用工具"时生效,工具调用轮次仍按标准 tool_calls 协议执行;若模型在尚未获取足够信息时就被结构化约束拦住,会直接输出"幻觉"结构化对象。因此 prompt 中应明确告知模型"先调用工具获取全部必要信息,最后一轮才输出结构化结果"。response_format 与 with_structured_output 的关系:create_react_agent 内部在最终轮调用 model.with_structured_output(response_format),与直接对模型使用 with_structured_output 等价。
9.5 模型能力要求与工具数量性能
ReAct Agent 对模型的核心要求是支持原生 tool_calls。模型支持度参考:
| 模型 | tool_calls 原生支持 | 多工具并行 | 备注 |
|---|---|---|---|
| OpenAI gpt-4o / gpt-4o-mini / o3-mini / o4-mini | 原生 | 支持 | 生态最成熟,稳定性最好 |
| Anthropic Claude 3.5 Sonnet、Claude 3.7 Sonnet/Opus | 原生(tool_use content block) | 支持 | 工具使用质量高、延迟略大 |
| Qwen2.5(72B/32B/14B)、Qwen3 系列 | 原生 | 支持 | 开源模型中工具调用能力较强 |
| DeepSeek-V3 | 原生 | 支持 | 性价比突出 |
| 不支持 tool_calls 的模型 | 否 | — | 需走 XML/Prompt-based 工具调用(正则解析 <tool> 标签),可靠性显著下降 |
工具数量对 prompt 膨胀、成本、延迟、准确率均有直接影响。每轮 LLM 调用都会把全部工具的 JSON Schema 放入请求(通常每个工具占 100–500 token,视描述长度而定),工具数线性增长会导致:prompt 输入 token 线性增长(成本+延迟上升);模型选择准确率下降(过多工具会让模型混淆);响应时间延长。经验阈值:
| 工具数量 | 建议方案 |
|---|---|
| 1–10 | 直接 bind_tools,无需额外处理 |
| 10–30 | 按场景拆分工具集,入口路由节点先判定场景再选择对应工具集绑定 |
| 30+ | 必须采用检索式工具选择(Tool Retrieval):先将工具描述做 embedding 建索引,每轮根据用户问题检索 Top-K(通常 5–10 个)相关工具再 bind_tools |
9.6 与同类 Agent SDK 对比
| 维度 | LangGraph create_react_agent | OpenAI Agents SDK | Anthropic Claude Tools |
|---|---|---|---|
| 模型支持 | 多模型(OpenAI/Anthropic/Google/国产/开源) | 仅 OpenAI 系列(含 Responses API) | 仅 Anthropic 系列 |
| 持久化 | Checkpointer 一等公民(Memory/Postgres/Redis/SQLite) | 内置 Thread 存储(云端) | 应用层自行实现 |
| HITL | interrupt/resume 原语原生支持 | 通过 handoff + guardrails 实现 | 应用层自行实现 |
| 可扩展性 | 图完全可手写,任意节点/边可替换 | 中等(Agent/Hook/ModelSettings 体系) | 低(单轮工具调用,无循环) |
| 部署灵活性 | Library + LangGraph Platform/Cloud + 自托管 FastAPI | Cloud 优先,支持本地轻量运行 | 仅 API 客户端,不含运行时 |
| 多 Agent 模式 | Supervisor / Router / Handoffs / Swarm / Map-Reduce 可自由构图 | Handoff 机制 | 应用层自行组装 |
| 可观测性 | LangSmith 深度集成 | OpenAI Traces | Anthropic Console |
| 适合场景 | 生产级长流程、需要深度定制、多模型混用 | 快速原型、OpenAI 单栈 | Anthropic 单栈简单工具调用 |
本章小结
ReAct 范式通过"思考—行动—观察"循环实现多步推理与外部工具交互。create_react_agent(LangChain 1.x+ 推荐统一入口为 langchain.agents.create_agent)是预构建工厂函数,内部编译出一张"agent 节点→tools_condition 条件边→tools 节点/END、tools→agent 回边"的标准循环图,默认使用 MessagesState 作为 State。手写等价实现需要显式构造 prompt、bind_tools、注册 ToolNode、配置条件路由与回边,获得完全控制权。ChatPromptTemplate 管理提示词模板与消息占位;response_format 参数结合 Pydantic BaseModel 可让最终输出结构化结果,底层基于 with_structured_output 实现。ReAct 循环要求模型支持原生 tool_calls;工具数超过 30 需引入检索式工具选择避免 prompt 膨胀。与 OpenAI Agents SDK、Anthropic Claude Tools 相比,LangGraph 在多模型支持、持久化、HITL、可扩展性、部署灵活性上具有优势,适合生产级深度定制场景。
常见错误
- 模型不支持 tool_calls 导致无限循环到 recursion_limit。现象:图一直执行直到抛 GraphRecursionError(默认 25 轮),观察 trace 发现模型每轮都回复自然语言文本,tools_condition 永远路由到 END 但可能因 prompt 设计错误回边继续。原因:使用了不支持原生 tool_calls 的模型(如纯文本补全模型、早期开源模型),或 bind_tools 未生效。修复:更换为支持 tool_calls 的模型;若只能使用不支持的模型,改用 XML/Prompt-based 工具调用循环(用正则解析模型输出中的工具标签),并明确解析失败时的兜底分支。
- 自定义 prompt 覆盖了默认工具描述注入导致工具调用失败。现象:传入字符串 prompt 或 ChatPromptTemplate 后模型不再输出 tool_calls,或工具参数格式错误。原因:prompt 中未给消息历史留占位符(MessagesPlaceholder),或 prompt 中显式规定了"不要使用工具"之类指令与默认行为冲突;部分版本下未正确使用 "placeholder" 会导致消息历史丢失。修复:使用 ChatPromptTemplate.from_messages 时保留
("placeholder", "{messages}");system 段简洁描述角色即可,不要重复工具使用说明(bind_tools 已自动注入工具 schema)。 - response_format 与工具调用冲突导致"幻觉结构化输出"。现象:设置 response_format 后模型不调用工具直接输出结构化对象,但对象字段内容是编造的。原因:response_format 让模型在最终轮强制结构化,但模型判断"可以回答"的条件过早触发(prompt 未告知必须先调工具)。修复:在 system prompt 中明确"回答前必须获取所有必要信息,不得编造数据;只有当所有工具结果都已拿到后才输出最终结构化答案";监控 structured_response 中字段是否为合理值。
- 递归深度超限(GraphRecursionError)。现象:复杂任务执行到 25 轮报错。原因:模型始终判定需要继续调用工具但没有终止条件(如问题本身超出工具能力范围);或工具结果无法让模型得出答案,模型反复换参数重试。修复:在 State 中加入
step_count: int字段并在 agent 节点中递增,条件路由判断 step_count>=上限后强制路由到 END 并让模型给出"信息不足"的回答;调大 recursion_limit 到业务合理值(复杂任务 50–100);优化 prompt 让模型明确何时应直接回复"无法获取该信息"而非反复重试。 - 多工具并行后模型漏掉部分工具结果。现象:模型并行调用 3 个工具,但最终回答中只引用了其中 1–2 个结果。原因:消息列表过长模型注意力分散,或工具调用顺序导致部分 ToolMessage 被模型忽视。修复:控制单轮工具调用数量(一次不超过 5 个);在 prompt 中明确要求"引用所有工具返回的结果";对长工具结果做摘要后再追加到 messages。
生产建议
- 业务 Agent 默认使用 create_react_agent(或 langchain.agents.create_agent)快速搭建,遇到定制需求(自定义节点注入记忆、特殊路由、消息裁剪)再下沉到手写图;
- prompt 统一通过 ChatPromptTemplate 管理存放于独立模块或配置中心,避免把系统提示词硬编码在节点函数内;版本化管理 prompt 修改;
- 结构化输出(response_format)只用于"最终答案格式约束",工具调用轮不要绑定 with_structured_output,避免与工具调用冲突;
- 工具描述遵循统一模板:"一句话功能概括 + 适用场景 + 不适用场景 + 参数语义说明",便于模型准确选择;
- 监控每个 Agent 的平均超步数(trace 中的 step 字段),P50 超过 5 步、P99 超过 15 步需评估是否拆分 Supervisor-Worker 多智能体以降低单图复杂度;
- 为每个 Agent 配置独立的 recursion_limit,简单对话 10–15,多步推理 30–50,复杂多 Agent 协作 50–100,绝不使用默认值不加评估;
- 在 prompt 中为模型提供"终止条件"显式说明(如"若已获取所需信息则直接回答,不要调用不必要的工具""若工具返回错误或找不到数据,直接告知用户并说明原因,不要反复重试超过 2 次")。
