Code Mode 什么时候更省:别只数工具调用,要数模型往返

判断 Code Mode 是否更省,最容易犯的错误,是盯着"工具调用了多少次"。
同样读取 10 个文件,可以是模型发起 10 次串行决策,也可以是模型先写一个有界程序,让程序完成 10 次读取、筛选和聚合,再把一份压缩结果交还模型。两种方案的工具调用数都可能是 10,但模型往返、重复上下文和中间输出完全不同。
所以真正应该优化的不是 Promise.all 这个语法,而是完整任务中的模型轮次、上下文回放、结果体积与失败边界。
一、先区分两条执行回路

直接工具调用的典型循环是:模型选择一个工具,观察结果,再决定下一步。它的优势不是"简单",而是每一步都可以利用新信息重新判断。搜索方向要不要改变、是否需要审批、写入前是否应停下来、工具的原生引用或文件能力是否必须保留,这些任务天然需要直接调用。
程序化工具调用则把一段可预测工作交给代码:一次模型请求生成 JavaScript,在隔离的 V8 Runtime 中完成多次调用、循环、条件判断、过滤、排序、去重或聚合,只把较小的结构化结果返回给模型。
它省下的不是工具本身,而是工具之间不必要的模型往返。

证据图 1:OpenAI Programmatic Tool Calling 文档按任务形状区分两种模式。可预测、可聚合并能返回更小结构化结果的阶段适合程序化调用;需要新判断、审批或保留原生能力时,直接调用更合适。
这里还要划清一个产品边界:OpenAI API 的 Programmatic Tool Calling 是公开能力说明;Codex 产品内部如何组合模型、执行环境、Responses Lite 与其他工具路径,是另一层实现。本文只借官方文档解释能力边界,不把 API 文档等同于 Codex 全部后端。
二、成本要按模型往返记账

可以先用一个简化式建立直觉:
text
任务成本 ≈ 模型轮次 × 每轮重复上下文
+ 工具结果输入
+ 模型输出
缓存输入比未缓存输入便宜,但不是免费。按当前 Codex Rate Card,GPT-5.6 Sol 的缓存输入为 12.5 Credits / 1M Token。假设 100k 缓存上下文被重复带入 30 次模型请求,仅这一项就是 3M cached input,也就是 37.5 Credits。这个例子是作者换算,不是官方对单任务成本的预测。
更关键的是,长上下文经常还会携带工具结果、计划、日志和历史决策。串行工具调用越多,模型越可能重复接收同一批背景信息。程序化阶段如果能在运行时先裁剪结果,只回传模型真正需要的字段,节省会同时来自"更少轮次"和"更小结果"。
三、一条长线程说明了什么,又不能说明什么

GitHub Issue #32503 给出了一条长 Codex Desktop 线程的追踪:GPT-5.6 Sol 产生 739 个 exec cells,其中只有 5 个使用 Promise.all。报告同时观察到每个含工具回合的模型请求数约高 5.3 倍、每回合总 Token 约高 7.9 倍,约 94% 的输入 Token 被报告为缓存。

它足以支持一个机制判断:如果多个已知的独立读取被拆成"模型一次、工具一次、再回模型"的串行链,重复上下文会放大任务成本。
但它不能证明一个普遍倍率。该追踪混合了不同模型、上下文窗口、推理强度和工具路径;5.3 倍与 7.9 倍不能直接推广到所有仓库、所有套餐或所有任务。正确用法是把它当作排查线索,而不是产品承诺或普遍 Bug 定论。
四、受控样本支持"条件收益",不支持"全部批处理"

Issue #35050 在两个无关代码库上做了同模型对照。重复 High/XHigh 样本中,显式有界批处理让加权使用分别下降约 45% 和 27%;一个 Max 配对下降 47.4%,但作者明确说明单个配对需要复现。
这组数据比跨模型长线程更接近可比较实验,但样本仍然只覆盖两类只读仓库任务,不能推广成"Code Mode 固定节省 27%---45%"。更重要的反例是:一个过度激进的提示变体反而多消耗 26.8% 加权 Credits。

