Skip to content

1. 工具到一百个之后 ​

一个团队接了六个外部服务:代码托管、工单、文档、数据库、监控、搜索。每个服务暴露十到二十五个工具,合计一百来个。

接入之后出现五个现象,一个比一个难查。

选错。 让它查工单,它调了代码托管的议题查询。两个工具的说明里都有"查询""列表"这类词。

漏选。 一个明显该用的工具没被用上。回头看上下文,它确实在清单里——只是在两百多条里排在中段,模型跳过了。

窗口被占。 上下文占用从三十几 KB 涨到一百多 KB。还没开始干活,两成窗口用掉了。

缓存反复失效。 每次外部服务重连,整段工具声明的缓存重建。这个成本不在任何一次调用的账上,在整体吞吐上。

成本上升。 每一轮都要为这一百来个声明付费,与当前任务是否相关无关。

同一个问题在公开实践里有几个量化。五个服务就可能引入数万 Token 的工具定义,在 200K 窗口里还没开始对话就用掉近三成。改成只暴露索引、按需查询定义之后,相关任务的总 Token 消耗减少了 46.9%。另一条路是按语义先筛出候选再注入,某模型在工具使用基准上的准确率从 49% 提升到 74%。更激进的做法是一条工具声明都不预先放,模型在执行中主动声明"我需要什么能力",系统再匹配注入——在约 2800 个工具的规模上比全量注入节省约 98% 的 Token。

五个现象与三个数字指向同一件事:工具数量本身就是需要设计的对象。


2. 预算账 ​

上下文窗口是固定预算,其中被三类内容长期占据,它们都不随任务推进变化。

内容量级是否随接入服务增长
系统提示8–20 KB否
内置工具声明10–40 KB否
外部服务工具声明每个服务 5–25 KB是,无上限
Skill 目录每条 0.1–0.2 KB是,增长慢
历史消息可变由压缩控制

设第 \(j\) 个服务暴露 \(m_j\) 个工具,每个声明平均 \(a\) 字节,共 \(k\) 个服务:

\[c = a \sum_{j=1}^{k} m_j \]

取 \(k = 6\)、\(m_j = 20\)、\(a = 450\):\(c \approx 54000\) 字节,约 54 KB。接到十个服务就是 90 KB。

一个只要求"改这一行代码"的任务,也要付这 54 KB。


3. 三层答案 ​

策略不是二选一,是一条从"全放"到"全不放"的谱系。按干预程度分三层,一层比一层更依赖运行时判断。

第一层,层次化组织。 声明仍事先备好,只是不全量塞进上下文。按服务分组、按信息源性质分组、或者只给名字不给参数结构。这一层不需要新基础设施,改的是组织方式。

第二层,主动发现。 模型在执行中意识到能力缺口,主动声明需求,系统匹配并注入。这一层需要索引、匹配、注入三件东西,工程量大。

第三层,Skill。 干脆不把能力当成需要注册、检索、注入的正式定义,当成一份可以随手翻阅的参考资料。这一层把"发现"变成"读文档"。

三层的代价不同。

层需要什么首次使用延迟漏选风险
层次化组织分组与命名约定一次加载中
主动发现索引加匹配加注入一次声明加一次匹配高(匹配不到就没有)
Skill一份写清的文档一次读取正文中(目录写得宽就漏,窄就错)

第二层最高效,也最容易出问题:它把"有没有这个能力"变成一次检索的结果。匹配不到时,模型会认为系统没有这个能力,转去用别的办法绕。

第三层最省心,因为它的默认形态就是渐进披露——不需要另建一层发现机制。这与专用工具形成对照:专用工具要达到同样的效果,必须额外建索引、检索元工具、命名空间这些基础设施。


4. 五种注入策略 ​

把谱系按"常驻成本"排开,有五种具体做法。

全量注入。 所有声明常驻。零延迟,占窗口最大,随服务数线性增长。

核心常驻加其余按需。 最常用的几个常驻,其余进待加载集合。常驻降到几个 KB,加上一份名字表。

