第 38 章:Deferred Tool Loading 与 Tool Search

"工具太多就延迟加载呗"——可工具数量是错的计量单位,而延迟错了一个工具,代价是每个会话的第一条回复慢 6 秒。


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

  1. Token 做预算,不是按工具数量——三十个小 Schema 可能比五个大的还便宜
  2. 两个触发条件:预算超限或者冷集合里有 always-defer 类别的工具
  3. 有些工具绝不能延迟——那些开场就要用的,一次检索往返全是纯延迟
  4. Warm Set 是 session 级的、每次从冷启动;用 Schema 指纹做 key,不是名字
  5. 延迟只是藏起 Schema,不是拒绝权限——这是两套系统

10 分钟路径:38.1-38.3 → 38.5 → Kocoro OSS Lab


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

38.1 从一个 60% 是目录的 Prompt 说起

你接了四个 MCP Server:GitHub、Postgres、Slack、浏览器自动化。加起来 74 个工具。

现在看看你每一个请求都在发什么:

system prompt        1,800 tokens
tool schemas        23,400 tokens    74 个工具的完整 JSON Schema
conversation         4,100 tokens
─────────────────────────────────
                    29,300 tokens

两万三千个 token 的工具定义,在每一轮的每一次迭代里重发一遍,就为了回答一个只碰两个工具的问题。模型在读一份 74 条的目录,只为从里面挑一行——而这份目录你每次都在付钱。

明显的解法是:别再发模型用不到的 Schema,让它自己来要。这就是 Deferred Loading,机制并不复杂。有意思的是,几乎每一种直觉版本都会让事情变糟——而且只在生产环境里现形。

38.2 工具数量是错的计量单位

第一反应通常是设个数量阈值:超过 30 个工具就切换到延迟模式。

但 Schema 的大小根本不是一回事。一个两个字符串参数的 read_file,几百个 token。一个有十五个可选参数、嵌套选择器对象、枚举等待条件的浏览器自动化工具,光它自己就能几千。三十个前者,比五个后者便宜。

所以,去给你真正在意的那个量做预算。公开 Runtime 直接估算 Schema token,再和预算比:

schemaTokenBudget = 8000      // 约 28K 字符的紧凑 Schema JSON
charsPerTokenSchema = 3.5     // 保守比率,与上下文估算器一致

八千 token,来自「约 28K 字符紧凑 Schema JSON、按一个刻意保守的字符/token 比率折算」。注意这个保守是故意的——把 Schema 成本估低了,等于捅穿你本来想守住的那个预算,所以比率宁可往「Schema 更贵」的方向偏。

给你真正花掉的东西做预算,不是给你数得出来的东西。 数量阈值会在某人接进来一个啰嗦 Server 的那天失效。

38.3 两个触发条件,因为只看预算会漏掉一种情况

这里比「查一下预算」有意思。真实的判断条件是一个「或」:

deferredMode := len(coldDeferred) > 0 &&
    (shouldDefer(a.tools, a.tools.SortedNames(), schemaTokenBudget) ||
        hasCategoricalDeferred(coldDeferred))

预算超限,或者冷集合里含有 always-defer 类别的工具。

第二个子句为什么要有?因为有些工具既贵,又在特定场景下几乎从不用。macOS GUI 自动化、调度、无头进程控制——一次性 CLI 任务一个都碰不到,但它们的 Schema 照样搭车进每个请求。always-defer 集合针对的就是这个:

Keeping them in the deferred set means cold-start tools[] ships ~5K fewer tokens; sessions that DO need them pay one extra tool_search round-trip per session.

交易条件写得很清楚:每次冷启动省 5K token,代价是真的需要 GUI 工具的那些会话,每个多付一次检索往返。对一个大多数会话根本不碰桌面的 CLI 负载,这笔账明显划算——而且请注意,这是关于你的负载的判断,不是普遍真理。换成一个桌面自动化产品,正确的 always-defer 名单几乎是反过来的。

38.4 那些绝不能延迟的工具

现在是最容易做错的部分,也是本章开头那句话里数字的来历。

Gateway 工具默认都是可延迟的。但有两个被显式豁免,理由值得完整读一遍:

web_search/web_fetch are the most common opener of a NEW session (e.g. "what's the news on X") — deferring them forces the model into an extra tool_search round-trip before it can call the tool at all, adding ~6s of observed latency to the very first reply of a session, every session, since the WorkingSet warm cache is session-scoped and starts cold each time.

仔细读,这里面是两件不同的事实。

第一:Warm 缓存是 session 级的,而且每次从冷开始。 上个会话模型摸清的工具情况,全没了。每个新会话都要为它的第一次延迟查找付全价。

第二:这个代价落在第一条回复上。 不是落在某个用户已经投入了注意力的深层迭代上,而是落在他问完之后看到的第一个东西上。六秒钟什么都没有,就在那个决定他觉得这系统快不快的交互上。

所以规则不是「把贵的 Schema 延迟掉」,而是:延迟那些检索代价落在用户没在等的地方的 Schema。 一个用来开场的工具,是最不该被延迟的,无论它的 Schema 多大。

memory_recall 被豁免的理由相关但不同:它是隐式记忆注入什么都没捞到时的兜底路径。把它延迟掉,实测到的失败模式是模型在第一次尝试时幻觉出一个旧版本的 Schema,白烧一轮。修复的代价是大约 1.5K 额外 Schema 字节——而关键在于,这些字节搭在可缓存的 system 前缀里。每个会话付一次,不是每一轮付一次。

