一、引言
大语言模型能够生成答案,却无法仅凭模型参数获取实时信息、查询企业数据库或执行精确计算。ReAct 将语言推理与外部行动交错组织:模型根据当前上下文判断下一步,应用执行搜索、查询或计算工具,再把执行结果作为 Observation 送回模型,循环持续到模型得到足够证据并生成最终答案。
lua
一次性问答
用户问题
|
v
+-------+
| LLM |
+-------+
|
v
最终答案
ReAct 循环
用户问题
|
v
+-------------------+
| Thought / Decision|
+-------------------+
|
v
+-------------------+
| Action: 调用工具 |
+-------------------+
|
v
+-------------------+
| Observation |
| 外部环境返回结果 |
+-------------------+
|
+------------------+
| 证据不足 |
+------> 回到决策层 |
|
| 证据充分
v
最终答案
ReAct 把这一过程描述为 Thought、Action、Observation 交替出现的任务轨迹,模型负责决策,程序负责执行,外部环境负责提供新事实。
二、Thought、Action、Observation 各自负责什么
2.1.Thought 负责决定下一步
在原始 ReAct 轨迹中,Thought 是模型生成的语言推理内容。它可以承担以下任务:
- 分解用户目标;
- 判断当前缺少什么信息;
- 选择下一项动作;
- 从 Observation 中提取关键事实;
- 在工具失败后修改计划;
- 判断证据是否足以回答。
例如,用户问:《ReAct》论文作者所在机构中,哪一家成立得更早?相差多少年?
模型无法靠一次搜索得到完整答案。它需要形成类似这样的决策过程:
yaml
Thought 1: 需要先找到论文作者及其机构。
Action 1: search("ReAct paper authors affiliations")
Observation 1: 作者来自 Princeton University 和 Google Research。
Thought 2: 需要查询两家机构的成立年份。
Action 2: search("Princeton University founded")
Observation 2: 1746。
Thought 3: 还需要 Google 的成立年份。
Action 3: search("Google founded")
Observation 3: 1998。
Thought 4: 需要计算年份差。
Action 4: calculate("1998 - 1746")
Observation 4: 252。
Final Answer: Princeton University 成立更早,两者相差 252 年。
Thought 的价值不在于"写得像人在思考",而在于让下一步动作受到当前证据约束,其作用可以概括为创建和维护计划、处理异常、提取 Observation 中的重要信息。
2.2.Action 是结构化执行请求
Action 表示模型希望外部系统执行的操作。它至少包含:
json
{
"name": "search",
"arguments": {
"query": "ReAct paper authors affiliations"
}
}
模型只负责产生调用意图,不应直接获得任意代码执行、数据库写入或操作系统权限。
工具调用是一个多步过程:
- 应用把工具定义交给模型;
- 模型返回工具调用;
- 应用执行对应代码;
- 应用把工具输出返回模型;
- 模型生成答案或继续请求工具。
模型返回 tool_use ,应用执行工具并提交 tool_result ,直到模型不再请求工具。
Action 更接近一条类型化命令,而不是自然语言里的"我准备搜索一下"。
2.3.Observation 是环境事实
Observation 是工具或环境执行 Action 后返回的结果。例如:
json
{
"results": [
{
"title": "ReAct: Synergizing Reasoning and Acting in Language Models",
"snippet": "Authors: Shunyu Yao, Jeffrey Zhao..."
}
]
}
Observation 有三个重要性质。
第一,它来自环境,不由模型凭空生成。搜索结果来自搜索服务,订单状态来自业务数据库,计算结果来自计算程序。
第二,它要进入下一轮上下文。模型如果看不到执行结果,就无法据此修改下一步决策。
第三,它只是数据,不自动等于真相。搜索摘要可能截断,数据库记录可能过期,网页内容还可能包含提示注入。
Agent 必须区分:
系统指令 > 开发者约束 > 用户任务 > 工具返回的外部内容
工具返回的文本应视为不可信输入,不能因为网页里写了"忽略此前指令"就改变 Agent 的权限或目标。
三、ReAct 循环的真正控制者是谁
sql
+--------------------------------------------------+
| Agent Runtime |
| |
| 1. 管理消息历史 |
| 2. 调用模型 |
| 3. 校验工具名与参数 |
| 4. 执行工具 |
| 5. 写入 Observation |
| 6. 控制步数、超时、权限和结束条件 |
+-------------------------+------------------------+
|
v
+-----------------+
| LLM |
| 选择工具或回答 |
+-----------------+
|
v
+--------------------------------+
| search | query | calculate |
+--------------------------------+
模型是策略生成器,Runtime 才是状态机。可以把第 t 轮抽象为:
arduino
decision_t = LLM(context_t, tool_schemas)
if decision_t is Action:
observation_t = execute(decision_t)
context_(t+1) = context_t + decision_t + observation_t
else:
return final_answer
以下能力都应由 Runtime 实现,而不是寄希望于提示词:
- 最大循环次数;
- 工具白名单;
- JSON Schema 校验;
- 超时和重试;
- 调用权限;
- 幂等控制;
- 日志与审计;
- 成本与 Token 预算;
- 用户审批。
如果循环没有最大步数,模型可能反复搜索同一个关键词;如果写操作没有审批,模型的一次错误选择就可能变成真实副作用。
四、搜索、查询、计算工具怎样划分
一个基础工具型 Agent 可以先接入三类工具。
| 工具 | 解决的问题 | 典型输入 | 主要风险 |
|---|---|---|---|
| search | 获取开放世界信息 | 搜索关键词、结果数量 | 过时信息、提示注入、来源质量 |
| query | 获取业务系统中的确定记录 | 用户 ID、订单号、时间范围 | 越权、数据泄露、SQL 注入 |
| calculate | 执行精确计算 | 表达式或结构化参数 | 任意代码执行、资源耗尽 |
4.1.搜索工具
搜索工具不应把整页网页原样塞进上下文。更稳妥的返回值是:
json
{
"query": "ReAct paper",
"results": [
{
"title": "页面标题",
"url": "https://example.com",
"snippet": "与查询相关的摘要"
}
]
}
每条结果保留 URL,Agent 才能在最终答案中提供来源。结果数量也要有限,否则 Observation 会迅速占满上下文。
对于证据要求较高的任务,可以把搜索拆成两个工具:
scss
search(query) -> 候选页面列表
fetch(url) -> 指定页面正文
模型先找到候选来源,再读取高价值页面,避免一次搜索返回大量噪声。
4.2.业务查询工具
不要向模型暴露一个通用 run_sql(sql) 工具。模型可能生成成本过高、越权或破坏性的 SQL。
更适合 Agent 的接口是业务语义明确的窄工具:
scss
get_order_status(order_id)
list_customer_invoices(customer_id, start_date, end_date)
get_inventory(sku)
权限校验必须位于工具实现内部。模型传入了某个 customer_id ,不代表当前用户有权访问该客户。
4.3.计算工具
模型擅长理解问题,不适合作为精确计算引擎。涉及金额、日期差、统计或复杂算术时,应把计算交给确定性程序。
最危险的示例是:
scss
eval(expression)
它可能把"计算器"变成任意代码执行入口。安全计算器应解析抽象语法树,只允许数字和有限的运算符,同时限制表达式长度、指数和结果范围。
五、工具定义就是 Agent 的 API 设计
模型通常根据工具名称、描述和参数结构选择工具。因此,工具定义并非普通注释,而是模型能够看到的接口契约。
下面是一个搜索工具的定义:
python
SEARCH_TOOL = {
"type": "function",
"function": {
"name": "search",
"description": (
"Search public information when the answer depends on external "
"or potentially changing facts. Do not use it for arithmetic "
"or internal order data."
),
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "A concise search query."
}
},
"required": ["query"],
"additionalProperties": False
},
"strict": True
}
}
一个好的工具描述需要回答四个问题:
- 工具做什么;
- 什么情况下调用;
- 什么情况下不要调用;
- 返回结果包含什么、缺少什么。
工具越多,选择冲突越明显。例如同时提供:
search_web
search_docs
search_database
find_customer
lookup_customer
get_customer
模型很难稳定选择。先设计少量、边界清晰的工具,比堆积几十个相似函数更可靠。
六、一个 Agent 如何知道该结束
ReAct 循环至少需要四类结束条件。
- 正常完成:模型不再返回工具调用,而是输出最终答案。
- 达到步数上限:能阻止重复搜索和无限循环,但不保证答案已经完成。因此,触发上限时应明确告诉用户任务未完成。
- 无法恢复的工具错误:例如所有数据源都不可用,或者用户没有查询权限。Agent 应停止继续尝试,并说明缺少什么。
- 需要人工确认:发送邮件、退款、删除文件和提交订单都具有副作用。模型可以生成操作建议,但 Runtime 应在真正执行前进入审批状态。
yaml
模型提出 Action
|
v
检查是否为高风险操作
/ \
否 是
| |
直接执行 请求用户确认
|
确认后再执行
搜索和只读查询通常可以自动执行,写操作则应默认加入审批、幂等键和审计记录。
七、为什么 ReAct 会失败
ReAct 给了模型纠错机会,没有消除模型和工具本身的错误。
7.1.选错工具
模型可能用搜索工具查询内部订单,也可能在应该计算时直接给出心算结果。
改进方法包括:
- 减少功能重叠的工具;
- 写清适用与不适用场景;
- 为复杂参数提供调用示例;
- 用评测集统计工具选择准确率。
7.2.参数正确但语义错误
下面的参数符合 JSON Schema:
json
{
"sku": "A100"
}
但真实 SKU 是 A-100 。结构校验只能保证字段类型正确,不能保证业务含义正确。工具应返回明确、可恢复的错误:
json
{
"ok": false,
"error": "SKU A100 was not found",
"suggestions": ["A-100"]
}
7.3.Observation 带来错误信息
纯 CoT 可能出现事实幻觉和错误传播,ReAct 通过外部检索为推理提供依据,但检索本身也可能失败。如果搜索命中了错误实体,后续推理仍会沿着错误证据继续。
因此,重要事实需要:
- 保存来源 URL;
- 区分事实与模型解释;
- 对关键结论进行多源核验;
- 给 Observation 附带时间戳和数据版本。
7.4.轨迹过长
每一轮 Action 和 Observation 都会进入上下文。长任务会不断增加 Token、延迟和信息噪声。
可以采用三种控制方式:
短任务:保留完整轨迹
中等任务:压缩早期 Observation,只保留事实摘要与来源
长任务:把工作记忆、长期记忆和原始工具结果分开存储
7.5.把可解释性等同于真实性
交错轨迹有助于人类检查行为和事实依据,但这并不意味着 Thought 一定反映模型真实的内部计算,也不意味着一段语言流畅的解释就是正确解释。工程审计应优先记录:
用户输入
工具定义版本
工具名与参数
权限判断
原始 Observation
最终输出
错误与耗时
这些数据比公开一段冗长的"思考过程"更稳定、更可验证。
八、从最小实现走向生产系统
最小 ReAct Agent 解决的是"如何形成工具反馈循环"。生产系统还需要补齐四层能力:
diff
+------------------------------------------------+
| 产品层:任务入口、审批、进度反馈、结果引用 |
+------------------------------------------------+
| Agent 层:决策循环、状态、上下文、结束判断 |
+------------------------------------------------+
| 工具层:Schema、鉴权、超时、幂等、错误语义 |
+------------------------------------------------+
| 治理层:日志、评测、预算、安全、权限、审计 |
+------------------------------------------------+
评测也不能只看最终答案。一个工具型 Agent 至少要观测:
| 指标 | 说明 |
|---|---|
| 任务成功率 | 是否完成用户目标 |
| 工具选择准确率 | 是否选对工具 |
| 参数准确率 | 参数是否满足业务语义 |
| 平均工具调用次数 | 是否存在无效循环 |
| 无依据回答率 | 工具失败后是否编造结果 |
| 关键事实引用率 | 结论能否追溯到 Observation |
| 越权调用率 | 是否尝试执行无权限操作 |
| 单任务成本与延迟 | 是否满足运行预算 |
如果只检查最后一句答案,Agent 即使"碰巧答对",错误的工具轨迹也会被忽略。
九、小结
ReAct 给出了单 Agent 工具调用的最小骨架:
markdown
理解当前状态
|
选择下一动作
|
执行受控工具
|
读取环境反馈
|
继续行动或结束
Thought 负责维护任务状态并选择下一步,Action 把意图转换成结构化工具请求,Observation 将外部世界的结果带回上下文。模型负责决策,Runtime 负责执行、权限和循环控制,工具负责访问真实系统。三者边界清楚后,搜索、数据库查询和计算都可以通过同一套协议接入。
ReAct仍有明显上限。任务变长后,单条上下文难以同时承担计划、记忆和证据管理;工具增多后,模型的选择空间迅速膨胀;存在副作用时,系统还需要审批和恢复机制。
后续可以沿着三条路线扩展:
-
从即时 Thought 走向显式规划,形成 Plan-and-Execute;
-
从完整消息历史走向工作记忆与长期记忆;
-
从"能调用工具"走向"可评测、可审计、可恢复"的生产 Agent Runtime。
ReAct 的价值正是在这里:它把大模型从一次性文本生成器推进到能够感知外部结果并继续决策的执行系统,也为理解后续 Agent 架构提供了共同起点。