导学阶段-ReAct 边想边做
本文整理自 CNB 上的开源课程 《Vibe Agent:从零搭建 Coding Agent》。课程讲义、示例代码与练习仓库均在 CNB 上公开,文中示例出自该仓库对应章节。
Function Calling 把工具调用收进协议字段,模型发一个结构化的 tool_calls,程序照着执行。可靠,代价是模型为什么选这个工具、为什么这么填参数,这些判断在协议里没有位置。ReAct 在 2022 年给出了另一种写法,时间上早于 Function Calling。
三段循环
ReAct 要求模型每一步输出一对内容:Thought 说明打算做什么,Action 指定具体动作。程序执行 Action,把返回值作为 Observation 拼回上下文,再让模型输出下一对。
每一轮的输入都要重新拼。可用工具说明、原始任务、已经发生的历史,组装成一段新 prompt 发出去;模型看不到上一轮的原始响应,只看到程序格式化后的历史。历史怎么写,直接决定下一轮能不能接上。
以”北京今天适合去哪儿”为例,轨迹大致是:
1 | Thought: 需要先确认北京天气 |
三轮结束。Thought、Action、Observation 交替出现,直到 Action 写成 Finish[…]。程序侧的工作是两段正则,一段取 Thought 与 Action,一段判断 Action 是工具调用还是结束。
1 | def parse_action(action: str): |
这段解析只认双引号形式的参数。换成单引号,kwargs 会解析成空字典,工具照常被调用,只是所有参数都丢了。
历史每轮追加三条记录,prompt 逐轮变长。工具返回的内容越长、步数越多,token 消耗上升得越快;一个需要七八步的任务,末尾一轮的输入可能已经包含前面所有工具输出。
Thought 不是装饰
让模型跳过 Thought 直接输出 Action,多步任务的成功率会下降。原因不复杂:Thought 是 chain-of-thought 的文本形式,模型写下”天气晴朗,适合户外”的同时,完成了三件事:解释上一步的观察、判断信息是否足够、决定下一步查什么。
Thought 还会被写回历史,下一步的模型能看到上一步的理由,推理因此可以接续。
把思考写进上下文还有调试价值。轨迹是纯文本,每一步的判断都能读到;结果不对时,能定位到是哪一步想偏了,而不是只看到一个工具调用和一段最终回答。
跟 tool_calls 的分工
| 维度 | tool_calls | ReAct |
|---|---|---|
| 调用怎么表达 | 协议字段 | prompt 约定的 Action 字符串 |
| 解析方式 | 读结构化字段 | 正则 |
| 思考过程 | 无显式位置 | 每步一段 Thought |
| 健壮性 | 协议保证 | 模型多写一句就可能失败 |
| 可读性 | JSON,对人一般 | 整条轨迹可读 |
真实工程里两者可以叠加。工具执行走 tool_calls 拿到可靠性和并行支持,system prompt 要求模型在 assistant.content 里先写一段思考再发 tool_calls。协议负责执行,文本负责推理。落地时改动很小:system prompt 多写一句,assistant 的 content 跟着消息一起追加进历史。
prompt 约定的脆弱
纯 prompt 约定格式的失败方式很具体:
- 模型在 Thought 前多写一句开场白,输出就不再是约定的那一种格式
- 模型一次输出两对 Thought 与 Action,程序要决定取哪一对
- 参数写成单引号,或者省略参数名,解析出空字典
- Action 换行写,后面跟一段解释
每一条都要补一层 fallback,容错代码很快超过主循环本身。换一种格式约定,脆弱点只是换个地方:把 Action 改成 JSON,解析器好写一些,模型仍然可能在外层多写一段解释。Function Calling 出现后大家集体迁移,原因在于字段解析不再依赖模型是否守规矩。课程示例把步数上限定为 6,到点直接返回,避免解析失败后无限重试。
今天还值得写一遍
ReAct 有几处价值至今成立。它的表述足够简单,三个词能概括 Agent 的工作流;它是纯 prompt,不需要框架就能跑起来;整条轨迹可读,调试时能直接看到模型的判断。后来的 Plan-and-Solve、Reflection、Tree-of-Thoughts,都能从这三段里找到影子。
课程示例故意用旧写法实现一遍,让两面都露出来:Thought 让推理可读,正则让工程脆弱。写过一次,协议化为什么更可靠就不需要解释了。
三种范式的示例共用同一套工具和同样的 5 个任务,横向跑一遍,差别来自范式本身,与工具集无关。
这对搭 Agent 意味着什么
执行交给协议,推理留给文本。工具调用用 tool_calls 拿确定性,思考过程用 Thought 拿可解释性,两者叠在同一个循环里,是现在比较常见的做法。需要权衡的是成本。Thought 要占 token,历史里的 Observation 同样要占,简单任务上多写一段思考未必划算。
ReAct 是走一步看一步。任务步骤一多,每步都重新决策就容易在中途偏离目标。下一篇的 Plan-and-Solve 换个顺序,先把计划列出来,再按计划逐步执行。


