ReAct 循环
🎯 学习目标
- 理解 ReAct 如何把推理与行动交织在同一条对话轨迹里
- 能写出带最大迭代、重复检测的 Agent Loop 伪代码
- 区分单轮 Function Calling 与多步 ReAct 的适用场景
- 设计可观测的 Thought / Tool / Result 前端展示便于调试
引言
单轮工具调用只能回答「查一次就够用」的问题。ReAct(Reasoning + Acting)让模型在循环中反复「想一步 → 做一步 → 看结果」,直到信息足够或触发终止。这是今天大多数 Agent 的运行骨架。
章节正文
第 1 步:ReAct 三要素与一次完整轨迹
一次 ReAct 轨迹通常包含:
- Thought(思考):模型解释为什么要做下一步(有的 API 不显式输出,但逻辑上等价)
- Action(行动):选择工具与参数,即
tool_calls - Observation(观察):工具返回写入
tool消息
示例轨迹:
- Thought:需要先查北京天气,再查用户日历是否户外行程
- Action:
get_weather(city=Beijing) - Observation:
{ forecast: '大雨' } - Action:
get_calendar(date=tomorrow) - Observation:
{ events: ['户外团建'] } - 最终回答:建议改期或备雨具
ReAct 的价值在于中间步骤可审查,比「黑盒一次性输出」更适合生产调试。
第 2 步:Agent Loop 伪代码(核心骨架)
下面是最小可用的 ReAct 循环,建议先跑通再叠加权限与观测:
async function reactAgent({ userQuery, tools, maxIterations = 8 }) {
const messages = [
{ role: 'system', content: '你是助手。需要时用工具,基于 Observation 回答。' },
{ role: 'user', content: userQuery },
]
const actionHistory = []
for (let i = 0; i < maxIterations; i++) {
const response = await llm.chat({ messages, tools })
const assistant = response.message
messages.push(assistant)
if (!assistant.tool_calls?.length) {
return { answer: assistant.content, iterations: i + 1, messages }
}
for (const call of assistant.tool_calls) {
const signature = `${call.function.name}:${call.function.arguments}`
if (actionHistory.includes(signature)) {
return { answer: '检测到重复操作,已中止。', aborted: true, messages }
}
actionHistory.push(signature)
const args = JSON.parse(call.function.arguments)
const observation = await executeTool(call.function.name, args)
messages.push({
role: 'tool',
tool_call_id: call.id,
content: JSON.stringify(observation),
})
}
}
return { answer: '达到最大步数,请缩小问题范围。', aborted: true, messages }
}注意:循环体是 LLM → 工具 → LLM,不是工具内部再调 LLM。
第 3 步:终止条件与 Token 预算
没有终止防护的 Agent 会无限「再查一下」。生产必备:
| 防护 | 说明 |
|---|---|
| maxIterations | 硬上限,如 8–15 步 |
| 重复 Action 检测 | 相同 name+arguments 连续出现则中断 |
| Token 预算 | 累计 input+output 接近上限时,插入摘要消息或强制收尾 |
| 无进展检测 | 连续 N 步 Observation 为空或错误 |
接近 Token 上限时,可插入 system 消息:「请基于已有 Observation 给出最佳努力结论,勿再调用工具。」
第 4 步:System Prompt 如何引导 ReAct 行为
ReAct 不强制模型输出 Thought: 文本,但你可以在 system 里约束风格:
规则:
1. 每次最多调用 2 个工具
2. 若 2 步内无法得到答案,向用户说明缺少什么信息
3. 不要重复相同查询
4. 最终回答必须引用 Observation 中的事实避免过长的「角色扮演」指令挤占工具结果空间。ReAct 的 system 应短、硬、可验证。
第 5 步:前端可视化与调试
开发阶段把每一步结构化展示,能节省大量排错时间:
// 每轮记录
trace.push({
step: i,
thought: assistant.content, // 部分模型会在 content 里留推理文本
actions: assistant.tool_calls?.map(c => ({
name: c.function.name,
args: c.function.arguments,
})),
observations: toolResults.map(t => t.content),
})UI 上建议分栏:左侧对话,右侧 Step 时间线(Thought → Tool → Result)。用户可选「展开推理过程」,默认折叠以免干扰。
动手练习
- 用伪代码或 Node 实现 reactAgent,mock 两个工具:search_docs 与 calculator,完成「先搜公式再计算」的两步任务。
- 设置 maxIterations=2,输入需要 4 步才能完成的任务,验证中止文案是否合理。
- 实现重复 Action 检测:故意让 mock 工具返回相同结果,观察是否会死循环以及防护是否生效。
- 为 trace 输出 JSON Lines 日志,每行一步,便于用 jq 分析。
- 对比「允许模型在 content 里写 Thought」与「禁止冗长推理、只要 tool_calls」对 Token 消耗的影响。
常见问题
Q:ReAct 和 Plan-and-Execute 有什么区别?
ReAct 是边想边做,每一步依赖上一步 Observation。Plan-and-Execute 先让模型输出完整计划,再逐步执行。前者更灵活适合探索型任务;后者更可控适合步骤可枚举的流程。很多系统采用混合:先粗规划,再 ReAct 微调。
Q:要不要把 Thought 展示给最终用户?
调试期建议对开发者全量展示;对终端用户默认隐藏或折叠,只展示结论与引用的工具结果。部分模型会在 Thought 里泄露内部策略或敏感推断,需做过滤。
Q:ReAct 一定比单轮 Function Calling 好吗?
不一定。单轮调用延迟更低、成本更低、行为更可预测。只有任务确实需要多步检索/计算/验证时才上 ReAct。能用 Workflow 固定步骤的,优先 Workflow(见 5.6 节)。
本节小结
ReAct = 在受控循环里交替推理与行动。写好 Agent Loop 伪代码、设清终止条件、做好 trace,比追求「更聪明的 Prompt」更能提升 Agent 可靠性。