ReAct 的本质是把隐藏的推理过程搬到台面上

上一篇的 60 行最小 Agent 其实藏着一个问题——模型在循环里是"黑箱"操作的。你只看到它发起了一次 tool_call,但它为什么选这个工具、为什么填这些参数、是不是已经偏离任务,你一无所知。等到结果跑偏,你拿到的只有一堆 messages,调试起来像盲拆炸弹。

ReAct 这个范式就是为了解决这件事而生的。它的核心命题一句话就能说清:让模型在每次行动前,把自己的思考写出来

论文的原话

Yao et al. 在 2022 年的 "ReAct: Synergizing Reasoning and Acting in Language Models" 里提出了一个朴素的结构,叫 Thought-Action-Observation 循环:

Thought: 我需要先知道用户所在城市,再查那个城市的天气
Action: get_location()
Observation: 北京
Thought: 现在我可以查北京的天气了
Action: get_weather(city="北京")
Observation: 15 度,晴
Thought: 已经拿到信息,可以回答用户了
Final Answer: 北京现在 15 度,晴朗。

和"直接调工具"相比,多出来的只有 Thought 这一行。但这一行有三个非常大的作用:

降低出错率。模型在决定 Action 之前被迫先组织一次思路,等于做了一次内部的自检。论文实验显示,在 HotpotQA 这种多跳推理任务上,ReAct 比单纯 Action-only 的 Agent 准确率高 10~20 个百分点。

暴露错误路径。当 Agent 跑偏时,Thought 会告诉你它是"在哪一步开始偏的、基于什么错误认知做出了错误决定"。这对调试是决定性的。

支持反思。后面几篇要讲的 Reflection、Plan-Execute,都建立在"模型能写出自己的思考"这个前提之上。没有 Thought,就没有可以被审视和修正的东西。

两种实现范式

2022 年 ReAct 诞生时,OpenAI 的 Function Calling 还不存在。所以原始实现是纯文本的——靠 Prompt 强制模型输出 Thought: ... Action: ... Action Input: ... 这种结构,再用正则解析出来。

PROMPT = """你是一个会用工具的助手。按以下格式输出:

Thought: 你的思考
Action: 工具名
Action Input: JSON 参数
Observation: (工具返回,会由系统填入)
... (可以重复多轮)
Thought: 已经可以回答了
Final Answer: 最终答案

可用工具:
- get_weather(city): 获取城市天气
- calculate(expression): 计算数学表达式

用户问题: {question}
"""

import re, json
from openai import OpenAI

client = OpenAI()

def get_weather(city): return f"{city} 现在 15 度,晴"
def calculate(expr): return str(eval(expr))
TOOLS = {"get_weather": get_weather, "calculate": calculate}

def run_text_react(question: str, max_steps: int = 8):
    scratchpad = ""
    for _ in range(max_steps):
        prompt = PROMPT.format(question=question) + scratchpad
        resp = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}],
            stop=["Observation:"],
        )
        text = resp.choices[0].message.content
        scratchpad += text

        if "Final Answer:" in text:
            return text.split("Final Answer:")[-1].strip()

        m = re.search(r"Action:\s*(\w+)\s*Action Input:\s*(\{.*?\})", text, re.S)
        if not m:
            return "解析失败:" + text
        name, args = m.group(1), json.loads(m.group(2))
        obs = TOOLS[name](**args)
        scratchpad += f"\nObservation: {obs}\n"

    return "超出最大步数"

这个版本有两个值得注意的细节。stop 参数让模型写到 Observation: 就停下,把观察权交还给系统。scratchpad 是一根不断变长的"草稿纸",每轮都把模型的 Thought-Action 和系统填入的 Observation 拼进去。这就是 Agent 的"记忆"在纯文本时代的朴素形态。

但这种实现在 2026 年已经不是主流了,因为它有两个天然弱点:

解析脆弱。模型偶尔会输出格式不对的 JSON,或者忘记打 Action Input:,整个 Agent 就崩了。要写一堆容错代码。

Token 浪费。"Thought: ... Action: ..." 这些格式 token 每轮都要重复输出,长任务 token 消耗爆炸。

现代范式:tools 协议 + 推理模型

OpenAI Function Calling(以及 Anthropic tool_use、Hermes tool_call)把 ReAct 的 Action 部分从文本里抽出来变成结构化字段。而推理模型(o 系列、R 系列、Claude extended thinking)又把 Thought 部分搬进了隐藏的 reasoning tokens

