从 ReAct 到 CodeAct:Agent 为什么开始用代码编排工具

我前面理解 ReAct 时,基本把它看成 Agent Loop:给模型任务、当前状态、可用工具和必要上下文,让模型决定下一步调用什么;工具返回结果以后,再把观察交给模型,循环到任务完成。

这个理解抓住了运行过程,不过还可以再拆一层。Agent Loop 是程序持续驱动模型和工具的外壳,ReAct 是模型在循环里交替进行推理、行动和观察的一种行为模式。CodeAct 又往前走了一步,它改变的主要是“行动”怎样表达:模型不再只吐出一次结构化工具调用,也可以写一小段可执行代码,在代码里组合、并发和处理多次工具调用。

CS329Z 第 06 讲正好把这些概念放到了一起。课程原始内容也可以在中文整理仓库查看。这一讲最有用的主线可以浓缩成一个问题:任务的控制流到底交给程序、模型,还是模型生成的代码?

先把 Workflow 和 Agent 分开

课程列了 5 种常用工作流。它们没有要求模型每一步都自由决定路线,控制流大部分仍由程序掌握。

模式控制方式适合的任务
Prompt Chaining固定顺序执行多个步骤,中间插入校验大纲、撰写、校对、翻译这类顺序明确的流程
Routing先分类,再进入不同分支售后分流、模型分级、不同工具集选择
Parallelization多个独立子任务并发,再聚合结果多维代码审查、多来源检索、多次投票
Orchestrator-Workers模型根据当前任务动态拆分,再交给多个 Worker跨文件改代码、资料研究、未知规模的子任务
Evaluator-Optimizer生成、评估、修改形成闭环有明确评价标准、可以迭代改进的内容

它们和 Agent 没有冲突。一个可控的生产系统常常在固定工作流里嵌入少量 Agent 环节。例如,代码先完成路由和权限检查,再让 Agent 处理无法预先列举的排障步骤,最后回到确定性的测试和验收。

判断依据也很直接。步骤已经清楚、失败代价高、结果需要复现时,优先让程序掌握流程。路径难以预先穷举,需要根据环境反馈持续探索时,再把更多决定交给模型。

ReAct 更接近“循环策略”

ReAct 论文把推理和行动交错起来。放到现代 Agent 框架里,可以把它理解成下面这条循环:

1
2
3
4
5
6
目标与当前上下文
→ 模型选择下一步行动
→ 工具执行
→ 返回观察、结果或错误
→ 模型更新判断
→ 继续或结束

这里的“给足上下文”不等于把所有历史记录和工具输出原样塞进去。模型需要的是当前目标、约束、可用工具、关键状态和足以延续任务的历史。无关日志越多,模型越容易忽略真正影响下一步行动的信息。

课程还介绍了几种围绕循环做改造的方法:

  • Reflexion 在失败后生成一段可复用的反思,让下一次尝试避开同样的问题。
  • CRITIC 引入搜索、代码执行等外部工具验证结果,降低模型凭感觉自我修改的风险。
  • ReWOO 先生成计划,再由 Worker 执行,减少每一个工具步骤都重新询问模型的开销。
  • LLMCompiler 把任务依赖组织成图,能并行的节点同时执行,并根据结果继续调度。

这些方法调整的是计划、反馈和调度。CodeAct 关注另一个位置:模型产生行动时,能不能直接使用编程语言表达循环、条件、并发和数据流。

CodeAct 到底改了什么

传统工具调用通常要求模型输出一个 JSON 对象:

1
2
3
4
{
"name": "get_price",
"arguments": { "country": "JP", "product": "camera" }
}

调用一个工具很合适。任务变成“查询 4 个国家的售价和税率,换算成人民币,过滤缺货项,再找出最低价”以后,模型需要反复发出调用。每次结果回到上下文,模型再决定下一步。循环和中间数据都隐含在多轮对话中。

CodeAct 允许模型把这段操作写成 Python:

1
2
3
4
5
6
7
8
9
10
rows = []
for country in countries:
price = get_price(country, product="camera")
tax = get_tax(country, category="electronics")
if price["in_stock"]:
total = convert(price["amount"] * (1 + tax), price["currency"], "CNY")
rows.append({"country": country, "total": total})

best = min(rows, key=lambda row: row["total"])
print(best)

循环、条件、变量和数据处理都成为显式代码。解释器执行以后,输出或错误再回到 Agent Loop。模型可以根据异常修改代码并继续执行。

