MCP 工具能列出但模型在实际对话中不用,根因是工具的 description 字段过于模糊;好描述需明确输入输出格式和使用场景。
你的服务器搭好了。tools/list 返回了全部六个工具。检查器(inspector)每个都亮了绿灯。你把它接进一个真实的聊天对话,问了一个它本来能回答的问题,结果模型选了错误的工具。然后它干脆编了一个答案,而不是调用正确的工具。
什么都没坏。协议没问题。手动着调用工具也都正常。问题比 bug 更隐蔽:模型在技术上能看到的工具,和模型在正确时机真正使用的工具,是两码事。
以下是这种差距通常会以四种方式出现,以及如何逐一解决。
模型不是按工具名称来挑选工具的。它按描述来挑选。这就是整个游戏的核心,也是人们最容易跳过的部分。
一个名为 get_data、描述为"获取数据"的工具,在实践中是隐形的。模型根本不知道什么时候该用它,而不是其他五个多少也能"获取数据"的工具。所以它靠猜,或者干脆跳过。
// 模型对此无动于衷
{
"name": "get_data",
"description": "Fetches data"
}
// 模型非常清楚什么时候该用它
{
"name": "search_orders",
"description": "Find orders by customer email or order ID. Returns status, items, and ship date. Use this before answering any question about a specific order — do not guess from memory."
}
写描述是为了模型,不是为了写文档页面。要说明什么时候该调用它、返回什么、以及它和看起来相似的相邻工具如何区分。最后这一点比人们想象的更重要——大多数"模型调用了错误的工具"问题,本质上都是"两个描述听起来太像了"。
为了回答"有多少个待处理订单?"这个问题,一个工具返回了四千个 token 的原始 JSON。数字在里面,其他所有东西也都在里面。模型在垃圾堆里跋涉,半回答了问题,然后接下来的几次工具调用都开始变得敷衍——因为上下文里塞满了它根本不需要的冗余内容。
你的工具返回值不是数据库导出。它是模型下一个思考步骤的输入。你每传回一个 token,模型在后续每个回合都必须读一遍。
所以返回的是做出决策需要的信息,而不是你拥有的所有东西。如果问题是计数,就返回计数。如果是列表,就做分页并告知还有更多。加一个摘要字段让模型可以依赖,而不必解析原始行数据。回答精准的工具让整个对话保持敏捷。大量倾倒数据的工具会把后面的一切都拖下水。
你的输入 Schema 是一份契约。当它出错时,模型会履行自己的那部分,但服务器依然会崩。
常见版本:某个字段实际上是可选的,但 Schema 没有标注,所以模型忠实地编造了一个值来填充它。又或者某个字段实践中可以为 null,但没有任何说明,所以模型发送了一个字符串,你的处理器直接抛出异常。然后工具返回 500,附带一行模型无法处理的堆栈跟踪,它要么用同样错误的调用重试,要么直接放弃。
需要两个修复,而且两者缺一不可。让 Schema 说真话——标注哪些是真正必需的、哪些可以为 null,像你对工具本身做的那样,在每个字段上加一行描述。同时在入口处验证输入,这样当有什么仍然不对时,你返回的是模型能读懂并纠正的纯英文错误,而不是语言运行时的堆栈跟踪。
五个工具,第三个抛出了未处理的异常。如果这个异常向上冒泡变成硬错误,Agent 循环就直接停止了。一个不稳定的依赖项就能让模型陷入困境,面对一个它根本无法绕过的失败。
在工具内部捕获错误,把它转换成模型能读懂的形式返回:
{ "error": "Order service timed out. Try again, or ask the user to retry in a minute." }
现在模型有了选择。它可以重试、尝试不同的工具,或者直接告知用户。可读的错误让 Agent 保持运转。抛出的异常让它冻结。同样的底层故障——完全不同的上层体验。
测试中的陷阱:inspector 变绿只能说明工具能运行。它完全无法说明模型是否正确选择了它们。这是两个独立的问题,而且对用户来说真正重要的只有第二个。
所以要测试第二个:
运行真实的对话,而不是手动调用工具。观察模型在每一步调用了哪个工具。选错了?那几乎一定是描述问题。
记录模型发出的每一个工具调用——名称和参数。参数能告诉你模型认为你的 Schema 需要什么,而这往往和你以为的完全不是一回事。
抛一个模糊的请求给它——那种可能匹配两个工具的——看它是否能落到正确的那一个上。如果它像抛硬币一样随机选,你的两个描述还不够区分。
这个循环——真实聊天、观察选择、修复描述、再跑一遍——才是真正的工作。协议是最简单的部分。让模型按照你的意图使用你的工具,才是那个需要反复迭代的环节。
我帮团队构建 MCP 服务器并接入 Claude Code,目的是让模型真正使用他们的工具,而不仅仅是暴露工具——大部分时间都花在这里,远在 tools/list 变绿之后。
那么,一个诚实的问题:在你这里,模型一直拒绝调用的那个 MCP 工具是什么——重写它的描述最终解决问题了吗,还是它仍然被晾在一边无人问津?说说它是干什么的。我对描述有个猜测。