你的 MCP 服务器不需要把所有工具都塞进模型的上下文
我构建了一个 MCP(Model Context Protocol,模型上下文协议)服务器,让 AI 代理能操控我自己的 WhatsApp:搜索聊天记录、发消息、转写语音留言,而且完全自托管(self-hosted),所以我的聊天记录永远不会离开我的设备。不过最后发现,最有趣的问题跟 WhatsApp 没什么关系,而是工具的数量。
MCP 有一个没人提起的隐性代价:每次请求,它都会把所有工具的定义注入到模型上下文里,不管这次请求用不用得到。我的服务器慢慢涨到了 96 个工具。也就是说,模型还没来得及干活,上下文里就先堆了大约 2 万个 token。token 成本只是让人头疼,还不是真正的问题。当我把全部 96 个工具一次性丢给模型时,它选对工具的能力反而变差了,而不是变好。选项越多,越容易选错。
渐进式披露
最直接的做法是删掉一部分工具,但那会损失能力。所以我换了个思路:直接提供一小组热门的核心工具,长尾部分通过搜索来触达。
现在模型看到的是 29 个核心工具,外加两个元工具(meta-tool):
find_tool(query):对完整的 96 个工具库排序,返回工具名、描述和参数签名。call_tool(name, arguments):分发调用其中任意一个工具。
常驻成本从大约 2 万 token 降到了 8 千,而且没有任何工具被移除,每个工具都离一次搜索的距离。检索默认走词法匹配(IDF 逆文档频率 + 词干提取 + 同义词映射),如果配置了供应商密钥,还会混入嵌入向量(embedding)检索。
容易踩的坑
call_tool 是在进程内部直接分发的,绕过了正常工具调用时要经过的中间件链。所以它必须自己重新实现那条链本来会做的事:每个工具的作用域强制校验和审计日志。跳过这一步,你的检索层就会悄悄变成一个绕过作用域限制的后门,同时也是一个审计漏洞。一个跳过授权的工具路由器,比没有路由器更糟糕。
用数据说话,别凭感觉
「感觉更好了」不算数。我准备了一个小型的带标注评估集(用自然语言任务映射到标准工具的确定性标注),用来衡量检索能否落在正确的工具上。词法检索在对抗性措辞下 recall@8 大约在 75%;混入嵌入向量后还能更高。这个评估集很小,而且是我自己做的,所以把它当作方向参考,而不是确凿证据。
结论
如果你要把大语言模型(LLM)接入一个大型 API,工具列表是需要优先对待的设计问题,而不是事后才补的边角料。提供核心工具集,让其余部分可搜索,把鉴权逻辑镜像到分发路径里,并给检索效果一个量化指标。