范式没变,变的是信封。聊聊 ReAct 和 function calling 到底是什么关系,以及 Claude Code 这类真实产品是怎么把它们揉在一起的。

两个高频词,一场误会

过去几年,LLM Agent 从论文概念变成了生产标配。随之而来的是两个高频词:ReActFunction Calling(tool use)。圈子里流行一种说法——"ReAct 已经过时了,现在都该用原生 function calling",仿佛两者是同一赛道的两个选手,一个被另一个淘汰了。

这个说法对了一半。ReAct 和 function calling 根本不在同一个维度上:

  • ReAct 是范式(paradigm)——回答"agent 该怎么思考和行动"。
  • Function calling 是机制(mechanism)——回答"工具调用该怎么传输和执行"。

两者不是替代关系,而是实现关系:现代 agent 产品,恰恰是用原生 function calling 这套机制,落地了 ReAct 这套范式。这篇文章不讲空话,我们用真实产品(Claude Code、客服退款 agent、SWE-bench 编码 agent)拆开来看。

01 先分清:范式 vs 机制

ReAct:推理与行动的交响乐

ReAct 出自 2022 年姚顺宇等人的论文 ReAct: Synergizing Reasoning and Acting in Language Models(arXiv:2210.03629)。核心思想一句话:让模型交错输出"推理轨迹"和"具体动作",再用外部环境的"观察结果"驱动下一步。推理帮助模型制定、跟踪、修正行动计划;行动让模型与现实世界交互、收集信息。

典型轨迹长这样:

Thought 1: 需要先查刘慈欣的出生地,确认来源再回答。
Action 1: Search[刘慈欣 出生地]
Observation 1: 刘慈欣(1963年6月23日—),出生于北京。
Thought 2: 观察已给出明确答案,直接回答。
Action 2: Finish[北京]

注意:动作是自由文本。harness 用正则或模板从文本流里抠出动作名和参数,执行后把观察结果拼回对话,让模型继续。

ReAct 的价值:不需要模型专门训练工具调用能力(2022 年也没有);推理过程以文本形式暴露,天然可解释;范式本身很轻,一个 prompt 就能跑起来。

ReAct 的代价:文本解析脆弱(格式一漂移就崩);动作非结构化;很难做并行工具调用;长轨迹里文本和动作混在一起,机器难以精确处理。

原生 Function Calling:结构化的工具协议

Function calling 是模型厂商(OpenAI 的 tool_calls、Anthropic 的 tool_use、Google 的 functionCall)训练出来的结构化输出能力。模型不再用文本写 Action: Search[x],而是直接吐出一个结构化的工具调用块:

{
  "type": "tool_use",
  "id": "toolu_01ABC",
  "name": "read_file",
  "input": { "path": "./src/auth.ts", "offset": 1 }
}

harness 侧的 agent loop 长这样(Anthropic Messages API 风格):

messages = [{"role": "user", "content": prompt}]

while True:
    resp = client.messages.create(
        model="claude-sonnet-4-5",
        tools=TOOLS,                # 每个工具一个 JSON Schema 定义
        messages=messages,
    )

    tool_uses = [b for b in resp.content if b.type == "tool_use"]

    # 没有工具调用 → 循环结束(stop_reason == "end_turn")
    if not tool_uses:
        break

    # 执行工具(可并行),结果回填为 tool_result
    results = [run_tool(b.name, b.input) for b in tool_uses]
    messages.append({"role": "assistant", "content": resp.content})
    messages.append({
        "role": "user",
        "content": [
            {"type": "tool_result", "tool_use_id": b.id, "content": r}
            for b, r in zip(tool_uses, results)
        ],
    })

看出来了吗——这个循环和 ReAct 在结构上一模一样:模型推理 → 产生动作 → 执行 → 观察结果回填 → 再推理。区别只在传输层:动作是结构化块而不是文本,解析换成了 schema 校验,还支持并行调用多个工具。

打个比方

ReAct 是全剧剧本:角色该想什么、说什么、做什么。
Function calling 是舞台调度:台词怎么递、道具怎么传、场记怎么记。

同一部戏,既可以在老式剧场演(演员念完台词,舞台监督靠耳朵听、靠笔记录),也可以用现代调度系统演(无线耳麦、cue 单、结构化场记)。戏没变,变的是舞台设施。