为什么批得更多反而可能更贵?
- 为了凑批次扩大调查范围,读取了原本不需要的数据;
- 把输出很大的操作塞进一轮,结果压缩失败;
- 一个子调用失败导致整批重跑,回滚成本高于串行;
- 把写入、审批或共享状态操作并发化,正确性成本上升;
- 模型为了设计复杂批处理程序,产生了更多推理与输出。
因此,"能并发"不是决策条件,"已知、独立、只读、结果可控、失败边界清楚"才是。
五、一个可落地的任务形状矩阵

适合程序化调用
- 多个已知且独立的数据源;
- 只读操作,不共享可变状态;
- 结果可过滤、聚合、去重或校验;
- 返回给模型的内容明显小于原始结果;
- 失败可以定位到单个子调用,并能局部重试。
适合直接调用或串行执行
- 下一步需要模型基于新结果重新做语义判断;如果依赖关系可预测、后续参数可由代码推导且失败边界明确,仍可留在程序化阶段;
- 搜索需要模型动态改变关键词或来源;
- 涉及写入、审批、权限和外部状态;
- 需要工具原生引用、文件或交互产物;
- 失败会改变后续策略,不能机械继续。
真实任务通常不是二选一。更稳妥的结构是"分阶段混合":模型先判断阶段目标;阶段内对已知只读操作做有界批处理;阶段结束后把压缩结果交回模型,再决定下一个阶段。
六、不要用调用数证明优化,要做完整任务 A/B

