Skip to content

ReAct 循环

🎯 学习目标

  • 理解 ReAct 如何把推理与行动交织在同一条对话轨迹里
  • 能写出带最大迭代、重复检测的 Agent Loop 伪代码
  • 区分单轮 Function Calling 与多步 ReAct 的适用场景
  • 设计可观测的 Thought / Tool / Result 前端展示便于调试

引言

单轮工具调用只能回答「查一次就够用」的问题。ReAct(Reasoning + Acting)让模型在循环中反复「想一步 → 做一步 → 看结果」,直到信息足够或触发终止。这是今天大多数 Agent 的运行骨架。

章节正文

第 1 步:ReAct 三要素与一次完整轨迹

一次 ReAct 轨迹通常包含:

  • Thought(思考):模型解释为什么要做下一步(有的 API 不显式输出,但逻辑上等价)
  • Action(行动):选择工具与参数,即 tool_calls
  • Observation(观察):工具返回写入 tool 消息

示例轨迹:

  1. Thought:需要先查北京天气,再查用户日历是否户外行程
  2. Action:get_weather(city=Beijing)
  3. Observation:{ forecast: '大雨' }
  4. Action:get_calendar(date=tomorrow)
  5. Observation:{ events: ['户外团建'] }
  6. 最终回答:建议改期或备雨具

ReAct 的价值在于中间步骤可审查,比「黑盒一次性输出」更适合生产调试。

第 2 步:Agent Loop 伪代码(核心骨架)

下面是最小可用的 ReAct 循环,建议先跑通再叠加权限与观测:

javascript
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 里约束风格:

text
规则:
1. 每次最多调用 2 个工具
2. 若 2 步内无法得到答案,向用户说明缺少什么信息
3. 不要重复相同查询
4. 最终回答必须引用 Observation 中的事实

避免过长的「角色扮演」指令挤占工具结果空间。ReAct 的 system 应短、硬、可验证

第 5 步:前端可视化与调试

开发阶段把每一步结构化展示,能节省大量排错时间:

javascript
// 每轮记录
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)。用户可选「展开推理过程」,默认折叠以免干扰。

动手练习

  1. 用伪代码或 Node 实现 reactAgent,mock 两个工具:search_docs 与 calculator,完成「先搜公式再计算」的两步任务。
  2. 设置 maxIterations=2,输入需要 4 步才能完成的任务,验证中止文案是否合理。
  3. 实现重复 Action 检测:故意让 mock 工具返回相同结果,观察是否会死循环以及防护是否生效。
  4. 为 trace 输出 JSON Lines 日志,每行一步,便于用 jq 分析。
  5. 对比「允许模型在 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 可靠性。