结果是一个更干净的 Agent 长这样:

from openai import OpenAI
client = OpenAI()

def run_modern_react(question: str, max_steps: int = 10):
    messages = [
        {"role": "system", "content": "你是一个会用工具的助手,行动前先仔细思考。"},
        {"role": "user", "content": question},
    ]
    for _ in range(max_steps):
        resp = client.chat.completions.create(
            model="o4-mini",  # 推理模型,内部会做 Thought
            messages=messages,
            tools=TOOL_SCHEMAS,
        )
        msg = resp.choices[0].message
        messages.append(msg)

        if not msg.tool_calls:
            return msg.content

        for tc in msg.tool_calls:
            result = TOOLS[tc.function.name](**json.loads(tc.function.arguments))
            messages.append({
                "role": "tool", "tool_call_id": tc.id, "content": str(result),
            })
    return "超出最大步数"

表面上看它和上一篇的最小 Agent 一模一样,只是模型换成了 o4-mini。但 o4-mini 在每次决定 tool_call 前,会在 reasoning tokens 里做完整的 Thought 过程——只是这部分对你不可见(可见版本要付 API 的 reasoning.summary 开销)。ReAct 的灵魂被编译进了模型内部

两种范式什么时候用哪种

纯文本 ReAct——适合你用的是不支持 tool_use 的开源模型,或者你想要 Thought 完全可见、可干预。Hermes 系列、老一代的 Qwen Base 版本、研究场景都用这种。

tools + 推理模型——这是 2026 年的生产默认选项。成本低、解析稳、模型本身的推理质量比 prompt 强制模板高很多。

有一种混合模式值得一提:用普通模型 + Chain-of-Thought Prompt 显式化 Thought,再用 tools 协议做 Action。代码长这样:

SYSTEM = """你是一个会用工具的助手。
在调用工具前,先在消息里写一段 <思考>...</思考>,讲清楚:
1. 当前任务进展到哪一步了
2. 你现在要调用哪个工具、为什么
3. 预期的结果是什么
然后再发起 tool_call。"""

这种做法本质上是在非推理模型上手动"注入" ReAct 的 Thought 环节,调试时能从 assistant 消息里直接读到完整思考链。成本比推理模型低,可见度比隐藏 reasoning 高,生产里很多 Agent 用的就是这种。

ReAct 的几个典型失败模式

写了一段时间 Agent 后,你会反复看到这些 ReAct 病:

原地打转。模型调了一个工具拿到结果,下一轮 Thought 写"我需要调用 X 工具",又调了一次同样的工具。解法:在 prompt 里明确"不要重复调用已经成功过的工具";更严厉的做法是在代码里检测重复调用并强制中断。

Thought 很漂亮,Action 不执行。模型在 Thought 里说"我现在要去查 A,然后查 B",结果只调了 A 就给了 Final Answer。解法:在 system prompt 里强调"Thought 里声明要做的所有步骤都必须真的执行"。

拒绝使用工具,自己瞎编答案。模型觉得自己"知道"答案,不愿意调工具。解法:system prompt 强制"对任何时效性、私有数据问题必须调用工具"。

思考冗长。模型在 Thought 里写一大段无关的哲学思考,Token 浪费。解法:限制 Thought 长度,或用更低 temperature。

这些病每一个都能单独写一篇,但你现在知道它们的存在,就能在真的遇到时快速识别。

ReAct 不是终点

ReAct 解决了"思考可见"这件事,但它本身是一个反应式的模式——每步都只看当下,没有全局计划。遇到需要 20 步的复杂任务,ReAct 很容易"走到哪算哪",中途忘了最初的目标。

下一篇讲 Plan-and-Execute,就是为了补上这个短板:先规划,再执行,执行偏了再重新规划。再往后的 Reflection 和 Tree of Thoughts,每一种都是在 ReAct 基础上叠一层更高级的控制结构。

但不管叠多少层,底层永远是"思考 → 行动 → 观察"这个循环。把 ReAct 搞懂,后面所有高级架构都是它的变奏。

相关阅读

版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:02. ReAct:把 Agent 的思考显式化

本文链接:https://www.sshipanoo.com/blog/ai/ai-agent/02-ReAct/

本文最后一次更新为 天前,文章中的某些内容可能已过时!