评估前还必须固定同一模型、推理档位、仓库快照、任务、验收标准和工具权限,并执行多次重复。之后至少同时记录四组指标:
- 质量:结论、引用、边界和最终验收是否一致;
- 资源:未缓存输入、缓存输入、输出和加权 Credits;
- 编排:模型轮次、工具调用、程序单元、失败与重试;
- 延迟:首次有效结果、总时长、P50 与 P95。
作者建议先用 6---8 个互相独立的只读调用做一个有界试验,并把 20% 的资源改善当作值得保留的观察阈值之一;这只是工程起点,不是官方门槛。若质量下降、失败重跑增加或输出无法裁剪,即使工具调用看起来更"并发",也不算优化。
七、批次太大、等待太久,同样会放大成本
程序化调用不是"批得越多越省"。至少有七条常见放大路径:批次太小导致频繁回模型;每个中间阶段都返回模型;结果没有在代码层压缩;批次过大触发限速、截断或整批重试;依赖任务被提前并发;慢工具等待期间反复唤醒模型;开放式搜索一次铺开过多宽泛查询。
Issue #33402 报告过外层 exec 聚合结果截断。它只是一个社区个案,不能说明所有 Code Mode 都存在同样问题,却足以提醒 Harness:每个工具和每个批次都要有 max_bytes、max_rows、max_items,不能把完整日志、网页或大文件无条件汇总给模型。
缓存也要分开看"命中率"和"总处理量"。94% 缓存命中可能与很高的累计缓存输入同时成立;命中率高只能说明重复前缀获得折扣,不能说明重复轮次已经消失。
八、Promise.all 不是目标,失败语义才是
Promise.all 中任一子调用拒绝,整个 Promise 就会拒绝。如果 Runtime 或提示随后重跑整个批次,已经成功的读取也可能重复。对于可以局部恢复的只读任务,更稳妥的基线通常是:
javascript
const results = await Promise.allSettled(
batch.map((item) => safeToolCall(item))
);
const failed = results
.map((result, index) => ({ result, index }))
.filter(({ result }) => result.status === "rejected");
并发还要有上限。每批 6---8 项只是工程起点,真实数值取决于工具限速、结果大小、资源竞争和失败率。读操作通常易于重试;写操作必须额外解决共享状态、幂等、事务、锁和回滚。真正的优化目标是:在不牺牲正确性与可恢复性的前提下,减少无价值模型往返和重复上下文。
九、用七问判断串行、并行还是分阶段
| 问题 | 若答案为"是" | 若答案为"否" |
|---|---|---|
| 调用之间有数据依赖吗? | 串行,或由代码推导确定依赖 | 继续判断 |
| 会写入共享状态吗? | 串行、加锁或事务 | 继续判断 |
| 需要逐项审批吗? | 串行 | 继续判断 |
| 单项失败会改变整体策略吗? | 串行或小批次 | 继续判断 |
| 输出可控并能本地压缩吗? | 可考虑并行 | 限制批次与返回体 |
| 外部服务能承受并发吗? | 可考虑并行 | 降低并发 |
| 所有调用是否预先已知? | 可批处理 | 自适应分轮 |
这也给出三类清晰边界:已知、独立、只读、可压缩的调用适合程序化批处理;测试、多仓库扫描、文档检索等任务只在局部条件满足时适合;数据库事务、删除、发布、付款、权限变更、自适应调试和开放式搜索不得盲目并行。
十、搜索和工具等待需要专门的 Harness
网络搜索的对象集合会被上一轮证据改变,不能照搬文件批处理。更稳妥的流程是:第一轮只发起 2---4 个高价值查询,抽取来源、日期、结论和证据等级;识别缺口与冲突后,第二轮只查缺口;连续没有新增证据时停止。Harness 同时限制每轮查询数和页面数,优先官方源,做 URL 去重,并避免把整页原文直接塞入主上下文。
工具等待则要同时处理三层:
- Runtime:使用异步完成事件,不因"仍在等待"反复唤醒模型;完成后一次聚合,只重试失败项;
- Harness:给慢工具设置超时与结果预算,用 DAG 表达依赖,超过等待预算时交付部分结果或降级;
- 提示:明确"工具未返回前不要重复请求同一资源,只在存在独立子任务时继续"。
自然语言提示不能代替 Runtime 约束,但可以减少不必要的中间响应。
十一、十步落地策略
- 先画依赖图,不按工具数量直接决定并行;
- 只对独立、只读、无审批、输出可控的调用批处理;
- 小批开始,按限速、大小和失败率调整;
- 使用
Promise.allSettled,局部失败局部重试; - 在代码层过滤、去重、计数和抽样;
- 给每项设置
max_bytes、max_rows或max_items; - 不把完整日志、网页和大文件直接返回模型;
- 写入保持串行,或使用显式锁、事务与幂等键;
- 搜索按证据缺口分轮;
- 单批失败率超过 20%、输出接近上限或出现重复读取时,停止并重新规划。
十二、完整 A/B 与四层根因
质量指标至少包括验收通过率、漏项、错误和人工返工;资源指标包括总 Credits、未缓存输入、缓存输入、输出、模型轮次、工具调用和重试;编排指标包括批次数、平均批大小、并发度、失败率、重复读取和截断;延迟指标包括总墙钟时间、工具等待时间和模型时间。还要按独立只读、依赖读取、写入、测试、网络搜索、多代理和高风险操作分组,不能把不同任务混成一个平均数。
如果结果异常,不要只怪模型。根因可能同时位于四层:模型策略可能批次偏小、重复验证或停止条件弱;系统提示可能鼓励全面工作却不给预算;Runtime 可能频繁生成中间响应、截断聚合或错误重试;Harness 可能没有去重、依赖图、输出上限和 P95/P99 熔断。公开证据只确认了部分现象,不能虚构每一层的贡献比例。
常见误解也应一并排除:Code Mode 不是任意代码执行;启用它不保证减少模型轮次;Promise.all 数量不是效率指标;网络搜索不能一次并行完;社区 Issue 提供的是机制线索和有限对照,不是总体因果估计。
最终结论很简单:先压缩不必要的模型往返,再考虑并发语法。Promise.all 是手段,任务形状、失败语义、结果预算与完整成本才是控制面。