笔记日期: 2026-05-25 笔记作者: Zhongzhu Zhou 论文标题: Executable Code Actions Elicit Better LLM Agents 作者: Xingyao Wang, Yangyi Chen, Lifan Yuan, Yizhe Zhang, Yunzhu Li, Hao Peng, Heng Ji arXiv: 2402.01030 状态 / Venue: ICML 2024(第41届国际机器学习会议,PMLR 235)
一句话总结
CodeAct 的核心洞见:与其让 LLM Agent 输出 JSON 对象逐一调用工具,不如直接让它写一段可执行的 Python 代码。代码天生支持循环、条件、变量和库导入,一个 <execute> 块就能完成 JSON 需要十几轮才能完成的任务——而且 Python 报错信息本身就是最好的调试提示,让 Agent 可以自主发现并修复错误。
前置知识
在深入论文细节之前,我想把几个基础概念讲清楚。CodeAct 涉及 LLM Agent 框架、工具调用机制和执行环境三个方向,读者不需要深入了解每个方向,但对以下概念有直觉认识会让后续内容容易很多。
1. LLM Agent 的基本循环
LLM Agent 的工作方式是一个多轮交互循环:模型接收到用户指令,产生一个”动作”(Action),动作被送入外部环境执行,环境返回一个”观察”(Observation),模型根据这个观察决定下一步动作,如此往复直到完成任务。
sequenceDiagram
participant U as 用户
participant A as LLM Agent
participant E as 环境 (Python 解释器 / API / 网络)
U->>A: 指令(自然语言)
loop 多轮交互
A->>A: 思考 / 规划(可选链式思维)
A->>E: 动作(工具调用 / 代码 / JSON / 文本...)
E->>A: 观察(执行结果 / API 响应 / 报错)
end
A->>U: 最终答案
这个循环听起来简单,但动作的格式至关重要——这正是 CodeAct 要解决的问题。
2. “工具调用”在 LLM 中意味着什么
现代 LLM(如 GPT-4、Claude)可以调用外部工具。在实现层面,“工具调用”通常是模型生成一个 JSON 字符串,描述要调用哪个函数以及传什么参数。例如:
{"tool": "lookup_phone_price", "country": "Germany"}
框架解析这段 JSON,调用实际的 Python 函数,把结果作为文本返回给模型。模型从来不”直接运行代码”——它只是生成一个被解释的字符串。
问题所在: JSON 是一种极为受限的语言。它一次只能描述一个函数调用。如果你需要调用五个工具并组合结果,就必须进行五轮独立的 JSON 交互。无法用一个 JSON 动作表达”循环调用工具 A,把所有结果取平均值”这样的逻辑。
3. 三种主流动作格式对比
在 CodeAct 之前,LLM Agent 领域主要有三种动作格式:
| 格式 | 示例 | 优点 | 缺点 |
|---|---|---|---|
| 自由文本 | Action: lookup_rates, country: Germany | 可读性强 | 解析不稳定;无结构 |
| JSON | {"tool": "lookup_rates", "country": "Germany"} | 可解析;可验证 | 每次一个工具;无循环;无数据流 |
| 代码(CodeAct) | rates = lookup_rates("Germany") | 完全表达力;库;自调试 | 模型需要 Python 能力 |
CodeAct 的核心洞见:代码不只是一种编程语言,它是一种通用动作语言,天然支持 JSON 做不到的一切:循环、条件、变量绑定、异常处理和库导入。
4. 为什么选 Python?
作者选择 Python 基于四点理由:
- 预训练数据丰富。 Python 是 TIOBE 指数 2024 年排名第一的语言,在 GitHub ML/数据科学仓库中占统治地位。LLM 见过的 Python 代码远比任何 JSON Schema 多。
- 现有生态系统。
import pandas、import numpy、import sklearn——整个科学 Python 栈都可以用。不需要预先定义工具。 - 内置反馈机制。 Python 的
TypeError、ValueError、traceback输出本身就是自然语言。Agent 可以直接阅读自己产生的报错并修复。 - 控制流与数据流。
for、if、while、列表推导——Agent 需要的一切计算能力都在语言本身里。
5. 自调试的工作原理
CodeAct 有一个核心机制经常被低估:基于报错信息的自动自调试。当一段 Python 代码执行失败时,解释器返回 traceback。这个 traceback 作为下一轮的”观察”送回给模型。能力足够的 LLM 读取 traceback 并生成修正后的代码。整个过程无需人工干预。
flowchart LR
A["Agent 输出 Python 代码\n(第 N 轮)"] --> B["Python 解释器执行"]
B -- "成功" --> C["观察:执行结果\nAgent 继续或回答"]
B -- "失败" --> D["观察:traceback\n(TypeError, NameError, ...)"]
D --> E["Agent 阅读报错\n自主修复\n(第 N+1 轮)"]
E --> A
对比之下,JSON Agent 调用失败只会收到 {"error": "Invalid argument"}——没有行号、没有类型信息、没有调用栈,无法从中推断出修复方案。
CodeAct 框架的设计与形式化
1. 统一动作空间
CodeAct 的核心主张:Agent 与环境的所有交互,都应表达为一段可执行的 Python 代码块。 形式化定义如下:
- Agent :语言模型本体
- 用户 :提供指令的人类
- 环境 :Python 解释器 + API + 外部工具(以 Python 函数形式暴露)
在第 轮,Agent 接收观察 (来自用户或代码执行结果),产生动作 (包裹在 <execute>...</execute> 标签中的 Python 代码块)。环境执行 ,返回 (stdout + stderr)。
一条完整轨迹的形式化表示:
其中 是用户初始指令,每个 是一段合法 Python 程序, 是执行输出。
2. 四个优势的精确阐述
优势一 — 控制流。 以计算手机在多个国家的最低购买价格为例:
countries = ["USA", "Japan", "Germany", "India"]
final_prices = {}
for country in countries:
local_price = lookup_phone_price("CodeAct 1", country)
rate = lookup_rates(country)
converted = convert_and_tax(local_price, rate, tax_rate)
shipping = estimate_shipping_cost(country)
final_prices[country] = estimate_final_price(converted, shipping)
best = min(final_prices, key=final_prices.get)
print(best, final_prices[best])
这是一轮 CodeAct 交互,而 JSON Agent 至少需要 4 × 4 = 16 轮(每个国家调用4个 API)。
优势二 — 数据流。 JSON 动作是无状态的:每次调用都是新的,前一次调用的返回值无法以变量形式保存供后续引用。Python 解释器维护跨 <execute> 块的会话状态(类似 Jupyter Notebook 的 kernel),第 轮定义的变量在第 轮仍然可用:
优势三 — 现有软件包。 对比添加新工具的代价:
| 动作类型 | 添加新工具的代价 |
|---|---|
| JSON / 文本 | 定义 schema、注册处理函数、更新提示词 |
| CodeAct | 无——import tool_library 直接调用 |
PyPI 上的约 50 万个包即为工具库,模型在预训练中已经学会如何使用它们。
优势四 — 自动反馈。 对比报错信息的信息量:
# JSON Agent 报错
{"status": "error", "code": -1}
# CodeAct Python traceback
TypeError: unsupported operand type(s) for *: 'str' and 'float'
File "<execute>", line 3, in <module>
converted = local_price * rate
traceback 精确告知:哪一行、哪种类型错误——这是可直接用于修复的信息。
3. 系统提示设计
CodeAct 的系统提示(论文附录 E)仅有 6 行,指示模型:
- 与”Python Jupyter Notebook”环境交互
- 代码包裹在
<execute>...</execute>标签中 - 用
!pip install [package]安装缺失包 - 每次尝试少一点,避免一个
<execute>块塞太多代码 - 获得足够信息后停止执行并给出答案
重要的是它的极简性。 许多 Agent 框架需要大量 few-shot 示例。CodeAct 的零样本提示只有 6 行——其余依赖模型在预训练中积累的 Python 能力。
graph TD
subgraph SYS["系统提示(6行,零样本)"]
S1["与 Python Jupyter 环境交互"]
S2["代码包在 <execute> 标签内"]
S3["缺包用 !pip install 安装"]
S4["每次块里做的事情少一些"]
S5["有答案了就停止执行"]
end
subgraph TURN["单轮结构"]
OBS["观察(用户或环境返回)"]
THK["可选思维链(CoT)"]
CODE["<execute> Python 代码 </execute>"]
RES["环境返回 stdout / stderr"]
end
SYS --> TURN
研究问题一:代码格式对原子工具调用有帮助吗?
实验设置
第一组实验测试最简单场景:单次工具调用。任务是用正确的参数调用一个 API。这个设置把代码的控制流/数据流优势完全屏蔽掉,只剩下一个问题:用代码语法调用工具,比用 JSON 或文本更自然吗?
数据集:API-Bank,113 个一级(单工具调用)实例,涵盖日历、金融、天气等 API。
评估指标:正确率——模型生成的 API 调用输出是否与标准答案完全一致。
测试模型:8 个开源(CodeLlama 系列、Llama-2 系列、Mistral-7B、Lemur-70B)+ 8 个闭源(claude-2、gemini-pro、gpt-3.5/4 系列、text-davinci 系列)。
结果与分析(表2)
关键观察:
-
开源模型偏好代码格式胜于 JSON。 对 CodeLlama-13B(38.1 vs 12.0)、Llama-2-70B(35.6 vs 14.3)和 Lemur-70B(58.6 vs 46.6),代码格式显著优于 JSON。原因:开源模型在预训练中见了大量代码,但没有经过 JSON 工具调用专项微调,代码格式才是它们的”母语”。
-
闭源模型差距更小。 GPT-4、claude-2 等都经过 JSON 函数调用专项微调,对单次调用场景有优势。但代码格式仍然有竞争力。
-
文本格式在少数基础模型上占优。 这表明对话式指令微调模型来说,文本比代码更自然,但 JSON 对开源模型一致是最差的选择。
xychart-beta
title "API-Bank 正确率(%)——按动作格式"
x-axis [CodeLlama-13B, Llama-2-70B, Lemur-70B, gpt-4-0613, gpt-4-1106]
y-axis "正确率 (%)" 0 --> 90
bar [38.1, 35.6, 58.6, 75.4, 76.8]
bar [12.0, 14.3, 46.6, 82.0, 82.7]
bar [14.0, 36.6, 56.1, 74.4, 75.4]
RQ1 的结论: 代码是有大量代码预训练数据的 LLM 的自然工具调用格式。JSON 对通用 LLM 并非显而易见的最佳选择。
研究问题二:控制流对复杂任务有帮助吗?
M³ToolEval 基准
第二组实验针对 JSON 根本无法处理的场景:需要多工具、多轮次、结果组合的复杂任务。作者构建了 M³ToolEval(Multiple Models Multi-turn Tool Evaluation),包含82个人工标注实例,覆盖:
- 网页浏览(7个工具:点击、滚动、回退等)
- 金融(价格查询、汇率转换、税费计算、运费估算)
- 旅行行程(机票搜索、酒店预订、天气查询)
- 科学信息处理(数据检索、计算、摘要)
每个实例都需要在一个任务中调用多个不同工具。表 A.7 对比了现有基准:
| 基准 | 多轮 | 多工具 | 无外部 API 依赖 |
|---|---|---|---|
| ToolBench | ✗ | ✓ | ✗ |
| APIBench | ✗ | ✗ | ✓ |
| API-Bank | ✗ | ✗ | ✓ |
| M³ToolEval | ✓ | ✓ | ✓ |
实验设置与结果(表3)
- 最多10轮交互
- 指标:成功率(答案与标准答案一致)+ 平均轮次(越少越好)
- 零样本设置(提示词中无示例)
这里 CodeAct 的优势变得决定性:
绝对提升 20.7%,是论文的核心数字。平均轮次:CodeAct 5.5 轮 vs JSON 7.6 轮。减少约 30% 的交互轮次。
xychart-beta
title "M³ToolEval 成功率(%)——CodeAct vs JSON vs 文本"
x-axis [claude-2, gpt-3.5-0613, gpt-4-0613, gpt-4-1106, gemini-pro]
y-axis "成功率 (%)" 0 --> 80
bar [54.9, 51.2, 67.1, 74.4, 22.0]
bar [39.0, 26.8, 56.1, 53.7, 19.5]
bar [29.3, 20.7, 45.1, 53.7, 11.0]
为什么 CodeAct 使用更少轮次? 因为它可以用一轮完成 JSON 需要多轮的事情。在手机价格例子中,CodeAct 用一个 for 循环在一个 <execute> 块里查询4个国家的价格和汇率;JSON Agent 至少需要16轮独立调用。
改进模式说明了一个重要原则: 优势对强模型(gpt-4)最大,对开源弱模型最小。这是因为 CodeAct 要求模型写出正确可运行的 Python——比生成语法正确的 JSON 字符串要求更高。弱模型写出的 Python 经常报错,部分抵消了概念上的优势。
为什么更多轮次的 JSON 无法弥补差距?
理论上,给 JSON Agent 更多轮次可以完成同样的任务——但每额外一轮都要付出 LLM API 时间、费用和延迟。更根本的是,数据流问题无法用更多轮次解决:JSON Agent 无法把第 轮的计算结果存储为变量供第 轮使用。Python 变量天然处理这个问题。
轮次效率的简化公式:
其中 是任务需要调用的工具次数, 是 Python 报错触发的额外自调试轮次。对强模型(gpt-4),,即 CodeAct 完成需要 次工具调用的任务平均只需 1.5 轮。
研究问题三:多轮交互与现有软件的价值
CodeActAgent 实际演示
论文图3展示了一个完整的数据分析会话示例:
sequenceDiagram
participant U as 用户
participant A as CodeActAgent(Mistral-7B)
participant E as Python 解释器
U->>A: "下载数据集,检查缺失值,训练回归模型"
A->>E: import pandas; df = pd.read_csv(url); df.info(); df.isnull().sum()
E->>A: [数据类型、缺失值统计——'price' 列含 '$' 字符]
A->>E: df['price']=df['price'].str.replace('$','').astype(float); ...train...
E->>A: MSE=10.79, R²=0.795
U->>A: "可视化回归系数,x轴旋转45度"
A->>E: plt.xticks(feature_names, rotation=45); plt.show()
E->>A: TypeError: tick_params() got unexpected argument 'ticks'
A->>E: plt.xticks(feature_names, rotation=45, ha='right', fontsize=12); plt.show()
E->>A: [Figure 6464×480 渲染成功]
A->>U: 可视化完成,回归系数图已生成
这个例子体现了三个关键能力:
- 自主使用 pandas/sklearn/matplotlib(无需预定义工具)
- 读取报错(
$字符类型错误)并自主修复 - 读取 traceback(
tick_params参数错误)并自主修复
没有任何一步需要人工提示”用什么 API”或”如何修复这个错误”——模型依靠预训练积累的 Python 知识完成了所有推理。
CodeActInstruct:构建训练数据集
设计哲学
CodeActInstruct 是专为 CodeAct 设计的指令微调数据集,覆盖5个领域:
| 领域 | 能力 | 来源数据集 | 实例数 |
|---|---|---|---|
| 信息检索 | 通过 Wikipedia API 进行网络搜索 | HotpotQA | 3,000 |
| 软件包(工具) | 用 sympy 做数学运算 | MATH | 1,732 |
| 软件包(工具) | 自调试 Python 代码 | APPS(代码生成) | 647 |
| 外部记忆 | SQLite + Pandas 表格查询 | WikiTableQuestion | 1,065 |
| 机器人规划 | 通过 ALFWorld 的具身任务 | ALFWorld | 2,031 |
| 合计 | 7,139 |
轨迹生成流程
flowchart LR
DS["源数据集\n(HotpotQA, MATH, APPS,\nWikiTableQuestion, ALFWorld)"]
CON["转为多轮交互\n(MINT 框架)\n单轮 → 最多5轮 + 自调试"]
GEN["轨迹生成\ngpt-3.5-turbo-0613\nclaude-2\ngpt-4-0613(难题)"]
FILT["数据筛选\n1. 代码动作格式过滤\n2. 自我改进过滤\n3. 指令遵循过滤"]
FINAL["最终:7,139 条轨迹\n(6,728 条来自 gpt-3.5/claude\n411 条来自 gpt-4)"]
DS --> CON --> GEN --> FILT --> FINAL
步骤一 — 转为多轮: MINT 框架把单轮问题改写为多轮交互形式。对 APPS 代码生成任务,这意味着给模型最多5次机会通过测试用例,而非要求一次性正确解答。
步骤二 — 生成轨迹: 大多数问题使用 gpt-3.5-turbo-0613 和 claude-2(成本更低),仅对 gpt-3.5 无法解决的问题使用 gpt-4-0613。
步骤三 — 三重数据筛选(关键):
-
代码动作过滤: 排除模型不遵循代码格式的轨迹(要么 API 调用格式错误,要么生成了无法执行的动作)。
-
自我改进过滤: 保留模型先出错后修复的轨迹;排除全程失败的轨迹(对学习无用);排除从未遇到错误就成功的轨迹(不教调试能力)。
-
指令遵循过滤: 排除交互轮次为奇数的轨迹(说明模型没有正确遵循轮次交替格式)。
筛选后:来自 gpt-3.5 和 claude 的 6,728 条 + 来自 gpt-4 的 411 条。
与前作数据集的对比
CodeActInstruct 是 FireAct 的 3.8 倍、AgentInstruct 的 5 倍(轨迹数量),覆盖 5 个领域(FireAct 只覆盖 QA + 搜索 2 个领域)。关键差异化点:明确包含先失败后修复的多轮自改进数据。
CodeActAgent:微调开源 LLM
训练设置
作者对两个开源基础模型进行监督微调(SFT):
- LLaMA-2 7B(Touvron 等,2023)
- Mistral-7B(Jiang 等,2023)
训练数据:CodeActInstruct(7,139 条 Agent 轨迹)+ 通用对话数据(69,230 条,来自 OpenOrca、ShareGPT、CapyBara)。
训练基础设施:
- 4× A100-40GB SXM 节点
- 使用 Megatron-LLM fork(Cano 等,2023)
- 张量并行度:4
- 学习率:,50步 warmup,余弦衰减到
- 5个 epoch,batch size 32
- 序列长度:LLaMA-2 为 4,096,Mistral 为 16,384
- 使用第3个 epoch 的 checkpoint(实验最优)
- 仅对助手回复计算 loss(不包括用户/系统 token)
训练目标:
其中 是助手回复 token, 是上下文(用户指令 + 系统提示 + 历史交互)。
评估协议
| 任务类型 | 基准 | 域内/域外 |
|---|---|---|
| 代码动作(Agent) | MINT(子集) | 域内(ID) |
| 文本动作(Agent) | Miniwob++、ScienceWorld | 域外(OD) |
| 通用 LLM | MMLU、HumanEval、GSM8K、MTBench | 域外(OD) |
关键结果(表5)
xychart-beta
title "CodeActAgent Mistral-7B vs. 基准线(MINT 基准)"
x-axis ["Mistral Base", "Mistral Instruct", "AgentLM-7B", "FireAct-7B", "CodeActAgent", "gpt-3.5-0613", "gpt-4-0613"]
y-axis "MINT 得分(ID)" 0 --> 80
bar [0, 18.8, 0, 0, 57.4, 33.9, 68.6]
CodeActAgent(Mistral,7B)在 MINT ID 上达到 57.4——高于 gpt-3.5-turbo-0613(33.9),接近 gpt-4(68.6)。相比相同规模基准线 AgentLM-7B 和 FireAct-7B,提升约24分。
三个关键发现:
-
泛化到文本动作(域外)。 CodeActAgent(LLaMA-2)在 MiniWob++ 文本动作上达到 25.5,ScienceWorld 达到 17.6——与专门为文本动作微调的 AgentLM-7B(28.9 / 13.7)相当。说明 CodeAct 训练能够泛化到其他动作模态。
-
保持通用 LLM 能力。 在 MMLU(59.1)、HumanEval(34.7)、GSM8K(58.0)上,CodeActAgent(Mistral)与 Mistral Instruct 持平或更好。混合训练不会损害通用性能。
-
LLaMA-2 变体意外未能改进。 LLaMA-2 版 CodeActAgent 在大多数 Agent 任务上几乎零增益。作者认为 LLaMA-2 基础的指令遵循能力较弱,在复杂多步规划上微调 Agent 数据无法弥补基础模型的局限。
消融研究
哪些数据组件真正重要?
表 A.8 系统消融 CodeActAgent(Mistral):
| 模型 | MINT(ID) | MINT(OD) | Miniwob++ | SciWorld | GSM8K | 总体 |
|---|---|---|---|---|---|---|
| CodeActAgent(Mistral) | 57.4 | 32.4 | 46.2 | 15.9 | 58.0 | 46.8 |
| 去掉 CodeAct 数据 | 32.9 | 23.0 | 47.8 | 17.0 | 59.5 | 46.2 |
| 去掉通用对话数据 | 50.5 | 13.9 | 0.0 | 11.0 | 26.8 | 22.6 |
解读:
- 去掉 CodeAct 训练数据:MINT ID 下降约25分,证明 Agent 专项数据不可或缺。
- 去掉通用对话数据:Miniwob++(46.2 → 0.0)和 GSM8K(58.0 → 26.8)出现灾难性遗忘——模型学会了总是以代码动作格式回应,面对需要文本动作的场景时”忘记了”其他输出形式也是合法的。
这是一个重要的负面结果:不能只用 Agent 轨迹训练,必须混合通用数据,才能同时保持 Agent 能力和通用能力。这是指令微调中格式锁定(format lock-in)的典型案例。
混合数据的数学表达:
其中 (按样本数量)。但由于 Agent 轨迹更长(约1482 token/条 vs 通用数据约797 token/条),按 token 数量计算约占 15%。
与相关工作的对比
vs. Voyager(Wang 等,2023)
Voyager 在 Minecraft 中也用代码作为动作,但写的是函数定义(整体规划),而非命令式代码块。这禁止了原子动作的动态调整:在 CodeAct 中,每个 <execute> 块是一个小的、针对性的动作;在 Voyager 中,Agent 必须一次写出处理所有可能情况的完整函数。CodeAct 更灵活,且零样本工作。
vs. OpenCodeInterpreter(Zheng 等,2024)
并发工作,专注于竞争性编程的代码调试。对代码生成/调试任务有用,但不是通用 Agent 框架,没有跨领域的 CodeActInstruct 等效训练数据。
vs. TaskWeaver(Qiao 等,2023)
概念上最接近的工作——同样在动作空间中使用代码。但 TaskWeaver 依赖闭源模型的定性示例,没有严格定量基准、开源模型或训练数据。CodeAct 三者兼具。
局限性与边界条件
1. 需要 Python 能力
CodeAct 的优势与模型的 Python 能力成正比。对 Python 预训练数据少的小模型,CodeAct 可能不带来优势,甚至因语法错误而表现更差。存在一个 Python 能力门槛,低于该门槛时 JSON 可能是更好的选择。
2. 安全沙箱要求
CodeAct 直接执行任意 Python 代码。论文坦诚承认:
“CodeAct 直接授权 Agent 在沙箱环境中自由执行代码。在最坏情况下,此类 Agent 可能突破沙箱限制,通过网络攻击对现实世界造成危害。”
生产部署中 CodeAct 需要健壮的沙箱执行环境(Docker 容器配合网络限制、资源限制和文件系统隔离)。这是 JSON 系统无需面对的工程负担。
3. 包 API 幻觉
LLM 有时会自信地写出 import nonexistent_package 或调用 df.some_made_up_method()。纯文本 Agent 这样的错误会静默产生错误答案。CodeAct 中会产生 ImportError 或 AttributeError——对能力强的模型反而更好(可检测并修复),但弱模型可能陷入错误循环无法收敛。
4. 开源与闭源模型的巨大差距
在 M³ToolEval 上,最好的开源模型(Lemur-70B)达到 13.4%,最好的闭源模型(gpt-4-1106-preview)达到 74.4%。CodeAct 按比例缩小了这个差距,但无法消除它。多步推理的基础能力差距依然存在。
5. 上下文窗口瓶颈
论文对 M³ToolEval 使用10轮限制。真实世界任务可能需要更多轮次。LLaMA-2 的 4,096 token 限制在长轨迹中是实际瓶颈——轨迹越来越长时,早期轮次的内容可能被截断。
可复现性说明
论文提供了较强的可复现性支持:
- 代码和数据:
github.com/xingyaoww/code-act,包含所有训练代码、模型权重和评估脚本 - 模型: CodeActAgent(LLaMA-2 7B)和 CodeActAgent(Mistral 7B)公开发布
- 数据集: CodeActInstruct(7,139 条轨迹,10.5M token)公开发布
- 基准: M³ToolEval(82 个实例,4 个领域)可用于评估
- 训练细节: 附录 D 完整说明——优化器、学习率 schedule、batch size、硬件、序列长度
一个重要提示: 轨迹生成用的是 gpt-3.5-turbo-0613 和 claude-2,这些版本可能已经废弃或更新。用当前模型版本重新生成轨迹可能得到不同结果。
使用的是第3个 epoch 的 checkpoint(不是最终 checkpoint)。这意味着后续 epoch 出现了过拟合——如果你复现训练,需要注意这个细节。
我的思考与评价
这篇论文为什么重要
CodeAct 的重要性不在于提出了新架构或新训练算法,而在于证明了一个设计原则:当你已经在让 LLM 生成结构化文本时,让那个结构是通用编程语言,而不是任务特定的 Schema。
这个洞见的影响远超论文实验本身。如果代码是 LLM 动作的自然格式,那么:
- 工具定义变得不必要。 不需要为每个能力编写函数 Schema——提供 Python 包,让模型从文档或试错中发现 API。
- 错误处理内置其中。 Python 的异常系统是 Agent 自我改进的天然课程,无需人工整理错误映射。
- 任意计算成为一等公民。 排序、过滤、数学、字符串处理——任何可计算的东西都直接对 Agent 可用。
动作空间的设计哲学
论文隐式指出了动作空间表达力与可学习性之间的权衡:
- 太受限(原子 JSON 调用):易解析,难组合,需要很多轮次
- 太宽泛(无约束自然语言):难解析,灵活但脆弱
- 代码:富有表达力,可解析(解释器),可学习(预训练数据),可组合(数据流)
代码恰好是那个”黄金分割点”。这正是这一概念此后被 OpenHands、Claude Computer Use 以及 2024 年后几乎所有主流 Agent 框架采用的原因。
尚未解决的问题
-
最优代码粒度。 每个
<execute>块应该包含多少代码?论文建议”每次少做一点”,但块长度与轮次数量之间的最优权衡没有被系统研究。 -
非 Python 语言。 Bash、JavaScript、SQL 都是潜在替代方案。什么时候数据 Agent 应该优先用 SQL 而不是 Python?这没有被探讨。
-
训练混合比例的优化。 作者使用了特定的 CodeActInstruct 与通用对话数据比例,但最优比例(以及它是否依赖测试时的任务分布)没有被系统消融。
-
更长上下文需求。 随着任务复杂度增加,轨迹长度增长。LLaMA-2 的 4,096 token 限制在论文中已经是瓶颈。未来工作应探索轨迹压缩。
总结
| 维度 | 内容 |
|---|---|
| 核心思想 | 用可执行 Python 代码作为 Agent 动作空间 |
| 四大优势 | 控制流、数据流、现有包、自动错误反馈 |
| 基准测试 | API-Bank(原子调用)、M³ToolEval(多工具多轮) |
| 核心结果 | 成功率最高提升 20%;轮次减少约 30% |
| 数据集 | CodeActInstruct:5个领域 7,139 条轨迹 |
| 开源模型 | CodeActAgent(Mistral-7B):匹配百亿级闭源模型 |
| 主要局限 | 需要 Python 能力;生产部署需沙箱 |
| 发表 | ICML 2024 |
| 代码 | github.com/xingyaoww/code-act |
CodeAct 是一篇设计简洁、执行严谨的论文,从根本上改变了这个领域对 Agent 动作空间的思考方式。其影响力体现在它发布后所有主流 Agent 框架都采用了代码优先的动作设计——这是一篇论文”点中了某个根本正确的东西”的最清晰标志。
深入拆解:CodeAct Agent 循环算法
让我把 CodeAct Agent 循环用伪代码精确形式化,然后逐行解释每一步的含义和设计动机。
伪代码:CodeAct 多轮 Agent 循环
算法 1:CodeAct Agent 循环
输入:
instruction : str -- 用户的自然语言任务描述
tools : dict -- 对 Agent 可用的 Python 函数(可为空)
max_turns : int -- 最大交互轮次(默认:10)
llm : LLM -- 语言模型主体(如 Mistral-7B、gpt-4)
interpreter : PythonInterpreter -- 有状态 Python 执行环境
输出:
answer : str -- 用户指令的最终答案
1: context ← [system_prompt] + [("user", instruction)]
2: turn ← 0
3: while turn < max_turns:
4: response ← llm.generate(context)
5:
6: if "<execute>" in response:
7: code_block ← extract_between_tags(response, "<execute>", "</execute>")
8: observation ← interpreter.run(code_block) # stdout + stderr
9: context.append(("assistant", response))
10: context.append(("environment", observation))
11: turn ← turn + 1
12:
13: else if "Answer:" in response:
14: answer ← extract_answer(response)
15: return answer
16:
17: else:
18: # 纯自然语言回复(例如向用户请求澄清)
19: context.append(("assistant", response))
20: user_reply ← wait_for_user()
21: context.append(("user", user_reply))
22: turn ← turn + 1
23:
24: return None # 超过最大轮次仍未回答
逐行解释:
-
第1行: context 以系统提示(CodeAct 附录 E 的6行指令)和用户指令初始化。如果
tools非空,工具定义(Python 函数签名 + 文档字符串)也附加在系统提示末尾。 -
第4行: LLM 基于完整 context 进行自回归生成。这是标准的文本生成步骤。
-
第6-11行: 如果回复中包含
<execute>块,提取代码并传递给 Python 解释器。解释器是有状态的——第 轮定义的变量在第 轮仍然可用。观察结果(stdout + stderr)作为”environment”轮次追加到 context。 -
第13-15行: 如果回复包含”Answer:“,Agent 认为已有足够信息,直接返回答案。这是成功路径。
-
第17-22行: 既无
<execute>也无”Answer:“,Agent 在向用户请求澄清。在实际操作中这种情况少见——系统提示鼓励执行代码而非提问。 -
第24行: 超过 轮仍无答案,交互终止。成功率评估中这记为失败。
对比:JSON Agent 循环的伪代码
算法 2:JSON 工具调用 Agent 循环(对比用)
1: context ← [system_prompt + tool_schemas] + [("user", instruction)]
2: turn ← 0
3: while turn < max_turns:
4: response ← llm.generate(context)
5:
6: if "Action:" in response:
7: json_str ← extract_json(response)
8: tool_name ← json_str["tool"]
9: args ← json_str["args"]
10: if tool_name in registered_tools:
11: result ← registered_tools[tool_name](**args)
12: else:
13: result ← {"error": f"未知工具 {tool_name}"}
14: context.append(("assistant", response))
15: context.append(("observation", str(result)))
16: turn ← turn + 1
17:
18: else if "Answer:" in response:
19: return extract_answer(response)
20:
21: return None
关键差异对比:
| 维度 | CodeAct | JSON Agent |
|---|---|---|
| 工具分发 | Python 解释器直接执行 | 解析 JSON → 查找处理函数 → 调用 |
| 未知工具 | NameError: name 'xxx' is not defined(信息丰富) | {"error": "Unknown tool"} (信息贫乏) |
| 跨轮数据流 | Python 变量在解释器 session 内持久存在 | 无法跨轮保存中间结果 |
| 控制流 | for/if/while 原生支持 | 无法表达;需要多轮模拟 |
| 工具注册 | 不需要;import package 即可 | 需要显式注册所有工具 |
统计分析的深度理解
为什么要测 17 个模型?
测试 17 个模型不是随意的。论文想证明 CodeAct 的优势不是特定模型专属——它在闭源和开源、大和小、指令微调和基础模型之间都成立。表2和表3底部都有”最佳格式频率”行:
| 指标 | CodeAct | JSON | 文本 |
|---|---|---|---|
| 最佳格式(API-Bank,开源) | 5 | 0 | 1 |
| 最佳格式(API-Bank,闭源) | 4 | 5 | 0 |
| 最佳格式(M³ToolEval,开源) | 5 | 4 | 3 |
| 最佳格式(M³ToolEval,闭源) | 7 | 2 | 1 |
在更有意义的 M³ToolEval 上,CodeAct 在 17 个模型中的12个上是最佳格式——强有力的多数。
闭源模型 JSON 异常的解释
为什么 JSON 在 API-Bank 上对某些闭源模型有时胜过 CodeAct?因为 GPT-3.5、GPT-4 和 Claude 都经过了 JSON 函数调用专项微调——它们的训练数据包含 JSON Schema 格式的函数调用示例。对于单次原子调用(API-Bank),这种针对性微调提供了优势。但对于多工具任务(M³ToolEval),这种优势消失了——因为再多的 JSON 微调也无法给动作空间加上 for 循环。
这个分析做出了一个预测:随着开源模型获得更多指令微调(包括函数调用),闭源模型的 JSON 优势会缩小。到 2025-2026 年,这个预测已基本成真——大多数顶级开源模型都具备了显式的函数调用能力,而代码优先的 Agent(如 Claude Computer Use、OpenHands)已成为主流范式。
数据混合比例的影响
训练分布的形式化表达:
在论文设置中,按样本数量 ;按 token 数量约为 0.15(因为 Agent 轨迹更长)。
去掉 (即 )导致 Miniwob++ 得分从 46.2 崩溃到 0.0。这可以理解为分布外推断失败:模型在训练集中从未见过文本动作格式,因此无法在测试时产生它。
去掉 (即 )则导致 MINT ID 从 57.4 降至 32.9。这是任务分布偏移:通用对话数据中很少有多工具多轮 Agent 交互,模型没有学会在这种场景下系统地规划和调试。
论文没有对 进行系统消融——最优比例仍是开放问题。
CodeAct 的后续影响:2024 年后的 Agent 领域
主流框架的采纳
timeline
title CodeAct 对 Agent 框架的影响
2024-02 : CodeAct 论文发布(arXiv)
2024-06 : ICML 2024 正式接收
2024-07 : OpenHands(原 OpenDevin)采用 CodeAct 循环
2024-10 : Claude Computer Use(代码优先动作)
2025-01 : 主流 Agent 基准默认使用代码动作
2025-06 : 代码即动作成为所有主流框架的默认选择
OpenHands(原 OpenDevin,2024): 直接在 CodeAct 框架上构建,使用代码执行循环作为核心 Agent 循环。CodeAct 论文的第一作者(Xingyao Wang)也是 OpenHands 的贡献者。
Claude Computer Use(Anthropic,2024年10月): 以 bash 命令 + Python 代码作为计算机控制任务的主要动作语言。JSON 工具调用可用但处于次要地位。
LangChain / LlamaIndex(2024+): 两者都在 CodeAct 证明优越性后将 Python REPL 工具添加为一等动作类型。
安全问题仍未解决
尽管被广泛采用,基本安全问题仍然悬而未决。在生产环境中执行任意 LLM 生成的 Python 需要:
- 进程隔离(Docker 容器或虚拟机)
- 网络限制(除白名单端点外无出站 HTTP)
- 文件系统限制(不访问敏感路径)
- 资源限制(CPU 时间、内存、磁盘 I/O)
- 代码审计(执行前检测潜在恶意模式)
如果沙箱配置有误,精心设计的指令可能导致 Agent 执行 os.system("rm -rf /") 等破坏性操作。论文承认这一问题但未提供解决方案——截至 2026 年它仍是活跃研究领域。
论文图表的详细解读
图1(论文):核心动机示例
论文图1展示了 JSON vs CodeAct 确定在哪个国家购买智能手机最便宜的对比。JSON Agent 至少需要8轮(每个国家至少2次调用 × 4个国家),而 CodeAct 用一轮的 for 循环完成。关键标注:
- “控制和数据流简化了复杂操作” — for 循环一次处理所有国家
- “复用现有软件基础设施(Python 库)中的 ‘min’ 函数” —
min(final_prices, key=...)是 JSON Agent 无法使用的内置函数 - “所需动作更少!” — 对比说明
图2(论文):Agent 框架图
图2展示了通用多轮交互框架。关键组件:
- Agent: 接收观察、执行链式思维、发出动作
- 环境(计算机接口): 信息检索、软件包(工具)、外部记忆
- 环境(物理世界): 机器人控制
- 用户: 提供指令、接收自然语言回复
- 规划模块: 链式思维、自反思、从先前观察中学习
图2明确表明 CodeAct 是通用框架——同一循环适用于网页浏览(Python 中的 click_url)、数据库查询(Python 中的 sqlite3)、数学(Python 中的 sympy)和机器人控制(Python 中的机器人 API)。
图3(论文):CodeActAgent 实战
图3的数据分析会话展示了4个自调试循环:
- 价格列类型错误(含
$字符)→ 用str.replace('$','')修复 tick_params()参数错误 → 切换到xticks(rotation=...)修复
两个修复都纯粹来自阅读 traceback,没有人工提示”哪里出了问题”或”如何修复”。
这是 CodeAct 自调试能力的标志性演示:Python 的异常系统充当了自然课程,驱动 Agent 不断改进,直到代码正确运行。
最终评价
从一个 LLM 工程师的角度看,CodeAct 最让我信服的不是实验数字,而是它的论证结构:
- 识别出 JSON 动作空间的根本局限(无控制流、无数据流、无反馈)
- 提出一个自然的解决方案(用代码替换 JSON)
- 分解优势为可测试的研究问题(RQ1、RQ2、RQ3)
- 用覆盖17个模型的严格实验验证每个优势
- 构建开源产物(CodeActAgent、CodeActInstruct、M³ToolEval)让结论可复现
这是理想的实证论文写法。每个主张都有实验支撑,没有夸大结论,局限性也坦诚承认。
我认为这篇论文真正”对”的地方:它认识到 LLM 的能力不只是”生成文本”——LLM 已经是优秀的程序员。让它们以程序为动作,是对这种能力最自然的利用方式。与其为每个任务发明一种新的 JSON Schema(本质上是在发明一门编程语言的子集),不如直接使用 Python——那门 LLM 已经非常熟练的语言。
这个洞见看起来简单,但在 2024 年之前并没有被系统证明——这就是 CodeAct 的价值所在。