MCP 刚接入的时候,最让人兴奋的是“Agent 终于能调用外部系统了”。查文档、发消息、搜知识库、读用户记忆、跑审批、查工单、改配置,好像只要把能力包成工具,模型就能顺着自然语言一路把事情做完。
但工具一多,问题很快反过来。
真正难的不是“怎么让 Agent 有工具”,而是:当它面前有几十个、几百个工具时,怎么让它在当前问题里只看见该看的那几个。
这就是 MCP 爆炸之后,Agent 平台必须面对的工具筛选问题。
一、工具太多时,直接全量下发会发生什么
最朴素的做法,是把所有 MCP Server 暴露出来的工具都塞进模型上下文,让模型自己判断。
这个方案在 5 个工具以内通常没什么问题。一旦工具数量上来,就会开始出现四类症状。
第一,上下文被工具说明挤满。每个工具都有名称、描述、参数 schema、使用规则。几十个工具一展开,真正的用户问题、历史对话、业务上下文反而被压缩到很小。
第二,模型会被相似工具干扰。比如“查项目”“查文档”“查知识库”“查 API”“搜索资源”同时存在,描述又都写得很泛,模型很容易选错。
第三,工具误触发成本变高。读工具误触发还只是答错;写工具、消息工具、审批工具误触发就可能变成生产事故。工具越多,误调用的攻击面也越大。
第四,治理失焦。如果所有工具都平铺给模型,优先级、权限、连接状态、灰度范围、参数约束都只能靠提示词提醒。提示词可以表达意图,但不应该承担系统边界。
所以工具调用系统要先承认一个事实:模型擅长语义判断,但不适合作为第一层路由器。
模型应该在一个足够小、足够相关、边界清晰的候选池里做选择,而不是在全平台工具集里大海捞针。
二、工具选择不是一次判断,而是一条漏斗
一个比较稳的工具筛选链路,可以拆成三层筛选,外加一层执行兜底。
用户问题
↓
Layer 1:静态可用性过滤
↓
Layer 2:动态相关性筛选
↓
Layer 3:模型运行时决策
↓
Layer 4:工具执行兜底(超时、重试、权限、审计)
↓
结果回填
前三层是一条逐层收窄的漏斗,解决的是不同问题。
Layer 1 解决“这个工具现在能不能给这个用户看”。
Layer 2 解决“这个工具和当前问题有没有关系”。
Layer 3 解决“在这些相关工具里,当前这一步到底要不要调用、调用哪个、参数怎么填”。
Layer 4 不参与选工具,它是横切的兜底层:模型选完之后,超时、重试、权限复核、审计都归它管(详见第九节)。
不要把三件事揉成一句 Prompt:“请你根据用户问题选择合适工具”。这句话听起来聪明,工程上却太薄。真正上线后,任何一层缺失都会变成不可解释的误调用。
三、第一层:静态过滤,先把不能用的工具挡掉
第一层不需要模型参与,它应该是确定性的系统逻辑。
至少过滤这些维度:
- 工具是否启用;
- 所属 MCP 连接是否启用;
- 当前用户是否有权限;
- 工具是否处在灰度范围;
- 工具是否仅管理员可见;
- 工具是否因为健康检查失败而临时下线。
这层的关键是:不要让模型看到它不应该使用的工具。
如果一个普通用户没有权限调用“删除用户”“发全员消息”“重建索引”这类工具,最好的处理不是在 Prompt 里提醒“不要乱调用”,而是从候选工具列表里直接移除。模型看不见,就不会误选;客户端伪造调用,也会在服务端权限边界被拒绝。
这一层常见的数据结构大概是这样的:
type ToolRegistryItem = {
id: string;
type: "mcp" | "skill";
name: string;
description: string;
inputSchema: Record<string, unknown>;
priority: number;
keywords: string[];
connectionId?: string;
connectionName?: string;
visibility?: "all" | "admin";
isEnabled: boolean;
};
这不是给模型看的完整上下文,而是平台自己的工具目录。它的职责是维护工具的元数据、权限和生命周期。
这里有个很重要的设计分工:工具目录是单一可信源,Prompt 只是目录的一种投影。
如果后台关闭一个工具,模型下一轮就不该再看到它;如果权限收回,工具不该再进入候选池;如果 MCP 连接不可用,候选池也不该假装一切正常。工具治理要落在目录和执行层,不能只落在自然语言提示里。
四、第二层:动态筛选,给模型一个小而准的候选池
静态过滤后,仍然可能剩下很多工具。第二层要做的是根据当前用户问题,从工具目录里选出最相关的一小批。
这个阶段不一定要一上来就做复杂向量检索。很多企业内部工具选择,用一个可解释的打分模型就足够好:
| 维度 | 作用 | 示例 |
|---|---|---|
| 关键词匹配 | 捕捉业务黑话和高频表达 | “企微”“审批”“知识库”“工单” |
| 描述匹配 | 让自然语言问题命中工具说明 | 用户说“查接口文档”,描述里有“查询 API 文档” |
| 工具名匹配 | 支持用户直接点名工具 | search_docs、send_message |
| 连接名匹配 | 支持按系统或品牌路由 | “飞书”“企微”“权限中心” |
| 优先级 | 同类工具排序和兜底 | 新工具替代旧工具,高质量工具排前面 |
打分之后按“匹配分优先,优先级其次”排序,截取 Top-K。K 不宜太大,常见可以从 8 到 12 起步。太小会漏工具,太大又回到全量下发的问题。
一个简化版打分可以这么写:
function scoreTool(tool: ToolRegistryItem, message: string) {
let score = 0;
score += keywordScore(tool.keywords, message) * 50;
score += descriptionScore(tool.description, message) * 30;
score += nameScore(tool.name, message) * 10;
score += connectionScore(tool.connectionName, message) * 10;
return Math.min(score, 100);
}
这里的重点不是公式本身,而是几个工程原则。
关键词要来自业务语言。
工具作者经常喜欢写系统语言,比如“调用企业消息平台发送通知”。但用户说的是“帮我发个企微”“通知研发群”“催一下审批”。关键词字段就是拿来补齐这些业务叫法的。
描述要写给模型,不只是写给人。
“文档管理工具”这种描述太泛。更好的写法是:“根据关键词检索技术开放平台文档,返回文档标题、摘要、链接和匹配片段,适合回答 API、接入流程、平台规则类问题。”
模型能从这句话里判断适用范围,也能知道结果大概长什么样。
优先级不应该掩盖相关性。
高优先级工具不是永远优先,而是在相关性接近、或者需要补齐候选池时优先。否则一个全局高优先级工具会挤掉更相关的低优先级工具,最后变成“权重越高越误触发”。
候选池可以有 fallback,但要克制。
如果只有 2 个工具命中了,可以用高优先级工具补到 10 个,给模型一点探索空间。但如果没有任何匹配,直接返回全平台最高优先级工具也要谨慎。更好的做法是配合工具类型、场景入口、用户角色和历史上下文继续收缩,而不是把“没匹配上”解释成“随便给几个”。
五、第三层:模型只在候选池里做运行时决策
候选池进入模型后,才轮到 LLM 的语义能力发挥作用。
这一层要回答的问题更细:
- 当前问题是否真的需要工具;
- 如果需要,应该调用哪个;
- 是否缺必填参数;
- 参数应该来自用户输入、历史对话,还是上一步工具结果;
- 工具失败后能否继续,还是必须如实告诉用户失败。
这时 system prompt 里的工具调用规则就很重要。比如:
1. 优先使用高优先级工具。
2. 调用前检查必填参数。
3. 不要传空参数。
4. 参数只能来自用户输入、历史上下文或工具结果。
5. 缺少必填参数时先追问。
6. 工具失败时不能编造答案。
注意,这些规则不是第一层防线,而是第三层的行为约束。它们可以提高模型调用质量,但不能替代权限校验、工具过滤和执行兜底。
这也是很多 Agent 系统前期容易犯的错:把所有控制逻辑写进 Prompt,然后期待模型永远遵守。Prompt 是表达意图的好地方,不是系统安全边界。
六、能力介绍类问题要走特殊分支
有一类问题很特殊:
- “你能做什么?”
- “有哪些可用 MCP?”
- “列一下工具列表。”
- “文档管理是什么?”
- “邮件能力怎么用?”
这类问题不是要调用某个具体工具,而是要介绍工具目录本身。
如果仍然走普通 Top-K,可能会出现一个尴尬结果:用户问“你有哪些工具”,筛选器只返回了和“工具”两个字相关的少数工具,模型反而看不到完整能力列表。
所以能力介绍类问题应该单独识别,返回更完整的工具清单,让模型基于目录介绍能力,而不是调用工具。
识别本身不需要很复杂。实用的做法是在动态筛选之前加一条意图规则:命中“你能做什么”“有哪些工具 / 能力 / MCP”“XX 是什么 / 怎么用”这类固定句式,就直接走能力介绍分支,返回按场景分组的完整目录。句式覆盖不住的长尾,再用一个轻量意图分类兜底。
有一点不能省:这条分支同样要过第一层静态过滤。介绍给用户的能力清单,永远只是他有权限看到的那部分,而不是全平台目录。
这类分支看起来像小优化,实际很重要。因为它决定了用户第一次认识 Agent 的方式。如果第一次问“你能干什么”,Agent 就答得支离破碎,后面再强也很难建立信任。
七、工具元数据比模型参数更重要
MCP 工具筛选做不好,很多时候不是模型不行,而是工具元数据写得太差。
新增一个工具时,至少要认真写四样东西。
1. name:稳定、短、可辨认。
工具名不一定要给最终用户看,但模型会看。query、search、run 这种名字太空。search_tech_docs、send_wecom_message、get_api_key_usage 会好很多。
2. description:说明适用场景和返回结果。
不要只写“查询数据”。要写清楚查询什么数据、适合回答什么问题、返回哪些关键字段。
坏例子:
查询用户信息。
好例子:
根据工号或姓名查询员工基础信息,返回姓名、部门、岗位、邮箱和在职状态。适合回答人员归属、联系方式和组织关系类问题。
3. keywords:补业务黑话。
description 适合完整说明,keywords 适合放用户口语、缩写、系统别名、历史叫法。比如同一个“企业微信消息”工具,关键词里可以放“企微”“群通知”“消息推送”“催办”。
4. inputSchema:把必填参数说清楚。
模型能不能正确补参数,很大程度取决于 schema。字段名要稳定,描述要具体,required 要真实。一个错误的 required,会让模型要么频繁追问,要么传空值硬调。
工具元数据不是配置杂项,而是 Agent 的路由索引。它写得越清楚,模型越像在用一张地图;它写得越含糊,模型越像在猜谜。
八、系统型 MCP 不一定要暴露成普通工具
不是所有 MCP 调用都应该交给模型临场决定。
比如用户长期记忆、身份注入、审计日志、会话摘要、健康检查,这些更像 Agent 运行时基础设施。它们有固定触发点:请求前加载、请求后沉淀、定时补偿、失败重试。
以长期记忆为例,更稳的做法通常是:
对话开始前:加载用户画像和长期记忆,注入上下文
对话过程中:模型按需调用 search_user_memory 做语义检索
对话结束后:异步提取持久事实,写入本地记忆或同步到 MCP
定时任务中:扫描遗漏记录,做补偿投递
这里至少有两个不同性质的工具。
list_user_memory 更像上下文加载的一部分,可以由系统固定调用。
search_user_memory 更像模型按需使用的检索工具,可以进入候选池。
ingest_user_memory 则更像对话结束后的异步副作用,不一定应该让模型每轮都看见。
这条边界如果不画清楚,模型会被迫决定太多“平台内部事务”。结果就是该稳定发生的事变成偶发,该偶发调用的事又变成每轮都触发。
一个实用判断是:
如果某个 MCP 调用是为了让 Agent 运行环境更完整,它应该在系统编排层;如果它是为了回答用户当前问题,它才应该进入工具候选池。
九、执行层要兜住模型的不确定性
模型选中了工具,不代表系统就可以裸调。
执行层至少要有这些保护:
- 超时控制:每个工具有默认超时,也允许按工具配置;
- 重试机制:只对网络错误、限流、临时 5xx、会话过期等可恢复错误重试;
- 会话重连:MCP session 过期或 401 时,清理缓存后重连一次;
- 结果格式化:把 MCP 标准 content 结构解析成模型容易读的结构;
- 调用日志:记录用户、工具名、输入、输出、状态、耗时和错误;
- 失败显式返回:工具失败就是失败,不能让模型拿常识补答案。
这层的职责很像数据库事务边界:上游可以灵活,下游必须确定。
尤其是写类工具,更要把权限、幂等、审计、二次确认这些能力放在执行层,而不是相信模型“应该不会乱调”。
十、新增一个 MCP 工具时,先问这 12 个问题
工具爆炸通常不是一天发生的,而是每次“再加一个工具”慢慢堆出来的。所以最有效的治理点,就是新增工具时的准入清单。
我会建议每个新 MCP 工具上线前问 12 个问题:
- 这个工具解决的是用户问题,还是平台内部编排问题?
- 它应该暴露给模型,还是由系统固定触发?
- 哪些用户能看见它,哪些用户能调用它?
- 它是 read、write,还是 dangerous 动作?
- 工具名能不能和同类工具明显区分?
- description 是否写清楚适用场景和返回结果?
- keywords 是否覆盖业务叫法、缩写和系统别名?
- inputSchema 的 required 是否真实,字段描述是否足够具体?
- 有同类工具时,它的 priority 应该排在谁前面?
- 调用失败时,用户应该看到什么错误信息?
- 输出结果会不会过大,是否需要摘要化或分页?
- 它的调用日志是否足够支撑审计和问题排查?
这 12 个问题看起来像流程,其实是在保护模型。你把工具边界定义得越清楚,模型需要猜的东西就越少。
十一、一个可落地的整体架构
把上面的逻辑合起来,一个工具选择系统大概会长这样:
Tool Registry
- MCP 工具元数据
- Skill 元数据
- 权限与可见性
- 优先级与关键词
- 连接健康状态
Tool Selector
- 静态过滤
- 动态打分
- Top-K 候选池
- 能力介绍特殊分支
Prompt Builder
- 注入候选工具描述
- 标注必填参数
- 标注优先级
- 注入工具使用规则
LLM Runtime
- 判断是否调用工具
- 选择工具
- 填写参数
- 多步调用
Tool Executor
- 权限复核
- 超时和重试
- MCP 调用
- 结果解析
- 日志审计
这套架构里,模型不是被削弱了,反而是被放到了更适合的位置。
它不需要维护全局工具目录,不需要记住每个用户的权限,不需要判断连接是否健康,也不需要承担审计责任。它只需要在当前问题、当前上下文、当前候选工具之间做语义决策。
这才是 LLM 最擅长的部分。
小结
MCP 让 Agent 获得了连接外部世界的标准方式。但当工具数量开始增长,平台真正要建设的就不只是 MCP Server,而是一套工具选择和治理系统。
我更喜欢把这件事总结成一句话:
MCP 解决“工具怎么接进来”,工具筛选解决“工具什么时候该出现”。
前者让 Agent 有手,后者让 Agent 不乱伸手。
一套可用的工具筛选逻辑,至少要包含四个层次:静态过滤管可用性,动态匹配管相关性,模型决策管语义选择,执行层兜住超时、重试、权限和审计。
当这几层边界画清楚之后,工具数量增长就不再必然带来混乱。相反,每个新 MCP 工具都能进入一个可解释、可观测、可治理的系统里,成为 Agent 能力的一部分,而不是上下文里的噪音。