最后这个细节值得带走:当一个 Schema 住在缓存前缀里,它的成本会被整个会话摊薄。待在稳定前缀里的「贵」Schema,比看上去便宜得多。

38.5 Warm Set,以及它为什么用指纹做 key

模型检索过一次、拿到了 Schema 之后,再发一遍在注意力上不花钱,在 token 上很便宜。所以你把它留下——这就是 Warm Set,一个 session 级的、装着模型已经取回过的 Schema 的工作集。

隐蔽的失效:某个 MCP Server 升级了,它的工具现在参数形状不一样了。你的 Warm Set 还揣着旧 Schema,模型按旧形状发调用,工具拒收——或者更糟,默默接受然后做了别的事。

所以 Warm Set 是按 Schema 指纹失效的,不是按名字。底层有效工具集一变,从旧定义派生出来的 warm 条目就不再有效。按名字缓存是 bug,按内容缓存才是修复。

38.6 延迟不是授权

这一条值得说得直白,因为它长得像安全机制,但不是。

把 Schema 对模型藏起来,并不能阻止这个工具被调用。模型照样可以按名字发出调用——它可能在检索目录里见过这个名字,可能在某个 Skill 描述里见过,也可能就是猜的。Deferred Loading 是对能力描述做的检索优化。它塑造模型看到什么,不塑造模型能做什么。

权限是另一套系统,在执行时强制。如果你对一个危险工具的唯一防线是「它的 Schema 没进 Prompt」,那你没有防线。 真正的授权层在第 25 章和第 33 章;本章只谈 token 经济学。

顺带一提,同样的分离也出现在 Skill 限制上:allowed-tools 是以执行时拒绝实现的,而不是过滤 Schema——正是为了让 tools 数组保持字节稳定,喂给 Prompt Cache。两套系统,两种机制,刻意不合并。这也正是第 39 章建立在其上的那条纪律。

38.7 快照证据

观察4ec6772 源码位置
Schema token 预算 8,000 及其推导toolbudget.go L15
保守的 3.5 字符/token 比率toolbudget.go L18
Deferred Mode 由预算或类别任一触发loop.go L2158
always-defer 类别,冷启动省约 5K tokentoolbudget.go L39
browser_* 按前缀延迟toolbudget.go L56
never-defer 豁免与 6 秒的理由toolbudget.go L63
Warm Set 按工具集指纹做 keywarmset.go L11
tool_search 的构造deferred.go L22

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

38.8 常见的坑

按工具数量做预算。 前面说过了。某人接进来一个啰嗦 Server 的那天它就废了。

把开场工具延迟掉。 六秒的税,落在第一条回复上,每个会话都收。选延迟对象之前,先去日志里看看到底是什么在开场。

Warm 条目按名字做 key。 一直好用,直到某个 Server 更新了 Schema,然后开始产出畸形调用——而且看起来像是模型出错。

忘了 Warm Set 从冷开始。 只按稳态行为推理、忘了每个新会话都从零开始,就是六秒问题在测试里被漏掉的原因——它在一次长的手工会话里永远不会出现。

把延迟当安全边界。 它不是。拒绝发生在执行时。

延迟得太狠,模型什么都发现不了。 如果兜底目录只列名字,而名字又晦涩,模型根本没法组织出一个好查询。兜底目录里的名字是承重的——把它们起得有描述性。

Kocoro OSS Lab(10 分钟上手)

  1. neverDeferTools 的注释,注意这个论证的结构:一个实测延迟数字,挂在一个具体的用户可见时刻上,再挂在一个具体的缓存性质上。
  2. alwaysDeferTools,问自己:换成一个桌面自动化产品,哪几条会是错的?这份名单编码的是一个关于负载的假设。
  3. 估一下你自己的 Schema token 开销:把工具定义序列化成紧凑 JSON,字符数除以 3.5,跟你的上下文窗口比。超过百分之几,你就有这个问题。

划重点

  1. 按 Token 做预算,不是按工具数量。 Schema 大小能差一个数量级。
  2. 两个触发条件。 预算超限,或者冷集合里有 always-defer 类别——后者专门抓那些「贵但没人用、又永远撑不爆预算」的工具。
  3. 绝不延迟开场工具。 检索代价会落在第一条回复上,落在一个每个会话都从冷开始的缓存上。
  4. 缓存前缀里的 Schema 比看上去便宜。 成本被整个会话摊薄,「贵但常驻」可以赢过「便宜但延迟」。
  5. 延迟是检索,不是授权。 拒绝属于执行时;把两者合并,你两样都得不到。

下一章:第 39 章讲的,正是让这个缓存前缀值得守护的那套纪律。

引用本文 / Cite
Zhang, Wayland (2026). 第 38 章:Deferred Tool Loading 与 Tool Search. In AI Agent 架构:从单体到企业级多智能体. https://waylandz.com/ai-agent-book/%E7%AC%AC38%E7%AB%A0-Deferred-Tool-Loading%E4%B8%8ETool-Search/
@incollection{zhang2026aiagent_38_-Deferred-Tool-Loading_Tool-Search,
  author = {Zhang, Wayland},
  title = {第 38 章:Deferred Tool Loading 与 Tool Search},
  booktitle = {AI Agent 架构:从单体到企业级多智能体},
  year = {2026},
  url = {https://waylandz.com/ai-agent-book/%E7%AC%AC38%E7%AB%A0-Deferred-Tool-Loading%E4%B8%8ETool-Search/}
}