引言:会用 create_react_agent 还不够,还要知道它背后发生了什么
上一篇我们已经用 create_react_agent 快速搭建了一个最小 ReAct Agent。
它可以:
- 判断是否需要调用工具
- 调用天气工具
- 调用计算器工具
- 根据工具结果生成最终回答
但很多人跑通 Demo 后,仍然会有一个疑问:
Agent 到底是怎么知道下一步该"思考",还是该"行动"的?
或者更具体一点:
- Prompt 是怎么设计的?
- 模型为什么会选择某个 Tool?
- Function Calling 到底发生了什么?
- Observation 是怎么写回上下文的?
- 一次 ReAct 循环内部到底经历了哪些步骤?
如果你只是调用封装好的 API,这些问题很容易被隐藏起来。
但只要你准备做更复杂的 Agent,例如:
- 多工具 Agent
- 企业系统 Agent
- LangGraph 自定义 Agent
- 带 Memory 的 Agent
- 可中断、可回放的 Agent
就必须理解 ReAct 循环的底层实现机制。
这篇文章,我们就把 ReAct 拆开,一层一层看清楚。
一、ReAct 循环到底在循环什么?
ReAct 的完整名字是:
text
Reason + Act
也就是:
text
思考 + 行动
但真正运行时,它不是只有两步,而是一个闭环:
text
Thought(思考)
→ Action(行动)
→ Observation(观察)
→ Thought(继续思考)
→ ...
→ Final Answer(结束)
可以理解成:
text
模型先判断当前信息够不够
如果不够,就调用工具
工具返回结果后,再把结果交给模型
模型继续判断是否完成
例如:
text
用户:北京今天25度,比昨天高3度,那昨天多少度?
Thought: 我需要计算 25 - 3
Action: calculator("25-3")
Observation: 22
Thought: 已经得到结果
Final Answer: 昨天是22度。
这就是最小的 ReAct 循环。
二、ReAct 的核心不是 Tool,而是"决策循环"
很多新手会误以为:
ReAct = Tool Calling
其实不完全对。
Tool Calling 只是 ReAct 里的 Action 部分。
真正关键的是:
模型每一轮都要判断:现在是继续调用工具,还是输出最终答案。
也就是说,ReAct 的核心问题是:
text
当前状态下,下一步应该做什么?
可能的答案只有两类:
1. 行动:调用工具
例如:
text
我需要查天气 → 调 get_weather
我需要计算 → 调 calculator
我需要搜索 → 调 search
2. 结束:输出最终答案
例如:
text
信息已经足够 → 直接回答用户
所以 ReAct 的本质是一个循环决策系统:
text
判断 → 执行 → 观察 → 再判断
三、Prompt 模板:ReAct 的"行为规则"
Agent 会不会正确行动,很大程度取决于 Prompt。
一个 ReAct Prompt 通常包含三部分:
- System Prompt
- Tool 描述
- Few-shot 示例
四、System Prompt:告诉 Agent 它应该怎么工作
System Prompt 是 Agent 的最高层规则。
它会告诉模型:
- 你是什么角色
- 你有哪些工具
- 什么时候应该调用工具
- 什么时候应该输出最终答案
- 不允许做什么
一个简化版 System Prompt 可以这样写:
text
你是一个会使用工具解决问题的 AI Agent。
你需要遵守以下规则:
1. 如果问题需要实时信息、外部数据或计算,必须调用工具。
2. 不要凭空猜测工具可以查询到的信息。
3. 每次工具返回结果后,都要基于 Observation 继续判断下一步。
4. 如果已经获得足够信息,就输出 Final Answer。
这段 Prompt 的作用非常关键。
如果没有它,模型可能会直接猜:
text
北京今天应该是晴天。
而不是调用天气工具。
所以,在 ReAct 中,System Prompt 的核心任务是:
限制模型不要乱答,引导它在需要时使用工具。
五、Tool 描述:模型选择工具的依据
Tool 本质上是你暴露给模型的能力。
例如:
python
@tool
def get_weather(city: str) -> str:
"""查询指定城市今天的天气和温度。"""
...
对模型来说,它能看到的不是工具代码,而是工具的"描述"。
通常包括:
- 工具名称
- 工具说明
- 参数名称
- 参数类型
- 参数描述
例如:
text
工具名:get_weather
功能:查询指定城市今天的天气和温度
参数:city,城市名称
模型会根据这些信息决定:
text
用户问天气 → 应该调用 get_weather
用户问数学 → 应该调用 calculator
所以 Tool 描述写得越清楚,Agent 越容易选对工具。
六、Few-shot:给 Agent 看"正确行动示例"
仅靠规则,有时候还不够。
更稳定的方式是给模型几个示例。
这就是 Few-shot。
例如:
text
示例1:
用户:北京天气怎么样?
Thought: 这个问题需要实时天气信息。
Action: get_weather({"city": "北京"})
Observation: 北京今天晴天,25度。
Final Answer: 北京今天晴天,25度。
示例2:
用户:37 * 12 等于多少?
Thought: 这个问题需要计算。
Action: calculator({"expression": "37*12"})
Observation: 444
Final Answer: 37 * 12 等于 444。
Few-shot 的价值是:
不只是告诉模型规则,而是演示模型应该怎么做。
尤其在以下场景非常有用:
- 多工具选择
- 参数格式复杂
- 输出格式必须固定
- 模型经常不调用工具
七、Agent 如何决定"思考"还是"行动"?
严格来说,模型每一轮并不会真的有一个显式按钮叫"思考"或"行动"。
它做的是:
根据当前上下文,生成下一步输出。
而这个输出可能是两种形式。
1. 普通文本输出
例如:
text
北京今天晴天,25度。
这表示模型认为:
当前信息已经足够,可以直接回答。
2. Tool Call 输出
例如:
json
{
"tool_name": "get_weather",
"arguments": {
"city": "北京"
}
}
这表示模型认为:
当前信息不够,需要调用工具。
也就是说,Agent 框架会根据模型输出判断:
text
如果模型返回 tool_calls → 执行工具
如果模型返回普通消息 → 结束或继续
这就是"思考还是行动"的底层判断。
八、Function Calling 原理:模型不是执行函数,而是生成调用请求
这是理解 Tool Calling 最重要的一点:
模型不会真的执行函数。
模型只是生成一个结构化调用请求。
例如用户问:
text
北京天气怎么样?
模型返回:
json
{
"name": "get_weather",
"arguments": {
"city": "北京"
}
}
然后真正执行函数的是 Agent 框架。
执行流程是:
text
模型生成 tool_call
↓
框架解析 tool_call
↓
找到对应 Python 函数
↓
传入参数并执行
↓
拿到返回结果
↓
把结果作为 Observation 写回上下文
所以 Function Calling 的本质是:
模型负责"决定调用什么",代码负责"真正执行"。
九、自定义 Tool 编写:三个关键点
写 Tool 并不难,但要写得让模型"会用",需要注意三个关键点。
1. 工具名要清楚
不推荐:
python
def func1(...):
...
推荐:
python
def get_weather(city: str) -> str:
...
工具名最好能直接表达用途。
2. docstring 要清楚
不推荐:
python
"""获取信息"""
推荐:
python
"""查询指定城市今天的天气和温度。参数 city 是城市名称,例如 北京、上海。"""
docstring 越清楚,模型越容易正确调用。
3. 参数要简单
不推荐:
python
def query(data: dict):
...
推荐:
python
def query_order(order_id: str):
...
参数越明确,模型越不容易传错。
十、示例:天气 Tool 与计算器 Tool
python
from langchain_core.tools import tool
@tool
def get_weather(city: str) -> str:
"""查询指定城市今天的天气和温度。参数 city 是城市名称,例如 北京、上海、广州。"""
data = {
"北京": "北京今天晴天,25度。",
"上海": "上海今天多云,28度。"
}
return data.get(city, f"暂时没有查询到 {city} 的天气。")
@tool
def calculator(expression: str) -> str:
"""计算一个简单数学表达式,例如 2+3、25-3、37*12。"""
try:
return str(eval(expression))
except Exception as e:
return f"计算失败:{e}"
这两个 Tool 对模型来说非常清楚:
- 天气问题 →
get_weather - 数学问题 →
calculator
十一、单次 ReAct 循环执行流程拆解
下面我们拆解一次完整执行。
用户输入:
text
北京今天25度,比昨天高3度,那昨天多少度?
第一步:构造消息
python
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": "北京今天25度,比昨天高3度,那昨天多少度?"}
]
此时模型看到:
- 系统规则
- 用户问题
- 可用工具描述
第二步:模型判断是否需要工具
模型可能生成:
json
{
"tool_name": "calculator",
"arguments": {
"expression": "25-3"
}
}
这说明模型判断:
当前问题需要计算。
第三步:框架执行工具
框架执行:
python
calculator("25-3")
得到结果:
text
22
第四步:Observation 写回上下文
框架会把工具结果追加到消息中:
text
Observation: 22
模型下一轮能看到:
text
用户问题:北京今天25度,比昨天高3度,那昨天多少度?
工具结果:22
第五步:模型生成最终答案
模型输出:
text
昨天是22度。
这就是一次最小 ReAct 循环。
十二、用伪代码理解底层循环
下面是一个简化版 ReAct 循环:
python
while True:
response = llm.invoke(messages, tools=tools)
if response.tool_calls:
for tool_call in response.tool_calls:
tool_name = tool_call["name"]
tool_args = tool_call["args"]
result = run_tool(tool_name, tool_args)
messages.append({
"role": "tool",
"name": tool_name,
"content": result
})
continue
else:
final_answer = response.content
break
这段伪代码解释了 ReAct 的核心:
text
模型输出 tool_calls → 执行工具 → 写回结果 → 再问模型
模型输出普通答案 → 结束
这就是 create_react_agent 背后最重要的机制。
十三、为什么 Observation 必须写回上下文?
很多 Agent 出错,问题就出在这里。
Tool 已经执行了,但模型最终还是瞎猜。
原因通常是:
Tool 的结果没有正确写回上下文。
例如:
text
Tool 返回:北京今天晴天,25度
但下一轮 LLM 输入里没有这条 Observation。
那么模型根本不知道工具查到了什么。
所以,ReAct 中最关键的状态更新是:
text
Tool Result → Observation → Messages / State
如果这一步断了,ReAct 循环就失效了。
十四、Agent 什么时候停止?
ReAct 循环不能无限执行。
它需要停止条件。
通常有几种:
1. 模型输出 Final Answer
也就是不再返回 tool_calls。
2. 达到最大循环次数
例如:
python
max_iterations = 5
防止死循环。
3. 工具调用失败次数过多
例如同一个 Tool 连续失败 3 次,就停止。
4. 业务规则强制停止
例如:
- 权限不足
- 参数非法
- 用户取消
生产环境中,一定不能只依赖模型自己停止。
建议同时设置:
- 最大循环次数
- 最大 Token
- 最大耗时
- Tool 调用次数限制
十五、为什么 ReAct 能减少幻觉?
普通 LLM 直接回答时,很容易出现幻觉。
因为它只能依赖模型内部参数。
ReAct 通过工具,把外部世界接进来:
text
不知道 → 查工具
算不准 → 调计算器
缺数据 → 搜索 / 查数据库
所以它减少幻觉的方式不是"让模型更聪明",而是:
让模型不要凭空猜,而是先获取证据。
但要注意:
ReAct 不能自动消除所有幻觉。
如果:
- 工具返回错
- Observation 没写回
- Prompt 没要求基于工具回答
- 模型忽略工具结果
仍然会产生错误。
所以 ReAct 只是减少幻觉的框架,不是万能保证。
十六、常见问题:为什么 Agent 不调用 Tool?
1. Tool 描述不清楚
例如:
python
"""获取信息"""
模型不知道什么时候用。
2. Prompt 没要求必须使用工具
如果你不告诉模型"实时数据必须调用工具",它可能直接回答。
3. 模型工具调用能力弱
有些模型不擅长 Function Calling。
4. 用户问题不明确
例如:
text
这个怎么样?
缺少上下文,模型不知道该调用哪个工具。
十七、常见问题:为什么 Tool 调用了但答案还是错?
可能原因:
- Tool 返回值本身错了
- Tool 结果没写回 messages
- 模型没有基于 Observation 回答
- State 字段名不一致
- 后续节点覆盖了正确结果
排查顺序建议:
text
先单独跑 Tool
↓
看 tool_call 参数
↓
看 Tool 返回值
↓
看 Observation 是否写回
↓
看最终 LLM 输入里有没有 Observation
十八、从预构建 Agent 到自定义 Graph
create_react_agent 很适合快速上手。
但当你需要更复杂控制时,就要自己写 LangGraph。
例如你想控制:
- 每一步 State 结构
- Tool 失败后的重试
- 某些工具需要人工确认
- 某些步骤需要中断
- 某些节点需要持久化
这时就需要手写:
- State
- Node
- Edge
- Conditional Edge
但无论怎么写,底层仍然是这套机制:
text
LLM 判断 → Tool 调用 → Observation 写回 → 再判断
结语
一句话总结:
ReAct 的底层机制,就是让模型在"输出答案"和"调用工具"之间不断做决策。
它真正重要的不是某个 API,而是这条循环:
text
Thought → Action → Observation → Thought → Final Answer
理解这条循环后,你就能看懂:
create_react_agent为什么能工作- Tool Calling 为什么重要
- Observation 为什么必须写回
- Agent 为什么会死循环
- 为什么复杂 Agent 最后都要管理 State
下一篇,我们可以继续深入:
手写 ReAct Agent:不用 create_react_agent,自己实现思考-行动-观察循环。