MCP Client实战翻车记:两个Server同时跑,AI调错了Tool

📅 2026/7/21 8:13:08
MCP Client实战翻车记:两个Server同时跑,AI调错了Tool
前面写完 MCP Server 那篇文章之后我又接了个需求让一个 Agent 同时调两个 MCP Server。一个查 SQLite 数据库一个查本地文件系统。Agent 跑了一轮返回了一个答案。我看着答案觉得不太对对了一下原始数据——AI 给出了文件系统里的张三但用户问的是数据库里张三的订单记录。不是 Server 写错了不是模型不行。是 AI 调错了 Tool。两个 Server 都有 search 方法AI 选了文件系统的 search但它应该调数据库的 query。这时候我才真正理解 MCP Client 是干什么用的——它不是简单的连接器而是一个翻译官把多组 Server 的能力转译成 AI 能正确理解的语言。Client 到底是干什么的先花最短的时间说清楚角色分工。MCP ServerMCP Client职责提供 Tool 给外部调用把 Tool 翻译给 AI 理解类比一个 API 接口API 文档 调用说明书出问题的地方Tool 内部逻辑出错AI 选错了 Tool / 参数传不对管理粒度每个 Server 独立运行一个 Client 可对接多个 ServerServer 只负责我有这个能力Client 负责AI 怎么知道该用哪个。这个区分听起来简单但只有当你手上有两个以上 Server 的时候才知道 Client 的设计直接决定了 AI 会不会乱来。翻车现场还原先说场景。我在本地分别启动了 Node.js 写的两个 Server一个暴露了 search、query、insert 三个 Tool 来操作 SQLite另一个暴露了 search、read、write 来做文件读写。然后写了一个 Client 把它们连起来。以下代码基于modelcontextprotocol/sdk0.6.x 版本。第一版 Client 长这样import{Client}frommodelcontextprotocol/sdk/client/index.js;import{StdioClientTransport}frommodelcontextprotocol/sdk/client/stdio.js;constdbClientnewClient({name:database-client});constfileClientnewClient({name:filesystem-client});awaitdbClient.connect(newStdioClientTransport({command:node,args:[db-server.mjs]}));awaitfileClient.connect(newStdioClientTransport({command:node,args:[file-server.mjs]}));// 列出各 Server 的 ToolconstdbToolsawaitdbClient.listTools();constfileToolsawaitfileClient.listTools();代码看起来没毛病对吧两个 Client 实例分别连接不同的 Server各管各的。但是问题出在 AI 这一侧——当我把两个 Server 的 Tool 列表合并给 LLM 时AI 看到的工具列表是这样的可用工具 - search来自 database-server - query来自 database-server - insert来自 database-server - search来自 filesystem-server - read来自 filesystem-server - write来自 filesystem-server两个 search。同名。LLM 选 search 的时候你不能保证它一定选数据库的那个。事实上我跑了三组对话测试其中有两次 AI 选了文件系统的 search。“张三这个关键词在文件里确实有——某个文档里提到了这个名字——但用户问的是张三的订单”应该在数据库里查 order 表。AI 拿到文件名和对应的内容片段以为那就是答案。这个场景其实挺典型。MCP 协议本身不限制 Tool 命名唯一性Server 开发者各自命名自己的 Tool撞名是常态。问题是LLM 在做 Tool 选择时依赖的是 Tool name description 的语义匹配。当两个 Tool 的名字一模一样description 又都跟搜索相关模型大概率猜错。翻车之后怎么修发现问题后第一个想法是给 Tool 名字加上命名空间前缀。classNamespaceClient{constructor(namespace,client){this.namespacenamespace;this.clientclient;}asynclistTools(){consttoolsawaitthis.client.listTools();returntools.map(tool({...tool,name:${this.namespace}_${tool.name},description:[${this.namespace}]${tool.description}}));}asynccallTool(name,args){constoriginalNamename.replace(${this.namespace}_,);returnthis.client.callTool(originalName,args);}}constdbClientnewNamespaceClient(db,originalDbClient);constfileClientnewNamespaceClient(fs,originalFileClient);// 现在 AI 看到的列表变成了// - db_search// - db_query// - db_insert// - fs_search// - fs_read// - fs_write加上前缀之后AI 看到的就是 db_search 和 fs_search名字不同选错的概率降了很多。我跑了七八轮测试没有再出现调错 Tool 的情况。不过这里有个细节值得说——description 也要改。光是改名字不够因为 LLM 选 Tool 时 description 权重很高。我在 description 前面加了[db]和[fs]标签相当于给 AI 一个视觉锚点。后面我翻过一些文章有人用 XML 标签、有人用 Emoji我试了一圈觉得纯文本前缀最稳定模型解析出错率最低。翻车二一个 Server 挂了全链路卡死修完命名冲突之后我以为这件事搞定了。直到第二个问题冒出来。数据库 Server 那边有一次查询跑了很久——那张表数据量到了一定规模索引没有建好一次模糊查询拖了近两分钟。Client 一直在等 db_server 返回file_server 的服务也跟着没法继续。我查了一下日志才发现问题Client 是串行处理 Tool 调用的。一个 Server 的 Tool 没返回后续的调用全部排队等着。这是一个设计上的取舍。MCP Client 默认不隔离不同 Server 的超时行为一个慢 Server 会拖慢整个链路。不是每次都会遇到但遇到就卡死整条链路。修复方式很直接——给每个 Server 配独立超时import{Client}frommodelcontextprotocol/sdk/client/index.js;constdbClientnewClient({name:database-client},{transportTimeout:8000}// 8秒超时);constfileClientnewClient({name:filesystem-client},{transportTimeout:3000}// 文件操作一般快得多);配了超时之后db_server 那次慢查询在 8 秒后被 Client 主动中断file_server 的调用正常执行。当然超时本身不是完美的方案——超时意味着那个 Tool 调用失败了AI 需要重试或者走 fallback。但至少不会让其他 Server 跟着陪葬。后来我又加了一层更细的隔离给每个 Server 的 Tool 调用包了一层 try/catch让一个 Server 的失败不会传播到另一个。asyncfunctionsafeCall(client,toolName,args){try{returnawaitclient.callTool(toolName,args);}catch(err){console.error([${client.id}] Tool${toolName}failed:,err.message);return{error:true,message:暂无法访问${client.id}};}}其实就是加了个错误边界但效果很明显——一个 Server 挂掉不会影响另一个。多 Server 管理的决策框架写完这个项目之后我自己整理了一个判断逻辑什么场景下用什么策略场景推荐方案原因两个 Server 功能完全不重叠命名空间前缀直接拆简单互不干扰Server 数量 3 但功能有交集代理聚合模式统一入口减少 AI 选择成本有 Server 偶尔超时/不稳定独立超时 try/catch 隔离一个挂了不拖累全局多个 Server 服务同一场景合并成一个 Server减少跨 Server 通信外部不可控 Server第三方包装层 降级策略不能假设第三方永远可用代理聚合模式是什么就是用一层代理 Server 包装下面多个子 Server 的 Tool对外只有一个入口。子 Server 的 Tool 全部通过代理转发Client 只需要连一个代理 Server 就行。适合 Server 数量多的时候减少 AI 面对的选择空间。这个方案的边界以上做法能解决大部分多 Server 集成问题但不是银弹。命名空间前缀有一类场景搞不定——当 LLM 需要跨两个 Server 的数据做推理时。比如对比数据库里张三的订单和文件系统里张三的简历AI 需要同时调 db_search 和 fs_search两次结果合并做分析。前缀方案只能防选错不能加速跨 Server 协作。跨 Server 数据融合是另一个话题了可能需要 Client 侧做一层缓存或结果聚合。这个我还没完全想好目前遇到的对比例子不多方案还不够成熟。另外超时配置的数值得根据实际场景调。我设的 8 秒和 3 秒是基于本地测试的放到线上环境网络延迟不同要重新压测。具体设多少没有万能公式我自己的做法是先设成 5 秒跑一周看日志如果有 Tool 频繁超时就调大如果服务器响应都很稳定就逐步缩紧。你现在就可以做的一件事打开你的 MCP 配置文件一般是mcp.json或claude_desktop_config.json看看有没有配多个 Server。如果有检查一下它们的 Tool 名字——有没有同名的如果有加个前缀花不了五分钟但能省掉后面排查AI 为什么拿错数据的时间。我当时就是觉得两个 search 应该没关系吧——结果查了接近两小时才发现是这个原因。这个亏吃一次就够了。