02 真实场景一:Claude Code——生产级编码 Agent 的活标本

Claude Code(Anthropic 的终端编码 agent)是观察这个问题的绝佳样本,因为它把两端都占了:官方文档明确说它走的是原生 tool use,但它的设计处处透着 ReAct 的骨架。

官方文档 How Claude Code works 把它的循环描述为三阶段:gather context(收集上下文)→ take action(采取行动)→ verify results(验证结果),并说:

The agentic loop is powered by two components: models that reason and tools that act. Claude Code serves as the agentic harness around Claude.

再看它修 bug 的真实轨迹(来自 Agent SDK 文档的示例,"Fix the failing tests in auth.ts"):

Turn 1  Bash        npm test                → 3 个用例失败
Turn 2  Read        auth.ts / auth.test.ts  → 拿到文件内容
Turn 3  Edit + Bash 修改 auth.ts + 重跑测试   → 全部通过
Turn 4  纯文本回复   "Fixed the auth bug, all three tests pass now."

这就是一条标准的 ReAct 轨迹:思考(看测试结果)→ 行动(读文件)→ 观察(文件内容)→ 行动(修改+验证)→ 观察(测试通过)→ 收尾。模型会链式跑几十个 tool call 并不断纠偏——正是 ReAct 论文里"用推理更新行动计划、处理异常"的工程化放大版。

而它的"实现细节"暴露在会话记录里:Claude Code 把每轮交互写成 JSONL 存在 ~/.claude/projects/ 下,text / thinking / tool_use / tool_result 块交错排列。这份日志,就是 ReAct 论文承诺的"可解释轨迹"的结构化形态——只是从"人类可读文本"升级成了"机器可读、可回放、可 diff 的日志"。

调试靠的也不是魔法:结构化工具记录(精确到工具名和 JSON 参数)+ thinking 块(部分可见的推理)+ checkpoint 快照回放。三件套缺一不可——其中前两件恰恰对应 ReAct 的"动作"和"推理"两个组件。

03 真实场景二:客服退款 Agent——function calling 的主场

换个场景:一个电商客服 agent,用户说"我上周买的耳机要退货"。

在原生 function calling 下,模型会并行发出三个工具调用:

[
  { "type": "tool_use", "name": "get_order",     "input": {"order_id": "A-8821"} },
  { "type": "tool_use", "name": "get_customer",  "input": {"customer_id": "U-117"} },
  { "type": "tool_use", "name": "fraud_check",   "input": {"order_id": "A-8821"} }
]

三个调用并行执行、一次回填,agent 拿到订单状态、用户身份、风控结果后决定走退款流程。这个场景里 function calling 的优势是碾压级的:

  • 参数精确、可审计:每次调用是 order_idcustomer_id 这样的明确字段,出问题能精确回放"这次用错了参数",而不是在文本里找 Search[A-8821] 这种可能被截断、被转义污染的字符串。
  • 并行:退款决策依赖三个独立查询,ReAct 文本格式只能串行,一个回合一个动作,延迟直接翻三倍。
  • 错误边界清晰:参数不合法在进工具前就被 schema 校验拦下,错误归属(模型吐错参数 vs 工具内部报错)一眼可辨。

如果这个 agent 用 ReAct 文本格式硬写,你会收获:正则解析失败、JSON 字符串转义地狱(退款备注里带个引号就崩)、动作串行导致的超时、以及审计时"这段 Action 文本到底对应哪个请求"的猜谜游戏。

04 真实场景三:SWE-bench 编码 Agent——工具设计比 Prompt 更重要

Anthropic 在 Building Effective Agents 里给了个非常直白的定义:

Agents are typically just LLMs using tools based on environmental feedback in a loop.

并强调:"gaining 'ground truth' from the environment at each step (such as tool call results or code execution)" 是 agent 运转的关键

这句话拆开就是 ReAct 的公式:环境反馈(观察)→ 驱动下一步动作。SWE-bench 编码 agent 的日常循环是:跑测试 → 读报错 → 定位文件 → 修改 → 重跑测试 → 再改……测试输出就是它的 Observation,工具的返回值就是它的 ground truth。