CodeAct 论文把这种方式称为“可执行代码动作”。论文测试了 17 个模型,在其 M³ToolEval 评测中报告了最高 20 个百分点的成功率提升,并观察到最多约 30% 的行动次数下降。这里的“最高”来自特定模型和基准组合,不能直接推成所有 Agent 任务都会获得相同增益。

它表现较好的原因并不神秘。模型训练时见过大量程序,代码也天然适合表达复杂控制流。工具越来越多、工具之间要传递结构化数据时,写代码往往比连续生成许多独立 JSON 更紧凑。

CodeAct 仍然运行在 Agent Loop 里面。两者可以这样组合:

1
2
3
4
5
6
7
ReAct / Agent Loop 决定“下一步做什么”
↓
CodeAct 用代码表达“这一组动作怎样执行”
↓
解释器执行代码和工具调用
↓
结果或错误回到下一轮观察

所以,ReAct 和 CodeAct 处在不同层级。前者描述循环怎样推进,后者描述行动怎样编码。

Pi Codemode 很像 CodeAct,但实现边界更窄

你的判断基本对:Pi Codemode 和 CodeAct 共享同一个核心思路,都是让模型写可执行代码来编排工具。两者不能直接画等号。

对比项CodeAct 论文Pi Codemode直接 MCP / Tool Call
定位Agent 的代码行动范式Pi 里的具体工具编排实现工具协议与结构化调用路径
语言PythonJavaScriptJSON / schema 定义的参数
运行环境Python 解释器,可利用现有库QuickJS 沙箱Agent 框架调用具体工具
外部能力取决于解释器和环境配置没有 Node API、文件系统、网络和计时器,只能通过注入的工具与模型能力访问外部每次调用一个已注册工具,也可由框架并发
多工具组合代码中组合、循环和自我调试脚本中组合、并发、筛选 Pi 与 MCP 工具通常由模型或宿主程序逐次编排
进入模型的结果执行输出和错误成为观察只有脚本主动输出的内容回到模型工具结果通常直接成为观察

根据当前 Pi Codemode 文档,模型生成的原始 JavaScript 会作为异步函数体在 QuickJS 中运行。脚本可通过 tools.<name>() 调用 Pi 工具,也能调用已接入的 MCP 工具。只有 text()、return 等明确输出的内容进入模型上下文。

这项限制很关键。假设 MCP 返回 1000 条记录,Codemode 可以在脚本内部完成筛选、排序和聚合,只把最后 20 条候选及统计结果交给模型。模型不用阅读全部原始数据,多次调用也可以在一个脚本中并发执行。

下面是一段接近 Pi Codemode 写法的伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
const countries = ["JP", "US", "DE", "SG"];

const rows = await Promise.all(countries.map(async (country) => {
const price = await tools.mcp__shop__get_price({ country, product: "camera" });
const tax = await tools.mcp__tax__get_rate({ country, category: "electronics" });
return normalize(price, tax);
}));

return rows
.filter((row) => row.inStock)
.sort((a, b) => a.totalCny - b.totalCny)
.slice(0, 3);

Pi 的沙箱不允许脚本直接访问网络和本机文件。它能产生哪些外部影响,取决于宿主注入了哪些工具。这个边界比论文中通用的 Python 解释器更保守,也更适合把权限、审计和工具生命周期留在 Agent Harness 中。

Pi 从“不支持 MCP”到内置 MCP,改的是哪一层

课程里的 Pi 案例描述了一个更早的版本:核心只保留少数工具,没有内置 MCP、子 Agent、计划模式和待办系统。用它理解“极简 Harness”仍然有价值,但这段已经不能代表 2026 年 11 月的 Pi。

Pi 团队确实公开说过不支持 MCP。2026 年 9 月 29 日,他们在官方文章 “You Said No MCP!” 中回顾了此前对 MCP 的排斥,并说明为什么把 MCP 收进核心。按固定版本的 Pi 1.0 Changelog,0.99.0 已经加入内置 MCP、Codemode 和 tool_search,0.99.2 又把默认 MCP 工具改成按需发现。1.0 继续压缩 Codemode 常驻提示,并加强 MCP OAuth。

