ContextChef (3):Pruner——把工具注册和路由彻底分开
7 Mar 2026
1 min read
English version: ContextChef (3): Pruner — Decoupling Tool Registration from Routing
工具幻觉通常被归因于”工具太多,模型选错了”。这个诊断是对的,但引出的解法往往是错的。
直觉上的解法是:每轮只注入当前任务相关的工具,不相关的不给模型看,自然就不会幻觉了。Manus 也尝试过这条路,最终放弃了。
动态增删工具的两个隐患
Manus 在 Context Engineering for AI Agents 里总结了为什么要 “Mask, Don’t Remove”:不要在对话中途添加或删除工具定义,原因有两个。
第一,工具定义通常序列化在上下文的前部,紧跟在 system prompt 之后。这意味着只要工具列表有任何变化,从这个位置往后的所有 token——包括完整的历史消息——都无法命中 KV-cache。每轮都重建 cache,成本就是全量 prefill,对于 input/output ratio 高达 100:1 的 Agent 来说代价极高。
第二,模型在生成过程中会参考已有的历史。如果历史里的某个动作引用了工具 A,但当前上下文里工具 A 已经不存在了,模型会感到困惑,可能产生 schema 违反,或者强行编造一个参数结构。工具不是状态,是契约——契约不能在会话进行中单方面修改。
设计角度:注册一次,路由策略自选
Pruner 的设计角度是把两件事分开:工具注册是一次性的,路由策略是每轮的决策。你在初始化时把所有工具注册进来,打好语义标签;在每次 compile() 前,选择用哪种路由策略决定实际暴露给模型的子集。这两件事解耦之后,带来几个具体的好处:
策略可以随时切换,不需要改注册逻辑。 同一批工具,你今天用 pruneByTask() 做语义过滤,明天想换成双层架构,只需要改路由调用,工具注册不动。这在产品迭代阶段很实用——先用最简单的方案起步,遇到瓶颈再升级,不需要重构注册层。
工具描述是声明式的,匹配逻辑对你透明。 语义过滤基于工具的 description 字段和你传入的任务描述做相似度匹配,你写好描述,Pruner 负责匹配。没有硬编码的 if/else 路由表,也不需要手动维护”哪个任务用哪些工具”的映射。
双层架构的稳定性来自结构,不来自你每轮的手动控制。 Namespace 工具列表一旦建立就不变,你不需要在每次 Agent 循环里判断”这轮该暴露哪些工具”——这个决策被前置到了架构设计阶段,运行时零额外负担。
两条路径
知道了设计角度,两条路径的取舍就很清晰:
扁平裁剪:工具列表每轮可以变化,适合不在意 cache 成本、任务边界清晰的场景。接入成本最低,注册时打标签,调用前 pruneByTask() 一行代码。对于 20 个工具以内的 Agent,这通常是足够的。
双层架构:核心工具以 namespace 分组注入,列表永远稳定;长尾工具注册成 XML 目录,模型通过 load_toolkit 按需请求加载。工具列表稳定意味着 cache 命中率最优,也意味着模型不会困惑于每轮不同的工具集。当工具超过 20 个、或者对 cache 命中率有要求时,这是更可靠的选择。
多一轮 load_toolkit 的调用是真实的代价,但它换来的是结构性稳定——模型无法幻觉出一个它的 schema 里不存在的工具名,因为它永远只能看到它已经加载的工具集。
Anthropic 对此有一句很直接的话:“如果一个人类工程师都无法确定在某个情况下该用哪个工具,AI agent 也别指望能做得更好。” 这不是模型智商的问题,是信息密度的问题。给模型更少但更精准的选项,它的表现通常比给它所有工具更好。
下一篇:Offloader/VFS——当工具返回 15000 字的日志,该怎么办。