更有意思的是他们的另一个观察:构建 SWE-bench agent 时,"我们花在优化工具上的时间比优化 prompt 还多"——比如把工具参数从相对路径改成绝对路径,模型用错率立刻归零。这跟 ReAct 思想是一脉相承的:工具就是行动空间,行动空间设计得越清晰,模型的推理-行动循环就越可靠。ReAct 论文里模型只能在 Search/Lookup/Finish 三个动作里选,Claude Code 把这个行动空间扩展成几十个经过精雕细琢的工具——范式没变,动作集变大了、变专业了。

05 真实场景四:ReAct 还没死——它是最务实的兜底

说了这么多 function calling 的好话,必须补一句公道话:ReAct 在今天依然大量存在,而且它是很多场景下唯一务实的选项

  • 自托管 / 开源模型没有 function calling 训练:很多本地部署的 7B/13B 模型没有经过 tool calling 微调,你没法用原生机制。ReAct 文本格式是纯 prompt 技巧,任何模型都能跑——这也是开源生态里 ReAct 至今活跃的原因。
  • 交互式决策环境:ALFWorld、WebShop 这类 benchmark 的接口本身就是文本动作(pick up 1 apple from 1 countertop 之类),ReAct 格式天然契合,直到今天论文里还在用它。
  • 教学与快速原型:手写一个 ReAct loop 只要几十行代码,不用任何 SDK,是理解"agent 到底在循环什么"的最佳入门路径。LangChain 的 ReAct agent 至今还是无数教程的起点。
  • 反面教材警告:AutoGPT 当年让模型在文本里"写 JSON 动作",解析层崩了无数回——这就是在缺结构化机制的情况下硬做结构化传输的代价。ReAct 用纯文本没问题,但别伪装成结构。

一句话:有原生支持用原生,没有就用 ReAct 文本兜底,但别用文本硬扮结构化。

06 一张表看懂:什么时候选谁

场景

建议

模型原生支持 tool calling(Claude / GPT / Gemini 等)

用 function calling

自托管开源模型,无 tool calling 训练

ReAct 文本格式兜底

环境接口是纯文本(游戏环境、老系统、终端脚本)

ReAct

需要并行工具调用(客服查询 + 风控 + 库存)

function calling

需要精确审计 / 回放(生产、金融、合规)

function calling + 全量日志

快速原型、教学、理解 agent 原理

先从 ReAct 跑通,再迁移

07 误区

误区一:"function calling 的执行路径确定、可预测。"
错。function calling 只让单次调用的记录确定(工具名 + 参数可复现),但执行路径——模型下一步调哪个工具——依然是 LLM 采样决定的,跟 ReAct 一样非确定。同一个 prompt 跑两遍,工具序列可能完全不同。结构化的是信封,信的内容照样随机。

误区二:"ReAct 更容易产生长轨迹和循环,所以难调试。"
轨迹长度和循环是 agent loop 的属性,不是 ReAct 文本格式的属性。Claude Code 一个任务链几十个 tool call,比经典 ReAct demo 长得多,照样该循环循环。"循环"是迭代式 agent 的固有特征,换机制一条都不会少。

误区三:"function calling 一定更好调试。"
调用记录更精确是真的,但"模型为什么选这个工具"这个深水区,function calling 反而可能更难看——因为 thinking 块默认常被隐藏或摘要化。ReAct 的文本推理至少是明牌。真正决定可调试性的是可观测性设计:结构化日志 + 推理可见 + 可回放,三者缺一不可。

08 写在最后

回到开头的争论:ReAct 过时了吗?

没有。它只是换了一身更合身的西装。

ReAct 贡献的"推理-行动-观察"闭环,至今仍是所有自主 agent 的底层骨架;function calling 贡献的是让这个骨架在生产环境里跑得稳的结构化传输层。Claude Code 们真正的工程秘密,也从来不是"用了哪种机制",而是三件事:循环怎么设计(何时收手、如何纠偏)、工具怎么设计(行动空间是否清晰)、可观测性怎么设计(出了事能不能回放)

机制会继续进化——今天的 tool_use,明天可能是 tool orchestration、tool composition。但只要 agent 还需要"先想、再做、再看结果、再想"地完成任务,ReAct 的骨架就不会过时。

参考

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