Skip to content

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, MessagesState

2.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" 或 END

2.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() 拿到时间,再连续调 addsubtract 算出结果,最后给出一句话总结。

三、与 LangGraph4j 的对比

如果你看过 LangGraph4j,会发现两者几乎一一对应:

概念LangGraph (Python)LangGraph4j (Java)
状态MessagesStateMessagesState
工具@tool 装饰器@Tool 注解
条件边tools_conditionToolsCondition
工具节点ToolNodeToolNode
编译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 + b

3. 工具返回值类型不对

@tool 会按返回值的类型注解序列化。返回自定义对象时要确保能 JSON 序列化,否则 tools 节点会抛错。简单办法:永远返回 str / float / dict

4. 模型不调工具

部分国产/本地模型工具调用能力弱,会直接"假装"调工具(输出文本而非 tool_calls)。换 gpt-4o-miniqwen-plus 这类支持工具调用的模型;或检查 bind_tools 是否调用。

五、小结

  • ReAct = 思考→行动→观察的循环,是智能体最经典的范式。
  • StateGraph 手搓只需 4 步:定义工具、写 agent 节点、加 tools 节点、连条件边。
  • 关键 API:MessagesStateToolNodetools_conditionbind_tools
  • 与 LangGraph4j 概念完全对应,差异只在语法。

下一章我们用 create_react_agent 一行搞定同样的智能体,看看预置函数能省多少事。