第 36 章:Tool Result 预算与外溢

"大的截一下不就行了"——截断之后模型会再读一遍。外溢到磁盘留个指针,它就不用了。


⏱️ 快速通道(5 分钟掌握核心)

  1. 截断是销毁,外溢是搬家。要同时留下预览可取回的指针
  2. 两个预算:单结果一个,单 Turn 一个——并行批次撑爆的是第二个
  3. 替换必须跨 Turn 持久化,否则你会永远重复外溢同一个结果
  4. 有些工具自己设界、被豁免;总量是尽力而为的目标,不是天花板
  5. 数 rune,不要数字节,否则预览会把一个字符切成两半

10 分钟路径:36.1-36.3 → 36.5 → Kocoro OSS Lab


公开源码快照:实现细节取自 Kocoro 公开仓库 origin/main4ec6772 提交,复核日期为 2026-07-27。章节讲的是不变量;具体常量应以当前源码为准。

36.1 从四个比答案还贵的 grep 说起

一个并行批次里发出四个 grep。单看没有一个过分。每个回来六万字符的匹配。

grep("handleRequest")       61,204 字符
grep("func New")            58,910 字符
grep("TODO")                74,332 字符
grep("import")              66,180 字符
                             ─────────────
                             260,626 字符  74K token

单独看,没有一个吓人。加起来是 200K 窗口的三分之一,在一轮里到齐,而模型回答这个问题大概只会从里面用到二十行。

第一反应是截断——每个结果封顶一万字符,收工。这招只灵一次。模型读到被截断的结果,没找到它要的东西,于是用更窄的 pattern 再 grep 一次。现在你付了两遍钱,内容还是没拿到。

截断销毁的是模型可能还需要的信息,外溢只是把它搬走。 本章的一切都建立在这个区别上。

36.2 预览 + 指针

机制很简单:一个结果超过阈值,就把完整内容写进 Runtime 管理的文件,并把 Prompt 里的内容替换成两样东西——开头的一段预览,以及一个模型之后可以去读的路径。

公开 Runtime 里,阈值是 50,000 字符,预览是开头 2,000 个 rune。于是模型仍然看得见它拿到的是什么东西、里面大概有什么;够用就往下走。不够用,就对外溢路径调一次 file_read 把剩下的取回来——而那个时刻,模型已经知道自己在找什么了。

最后这半句才是真正的收益。取回发生在模型把问题收窄之后。 你把「以防万一先扛着 26 万字符」换成了「现在只带 8K,需要什么之后精确去取」。

注意预览是按 rune 数,不是字节数算的。在 UTF-8 字符串的第 2000 个字节上切一刀,很可能切在一个字符中间,模型看到的是乱码;而在中日韩文本里,按字节数还会让你实际拿到的预览只有预期的三分之一左右。

36.3 被并行执行撑爆的那个预算

只有单结果阈值是不够的,36.1 就是原因。那四个 grep 每一个都不到 50,000,没有一个触发单结果外溢。这一轮照样是 26 万字符。

所以还有第二个预算:单 Turn 总量,快照里是 200,000 字符。批次跑完之后,Runtime 从最大的可外溢结果开始外溢,直到总量落回目标之下——从大到小,因为这样能用最少的外溢操作腾出最多的预算。

这里还有一条下限:在总量削减过程中,小于 5,000 字符的一律不外溢。低于这个体量,外溢的开销——一次文件写、Prompt 里多一个路径、之后大概率还要一次 file_read——比你腾出来的内容还贵。

总量是尽力而为的目标,不是硬天花板。 一轮完全可能正当地停在它之上:有些工具声明自己不限大小,在挑选外溢对象时会被跳过;而一次失败的外溢会把内容留在原地,而不是把它丢掉。超出目标应当被当作值得排查的信号,而不是一个你的代码可以假定不会发生的状态。

36.4 那个看起来像 bug 的豁免

file_read 被豁免于外溢。乍看不对,直到你看见它换了什么做法:它在工具内部自己封顶在 500,000 rune,并留下明确的截断标记。

这里的理由值得抽出来。给 file_read 做外溢,等于把一个文件的内容写进另一个文件,然后递给模型一个路径——而模型本来就有路径,就是它刚读的那个。你除了加一层间接和一次白跑的往返,什么也没做成。

所以规则可以推广:一个本来就返回可取回引用的工具,不需要外溢,它需要的是自我设界。 外溢是给那些「输出在别处不存在」的工具用的。读取类工具,按定义就有来源。

这也是为什么这个豁免是工具自己声明的属性,而不是分发器某处一份名单上的名字。只有工具知道自己的输出可不可恢复。

36.5 替换必须活得比这一轮长

这是最容易被忽略、而做错代价很大的部分。

你在第 12 轮外溢了一个结果。第 13 轮,对话历史被重建后送给模型。如果这次重建用的是原始工具结果,而不是外溢后的替换,那六万字符就原样回到 Prompt 里——然后你再外溢一次,每一轮都外溢一次,永远。

