Appearance
ReAct 智能体
前面几章我们已经会用 StateGraph 编排节点和边,也学会了工具调用。把它们组合起来,就能得到一个真正"会思考、会动手"的智能体——这就是本章要讲的 ReAct 智能体。
一、什么是 ReAct
ReAct = Reason + Act(推理 + 行动),是 2022 年提出的一种让大模型解决复杂任务的范式。它的核心思想很简单:
让模型先思考(Thought)该怎么做,再行动(Action)调用工具,然后观察(Observation)工具返回的结果,再继续思考……如此循环,直到得出最终答案。
一个典型的 ReAct 循环长这样:
mermaid
flowchart LR
A[用户问题] --> B[思考 Thought]
B --> C[行动 Action 调用工具]
C --> D[观察 Observation 工具返回]
D --> B
B --> E[最终答案 Final Answer]为什么 ReAct 适合做智能体
- 模型本身只会说嘴:LLM 知识有截止日期、不会算精确数学、不能上网。给它工具,它就能"动手"。
- 思考可解释:每一步都先想后做,错误容易定位。
- 循环可终止:通过条件判断,模型认为够了就停,不会无限调工具。
如果你有 Java/LangGraph4j 背景:LangGraph4j 里的 ReactAgent 就是同一个范式的 Java 实现,逻辑完全一致,只是 API 换成了 Java 风格。
二、用 StateGraph 手搓一个 ReAct
LangGraph 提供了预置的 create_react_agent(下一章讲),但手搓一遍你才能真正理解原理。我们用 StateGraph 自己搭。
2.1 整体结构
ReAct 智能体其实就是一个带条件边的环形图:
mermaid
flowchart LR
S([START]) --> A[agent 节点<br/>调 LLM 决定下一步]
A --> C{是否要调工具?}
C -->|是| T[tools 节点<br/>执行工具]
T --> A
C -->|否| E([END])- agent 节点:调用 LLM,让它决定"直接回答"还是"调某个工具"。
- tools 节点:执行 LLM 选定的工具,把结果塞回状态。
- 条件边:判断 LLM 的输出里有没有
tool_calls,有就走 tools,没有就结束。
2.2 定义状态
LangGraph 提供了 MessagesState,内置一个 messages 列表,正好够用:
python
# ReAct 不需要自定义状态,直接用内置的 MessagesState
from langgraph.graph import StateGraph, START, END, MessagesState2.3 定义工具
我们准备三个简单工具:加法、减法、获取当前时间。每个工具用 @tool 装饰器声明,LangChain 会自动把函数签名转成 LLM 能理解的 schema。
python
from langchain_core.tools import tool
from datetime import datetime
@tool
def add(a: float, b: float) -> float:
"""两数相加。例如 add(2, 3) 返回 5。"""
return a + b
@tool
def subtract(a: float, b: float) -> float:
"""两数相减。a 减 b。"""
return a - b
@tool
def now() -> str:
"""获取当前日期和时间,格式 YYYY-MM-DD HH:MM:SS。"""
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
tools = [add, subtract, now]注意:函数的 docstring 就是给 LLM 看的工具描述,写不清楚模型就不知道何时该用。
2.4 agent 节点
agent 节点干两件事:把 bind_tools 后的模型拿出来,对当前 messages 调一次 invoke。
python
from langchain_openai import ChatOpenAI
# 用国产模型可换成 ChatTongyi / ChatZhipuAI,详见模块06
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm_with_tools = llm.bind_tools(tools)
def agent_node(state: MessagesState):
# 拿到历史消息,调一次模型
response = llm_with_tools.invoke(state["messages"])
# 返回新消息,LangGraph 会自动 append 到 messages
return {"messages": [response]}2.5 tools 节点
tools 节点执行工具。可以直接用预置的 ToolNode,省得自己解析 tool_calls:
python
from langgraph.prebuilt import ToolNode
tool_node = ToolNode(tools)2.6 条件边
判断 LLM 输出里有没有 tool_calls,有就路由到 tools,没有就到 END。LangGraph 提供了现成的 tools_condition:
python
from langgraph.prebuilt import tools_condition
# tools_condition(state) 返回 "tools" 或 END2.7 拼装图
python
from langgraph.graph import StateGraph, START, END, MessagesState
builder = StateGraph(MessagesState)
builder.add_node("agent", agent_node)
builder.add_node("tools", tool_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges(
"agent",
tools_condition, # 返回 "tools" 或 END
)
builder.add_edge("tools", "agent") # 工具执行完回到 agent 再思考
graph = builder.compile()2.8 完整可运行代码
把上面拼起来,加一个测试问题:
python
from langchain_core.messages import HumanMessage
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.prebuilt import ToolNode, tools_condition
from datetime import datetime
# 1. 工具
@tool
def add(a: float, b: float) -> float:
"""两数相加。例如 add(2, 3) 返回 5。"""
return a + b
@tool
def subtract(a: float, b: float) -> float:
"""两数相减。a 减 b。"""
return a - b
@tool
def now() -> str:
"""获取当前日期和时间。"""
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
tools = [add, subtract, now]
# 2. 模型 + agent 节点
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm_with_tools = llm.bind_tools(tools)
def agent_node(state: MessagesState):
response = llm_with_tools.invoke(state["messages"])
return {"messages": [response]}
# 3. 拼图
builder = StateGraph(MessagesState)
builder.add_node("agent", agent_node)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", tools_condition)
builder.add_edge("tools", "agent")
graph = builder.compile()
# 4. 测试
result = graph.invoke({
"messages": [HumanMessage(content="现在几点?另外帮我算 123 + 456 - 78 等于多少。")]
})
# 打印最后一条消息(即最终答案)
print(result["messages"][-1].content)预期输出大致是:模型先调 now() 拿到时间,再连续调 add 和 subtract 算出结果,最后给出一句话总结。
三、与 LangGraph4j 的对比
如果你看过 LangGraph4j,会发现两者几乎一一对应:
| 概念 | LangGraph (Python) | LangGraph4j (Java) |
|---|---|---|
| 状态 | MessagesState | MessagesState |
| 工具 | @tool 装饰器 | @Tool 注解 |
| 条件边 | tools_condition | ToolsCondition |
| 工具节点 | ToolNode | ToolNode |
| 编译 | builder.compile() | builder.compile() |
主要差异是语法层面:Python 用装饰器,Java 用注解;Python 的 messages 是 list,Java 是 ArrayList<Message>。逻辑完全一致。
四、常见踩坑
1. recursion_limit 报错
LangGraph 默认递归上限是 25 步。复杂任务里 agent→tools→agent 反复循环很容易超。报错信息类似 Recursion limit of 25 reached。
解决:
python
graph.invoke(inputs, config={"recursion_limit": 50})但根本办法是让工具更"好用"——一次能干完的事别让模型来回调。
2. 工具描述写得太烂
模型选工具全靠 docstring。如果写 add: 加法 这种,模型经常不知道参数含义。要写清楚参数类型、含义、示例。对比:
python
@tool
def add(a, b): # ❌ 没类型、没说明
"""加法"""
return a + b
@tool
def add(a: float, b: float) -> float: # ✅ 类型+docstring+示例
"""两数相加。例如 add(2, 3) 返回 5。"""
return a + b3. 工具返回值类型不对
@tool 会按返回值的类型注解序列化。返回自定义对象时要确保能 JSON 序列化,否则 tools 节点会抛错。简单办法:永远返回 str / float / dict。
4. 模型不调工具
部分国产/本地模型工具调用能力弱,会直接"假装"调工具(输出文本而非 tool_calls)。换 gpt-4o-mini 或 qwen-plus 这类支持工具调用的模型;或检查 bind_tools 是否调用。
五、小结
- ReAct = 思考→行动→观察的循环,是智能体最经典的范式。
- 用
StateGraph手搓只需 4 步:定义工具、写 agent 节点、加 tools 节点、连条件边。 - 关键 API:
MessagesState、ToolNode、tools_condition、bind_tools。 - 与 LangGraph4j 概念完全对应,差异只在语法。
下一章我们用 create_react_agent 一行搞定同样的智能体,看看预置函数能省多少事。