Peri Code 的工具分层------LLM 面对 50 个工具时会停止调用工具
Peri Code --- 用 Rust 写的开源 Coding Agent,兼容 Claude Code 生态。
curl -fsSL https://raw.githubusercontent.com/konghayao/peri/main/scripts/install.sh | bash
接入 MCP 之后,Peri Code 的行为开始变奇怪。
工具列表膨胀到 50 个出头,DeepSeek、GLM、Qwen 的 tool 调用命中率明显下降。不是选错了工具------是完全不调用工具,直接凭记忆回答,Bash 不执行,文件不读写,该搜索的不搜索。工具越多,LLM 越懒。这不是某个模型的特例,三个不同提供商都复现了。
Peri Code 的答案是只给 LLM 看 14 个工具,剩下的按需发现、代理执行。
工具列表越长,命中率越低
这个结论有点反直觉,但可以解释。
LLM 做工具调用本质上是在所有选项里挑一个最合适的。选项越多,「不调用」也是一个合理的选择,而且是回避风险最安全的选择。50 个工具摆在那里,模型好像在一张巨大的菜单面前选择直接背菜单,而不是点菜。
还有一个更硬的问题是缓存。工具列表是每轮 API 请求的一部分,也是 Prompt Cache 前缀的组成部分。50 个工具的完整 schema 大概要吃掉上万 token,如果工具列表每轮内容不一致,缓存前缀就断了,所有命中全部失效,这万 token 每次都要重新传输。一个中等规模的 MCP 服务器接进来,工具列表动辄多出十几二十个,而且连接状态可能在会话中变化------缓存根本稳不住。
工具列表不应该无限膨胀,而且膨胀本身就是在破坏系统。
固定的 12 个核心工具
一个 Coding Agent 真正高频用到的工具并不多。文件读写 6 个(Read、Write、Edit、Glob、Grep、folder_operations),执行 1 个(Bash),Web 2 个(WebFetch、WebSearch),交互 2 个(Agent、AskUserQuestion),管理 1 个(TodoWrite)------总共 12 个,覆盖编码场景的绝大多数需求。
这 12 个工具始终对 LLM 可见,每轮请求都在工具列表里,顺序固定,缓存前缀稳定。
剩下的是长尾------Cron 定时任务、各种 MCP 外部服务(Slack、GitHub、Notion 等)、LSP 工具等。一次会话可能只用其中一个,也可能一个都不用。这些工具不该出现在工具列表里,但不能就此消失,需要能找到和执行它们。
这就是另外 2 个元工具的职责------SearchExtraTools 和 ExecuteExtraTool。
Agent 是怎么知道要填什么参数的
这是这套方案里最关键的一环,值得展开说清楚。
当 Agent 需要调用一个不在工具列表里的能力时,整个流程分两步走。
第一步是搜索。 Agent 调用 SearchExtraTools,传入一段自然语言描述,比如「发送 Slack 消息」。搜索引擎在所有延迟工具里检索,返回匹配度最高的几个结果。但这里的关键是------返回的不只是工具名,而是完整的工具定义,包括这个工具接受哪些参数、每个参数是什么类型、哪些是必填、每个参数的含义是什么。Agent 拿到的信息和这个工具「挂在工具列表里」时完全一样。
这就解决了「怎么知道填什么」的问题。Agent 不是靠猜,是读到了工具的完整规格书之后再决定怎么填的。如果搜到的工具不是它想要的,或者参数不够用,它可以继续搜索其他工具,或者直接告诉用户它没找到合适的。
第二步是执行。 确认了要调用哪个工具、参数是什么,Agent 调用 ExecuteExtraTool,把工具名和填好的参数打包传过去。框架在内部注册表里找到对应工具,透传参数执行,把结果原样返回给 Agent。整个过程 Agent 只需要两次工具调用,从外部看就是「搜索 + 执行」,比直接调用多一步,但信息是完整的。
这个设计有一个很重要的性质------它不依赖 Agent 事先知道工具的存在。Agent 只需要知道自己想做什么,搜索会负责把候选项找出来,工具定义会负责告诉 Agent 怎么用。
为什么不动态注册
搜到工具之后,把它动态加到 LLM 的工具列表里不就省事了,下次直接调用?
不行,原因还是缓存。工具列表是 Prompt Cache 前缀的一部分,这一轮 14 个工具,下一轮变成 15 个,前缀就断了,之前积累的缓存命中全部失效。代理执行多一次工具调用,换来的是工具列表永远稳定在 14 个,缓存前缀从不变动。这个交换是值得的。
MCP 服务器后续连接时,新工具会加入搜索索引,但不会出现在 LLM 的工具列表里。MCP 工具也不会出现在会话开始时注入的工具摘要里,因为 MCP 连接状态可能在会话中途变化,注入进去反而会让系统提示词不稳定,同样破坏缓存。
这套框架天然支持动态工具
延迟加载的架构有一个附带的好处------工具的注册和 LLM 的工具列表完全解耦。
Peri Code 的工具注册表是一张运行时可写的全局表。中间件在启动时往里注册工具,MCP 服务器连接时往里加工具,后续如果支持插件自定义工具,也是往同一张表里注册。工具进了注册表,搜索索引会自动更新,Agent 下次搜索就能找到它------全程不需要重启,不需要改工具列表,也不需要通知 LLM 有新东西进来了。
这意味着后续要支持用户自定义工具、工作流节点、甚至让 Agent 自己生成工具并注册,都可以建在同一套机制上------注册进来,能搜到,能执行,就完成了接入。LLM 侧看到的工具列表永远是那 14 个,不管后端挂了多少扩展。
工具少,LLM 反而更敢用
接入 MCP 之后,Peri Code 的工具总数没变,但 LLM 每轮看到的还是 14 个固定的工具。tool_choice 命中率回来了。
这件事有点像工作台整理------工具全摆出来,每次要找什么都要在一堆东西里翻,反而什么都不想动;工作台上只放常用的几件,需要特殊工具的时候去工具箱里拿,效率反而更高。
工具少不是功能少,是决策空间干净。