修法是:把替换做成持久化状态,而不是一次按轮次的变换。记录哪些结果被替换过、被替换成了什么,并且跨 Checkpoint 以及终态保存一起带着,这样重建历史时重构出的是外溢版本,而不是原件。

这份状态里的 Seen 那一半和替换表一样重要:它区分「这个结果从没被外溢过」和「这个结果被外溢过,替身在这里」。没有它,一条缺失的记录就是歧义的,而这里的歧义意味着要么重复外溢,要么悄悄丢内容。

任何没有被持久化的 Prompt 整形操作,都会每一轮重做一遍。 这对外溢成立,对压缩成立,对第 37 章里的每一种重写都成立。

36.6 快照证据

观察4ec6772 源码位置
单结果外溢阈值 50,000 字符spill.go L15
上下文内预览 2,000 runespill.go L17
单 Turn 总量目标 200,000 字符spill.go L22
总量削减时的最小外溢体量 5,000spill.go L26
挑选外溢对象时跳过不限大小的工具spill.go L75
Rune 安全的预览切片spill.go L55
替换状态同时携带 Seentoolresult_budget.go L19

以上描述的是一个有日期的实现快照,不是普遍契约。

36.7 什么时候不该外溢

外溢有一项被阈值掩盖的成本:它把「模型已经拿到的内容」变成了「模型必须开口要的内容」。每一次外溢都是一次潜在的额外往返。

所以别外溢小结果——那条下限存在正是为此。别外溢模型必定要看全文的结果;如果下一步明摆着就是「总结这份文档」,搬走它只是多加一次取回。也别在内容本身就是答案而不是证据时外溢:一个输出就是用户所要之物的工具,应该把东西送到用户手里,而不是一份预览加一个路径。

还有一个常被跳过的生命周期问题。外溢文件会堆积。它们是 session 级的,需要清理,而且它们装着工具返回的任何东西——很可能正是你的权限模型在努力控制的那些数据。一个外溢目录就是工具输出的一份明文副本,躺在磁盘上。 给它划范围、设权限、并且删掉。

36.8 常见的坑

用截断代替外溢。 模型会换个更窄的 query 重跑工具。你付两遍钱,还丢了原件。

只做单结果预算。 四个 60K 的结果每一个都能通过单结果检查,照样撑爆这一轮。36.1 就是这个 bug。

替换不持久化。 每一轮重复外溢,永远。而且症状看起来像是莫名其妙的反复变慢,不像一个 bug。

按字节切预览。 切坏多字节字符,并且悄悄让中日韩用户只拿到三分之一长度的预览。

给自我设界的工具做外溢。 纯粹的间接,没有收益——那份内容本来就有地址。

把总量当硬上限。 豁免工具和失败的外溢都会正当地超过它。假定不会超过的代码,会在生产上吃惊。

Kocoro OSS Lab(10 分钟上手)

  1. spill.go,找到总量削减那一趟是怎么挑候选的。注意它按体量排序、并跳过不限大小的工具——两个决定,两个都是承重的。
  2. 找到 minAggregateSpillSize 这个常量,想清楚把它设成 0 会坏在哪里。
  3. 在你自己的 Agent 里:一个并行工具批次结束后,把各结果大小加起来。如果这个数在你的上下文窗口里占了可观的比例,你就有 36.1 那个问题,而单结果上限救不了你。

划重点

  1. 截断是销毁,外溢是搬家。 留下预览和指针,让取回始终可能。
  2. 单结果和单 Turn 都要有预算。 并行批次能通过每一个单结果检查,然后照样溢出。
  3. 持久化替换。 一个没被持久化的 Prompt 整形操作,会每一轮重做一遍。
  4. 自我设界的工具不需要外溢。 输出已经有地址,搬家买不到任何东西。
  5. 总量是目标,不是天花板。 豁免工具和失败的外溢会正当地超过它。

下一章:第 37 章处理那些留在 Prompt 里的结果——按年龄降级,而不是搬出去。

引用本文 / Cite
Zhang, Wayland (2026). 第 36 章:Tool Result 预算与外溢. In AI Agent 架构:从单体到企业级多智能体. https://waylandz.com/ai-agent-book/%E7%AC%AC36%E7%AB%A0-Tool-Result%E9%A2%84%E7%AE%97%E4%B8%8E%E5%A4%96%E6%BA%A2/
@incollection{zhang2026aiagent_36_-Tool-Result,
  author = {Zhang, Wayland},
  title = {第 36 章:Tool Result 预算与外溢},
  booktitle = {AI Agent 架构:从单体到企业级多智能体},
  year = {2026},
  url = {https://waylandz.com/ai-agent-book/%E7%AC%AC36%E7%AB%A0-Tool-Result%E9%A2%84%E7%AE%97%E4%B8%8E%E5%A4%96%E6%BA%A2/}
}