名称目录加正文按需。 常驻全部名字(不含参数结构),正文按需加载。

检索式发现。 零常驻,两段式调用。首次使用要多一次往返。

命名空间分组。 按服务分组,组内仍需枚举。它解决的是同名冲突,对总量帮助有限。

四种(除全量外)的对照:

策略常驻量命中项往返漏选风险
全量注入54 KB无0低
核心常驻约 6 KB 加名字表一次加载一次中
名称目录约 5 KB(120 个名字)一次加载一次中
检索式发现约 0.5 KB一次加载两次高
命名空间分组约 8 KB一次加载一次中

5. 缓存改写排序 ​

上一节的排序只看静态占用。加上缓存折扣后,结论会变。

缓存把输入分成稳定前缀与变化后缀,稳定部分按折扣价计费:

\[c' = \alpha f + v \]

其中 \(f\) 是稳定前缀,\(v\) 是变化部分,\(\alpha\) 是折扣系数(量级上 0.1)。

全量注入的 54 KB 全在 \(f\) 里,实际约 5.4 KB。检索式发现的目录在 \(v\) 里,每轮全价——虽然只有 0.5 KB,但加上两段式往返的开销,总量未必更低。

这个反转解释了一件事:常驻成本最低的策略,对缓存最不友好。因为它依赖的目录内容随查询变化,每轮前缀都不同,缓存无法复用。

5.1 三种规模的完整代入 ​

小规模:三服务四十五工具。 全量常驻 20 KB,缓存后约 2 KB。检索式 0.5 KB 加两次往返。

这个规模下全量注入明显更优。20 KB 的原始占用在缓存之后只有 2 KB,而两次往返每次都要模型做一次决策——两次决策的成本通常超过 2 KB 的窗口占用。

结论:三个服务以内,不做延迟机制。

中规模:六服务一百二十工具。 全量 54 KB,缓存后约 5.4 KB。核心常驻:白名单十个工具约 6 KB 加名字表约 5 KB,缓存后约 1.1 KB,每次用到非核心工具多一次往返。

这个规模是转折点。5.4 KB 与 1.1 KB 差了 4.3 KB,但核心常驻要为每次非核心调用付一次往返。如果一次任务平均用到两个非核心工具,就是两次额外往返。

判断依据变成"一次往返值不值 2 KB"。长任务里(几十轮)2 KB 会被反复付;短任务里往返次数少,全量更划算。

大规模:十服务两百工具。 全量 90 KB,缓存后约 9 KB。核心常驻约 14 KB(6 KB 声明加 8 KB 名字表),缓存后约 1.4 KB。

这个规模下核心常驻与检索式的差距缩小到 1 KB 以内,因为名字表本身变大了。真正的分界不再是"常驻还是延迟",而是"名字表要不要分组"。

5.2 一条经验式 ​

三种规模合起来看,延迟机制值得引入的判据是:

\[\frac{\left(f_{\text{全量}} - f_{\text{延迟}}\right)\alpha}{t} > n_{\text{往返}} \]

左边是延迟节省的窗口占用(按折扣计)除以单次往返的等效成本,得到"节省值多少次往返";右边是一次任务里预计的非核心调用次数。

代入中规模:左边 \(= (5.4 - 1.1)/2 \approx 2.15\),右边若为 2,延迟刚好值得。小规模左边 \(= (2 - 0.6)/2 = 0.7\),右边若为 1,不值得。

这个式子不精确,但它把"值不值得"变成两个可以估的量:单次往返的等效成本,与一次任务的非核心调用次数。


6. 名字表本身的成本 ​

延迟机制常被误解成"常驻成本接近零"。实际它有一个不可省略的常驻项:名字表。

一个名字加一点标点约 30 到 60 字节。一百二十个约 5 KB,两百个约 8 KB,五百个约 20 KB。

三个后果。

名字表会随时间膨胀。 每接一个服务就加一批名字,没有上限。这与工具声明的增长是同一件事,只是斜率小一些。

名字的信息量决定选择质量。 名字表是模型唯一能看到的线索。svc1_search、svc2_search 这类名字需要额外查一次说明才能区分,而查说明就是一次往返。

分组能压缩语义负担,但压不了字节数。 按服务分组后,模型先选组再选工具,两次决策。字节没省,决策次数增加。

三种对策各自适用不同规模。

对策适用规模代价
名字里加领域词几十个名字变长
按服务分组上百个多一次决策
把不常用的整组改为按需接入数百个需要判断哪些组不常用

第三种是唯一能真正压住增长的办法:把整个服务改成"需要时接入",接入后才有名字。这相当于两级延迟——先接服务,再选工具。代价是首次使用多两次往返,收益是名字表不随服务数增长。


7. 往返的成本 ​

第 5 节的式子里有个量没有定义清楚:\(t\),一次往返的等效成本。

往返不是免费的。它至少占三样东西。

一轮生成。 模型要生成一次搜索调用,参数是工具名或一句需求描述。这一轮的输出通常几十到上百 Token。

一次决策步。 搜索之后,模型要读返回、判断是否命中、决定继续搜还是执行。这一步在本该直接调用的情况下是纯增量。

一段上下文。 搜索结果本身要进上下文,形成一条新的消息。

三样加起来,量级上相当于"一次很短的工具调用"。可以拿一个具体的对照:如果一次普通工具调用平均花 800 Token(声明分摊加参数加结果),那么一次往返大约 300 到 500 Token;再加上多出来的一轮决策,实际成本接近一次普通调用的三分之二。

按这个量级回代第 5 节的式子:\(t \approx 0.4\) 次调用的成本,而 \(\alpha \approx 0.1\)。规模二的左边是 \((5.4 - 1.1) \times 0.1 / 0.4 \approx 1.08\),也就是说延迟机制省下的窗口只值一次往返。

这个结果比第 5 节的估算更悲观:在那个规模上,延迟机制只要一次任务里用到两个以上非核心工具,就不划算。

7.1 反直觉的推论 ​

往返成本与"任务长度"的关系是反向的。

短任务里,非核心工具用得少(可能一次都不用),往返次数是零或一,延迟机制稳赚——省下的常驻成本是确定的,付的往返成本是零。

长任务里,非核心工具用得多,往返次数累积,延迟机制的收益被逐渐吃掉。一个跑二十轮的任务,如果每三轮用一次非核心工具,就是六七次往返。

这与直觉相反:多数人认为"任务越长越需要省窗口"。省窗口是对的,但延迟机制不是省窗口的唯一办法——压缩历史也是,而压缩不增加往返。

所以判据要更精确:长任务优先压缩,短任务优先延迟。两者都省窗口,成本结构不同。

7.2 什么时候往返几乎免费 ​

有一种情况往返成本可以忽略:一次任务里要连续用同一批工具。

第一次搜索加载之后,后续调用不再需要搜索——工具已经在集里。这时候往返只发生一次,而它换来的是整批工具都不常驻。

这类任务的形状是"调查一个领域,反复用该领域的几个工具"。探索型任务多属此类。

反过来,形状是"跑一遍固定流程,偶尔碰到需要额外能力的点"的任务,往返次数与"偶发点"的个数成正比,而每个点只用一次。


8. 命中率是最难估的一项 ​

第 5 节的式子里,\(q_s\)(命中率)最难估,而它对结论影响最大。

两种策略的 \(q_s\) 含义不同。

常驻策略假设 \(q_s = 1\),因为它不需要"命中"这个动作,工具一直在。

按需策略的 \(q_s\) 有两层:任务需要这个工具的概率,与检索能找对的概率。两者相乘才是有效命中率。

第二层常被忽略。设某任务真的需要工具 A,检索时返回了 A、B、C 三条,模型选了 B——这时"命中率"在指标上算成功(检索到了 A),在实际效果上是失败。

一个可用的估算是看两个数:工具被调用的次数,与该工具被加载的次数。比值低说明加载了但没被用(\(q_s\) 高估),比值高说明加载即用。


9. 四家怎么选 ​

四家的选择不是偏好,是各自的部署形态推出来的。

Claude Code 面对任意外部服务接入。 用户随时可以接一个新服务,来源不可预知。这决定了它必须有统一的延迟机制,不能靠"部署时确定工具集"。

机制分三层。第一层,核心工具白名单常驻:读文件、编辑、写文件、执行命令、文件匹配、内容搜索、子代理、网页抓取、网页搜索、技能。第二层,其余全部延迟——不在白名单里且没有显式标记"总是加载"的,一律进延迟集合。第三层,两段式调用。

Codex 面对相对可控的来源。 它把搜索做成一个独立工具,与"可加载工具"分开。搜索结果默认返回 8 条,来源说明的总预算 512 KB。

与 Claude Code 的差别在协议:Codex 不需要"先搜再执行",搜到的工具直接进入工具集,下一轮可见。这简化了调用流程,代价是工具集的动态变化要被正确处理。

OpenCode 面对装配时确定的工具集。 它没有统一的延迟池,工具按位置装配:每个叶子工具自己声明需要什么权限动作,能力由所在组合决定。要减少常驻得改装配配置。

DeepSeek Harness 面对多套配置共存。 工具与 Skill 注册表都用"宿主层加作用范围层"的分层结构,读取时把全局层与观察范围的链合并,最近层的条目赢得重名。单层内按提权字段、提供方顺序、本地顺序裁决。

这个设计解决的是"谁覆盖谁",对注入只有间接影响:分层让某个范围只需要某几个工具成为可能,从而减少该范围的常驻量。

9.1 两段式协议的细节 ​

Claude Code 的延迟机制有一份可查的协议细节,是这类机制里最完整的实现。

四种查询形态。

形态写法用途
精确名select:CronCreate已知名字时最快
多选select:CronCreate,CronList一次拿多个
取详情discover:schedule cron job返回名称、说明、结构但不加载
关键词cron schedule不知道确切名字时
必含词+slack send要求名字含某词,其余词参与排序

四种形态的意图清楚:已知名字时省一次检索,不知道名字时给模糊匹配,需要先看再决定时给"取详情"这一档。

两段式调用。

text
第一步:搜索工具({"query": "select:CronCreate"})
        → 返回:找到目标工具,可调用
第二步:执行工具({"tool_name": "CronCreate", "params": {...}})
        → 返回:实际结果

一条硬约束:失败后禁止重复搜索。 提示词里明确写:如果执行失败,不要重复搜索同一个工具,会陷入循环,应当停下并告知用户。这条针对一个具体故障——搜索成功但执行失败时,模型倾向于"再搜一次确认",而那只是重复同样的失败。

延迟工具的呈现。 只有工具名出现在附件里,搜索提示不渲染。文档说明这是做过对照实验的结论:搜索提示没有带来收益。这条违背直觉,值得记。

一处缓存优化。 代理列表原本嵌在子代理工具的说明里,导致说明随外部服务异步连接、插件重载、权限模式变化而变,进而使全部工具结构的缓存失效。文档给出的量化是:约占全集群缓存重建的 10.2%。修法是把列表挪进附件,让工具说明保持静态。

这条改动不减少任何内容,只是把会变的内容从不该变的位置挪走。

9.2 另一种协议:搜到即进入工具集 ​

另一个实现把搜索做成独立工具,搜到的工具直接进入下一轮的工具集,不需要两段式。

维度两段式搜到即加载
调用步骤搜索加执行搜索后直接调用
模型要记的搜索结果的工具名与参数无(工具已在集里)
加载时机每次调用前一次,之后一直在
风险两段之间状态漂移加载后长期占用窗口

两种协议对应两种使用形状。两段式适合"一次任务只用一两次某个工具",用完不再占窗口;搜到即加载适合"一次任务反复用同一批工具",加载一次后无额外成本。

前者的风险是两段之间的记忆负担,后者的风险是加载后不释放。两个实现各自接受了一种。


10. 迁移与过渡 ​

策略不是一次选定的。从一种换到另一种,中间有一个过渡期,而过渡期最容易出问题。

从全量迁到核心常驻。 需要决定哪些进白名单。决定之后,清单要能解释:为什么这个常驻而另一个不常驻。没有解释的清单会在几个月后变成"谁都不敢删"。

从核心常驻迁到检索式。 需要验收一件事:原来直接可调的工具,现在要两步。这个变化对模型行为影响很大,需要观察一段时间——工具调用次数可能先降后升,因为模型要先学会"搜一下"。

从检索式迁回常驻。 需要理由:通常是发现漏选造成的损失超过节省的常驻成本。这个判断需要数据。

过渡期的失败有两种。

半迁移。 部分工具进了延迟集合,但说明里没有写"需要先搜索"。模型看到名字就直接调用,协议层报错。修法是在延迟工具的名字附近明确写"不能直接调用"。

清单腐化。 白名单只增不减。每加一个工具就多加一个常驻项,半年后常驻量回到迁移前。修法有两个:定期检查每个常驻工具的实际调用次数,或给常驻项设数量上限,超限时必须删一个才能加一个。


11. 失败模式 ​

失败模式症状成因修的位置
延迟工具直接调用协议层报错说工具不存在模型看到名字就调名字附近写明需先搜索
两段式状态漂移搜到 A,执行时用了 B 的参数模型需自己记住对应关系搜索时把结构一起给出
目录过长名字表里找不到对的那个名字不含语义名字带领域词,或分组
连接变化引发前缀失效每次重连,工具结构缓存全废声明嵌在稳定位置会变的部分移到附件
检索形态单一不知道名字时反复试关键词未提供"取详情"档增加按描述检索的形态
失败后重复搜索同一工具反复搜索不执行无禁止重复的约束提示词明确禁止
名字表膨胀常驻量半年后回到迁移前只加不减常驻项设数量上限

第四个是那段 10.2% 的直接来源,也是最容易被忽略的一类成本——它不在任何一次调用的账单上,而在整体吞吐上。


12. 选择顺序 ​

四步走。

第一步,工具是否每轮都要用。 是,全量注入或核心常驻。多数任务的工具使用集中在少数几个上,全量注入的收益率很低。

第二步,首次调用延迟能否接受。 交互式对话里,多一次往返是可感知的;批处理任务里几乎无感。

第三步,漏选代价有多高。 漏选一个只在特定场景用的工具,代价是那次任务失败重试;漏选一个关键工具,代价是模型绕路或用错方式完成。后者严重得多。

第四步,缓存是否命中。 稳定常驻按折扣计价,变化目录按全价。这一项常被忽略,但它在长任务里的影响超过前两项。

四步之后的选择通常落在"核心常驻加其余延迟"——它在延迟、漏选、缓存三项上都没有明显短板。

工具总数少(十个以内)时,第四步会推翻前几步:全量注入的缓存友好性足以抵消占用,此时不必引入延迟机制。真正的判据是"常驻成本乘以折扣后是否仍显著大于往返成本",数量只是它的近似。

12.1 四步之后要记的数 ​

做完判断,应该能回答三个数。

常驻量。 当前每轮固定占多少 KB。这个数决定下限——不可能低于它。

一次任务的平均往返次数。 这个数决定延迟策略的实际成本。它需要统计,不能靠估。

非核心工具的使用率。 一次任务里用到几个不在白名单里的工具。这个数直接决定延迟机制的收益。

三个数共同决定"值不值得"。缺任何一个就变成凭感觉选。

12.2 一处常见的过度设计 ​

一个团队接了四个服务、六十个工具,仍然上了两段式延迟,理由是"以后会更多"。

代价是每次调用非核心工具都要两步,而他们的任务里平均每次用到三个非核心工具——一次任务多出三次往返。

错误在于按未来规模选策略,没看当前规模。第 5 节的式子给了正确做法:当前左边是 \((3 - 1)/2 = 1\),右边是 3,不值得。等规模翻倍再说。

延迟机制可以后加。工具集增长到转折点再引入,比一开始就引入更划算——因为引入的成本与任务形状有关,而任务形状在早期还不清楚。