假设接了五六个 MCP Server,工具注册表里攒下两百多个工具。
但大部分任务其实只用得上五六个。如果把这些工具的完整 Schema 全部放进主模型的上下文,模型还没开始干活,就已经先读了一遍日历、邮件、GitHub、Notion 的接口文档。
所以我给 Biny 设计的披露方式是:基础工具常驻,辅助模型提前预选,执行中再用 ToolSearch 按需发现。
这篇把这条链路拆开讲,也聊聊它和 Codex 那套按需检索的区别,以及这套设计目前值不值得。
一、问题不在工具多,在 Schema 全暴露
先分清两个集合。
系统拥有的工具:ToolRegistry 里的全部能力,包括内置工具、MCP 工具、插件、Skill 入口和 Subagent 入口。每个工具都有名称、描述、参数 Schema 和来源信息。
模型当前可用的工具:这一轮真正写进请求里的工具定义。
只读一个配置文件,却要连带把两百个工具的 JSON Schema 送进去,这笔输入开销是白付的。工具越多,问题越明显:
| 全部注入 | 按需披露 |
|---|---|
| 输入 Token 随工具数量线性增长 | 只付当前相关工具的代价 |
| 无关描述干扰模型选择 | 候选变少,选择更集中 |
| 每轮都要重复支付 | 固定部分只剩基础能力 |
| 实现简单,无需额外判断 | 需要一套选择与补充机制 |
所以我让工具目录保留完整能力,模型侧只开放当前需要的一部分。
自动选择模式下,固定留在主模型身边的是 Read、Write、Edit、Bash,加上工具发现、结果回读这类运行时入口。其余工具要靠预选、搜索,或者用户直接点名才能进来。
目录负责「有什么」,工具集负责「现在用什么」。 这两个集合不需要始终相等。
二、辅助模型怎样挑工具
回合开始前,capabilityPreselection 会做一次预选。
它从注册表读取工具元信息,整理进辅助模型的系统提示词。这里有个关键取舍:只放名称和描述,不放完整参数 Schema。
参数 Schema 通常是最占篇幅的部分。一个带嵌套对象和枚举的工具,定义能有上千 Token。预选阶段不需要知道每个字段怎么填,只需要判断「这个能力跟当前任务有没有关系」。
辅助模型拿到的输入大致是:
- 当前用户请求
- 最近几轮对话(截断后)
- 每个工具的
name和一段截断的description
它输出一组工具名。Biny 再把这些名字和注册表核对一遍,只接受真实存在的条目。
Skill 有独立的预选流程,和工具预选并行发起。两侧互不阻塞,一边失败不会抹掉另一边已经选好的结果。
最后合并出这一轮的集合:
注意分工:辅助模型决定「选哪些工具」,主模型决定「何时调用、怎么传参」,实际执行交给统一的工具执行器。
预选不是无限制的
我给预选设了超时和预算。
超时到了或者辅助模型调用失败,不会退回「把全部工具展开」这种兜底——那等于放弃了这个设计的初衷。失败时保留基础能力和仍然有效的历史工具,本轮继续跑。
预算限制的是这一轮新自动选进来的工具。已有的有效工具和必要入口不受影响,所以会话工具集可以随任务推进逐步长大,而不是每轮从零开始。
这里有个必须承认的副作用:工具集会长大,而且不会自己缩小。任务不断切换,累积的工具定义会越来越多。淘汰策略我还在观察,暂时没有拍板。
三、主模型缺工具时:ToolSearch
预选是回合开始前的预测,预测一定会有不准的时候。
多步任务走到后面,突然要读一个关联 Issue,或者需要调用一个预选时没判断出来的能力,主模型可以自己调用 ToolSearch。
图 1:目录可以很大,但每一步只披露当前相关的工具。发现成功后,下一步模型才能使用新增能力。
ToolSearch 有两条路径:
精确匹配。 查询里包含完整的注册工具名时,直接从本地注册表返回。这一步不请求任何模型,纯粹是字符串查找。用户或者模型已经知道工具叫什么,就没必要再让辅助模型猜一遍。
语义选择。 没有精确命中时,把当前合格的工具目录交给辅助模型,由它根据名称、描述和查询语义挑选。
这里要说清楚:这一版 ToolSearch 没有用 BM25,也没有向量数据库。 没有精确命中时,相关性判断整件事是直接交给辅助模型做的。
辅助模型返回的名称同样要和注册表核对。只开放已经注册、当前仍然可见的工具,不会因为一次搜索就顺带展开整个 MCP Server。
成功发现的工具会在会话后续回合复用,前提是注册仍然有效。这减少了重复搜索,也加重了前面提到的累积问题。
这一版能跑,但代价很明确:每次语义搜索都要把完整目录送进辅助模型。工具越多,这笔输入越贵。后面第七节会讲我怎么改这个结构。
四、和 Codex 的差别在哪
Codex 开源实现里也有延迟工具发现,但路线不太一样。
它的做法更接近纯按需检索:
Codex 的 tool_search 处理函数会用 BM25 索引延迟加载的工具元数据,查询时做相关性排序,把命中的工具定义返回给模型。这条路径不需要额外调用辅助 LLM。
它用的 bm25 crate 默认按英文分词:先做 Unicode 归一化,再按词边界切分,然后去停用词、做词干还原。这对英文工具名和描述很合适,对中文查询就没那么友好了。
两边放在一起看:
| 维度 | Biny-Agent | Codex |
|---|---|---|
| 工具发现策略 | 回合前预选 + 执行中补充 | 主要按需发现 |
| 相关性判断 | 辅助 LLM 语义选择 | 本地 BM25 排序 |
| 额外模型请求 | 预选和部分搜索需要 | BM25 搜索不需要 |
| Schema 优化 | 只披露选中的工具 | 延迟加载相关工具 |
| 主要代价 | 额外 Token 和模型延迟 | 额外检索步骤与召回偏差 |
有一点要说清楚:减少主模型上下文占用,并不是 Biny 独有的收益。 Codex 的延迟加载同样能做到,甚至因为它完全不给主模型预选,常驻部分可能更小。
真正的差别在后面两件事上。
五、那为什么还要用辅助模型
我当初加预选,主要是想解决两个问题。
第一,减少主模型的工具上下文占用。 这一点前面说了,Codex 也能做到,不算独有。
第二,在主模型执行前预测需要的能力。 如果预选准,主模型第一次请求就能直接调用相关工具,省掉执行中再搜索一轮的交互。
第二条才是关键。
BM25 是词项匹配。它擅长「查询里出现的词,文档里也有」这种场景,但有几类查询它天然吃亏:
- 用户说「看看这个 PR 的讨论」,工具叫
get_pull_request_comments,词项能对上,还好。 - 用户说「把这段逻辑整理成文档」,工具叫
create_note,字面上一个词都不重合。 - 用户用中文描述,工具描述是英文,词表根本对不上。
- 任务意图分散在多句话里,没有单一关键词能概括。
LLM 在这些场景下的优势很直接:它能理解同义表达、跨语言描述和复合意图。
代价也很直接:
- 预选需要额外读一遍工具目录,再生成一次选择结果
- 首轮等待时间变长
- 预选不准时,主模型仍然要调用 ToolSearch,极端情况下还要再请求一次辅助模型
所以准确的说法是:Biny 优化的是主模型的 Schema 暴露范围,并尝试减少执行中发现工具的步骤;代价是额外的辅助模型计算和潜在延迟。
六、预选到底值不值
我拿同一批任务比过两条路。
每回合都做辅助预选的一侧,8 个任务全部做完。只留基础和历史工具、缺了再用 ToolSearch 的一侧,做完 4 个。没做完的那一侧,常常一次搜索都没发出去——模型不觉得自己缺工具,就这么硬着头皮往下做了。
成本上,预选更慢也更贵:
| 指标 | 每回合预选 | 按需搜索 |
|---|---|---|
| 完成任务数 | 8 / 8 | 4 / 8 |
| 首个主模型请求中位延迟 | 2.28 s | 0.106 s |
| ToolSearch 调用次数 | 0 | 2 |
| 主模型 + 辅助模型总 Token | 95,982 | 65,312 |
按需搜索那一侧看起来更便宜,但它只完成了一半任务,所以这个成本对比要结合完成率看,不能直接下结论说它更划算。
基于这 8 个任务的结果,我暂时保留了每回合预选。
不过我也清楚,这个样本很小,而且任务类型偏向「需要主动发现能力」。换成常规代码修改,结论可能反过来——Read、Edit、Bash 基本就够用,提前调一次辅助模型,收益接近于零。
七、目前的取舍
我现在的判断是:预选的价值取决于任务有多依赖扩展能力。
对常规代码修改,基础工具就能闭环,预选大概率是纯开销。对需要跨系统操作的任务,预选能省掉一轮搜索,甚至能救回那些「模型不知道自己缺工具」的情况。
第二个现实是模型在变强。
性价比高的小模型(比如 Luna、Haiku 这一档)做语义判断已经够用了,成本也比一年前低不少。但模型变强的同时,工具数量也在涨,两边的账都不太好算死。
所以我现在把两条路拼在一起,而不是二选一:
图 2:本地检索负责缩小范围,模型只在候选之间做语义判断,不再面对整个目录。
这样做的理由是:
- 本地检索便宜且稳定。 名称和描述打分是纯计算,没有网络请求,延迟可以忽略。
- 辅助模型只看候选。 从两百个工具缩小到十几个,输入 Token 大幅下降,判断难度也下降了。
- 模糊时才付模型的代价。 精确命中或高置信度命中直接返回,这类查询占多数。
不过这条路也有它自己的问题:本地检索的召回决定了上限。如果候选里没有正确的工具,后面让多强的模型来选都没用。所以本地检索的召回率,比它的排序精度更重要。中文查询还要单独处理分词,否则整句中文会被当成一个词。
最后说明一下现状:公开仓库里能跑的还是第三节那版「整目录交给辅助模型」,本地检索这条路径是我正在改的部分,所以上面的实现索引只指向已经合进去的代码。
至于「语义与关键词检索在常规任务上到底有多有效」,我还没有足够的数据下结论。目前的实现是一个还在调整的中间状态,不是最终答案。
最后
回头看,工具发现这件事真正难的不是「怎么把工具找出来」,而是在主模型开始工作之前,帮它判断清楚自己需要什么。
辅助模型预选是我目前找到的一种解法:它用额外的计算换更少的上下文占用,用额外的延迟换更少的往返。这个交换在什么任务上划算,取决于任务本身依赖多少扩展能力,也取决于你愿意为首轮等待付多少。
我还没有把这笔账算完。但至少现在,主模型不用为了改一个配置文件,先读完两百个工具的接口文档了。
项目地址: Biny Agent
工具系统里和这篇相邻的两块,可以接着看 工具注册、执行与失败恢复 和 记忆系统的分层与召回。
实现索引: