Agent 的瓶颈不在模型:程序化工具调用让 312 次工具调用只花 2 次请求
Agent 的瓶颈不在模型:程序化工具调用让 312 次工具调用只花 2 次请求
原文:Factory Research - 《Droid 1.0: Building a Frontier Harness》(https://factory.com/news/frontier-harness)
同一批工具调用,一种写法要 10 次模型请求、56.8 秒,另一种只要 5 次请求、41.6 秒。换成 312 次调用的长任务,差距拉大到 36 次请求对 2 次请求,310 万输入 token 对 6.1 万。
模型没换,提示词也没改。变的是 harness 里一行设计决定:工具由模型一个一个调,还是由模型写一段程序去调。
Factory 在 10 月 29 日发布 Droid 1.0 时把这套机制讲透了。原文里关于程序化工具调用(Programmatic Tool Calling,下文简称 PTC)的部分,对所有自己写 Agent 的人都有直接价值——大多数人写的循环,还停在"一次模型请求只干一两件事"的版本上。
一、先说清楚 harness 是什么
原文的定义很朴素:harness 就是模型之外的一切,包括提示词、工具、工具结果怎么回传,以及把这一切跑起来的循环。
这个定义值得停一下。很多 Agent 项目里真正写业务逻辑的代码不多,剩下几百行全是 harness:拼 system prompt、注册工具、把返回值塞回 messages、判断该不该继续下一轮。模型是别家的,你能优化的只有这堆胶水。
Agent 越跑越长,胶水的代价就越明显。原文说,现在 Agent 会连续工作几小时甚至几天、跨数百个轮次,每一轮都要重新发送指令、工具和完整历史。那个 while 循环每一圈都在为一个越来越长的请求付钱,同时还在等一次可能只换来一个文件读取的模型响应。
PTC 要动的就是这个循环的形状。
二、两种循环的差别
标准循环是:模型决定调一个工具,你执行它,把结果 append 进上下文,再请求模型。工具输出直接进入模型视野。
PTC 换了个路子:让 Agent 写一段程序,由这段程序去调用工具。
原文的机制描述是,Agent 可以提前推理、把整套工具调用流程写成代码,在一次调用里把整个程序跑完。比如写一个流程读上百个文件、找出提到某个主题的那几个;或者分诊几十个 Linear 工单;或者按确定性步骤分片执行一次复杂的数据库迁移。
这个思路不算新。原文引了 CodeAct(Wang et al., 2024),指出用代码作为动作载体在 17 个模型上都优于 JSON 和文本形式的工具调用;Cloudflare 和 Anthropic 在 2025 年底把它带进生产,Codex 今年把它带到了编码 Agent 上。
Factory 把 PTC 设成默认模式,给了三条理由,第三条最容易被忽略:
- 上下文干净。Agent 只收到程序打印出来的东西,原始文件、搜索结果、日志留在程序里当变量。
- 模型请求变少。一段程序跑很多调用,复杂任务因此更快更便宜。
- Agent 更可靠。检查和循环在代码里确定性地跑,消除了模型偷懒漏掉某个案例的可能——它原本计划要做的,代码会真的做完。
第三条不是省钱问题,是正确性问题。自然语言驱动的循环里,"把这一百个文件都看一遍"永远是模型的一次承诺;写成 for 循环之后,它变成了一次可以验证的事实。
三、Script 工具:沙箱、并发与输出上限
Droid 把 PTC 实现成一个叫 Script 的工具。模型写 JavaScript,跑在一个没有文件、网络、系统访问权限的沙箱里。
原文给了四条工程约束,每一条都值得抄进自己的实现:
Script 能在一次模型请求里发起数百次工具调用,所以指令和历史只发一次,而不是每几次调用就重发。这直接决定了 token 成本的天花板——请求数从几十降到个位数,重复发送的固定开销自然被摊薄。
打印输出有大小上限,完整结果记到日志里,模型之后可以自己去搜。这一条是在防"程序把一万行日志 print 给模型"的翻车现场。
彼此不依赖的调用并发执行,一批的耗时等于其中最慢的那次。这就解释了后文那 15.2 秒是怎么省出来的。
注意沙箱边界。没有文件、网络、系统访问,意味着所有实际能力都必须经由注册的工具完成,程序只是编排者,不是执行者。这个设计把"给模型一个能跑代码的执行环境"这个高风险动作,收敛成"给模型一个只能调工具的调度器",风险面小得多。
四、一组可复盘的时序数据
原文做了一次双路回放:同一个失败单测修复任务,GPT-6.1 Sol 高推理档,同一块秒表,分别用直接调用和 Script 工具跑。
直接调用用了 10 次模型调用加 15 次工具调用,耗时 56.8 秒。Script 工具用了 5 次模型调用和同样 15 次工具调用,耗时 41.6 秒,提前 15.2 秒完成。
拆开看更有意思。直接调用版本里,模型调用耗时依次是 8.52、3.27、4.04、3.42、3.55、3.57、3.42、4.73、5.49、2.33 秒,每次之间夹着一到两次工具调用,每调完一批就得等模型读完结果才能决定下一批。Script 版本里,模型调用是 8.64、5.35、3.36、8.42、2.27 秒,每次调用写出一段程序,程序在里面把一批工具调用做完,再统一返回结果。
模型侧总耗时从 42.3 秒降到 28.0 秒。工具调用的总时长其实没变,变的只是它们被几次请求带出去。
所以 PTC 省的不是工具执行时间,省的是"为了拿到下一个工具调用指令而必须走的那一次模型往返"。工具越多、越彼此无依赖,省得越多。
五、长任务上差距才真正拉开
原文给了两个来自真实会话的 Script,已简化、改名,很值得逐行看。
第一个是 Linear 清理:151 个工单在两个队列之间搬家,每次搬完都要验证,一共 312 次工具调用。
直接调用版本,每个请求重发指令和不断增长的历史,即便按每个请求 10 次调用算,也要 36 个请求、读进约 310 万输入 token。同一个任务写成 Script,只用了 2 个请求、约 6.1 万 token——一个请求写程序,一个请求读它打印的结果。
第二个是 CI 巡检:查了 547 次运行的 5384 个桌面任务,550 次工具调用,每次并发 60 个,2 个请求完成;按直接调用估算要 59 到 187 个请求。模型最终只看到一个五元组摘要,里面包含 41 个失败任务,不到 0.1% 的工具结果进入了上下文。这次巡检在后台跑了大约 8 分钟,期间模型还在处理别的任务。
原文那段 Script 的结构大致如下,我改写成伪代码,保留关键控制流:
// 1. 待处理工单 id 读成集合,后续过滤都在内存里做,不花 token
const remaining = new Set(readReport('<report>').notAttempted); // 151 个
// 2. 两个队列并发拉取,不排队
const queues = await Promise.all([listAll('Triage'), listAll('<queue 2>')]);
// 3. 过滤也在程序里完成:只处理交集
const targets = queues.flat().filter((i) => remaining.has(i.id));
const verified = [];
const failures = [];
// 4. 每批 3 个,改完立刻回读校验
for (const batch of chunks(targets, 3)) {
await Promise.allSettled(batch.map((i) => setStatus(i, 'Backlog')));
const after = await Promise.allSettled(batch.map((i) => getIssue(i)));
after.forEach((r, n) => {
// 比对 15 个字段,只有 status 变了才算通过
(onlyStatusChanged(batch[n], r.value) ? verified : failures).push(batch[n].id);
});
if (failures.length) break; // 出现意外变更立刻停
if (verified.length % 30 === 0) text(`${verified.length} of 151 verified`);
}
writeResult('<result file>', { verified, failures });
text({ verified: verified.length, failures }); // 只有这一行回到模型上下文
逐段说明。
readReport 读的是上一轮报告,转成 Set 之后,判断某个工单是否已处理是 O(1) 的哈希查找。同样这个判断,在直接调用模式下每次都要花一次模型请求去想。
Promise.all 并发拉两个队列,对应原文那句"彼此不依赖的调用并发执行,一批的耗时等于最慢的一次"。
chunks 分批是必要的。并发太高会被下游限流,太低则退化成串行。原文这例选 3,CI 巡检那例选 60,取决于下游接口的承受能力。
用 Promise.allSettled 而不是 Promise.all:一次失败不该让整批结果作废,allSettled 会把成功和失败都返回,后面的校验才有完整数据。
onlyStatusChanged 比对 15 个字段,这是整个脚本的质检核心。判定标准写在代码里,意味着 151 次校验完全一致,不会因为模型在第 100 次时偷懒而放松。
writeResult 把明细写文件,text 只把统计交给模型。这就是原文说的"原始结果留在程序里"——模型需要细节时再去搜那份文件。
六、把 PTC 做对,难在细节
原文有句话很实在:程序化工具调用加起来简单,做对很难。它列了五条设计选择:
- 程序拿到的是真实数据。整份文件、结构化带类型的搜索结果,过滤和计数在代码里做,不花 token。
- 字面文本原样传输。文件内容、补丁、命令走原始输入块,不做字符串转义,token 更少,畸形调用也更少。
- 编辑结果保持简短。程序里的编辑操作返回的摘要和直接编辑一样短,文件不会被回贴进上下文。
- 永远不被长任务卡住。程序最多等 5 分钟,然后把已有输出交回来,也可以开在后台立即返回。
- 不做轮询。有专用工具负责查看、等待、停止运行中的程序;后台任务完成时主动唤醒模型,不花请求去问"做完了吗"。
第二条最容易被忽略。用 JSON 传文件内容时,换行、引号、反斜杠都要转义,文件越大 token 越膨胀,模型写出的调用也越容易在转义上出错。把字面文本放进原始输入块,是为了避开这层无谓损耗。
第五条是很多人自己实现时会漏的。程序跑 5 分钟、模型干等,等于把省下的往返又赔回去。正确做法是让程序进后台,用事件唤醒模型,而不是让模型循环问"好了吗"。
七、哪些模型默认开了
原文列出 Script 模式默认开启的模型:GPT-6.1 Sol、GPT-6.1 Terra、GPT-6 Astra、GPT-6 Sol、GPT-5.6 Sol、Claude Fable 5.1、Claude Fable 5、Claude Opus 5.5、Claude Opus 5、Kimi K3。
这个名单本身是个信号。PTC 不是"模型越差越需要",恰恰是能力强的模型才能把程序写得足够复杂、在单次请求里做更多事。原文的判断是,前沿模型已经能从 PTC 里拿到最多收益,而且随着前沿推进,收益会继续扩大。
原文还提到,在 Claude Opus 5.5 上,Script 模式相对直接调用把成本降低了 15%。这个数字不大,原因也写清楚了:大部分 Script 很小,省下的钱集中在 12.5% 那些发起六次以上工具调用的 Script 上。
如果你的 Agent 每个任务只调两三次工具,PTC 收益几乎为零;只有当任务变成"翻一百个文件"“巡检几千个 job”"批量改几百条记录"时,它才开始赚钱。判断要不要上 PTC,看你的 P99 工具调用次数就够了。
八、顺手学到的两个 harness 细节
原文后半段还有两块跟 PTC 无关,但同样是"模型之外的一切",对做性能优化的人很有参考价值。
第一块是提示词缓存。厂商按前缀缓存请求,前缀一变,后面全部重算。Droid 的请求本来已经是只追加的,唯一例外是工具发现:工具定义位于请求靠前的位置,会话中途加载一个 MCP 或连接器工具,整段对话就要重新处理一遍。改成把新发现的工具追加到请求末尾之后,缓存前缀不受影响。测试里加载一个新工具,改动前是 7.35 万 token 重新处理、2.8 万走缓存,改动后是 1500 个新 token、10 万走缓存。原文披露的生产数据是,典型请求 98% 的输入来自缓存,98.6% 的请求严格只追加。
第二块是基础提示词瘦身。从 Droid 0.170.0 到 0.250.0,用户输入 hi 时的首轮请求从 20887 个输入 token 降到 13055,减少 37%。省下来的大头在工具定义:原来一次发 11 个,现在发 2 个,其余按需加载;工具定义减 46%,启动提醒减 32%,反倒是系统提示词涨了 33%,因为新增了"怎么跟用户说话"和"怎么处理图片、已连接应用、个人数据"这两块指引。
这个配比变化挺说明问题。模型之外那部分代码,值得优化的不是"写得更少",而是"什么时候发、发多少、发到哪个位置"。
九、想在自己项目里试一次
最小可行做法,不需要换框架:
- 先量现状。统计每个任务的模型请求数和工具调用数,算出平均每个请求带几次工具调用。低于 3 基本没收益。
- 挑一个工具密集的任务试水,比如批量检索加过滤、批量状态变更加校验。这类任务的步骤在动手之前就是确定的,正好适合写成程序。
- 把 Script 工具做成一个只有工具调用能力、没有文件和网络访问的执行器。输入是模型写的代码,输出是有限的打印内容,完整结果落到文件。
- 给输出加硬上限,并提供"去日志里搜"的工具。别指望模型自己控制 print 的规模。
- 长任务走后台加唤醒,不要让模型轮询,轮询会把省下的请求又花回去。
- 用同一批任务跑双路对比,比请求数、输入 token、端到端耗时三个数,而不是只看最终成功率。
这里提醒一点。原文的 benchmark 数字来自厂家自己的测试环境,Terminal-Bench 4、指定版本号、三次试验取中位数,方法论写得很清楚,但它毕竟是发布方数据。要判断在自己场景里有没有用,还得自己跑一遍对照。
同一个方向,本月还有两条同源新闻:Microsoft 在 10 月 28 日通过 Foundry 向开发者开放了 GitHub 团队打造的 Copilot harness,LangChain 在 10 月 27 日开源了自研设计 Agent 的 harness。三家在同一个星期把 harness 单独拿出来讲,说明这一层的竞争已经开始了。
推荐标签 程序化工具调用 AgentHarness 上下文工程 AI智能体 FactoryDroid
更多推荐



所有评论(0)