现在和我交谈的每个人对 code mode 都有自己的看法。我的看法则是由 Prime Intellect 的 Prime Agent点燃的,这是一个围绕持久化 REPL 构建的编码工具框架,此外还有 Alex Zhang 的 推测性程序化工具调用,它(除其他功能外)能在模型仍在流式输出代码时运行工具。 Cloudflare 就有一个。 Anthropic 也有一个。我们自己也有过几个,从 MCP 代码执行 到 Pydantic Monty编码智能体也在朝同一方向演进: OpenCode 2 现在默认通过 Code Mode 发送 MCP 工具,而 Pi 则自带一个 codemode 工具。常见的宣传话术大致是这样的:别再让模型一次调用一个工具,而是让它写一个小程序来调用这些工具,这样一切都会更便宜、更快、更好。
我信这一点。但我见过的几乎每个数字都来自拥有数百个工具或庞大负载的智能体,而现实中的许多智能体其实很普通:几个 CRUD 工具,外加一项待办任务。所以我正是基于这种情况构建了一个基准测试。五个小型 Pydantic AI 应用、仅限单项工具,以及三类工作:日常请求、长时间多步骤任务,还有五项任务,我可以把条目数从 5 调到 200。八个模型,代码模式关闭和开启,在内部基准测试上共进行了 5,276 次评分运行。
简而言之:任务越长,收益越大。而且收益会变得很大。
TL;DR
- 在长任务上,代码模式大致将成本和实际耗时减半。 在全部八个模型上:成本 -58%,实际耗时 -51%,成功率 +16.0 个百分点在没有代码模式的情况下,288 次运行中有 67 次失败;启用后,失败次数为 21 次。
- 它可以扩展。 对于 gpt-6.1-sol,在没有代码模式时,每增加 100 个条目,每次运行会多花 30 到 79 秒,成本增加 0.04 到 0.07 美元。启用代码模式后:在五个任务中的四个上,耗时为 4 到 9 秒,其中三个任务的成本不到一美分。在 200 个条目时,代码模式便宜了 47% 到 75%。
- 没有它,正确率会下降。 从 5 个条目到 200 个条目,没有代码模式时的成功率从 92-100% 下降到 62-79%。启用代码模式后,在 200 个条目时成功率仍为 92-100%。
- 这与往返次数无关。 模型已经在批处理:一次响应携带了 200 次工具调用。在 200 个条目时,对八个模型取平均,每个任务的模型请求减少了 1% 到 22%;deepseek-v4.1-flash 单独来看下降得更多。代码模式削减的是进入 每个请求的内容。 每个请求。
- 小型任务反而要付出代价。 在 5 个条目的任务上,代码模式在五个任务中的三个上成本更高(+21% 到 +38%)。在 50 个日常任务上,它总体便宜了 16%,但成功率持平(+1.6 个百分点,属于噪声)。
- 它运行在沙箱中,并且会留下痕迹。 在 Pydantic AI Harness 中,模型生成的代码在 Monty 中运行,Monty 一开始无法访问宿主机,并且默认会在代码片段执行 30 秒后或堆内存达到 256 MiB 时将其停止。借助 Pydantic Logfire 对智能体进行追踪, 代码发起的每一次工具调用都会作为独立的 span 显示出来。
我的解读是:模型更擅长编写循环,而不是充当循环本身,而且这种差距会随着任务长度的增加而扩大。
循环,以及它为何有害
在常规的工具调用智能体中,模型选择一个工具,输出一些 JSON,然后停下,等待运行框架执行该工具,读取结果,再重新选择。模型处在每一步的中间,成本会不断累积:工具定义挤占上下文,每个中间结果都要流经模型,而且按通常的说法,每次工具调用都是又一次模型回合,会重新发送整个不断增长的上下文。
代码模式则反其道而行之。工具变成沙箱内的函数,模型编写一个程序,在其中对工具进行循环和分支,只有结果会返回。在 Pydantic AI Harness 中,该沙箱就是 Monty,这是我们用 Rust 编写的 Python 解释器,模型看到的是一个单一的 run_code 工具。
以下是 50 个任务中的一个,逐字引用: “能不能把我购物车里所有单价超过 50 美元的东西都删掉?这已经失控了”. 购物车有 122 行,一个列表工具每页最多返回 50 条,还有一个删除工具每次接收一个 id。需要删除 22 行。没有代码模式的话,就是三页列表加 22 次删除,每一次都由模型来选择。有了代码模式,就是这样:
import asyncio
items, offset = [], 0
while offset is not None:
page = await list_items(offset=offset, limit=50)
items += page["items"]
offset = page["next_offset"]
pricey = [i for i in items if i["unit_price"] > 50]
await asyncio.gather(*(remove_item(item_id=i["id"]) for i in pricey))
len(pricey)
模型从不读取那 100 行便宜的商品。它也不必记住自己已经删除了哪些 id。比较 > 50 由 Python 完成,而不是由语言模型眯着眼看浮点数。
带护栏的代码模式
那个程序是模型编写的代码,必须有东西来运行它。这个“东西”允许代码接触什么,是代码模式中最大的设计决策。
Prime Agent 做出了开放的选择。其模型的唯一工具是一个持久的 IPython 内核,而运行框架的其余部分(包括子智能体)都以函数的形式存在于该内核中。IPython 内核是一个完整的 Python 进程:除非周围有东西加以限制,否则代码可以导入任何库、读写文件并打开网络连接。对于你自己机器上的编码助手来说,这正是你想要的。但我不会把它放进 Web 服务器,让每个用户的提示词来驱动它。
Pydantic AI Harness 做出了相反的选择。 CodeMode 在 Monty中运行模型的程序,这是我们用 Rust 编写的 Python 解释器,它从零开始,只添加你允许的内容:
- 默认无主机访问权限。 沙箱代码 初始没有任何访问权限 访问主机的文件、环境或时钟,并且没有网络。如果智能体需要一个目录、一个环境变量或时间,你就按请求授予它。这个基准测试授予了内置的
OSAccess处理器:一个内存中的文件系统和环境,以及主机时钟。 - 你的工具就是通往外界的出口。 程序通过你注册的函数,以及你授予它的任何操作系统访问权限来接触世界,每次工具调用都经由 Pydantic AI 到达你的真实工具。它只能做这些所允许的事情,别的什么都做不了。
- 硬性限制。 默认情况下,每个
run_code代码片段获得 30 秒的执行时间和 256 MiB 的堆内存(详情),这正是阻止失控循环的机制。 - 启动快。 每个 Monty 会话都在本地工作进程中运行,而从运行中的池里获取一个 新沙箱 耗时不到一毫秒,因此这些护栏不会为每个代码片段增加容器启动开销。
所以你得到的是带护栏的代码模式。本次基准测试中的每次代码模式运行都在 Monty 中执行。
代码即图
Samuel 为此写了一整篇文章, 代码就是图, and if you build agents you should read it. His argument is that graph libraries rebuild control flow badly: "if 是一个分支。 for 是一个循环。函数调用是一条边", and asyncio.gather 是一次扇出和一次汇合。或者用他关于一个五行示例的话来说:“扇出、汇合、分支。这就是一张图,同时它也是一个 Python 函数。”
上面的程序正是如此。下面是它所编码的图:
我写那个程序是为了配图,但在基准测试中,模型在运行时、针对一个它们从未见过的任务,自己写出了同样的形状(在一次购物车运行中,gpt-6.1-sol 把它分散到了三次 run_code 调用中)。没有人事先画出那张图。这正是让我认为代码模式是智能体发展方向的原因:模型不只是更便宜地调用你的工具,它还在即时设计工作流。
但这是一篇带有基准测试的博客文章,所以让我们来看看其中有多少是真的。
基准测试
这个基准测试是刻意设计得简单的。五个小型 Pydantic AI 应用:一个购物车、一个日历、一个聊天室、一个看板和一个库存系统。每个应用只有针对单个条目的 CRUD 工具(list、get、create、update、delete),列表带分页。没有 shell,没有文件系统,没有批量端点。如果智能体想改动 200 个东西,它就得发起 200 次调用,或者写一个循环。
这里没有任何单独一步是难的。难的是把一个简单的步骤做上几百次,顺序正确,还不数错,而这正是我想单独隔离出来的部分:长程迭代。代码就是图。让你的智能体来写这张图。
在这些应用之上,有三层工作:
- 日常任务: 50 个测试,每个应用十个。单条编辑、批量修改、带分支的规则、查找与求和。不使用代码模式时,一次运行大约发起 13 次工具调用。
- 长程任务: 12 个多步骤任务,涉及 60 到 500 条记录,每个应用两个,另有两个跨应用任务。 “Alex 要离开团队了。按 id 顺序处理 Alex 的未完成任务(未完成的):如果任务是紧急或高优先级,就把它交给当时未完成点数最少的人……” 每次运行约 68 次工具调用。
- 扩展性: 每个应用一个任务,在 N = 5、20、50、100 和 200 个条目时应用相同规则。 “我有一堆从未回复的会议邀请。对每一个:如果是在工作日且下午 5 点前结束,就接受;如果是在周末或下午 5 点后结束,就拒绝。” 在 N = 200 时,不使用代码模式的运行平均发起 168 次工具调用。
每个应用都是一个 Pydantic AI 智能体。开启代码模式后,它会获得 Pydantic AI Harness 的 CodeMode 的能力, 即时执行(eager execution),它可以在模型写完整个 run_code 调用之前就开始运行一条完整的语句。关闭代码模式时,同一个智能体直接调用同样的工具。所有内容都固定到同一个提交,代码模式通过一个设置项切换,因此两组之间唯一的差别就是代码模式本身。提示词按照人们实际打字的方式编写,每次运行都由一个确定性的判定器评分:应用的最终状态必须与预期状态完全一致(金额精确到分),回复中必须包含正确的数字。不使用 LLM 评判。
下文所有内容都是代码模式开启与代码模式关闭的对比。
任务越长,差距越大
先从扩展性这一档开始,因为它是整个基准测试中最干净的实验:同一个应用、同一条规则、同一个提示词,唯一变化的是规则涉及的条目数量。下面是 gpt-6.1-sol 在全部五个任务上的表现,上方是实际耗时,下方是成本:
关闭代码模式时,两条线都随每个条目上升。这就是工具调用 :每个条目都是模型必须编写的一次调用,每个结果都是它必须读回的内容。对于 gpt-6.1-sol,每多 100 个条目,就会增加 30 到 79 秒和每次运行 $0.04 到 $0.07。:每个条目都是模型必须编写的一次调用,每个结果都是它必须读回的内容。对于 gpt-6.1-sol,每多 100 个条目,就会增加 30 到 79 秒和每次运行 $0.04 到 $0.07。
在代码模式下,五条线中有四条接近持平。完成一个包含 200 张卡片的冲刺,不使用代码模式耗时 166.6 秒,使用代码模式耗时 26.6 秒。每增加 100 张卡片,不使用代码模式会增加 79 秒,使用代码模式则增加 4 秒。
每多 100 个条目,gpt-6.1-sol 在每个任务上的成本增加:
| 任务 | 代码模式关闭 | 开启代码模式 |
|---|---|---|
| 购物车 | +66 秒,+$0.065 | +9 秒,+$0.008 |
| 日历 | +54 秒,+$0.068 | +6 秒,+$0.002 |
| 聊天 | +30 秒,+$0.039 | +6 秒,+$0.016 |
| 看板 | +79 秒,+$0.067 | +4 秒,+$0.006 |
| 库存 | +63 秒,+$0.061 | +29 秒,+$0.028 |
库存是例外,而且这个例外是合理的:交付内容会以 N 行列表的形式粘贴到提示词中,因此无论是否启用代码模式,模型都必须把全部内容读完。
在全部八个模型中,以下是代码模式开始显现优势的地方:
在 5 个条目时,代码模式在五个任务中的三个上成本更高,高出 21% 到 38%。它的指引会随每个请求一起发送,而五次调用要折叠进一个循环也不算多。到 50 个条目时,它在五个任务中的四个上更便宜;到 200 个条目时,它在每个任务上节省 47% 到 75% 的成本和 42% 到 67% 的实际耗时。
不过,请看底部面板:即使达到 200 个条目,模型请求平均也几乎不变,仅下降 1% 至 22%。详情见下文。
接下来是正确性:
在没有代码模式的情况下,成功率会随着 N 的增大而下滑:从 5 个条目时的 92-100% 降到 200 个条目时的 62-79%。使用代码模式时,最终成功率为 92-100%。在所有规模上,不使用代码模式的 600 次运行中有 518 次正确,使用代码模式的 600 次运行中有 577 次正确。按规模来看,样本量很小(每组 24 次运行),25 个差距中只有 5 个单独来看置信区间不包含零,但五个任务的方向都一致:在 200 个条目时,代码模式在每个任务上都领先。
最弱的模型表现得最明显。在 200 个条目时,gpt-6-luna 答对了 在 200 个条目时,gpt-6-luna 答对了 在没有代码模式的情况下运行正确,而其中 7 次它根本没有改动任何东西。它只是解释了为什么做不到:
我找到了 200 张卡片,标注为“
sprint-42,但我还没有改动它们:看板工具要求逐张更新每张卡片,而卡片列表太大,无法在这里安全地一次性处理。
在使用代码模式时,同一个模型在这 15 次运行中答对了 8 次。这不是崩溃、超时或上下文溢出:没有代码模式的运行中都没有出现这些情况。这是一个模型面对 200 次调用,却拒绝执行它们。
有些模型在使用代码模式时从未出错。在全部五个任务中,muse-spark-1.3-contributor、muse-wonder-1.0-contributor 和 deepseek-v4.1-flash 在代码模式下的 225 次运行全部正确,而不使用代码模式时为 225 次中 202 次正确。
| 模型 | 正确运行数,关闭 → 开启 | 200 个条目时的实际耗时 |
|---|---|---|
| muse-spark-1.3-contributor | -28% 到 -50%muse-wonder-1.0-contributor75 → 75 of 75-41% 到 -61%deepseek-v4.1-flash62 → 75 of 75-58% 到 -87%muse-spark-1.3-contributor 十次失败中有八次源于同一个习惯:它正确地分诊了聊天,然后把摘要作为新消息发布到 #support,而提示词并未要求这样做。按评分标准,这些运行是错误的。忽略已发布的消息,它在没有代码模式的情况下 15 次聊天运行中答对了 14 次。使用代码模式时,它从未发布过一条。200 个条目下的一个任务其中一次运行,近距离观察:gpt-6.1-s | -28% 到 -50% |
| muse-wonder-1.0-contributor | 75 → 75,共 75 | -41% 到 -61% |
| deepseek-v4.1-flash | -58% 到 -87%muse-spark-1.3-contributor 十次失败中有八次源于同一个习惯:它正确地分诊了聊天,然后把摘要作为新消息发布到 #support,而提示词并未要求这样做。按评分标准,这些运行是错误的。忽略已发布的消息,它在没有代码模式的情况下 15 次聊天运行中答对了 14 次。使用代码模式时,它从未发布过一条。200 个条目下的一个任务其中一次运行,近距离观察:gpt-6.1-sol 回答 200 个邀请,两组均评分正确。没有代码模式时,它进行了 205 次工具调用,pac | -58% 到 -87% |
muse-spark-1.3-contributor 的十次失败中有八次源于同一个习惯:它正确地分诊了聊天,然后把摘要作为新消息发布到 #support,而提示词并未要求这样做。按评分标准,这些运行是错误的。忽略已发布的消息,它在没有代码模式的情况下 15 次聊天运行中答对了 14 次。使用代码模式时,它从未发布过一条。
200 个条目下的一个任务
其中一次运行,近距离观察:gpt-6.1-sol 回答了 200 个邀请,两组均评分正确。
没有代码模式时,它进行了 205 次工具调用,打包在 7 个响应中,单个响应最多包含 43 次调用。批处理本来就已经做得很好。问题在于每个请求所携带的内容:到目前为止的对话,包括每一个工具结果。到最后,它读取了 183,723 个输入 token,写出了 4,430 个输出 token 来拼出这 205 次调用和答案,并花了两分钟。
使用代码模式时,它编写了 5 个片段,日历服务处理了 215 次调用,模型读取了 34,461 个 token,写入了 573 个。实际耗时下降了 76%,成本下降了 84%。请求数从 8 次减少到 6 次。
模型已经会批处理了
我一开始以为“移除 22 个条目”意味着在没有代码模式的情况下进行 22 次往返。事实并非如此。现代模型会输出 并行工具调用而且它们会发出大量调用。在长任务层级中,最繁忙的单次响应携带了来自 gpt-6.1-sol 的 77 次调用,以及来自两个 Claude 模型的 119 次调用。在 200 个条目时,claude-haiku-5-5 将全部 200 次调用放在一个响应中。
所以往返次数并不像宣传中所说的那样下降,至少对批处理最激进的模型来说并非如此。在长任务层级中,模型请求数下降了 22%,但这取决于具体模型。GPT 模型的请求数减少了三分之一(-33%),而 Claude 模型的请求数则 ,claude-haiku-5-5 上升 24%,claude-sonnet-5-5 上升 26%。,claude-haiku-5-5 上升 24%,claude-sonnet-5-5 上升 26%。没有代码模式时,它们会一次性发出一个巨大的突发;有了代码模式,它们每次运行大约写五个短片段,并在写下一个之前读取每个结果。它们的成本仍然低 67% 和 70%。批处理最少的模型获益最多:muse-spark-1.3-contributor、muse-wonder-1.0-contributor 和 deepseek-v4.1-flash 在单个响应中最多发出 25 到 50 次调用,在没有代码模式时每次运行发出 13 到 22 次请求,有了代码模式则少 27% 到 52%(muse-spark-1.3-contributor 的下降在噪声范围内)。
这就是我会强调常见宣传说法的地方:每次工具调用都要多消耗一轮模型。对于当前模型上的独立调用,这大多并不成立。仍然要消耗一轮的是“ 步骤:在列出邀请函所在的页面之前,你无法回复邀请。代码模式消除的是每一轮中随工作量增长的部分:模型必须写出的调用,以及它必须读回的结果。 步骤:在列出邀请函所在的页面之前,你无法回复邀请。代码模式消除的是每一轮中随工作量增长的部分:模型必须写出的调用,以及它必须读回的结果。
例外的是 deepseek-v4.1-flash。它确实会批处理,一次响应最多 80 次调用,但在 200 个条目、没有代码模式的情况下,它每次运行平均发出 64 次请求(最高 171 次),读取了 280 万输入 token。使用代码模式时:11 次请求和 86,000 token。回答 200 个邀请,在没有代码模式时每次运行耗时 1,070 秒,有代码模式时耗时 138 秒。它运行在一台我们无法控制负载的 vLLM 服务器上,所以它的秒数只能粗略看待;请求数和 token 数不依赖于服务器。
长周期任务:更便宜、更快,而且正确率更高
长层级任务是这里最接近真实智能体工作的东西:十几步、相互作用的规则,还有几百条记录要保持清晰。 “供应商价格更新以及一些日常事务,按此顺序:1. 将每个紧固件和每个电气产品的单位成本提高 6%……”
在每个模型上,代码模式的正确率至少不低,而且模型越弱,提升幅度越大:gpt-6-luna 从 14% 提升到 58%,claude-haiku-5-5 从 64% 提升到 94%,deepseek-v4.1-flash 从 81% 提升到 100%。gpt-6.1-sol 和 claude-sonnet-5-5 也达到了 100%,而 muse-wonder-1.0-contributor 在两种模式下都没有出错。 58%,claude-haiku-5-5 从 64% 提升到 94%,deepseek-v4.1-flash 从 81% 提升到 100%。 100%gpt-6.1-sol 和 claude-sonnet-5-5 也达到了 100%,而 muse-wonder-1.0-contributor 在两种模式下都没有出错。
而且它在每个模型上都做到了这一点,同时降低了成本和实际耗时,每个区间都明显不为零:
以美元和每次运行的秒数计:
| 模型 | 每次运行成本 | 实际耗时 |
|---|---|---|
| gpt-6.1-sol | $0.074 → $0.033 (-55%) | 75.0 → 28.2 秒(-62%) |
| gpt-6-luna | $0.0029 → $0.0021 (-30%) | 48.5 → 38.2 秒(-21%) |
| gpt-6-astra | $0.365 → $0.149 (-59%) | 72.8 → 28.4 秒(-61%) |
| claude-haiku-5-5 | $0.0127 → $0.0042 (-67%) | 55.2 → 23.0 秒(-58%) |
| claude-sonnet-5-5 | $0.176 → $0.053 (-70%) | 56.7 → 21.6 秒(-62%) |
| muse-wonder-1.0-contributor | $0.245 → $0.110 (-55%) | 137.0 → 84.1 秒(-39%) |
| muse-wonder-1.0-contributor | $0.470 → $0.207 (-56%) | 57.8 → 29.1 秒(-50%) |
| deepseek-v4.1-flash | $0.027 → $0.0072 (-73%) | 311.9 → 136.4 秒(-56%) |
| 所有模型 | -58% | -51% |
在没有代码模式时出现的失败,与我在日常任务中看到的是同一类,只是数量更多:对数百条记录进行算术运算和记账。在供应商价格测试中,不使用代码模式的 24 次运行中有 16 次失败,而使用代码模式的 24 次运行中只有 1 次失败。其中六次运行来自三个不同的模型,它们报告的库存价值为 $1,671,683.13。正确答案是 $1,671,682.13,而它们自己的最终状态价值恰好就是这个数。编辑没有问题;回复中的求和结果有问题。每一次给出数值的代码模式运行都给出了 $1,671,682.13。
统计长任务层级中的每一次失败,不使用代码模式时:52 次最终状态错误,11 次最终状态正确但回复错误,以及 4 次运行没有产生任何变化(均为 gpt-6-luna)。使用代码模式时:分别为 16 次、5 次和 0 次。
代币都花在哪里了
代币的分布情况正是成本差异的原因:
在八个模型上,输出 token 下降了 66%,从 gpt-6-luna 的 -43% 到 claude-sonnet-5-5 的 -80%。输出是缓慢而昂贵的部分,也是模型在写出每次调用和每个中间值时消耗的部分。输入 token 也下降了 53%,因为堆积在上下文中的结果变少了。
在短任务上,输入侧的情况则相反。代码模式的引导(如何 run_code (沙箱中可用的内容)会让每个请求增加约 2,600 个字符,这是一笔按请求收取的固定费用,无论任务多小都要支付。在日常任务上,输入上升了 29%,而输出下降了 37%。在扩展层级中,当条目数为 5 时,总 token 上升了 65% 至 114%。这笔固定费用构成了大部分额外开销,而在长任务中,它会消融在节省之中。
日常任务:更便宜,正确率大致相当
这 50 个日常测试都很短:每个只有四五个请求和十几次工具调用。这正是我起步的地方,也是代码模式优势最不明显的地方。
| 模型(4 次重复,3,200 次运行) | 成功率 | 每次运行成本 |
|---|---|---|
| gpt-6.1-sol | 96% → 98%(噪声) | -9% |
| gpt-6-luna | 66% → 85% | +19% |
| gpt-6-astra | 94% → 98% | -7%(噪声) |
| claude-haiku-5-5 | 96% → 96%(噪声) | -40% |
| claude-sonnet-5-5 | 97% → 95%(噪声) | -38% |
| muse-spark-1.3-contributor | 98% → 92% (更差) | -13% |
| muse-spark-1.3-contributor | 98% → 94%(噪声) | -8%(噪声) |
| deepseek-v4.1-flash | 96% → 95%(噪声) | -34% |
| 所有模型 | +1.6 分(噪声) | -16% |
在全部八个模型中,代码模式便宜了 16%、快了 11%,正确率则大致相当。平均值掩盖了一种分化。大多数模型在没有代码模式时就已经有 94% 到 98% 的正确率,并且保持在这一水平。gpt-6-luna 是个例外,而且它的大部分提升都体现在问题上。在第一批测试中,有十二个测试要求给出一个数字(“如果乳制品打九折,我的购物车总价会是多少?”),gpt-6-luna 在没有代码模式时答对了其中 38%,有代码模式时答对了 88%。没有代码模式时,这些模型在脑子里把 30 到 120 行数字加起来,结果算错了,或者在三页中的两页之后就停下了,或者干脆拒绝回答: “购物车里有这么多行,我没法可靠地算出总价。” 反方向上,muse-spark-1.3-contributor 在代码模式下答对的频率反而更低,而且每一次失误都让应用陷入了错误状态。
按任务类型拆分后,可以看出成本落在哪里:
聚合类任务(涉及多行的查询)正是代码模式大放异彩之处:正确率 90% 到 98%,成本降低 27%,速度提升 26%。接下来是对照组,五个测试都只涉及单一操作(“请把酸面团改成 2”)。在这些测试中,代码模式成本高出 8%、耗时多出 16%,因为模型要为引导信息付费,还要写一个程序来发起一次调用。批量编辑也更便宜,但正确率较低(95% 降到 90%),而这一下降主要来自 Muse 系列模型。每个类别只有 5 到 10 个测试,样本太少,无法计算置信区间,因此这些柱状图只能看作方向性参考。
代码模式在哪些情况下代价更高
这份诚实的清单,因为确实存在这样一份:
- 小任务。 在 5 个条目时,代码模式在五个扩展任务中的三个上成本更高;到 20 个条目时,它在三个任务上更便宜,在一个任务上持平。聊天分诊最后才出现反转:在 5 条和 20 条消息时它成本更高,直到 100 条时才变得更便宜。要从其他消息中分辨出退款请求,就必须阅读文本,因此使用代码模式时,这些消息仍然要经过模型。
- 写出的代码有 bug 的小模型。 gpt-6-luna 在日常任务上多付了 19%,有 0.56 次失败
run_code每次运行产生的片段,而每一次失败都意味着又一次往返。按每次正确运行计算,它仍然更便宜:0.00121 美元对 0.00130 美元。在库存扩展任务上,它的成本增长 更快 在代码模式下,15 次运行中有 9 次正确,而不用代码模式时只有 3 次。 - 把数据打印出来的模型。 muse-spark-1.3-contributor 的代码模式会把大量数据打印进对话,因此在 200 个条目时,其 token 仅下降 5% 至 64%。而 muse-wonder-1.0-contributor 和 deepseek-v4.1-flash 则下降了 57% 至 99%。
- 重试可能会重复写入。 一个中途失败并被重跑的片段可能会把它的前半部分执行两次。gpt-6-luna 在长任务层级中的三次失败都重复创建了相同的记录。
- 它仍然可能出错。 在日常任务上,muse-spark-1.3-contributor 在代码模式下有 92% 的正确率,不使用代码模式时则为 98%,这是唯一一个置信区间跨过零点的下降。它的大多数失误都是批量编辑,导致应用处于错误状态。在扩展层级中,有两个单元格朝错误方向变化,且都在噪声范围内。在 N = 20 的日历邀请任务中(92% → 83%),gpt-6-luna 留下了未回复的邀请,而 claude-haiku-5-5 则把自己的工作数错了。在 N = 5 的库存任务中(100% → 92%),claude-haiku-5-5 有两次在回复中漏掉了一个必需的数字。
因此,一旦数据或任务规模超出普通工具调用能妥善处理的范围,代码模式就开始体现其价值。在这些应用上,对大多数任务而言,这个临界点大约在 20 到 50 个条目之间。
那么,为什么说它代表着未来?
因为这条曲线是真实的,而且它只朝一个方向走。代码模式按固定费用计费:每次请求大约两千个字符的指导。工具调用按条目计费:每次调用都要写出来,每个结果都要读回去,每个累计值都要保存在模型的“脑子”里。在单次编辑上,固定费用会吃亏。但到了几十个条目,它就开始占优,而到 200 个条目时,差距已经很明显。我预计智能体任务会越来越长。我没有测量过这一点,但如果真是这样,越来越多的任务会落在这条曲线的正确一侧。
这些取舍是真实存在的,值得认真对待:
- 你正在运行由 LLM 生成的代码。 是的,这正是沙箱与模型同样重要的原因。生成的代码只能触及你交给它的东西,并且它在时间和内存限制内运行。你的工具就是你交给它的东西,它们以你应用的权限运行,因此它们仍然需要自己的验证和授权。 Samuel 的 Monty 文章 解释了为什么我们要构建一个从零开始的解释器,而不是锁定一个完整的 Python。
- 它更难调试。 是的,这一点也没错。失败的程序会把中间值藏在沙箱里,而正如 gpt-6-luna 所展示的,重跑片段可能会重复它的写入。你现在确实必须读代码,而不只是看 JSON。有帮助的一点是,程序的调用仍然通过 Pydantic AI 走到你真实的工具,所以借助 Logfire 或任何为智能体提供插桩的 OpenTelemetry 后端,每次
run_code调用是一个 span,而它内部的每一次工具调用 都会在它下面拥有自己的 span。 在它下面。 - 你可能根本不需要它。 没错,我们的小样本量结果也支持这一点。那里的答案是减少代码模式,而不是完全不用。Pydantic AI 的
CodeMode采用了 工具选择器这样你就可以把被循环遍历的工具放进沙箱,其余的工具则保持为普通的工具调用。
代码模式也正是模型的发展方向:写代码似乎是它们进步最快的技能。我猜这是因为代码是可检验的,因此很容易用来训练。无论如何,给模型一门编程语言来驱动你的工具,正好发挥了进步最快的那项优势。
它还必须能安全运行。上面的每一个结果都来自沙箱内的代码模式,该沙箱只能访问这些应用的工具、一个内存中的文件系统和环境以及时钟,除此之外别无他物:没有宿主机文件,也没有网络。这正是我们构建 Monty 所针对的配置。
下一步是把同样的思路再往上提一层。在 动态工作流中,沙箱里的函数是子代理而不是工具,而模型编写的程序就是编排。Samuel 的图由模型在运行时编写,代理作为节点。这就是我所说的未来。
试一试
在你自己的 Pydantic AI 智能体中,code mode 是 Pydantic AI Harness 提供的一项能力:
uv add "pydantic-ai-harness[codemode]" "pydantic-ai-slim[openai]"
export OPENAI_API_KEY="your-api-key"
然后给智能体提供它的工具:
from pydantic_ai import Agent
from pydantic_ai_harness import CodeMode
CART = {
1: {'name': 'Espresso machine', 'unit_price': 249.00},
2: {'name': 'Oat milk', 'unit_price': 3.49},
3: {'name': 'Cast iron pan', 'unit_price': 64.95},
}
agent = Agent(
'openai:gpt-6.1-sol',
instructions='You help the user manage their shopping cart. Prices are in US dollars.',
capabilities=[CodeMode()],
)
@agent.tool_plain
def list_items(offset: int = 0, limit: int = 20) -> dict:
"""List cart items in id order, one page at a time."""
ids = sorted(CART)[offset : offset + limit]
next_offset = offset + limit if offset + limit < len(CART) else None
return {'items': [{'id': i, **CART[i]} for i in ids], 'next_offset': next_offset}
@agent.tool_plain
def remove_item(item_id: int) -> str:
"""Remove one item from the cart."""
return f'removed {CART.pop(item_id)["name"]}'
result = agent.run_sync(
'can you get rid of everything in my cart that costs more than $50 a piece?'
)
print(result.output)
模型只看到一个工具, run_code,并调用 list_items 以及 remove_item 作为其中的异步函数。那段代码在 Monty 中运行,因此这两个函数就是它能触及的全部。基准测试用 eager=True。 代码模式文档 涵盖 eager 和 推测性执行、工具选择、资源限制等等。
那推测性执行呢?
Pydantic AI Harness 的 CodeMode 还提供 推测性执行:就是本文开头提到的推测性程序化工具调用,它会在模型仍在编写 run_code 调用。这些应用的工具都没有为它声明,所以推测执行没有提前启动的依据。在第一个日常批次中,它与普通代码模式的区别仅在于更长的指引,而这一点也体现了出来:成功率提升 +7.7 个百分点而非 +10.3,成本增加 +4% 而非 -2%。后续批次跳过了它。这不是对推测执行的测试,所以我不会从中得出结论。
感谢阅读!
致谢
- Samuel Colvin 感谢你为图表论证提供的帮助,也感谢你推动我去寻找更大的差异。
- Maverick(@maverick-jackson),其代码模式研究确立了本基准所遵循的 A/B 方法。
- Ruben,感谢你发现我的成功图表第一版把“更正确”的方向指反了。
在 Google 搜索中查看更多来自 Pydantic 的内容
将 Pydantic 添加为首选来源,以便更容易找到我们的最新文章。
在 Google 上选择 Pydantic 作为首选来源(在新标签页中打开)准备好看看你的智能体实际在做什么了吗?
免费开始 联系我们 联系我们探索我们的开源软件包