这个转向没有丢掉 Pi 原来的极简思路。旧版担心的核心问题包括:

  • 服务器一连接就把大量工具 schema 塞进上下文。
  • 工具返回长文本,后续每一轮都继续携带。
  • 单次 JSON 调用不方便组合多个工具。
  • 大量常驻工具会分散模型注意力,也增加提示词成本。

新版用 Codemode 和工具暴露策略处理这些问题。当前 Pi MCP 文档提供 4 种 exposure:

exposure模型怎样接触工具
codemode默认值。工具不直接声明给模型,在 Codemode 脚本里发现和调用
deferred先由 tool_search 找到,后续模型调用再加载声明
direct直接声明给模型,也能从 Codemode 调用
hidden注册但不可达

这相当于接纳 MCP 作为连接工具的标准,同时拒绝“把所有工具一次性灌进提示词”的用法。MCP 解决工具怎样注册、发现和调用;Codemode 解决多次调用怎样组合、计算和裁剪结果。两者是互补关系。

Pi 1.0 的发布说明称新版 Codemode 的提示词约减少 40%。官方例子是默认工具加 Codemode 时,某次 GPT-5.6 请求从约 5300 个 prompt tokens 降到约 3300 个。这是官方特定配置的测量,适合用来说明优化方向,不能当成所有 MCP 会话的固定节省比例。

前面已经写过一篇 Pi Agent 1.0 的 MCP 集成源码解析,里面按 0.99.0、0.99.2 和 1.0.0 的固定版本拆过工具发现、结果过滤、QuickJS 沙箱和 OAuth。放在这篇的框架里看,那些实现都服务于同一个目标:工具能力可以很多,常驻上下文仍要尽量小。

CodeAct 适合什么任务

CodeAct 或 Codemode 很适合下面这些场景:

  • 同类工具要循环调用很多次。
  • 多个独立调用可以并发。
  • 一个工具的结构化结果要传给另一个工具。
  • 原始返回很大,只需要过滤、聚合后的少量结果。
  • 中间步骤需要条件、重试、排序或统计。

直接工具调用在另外一些任务里更合适:

  • 只查一次天气、读一个文件或创建一条记录。
  • 操作会付款、删除、发布或修改权限,需要逐次确认。
  • 每一步都要清楚展示给用户,方便审计和中止。
  • 工具参数短,返回结果也不大。

代码动作还会带来新的工程问题。脚本可能语法错误、陷入低效循环,或在后半段失败时已经完成前面的外部操作。Pi 文档也明确说明,脚本失败不会撤销已经发生的工具调用。因此,写入型工具仍要设计幂等键、权限检查、超时和调用记录,不能只依赖 JavaScript 沙箱。

还有一个容易忽略的选择:稳定、重复的业务流程可以直接写成普通程序。模型每次现场生成一段 CodeAct 脚本,会引入额外的不确定性。Codemode 更适合结构随任务变化、但每次又包含多步数据处理的中间区域。

上下文管理才是长任务能不能跑完的底座

课程后半段把上下文管理归纳为两个目标:保持聚焦和维持连续性。这部分和 CodeAct 的关系很紧密。

持久化事件日志负责保存完整事实;模型上下文只是根据当前任务投影出来的工作视图。任务变长以后,可以使用压缩、观察屏蔽、记忆卸载和上下文隔离减少噪声。Skills 一类渐进披露机制则先告诉模型“有哪些能力”,等需要时再加载完整说明。

Codemode 能在工具结果进入模型以前做一次程序化过滤,解决的是观察过大的问题。它不能替代任务记忆、历史压缩和状态恢复。一个长程 Agent 仍然要区分:

  • 哪些是不可丢失的事件和产物。
  • 哪些是当前回合需要看到的信息。
  • 哪些结果只需要保存到文件或数据库。
  • 哪些工具说明可以按需发现。

把这一讲串起来后,Agent 的设计就不再只是“选一个更强的模型”。程序工作流负责确定性,Agent Loop 负责根据观察持续决策,CodeAct/Codemode 负责表达复杂行动,MCP 负责连接外部工具,上下文工程负责让模型在长任务里仍能看到正确的信息。

如果要给实际项目一个选择顺序,我会先判断任务能否用固定工作流完成,再判断开放环节是否需要 Agent Loop。遇到多工具、多数据和动态控制流时引入 CodeAct;接入 MCP 时再决定工具应该 direct、deferred 还是只通过 Codemode 暴露。这样能获得代码动作的灵活性,也不会把所有控制权和上下文成本一起交给模型。