# Peri Code 的工具分层——LLM 面对 50 个工具时会停止调用工具

> **[Peri Code](https://github.com/konghayao/peri)** — 用 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 命中率回来了。

这件事有点像工作台整理——工具全摆出来，每次要找什么都要在一堆东西里翻，反而什么都不想动；工作台上只放常用的几件，需要特殊工具的时候去工具箱里拿，效率反而更高。

工具少不是功能少，是决策空间干净。

项目地址：[github.com/konghayao/peri](https://github.com/konghayao/peri)
