1. 为什么 MCP Tool 的设计逻辑和普通 API 完全不同
1.1 一个被大多数人忽略的前提:调用者不是人
做后端开发的人都有一个肌肉记忆:设计接口的时候,脑子里想的是“前端工程师怎么调这个接口”“文档怎么写才清楚”“参数命名怎么才规范”。这套思维模式用了十几年,没什么问题。但 MCP Tool 的出现把这件事彻底搅了。
MCP(Model Context Protocol)工具的本质是什么?它是一个让 AI Agent 能够调用外部能力的标准化接口。注意关键词——调用者是 Agent,不是程序员。
这个区别有多大?我举个例子你就明白了。
你设计一个 RESTful API,返回一个 JSON 对象,里面有user_id、user_name、created_at、status四个字段。程序员看到这个结构,秒懂。但 Agent 看到的是什么?它看到的是一段文本描述加上一个 JSON Schema。它没有“经验直觉”,没有“行业常识”,它只有你给它的那些文字。你写“获取用户信息”,它可能理解成“获取当前登录用户”,也可能理解成“根据 ID 查用户”,还可能理解成“获取用户列表”。它不知道你的意图,它只能猜。
所以 MCP Tool 设计的第一个核心原则就是:你不是在写接口文档,你是在给一个极其聪明但完全没有任何背景知识的实习生写操作手册。这个实习生智商很高,学习能力极强,但它对你的系统一无所知,对你的业务术语一无所知,对你的隐含约定一无所知。
1.2 Agent 调用工具时的真实决策链路
要设计好 MCP Tool,你得先搞清楚 Agent 在调用一个工具时,内部到底经历了什么。根据我实际搭建和调试多个 Agent 系统的经验,一个典型的工具调用决策链路大致是这样的:
- 意图识别:Agent 先理解用户说了什么,判断是否需要调用工具
- 工具匹配:从可用工具列表中筛选出可能相关的工具
- 工具选择:在候选工具中选出最合适的那一个
- 参数构造:根据工具的输入 Schema 构造调用参数
- 结果解析:拿到工具返回结果后,理解结果含义
- 后续决策:判断是否需要继续调用其他工具,还是直接回复用户
这六个步骤里,每一步都可能因为工具设计不当而出错。而且最要命的是,Agent 出错的时候不会像程序员那样报一个“参数缺失”的异常然后去翻文档,它可能会:
- 选错工具(该用 A 结果用了 B)
- 参数填错(格式不对、类型不对、语义不对)
- 结果误读(返回的数据理解偏了)
- 死循环(反复调用同一个工具但解决不了问题)
我见过太多团队把已有的 REST API 直接套一层壳就当成 MCP Tool 发布出去,结果 Agent 用得一塌糊涂。问题不在 Agent 不够聪明,在于工具设计者根本没有站在 Agent 的视角思考问题。
1.3 高质量 MCP Tool 的四个核心标准
那什么样的 MCP Tool 算是“高质量”?我总结下来有四个标准,后面会逐一展开讲:
第一,可发现性。Agent 能在需要的时候“想到”你这个工具。这要求工具的名称和描述必须精准命中 Agent 的语义理解空间。
第二,可理解性。Agent 看到你的工具定义后,能准确知道它是干什么的、什么时候该用、什么时候不该用。
第三,可调用性。Agent 能一次性构造出正确的参数,不需要反复试错。
第四,可恢复性。当调用失败或结果异常时,Agent 能理解发生了什么,并做出正确的后续决策。
这四个标准看起来简单,但每一个背后都有一堆设计细节。接下来我会逐个拆解。
2. 工具描述:Agent 唯一能看到的“说明书”
2.1 描述文本的写法决定了 Agent 会不会选你
在 MCP 协议里,每个 Tool 都有一个description字段。很多人把它当成注释随便写两句就完事了。但实际上,这段描述是 Agent 决定是否调用你的核心依据。
我做过一个对比实验:同一个功能,写两种不同的描述,观察 Agent 的工具选择准确率。
| 描述写法 | 示例 | Agent 选择准确率 |
|---|---|---|
| 模糊描述 | “查询数据” | 42% |
| 精确描述 | “根据城市名称查询当前天气,包括温度、湿度、风力。当用户询问某个城市的天气状况时使用” | 89% |
差距一目了然。模糊描述的问题在于,Agent 不知道你查的是什么数据、什么条件下该用你、用了之后能得到什么。它只能靠猜。
那好的描述应该包含哪些要素?我的经验是四个:
- 功能定义:这个工具做什么(一句话说清楚)
- 使用场景:什么时候该用这个工具(这是最关键的)
- 输入说明:需要什么参数,每个参数的含义
- 输出说明:返回什么结果,结果的格式和含义
2.2 使用场景描述:告诉 Agent “什么时候该想起我”
大部分人在写工具描述的时候,只写了“这个工具能做什么”,但忽略了“什么时候该用”。这两件事看起来差不多,实际上对 Agent 来说差别巨大。
举个例子。假设你有一个查询订单状态的工具。
只写功能的版本:
查询订单状态信息Agent 看到这个描述,知道它能查订单。但问题来了:用户说“我买的东西到哪了”,Agent 能匹配上吗?用户说“帮我看看上周下的单”,Agent 知道该用这个工具吗?用户说“我的包裹怎么还没来”,Agent 能联想到订单查询吗?
加上使用场景的版本:
根据订单号查询订单的当前状态和物流信息。 适用场景:用户询问订单进度、物流状态、包裹位置、配送时间、 订单是否已发货等问题时使用。 不适用场景:用户需要修改订单、取消订单、退款时,请使用对应的 订单操作工具。这个版本不仅告诉了 Agent “我能干什么”,还告诉了它“用户说哪些话的时候你应该想到我”,甚至告诉了它“哪些情况不要用我”。这就是场景锚定——把工具和用户可能的表达方式建立关联。
2.3 负面边界:告诉 Agent “什么时候别用我”
这一点是很多人完全忽略的。你不仅要告诉 Agent 什么时候该用你,还要告诉它什么时候不该用你。
为什么?因为 Agent 在工具选择时,倾向于“过度匹配”。它宁可多调一个工具,也不愿意漏掉一个可能相关的工具。这就导致了很多误调用。
比如你有一个“发送邮件”的工具,描述里只写了“发送邮件给指定收件人”。用户说“帮我看看有没有新邮件”,Agent 可能就会调用这个发送邮件的工具——因为“邮件”这个词匹配上了。
如果你在描述里加上一句“此工具仅用于发送邮件,不用于查询或接收邮件。查询邮件请使用 query_email 工具”,Agent 就不会搞混了。
实操心得:负面边界的描述不需要很长,一两句话就够。关键是要覆盖那些容易混淆的场景。我通常会在写完正面描述后,自己想一下“Agent 可能会在什么情况下误用这个工具”,然后把这些情况写进负面边界里。
2.4 描述长度的取舍:太短没用,太长干扰
有人可能会想:那我描述写得越长越详细不就好了?
并不是。描述太长会带来两个问题:
第一,占用上下文窗口。每个工具的描述都会占据 Agent 的上下文。如果你有 20 个工具,每个描述 500 字,那就是 10000 字的上下文消耗。这会挤压 Agent 处理用户实际需求的空间。
第二,关键信息被稀释。描述太长的时候,Agent 的注意力会被分散。真正重要的“使用场景”和“参数说明”可能被淹没在一堆无关紧要的细节里。
我的经验值是:单个工具的描述控制在 100-300 字之间。核心信息(功能、场景、参数、输出)必须覆盖,但不要写废话。如果你发现描述写得太长,大概率是这个工具的功能太复杂了,应该考虑拆分成多个工具。
3. 参数设计:让 Agent 一次就能填对
3.1 参数命名的语义清晰度
参数命名这件事,给程序员看和给 Agent 看,要求完全不同。
给程序员看的时候,你可以用缩写、可以用行业惯例。比如uid、ts、cfg,程序员一看就懂。但 Agent 看到uid的时候,它需要从上下文中推断这是user_id还是unique_id还是别的什么。
所以 MCP Tool 的参数命名原则是:完整、明确、无歧义。
| 差的命名 | 好的命名 | 原因 |
|---|---|---|
| uid | user_id | 消除歧义 |
| ts | timestamp | 完整拼写 |
| q | search_query | 明确含义 |
| n | max_results | 表达用途 |
| type | order_type | 限定范围 |
看起来是小事,但实际测试中,参数命名从缩写改成全称后,Agent 参数填充的准确率能提升 15%-20%。这个提升非常可观。
3.2 参数类型的陷阱:为什么 string 比 integer 更安全
这是一个反直觉的结论:在 MCP Tool 的参数设计中,能用 string 就不要用 integer 或 boolean。
为什么?因为 Agent 生成参数值时,本质上是在生成文本。当它需要生成一个整数时,它需要做一次“文本到整数”的转换。这个转换大多数时候没问题,但在边界情况下容易出错。
比如一个参数是year(年份),类型是 integer。用户说“查一下去年的数据”,Agent 需要:
- 知道当前是哪一年
- 做减法算出“去年”
- 把结果转成整数
每一步都可能出错。但如果类型是 string,Agent 直接输出“2025”就行了,少了一步转换。
再比如 boolean 类型。用户说“帮我查一下已完成的订单”,Agent 需要把“已完成”映射到true。但如果参数名是is_active,Agent 可能会困惑:“已完成”对应is_active=true还是is_active=false?这种语义映射很容易出错。
我的建议是:
- 对于枚举值,用 string 类型 + enum 约束
- 对于数值,如果范围不大,考虑用 string 类型 + pattern 约束
- 对于布尔值,考虑用 string 类型 + enum ["yes", "no"] 或 ["true", "false"]
注意:这不是说所有参数都要用 string。对于明确的计算类参数(如数量、价格),integer 和 number 类型是合适的。关键是要评估 Agent 在生成这个参数值时,需要做多少“推理转换”。转换步骤越多,出错概率越大。
3.3 必填与选填的博弈:少即是多
很多工具设计者习惯把参数分成必填和选填,选填参数给一堆默认值。这在给程序员用的 API 里没问题,但在 MCP Tool 里会带来麻烦。
问题在于:Agent 看到选填参数时,会倾向于去填它。哪怕这个参数不是必须的,Agent 也会尝试构造一个值。如果构造的值不对,就会导致调用失败或结果异常。
我踩过的一个坑:设计了一个搜索工具,有一个选填参数sort_by(排序方式),默认按相关性排序。结果 Agent 每次调用都会填这个参数,有时候填"relevance",有时候填"相关度",有时候填"default"。各种五花八门的值,导致后端经常报错。
后来我把sort_by从参数列表里去掉了,排序逻辑固定在后端。Agent 的调用成功率立刻上去了。
所以我的原则是:如果一个参数 90% 的情况下都用默认值,那就不要把它暴露给 Agent。让 Agent 做更少的决策,就是让系统更稳定。
3.4 参数描述的写法:每个参数都要有“说明 + 示例”
参数的description字段同样重要。一个好的参数描述应该包含:
- 这个参数是什么
- 格式要求是什么
- 举个例子
比如:
{ "name": "city", "type": "string", "description": "城市名称,使用中文全称。例如:北京、上海、广州。不要使用拼音或英文。", "required": true }这个描述告诉了 Agent 三件事:参数含义(城市名称)、格式要求(中文全称)、示例(北京、上海、广州)。Agent 拿到这个描述,基本不会填错。
对比一个差的参数描述:
{ "name": "city", "type": "string", "description": "城市", "required": true }Agent 看到这个,可能会填“Beijing”,可能会填“北京市”,可能会填“bj”。不确定性太大了。
4. 返回值设计:让 Agent 能“读懂”结果
4.1 返回结构的稳定性比丰富性更重要
很多工具设计者在设计返回值时,总想返回尽可能多的信息。查询用户信息,恨不得把用户的所有字段都返回。查询订单,恨不得把订单的每一个细节都塞进去。
但 Agent 处理返回结果的能力是有限的。返回的数据越多,Agent 解析的负担越重,出错的可能性越大。
我的经验是:返回值应该只包含 Agent 完成当前任务所必需的信息。多余的字段不仅没用,还会干扰 Agent 的判断。
更重要的是,返回结构必须稳定。什么叫稳定?就是同样的调用,返回的 JSON 结构应该是一致的。不能这次返回{"data": {...}},下次返回{"result": {...}}。Agent 会根据第一次调用的结果来理解后续调用的结果,如果结构变了,它就会懵。
4.2 错误信息的写法:给 Agent 修复的机会
工具调用失败是常态。网络超时、参数错误、权限不足、资源不存在——各种情况都可能发生。关键在于:当调用失败时,返回的错误信息能不能让 Agent 知道下一步该怎么做。
差的错误信息:
{"error": "E4001"}Agent 看到这个,完全不知道发生了什么。它只能重试,或者放弃。
好的错误信息:
{ "error": "CITY_NOT_FOUND", "message": "未找到城市'北金'。请检查城市名称是否正确,使用中文全称,例如:北京、上海。", "suggestion": "请确认城市名称拼写,或换一个城市重试。" }这个错误信息告诉了 Agent:发生了什么(城市没找到)、为什么(名称可能拼错了)、怎么办(检查拼写或换城市)。Agent 拿到这个信息,就能做出正确的修复动作。
实操心得:我在设计错误返回时,会模拟 Agent 的思考过程——“如果我是一个 Agent,看到这个错误信息,我知道下一步该干什么吗?”如果答案是否定的,这个错误信息就需要重写。
4.3 分页与截断:数据量大时怎么返回
当工具返回的数据量很大时(比如搜索结果有几百条),你不能一次性全返回。这时候就需要分页或截断。
但分页对 Agent 来说是个麻烦事。Agent 需要理解“还有更多数据”这个概念,然后决定是否继续请求下一页。很多 Agent 在处理分页时会陷入循环——反复请求下一页,直到耗尽上下文窗口。
我的建议是:
- 优先截断,而非分页。返回前 N 条最重要的结果,并在返回中说明“共找到 X 条结果,已返回前 N 条”。
- 如果必须分页,提供明确的终止信号。比如返回
has_more: false时,Agent 就知道不用再请求了。 - 给 Agent 一个“获取更多”的工具,而不是让它自己翻页。比如
get_more_results(cursor)这样的工具,Agent 只在需要时才调用。
4.4 返回值的“可读性”优化
Agent 处理结构化数据的能力很强,但处理自然语言描述的能力更强。在某些场景下,把结构化数据转成自然语言描述,反而能提升 Agent 的理解准确率。
比如查询天气,返回:
{"temp": 25, "humidity": 60, "wind": "3级", "condition": "晴"}Agent 需要解析这个 JSON,理解每个字段的含义,然后组织成自然语言回复用户。
但如果返回:
{"summary": "当前天气晴,气温25摄氏度,湿度60%,风力3级。"}Agent 直接拿这个 summary 就能回复用户,少了一步解析和重组的过程。
当然,这不是说所有返回值都要转成自然语言。对于需要 Agent 做进一步计算或判断的场景,结构化数据更合适。关键是要根据 Agent 的后续动作来决定返回格式。
5. 工具粒度:拆得越细越好吗
5.1 粗粒度 vs 细粒度的权衡
工具粒度是 MCP Tool 设计中最容易纠结的问题之一。
粗粒度工具:一个工具干很多事。比如manage_order(action, order_id, ...),通过 action 参数来区分是查询、取消还是修改。
细粒度工具:每个功能一个工具。比如query_order(order_id)、cancel_order(order_id)、modify_order(order_id, changes)。
两种方式各有优劣:
| 维度 | 粗粒度 | 细粒度 |
|---|---|---|
| Agent 选择难度 | 低(工具少) | 高(工具多) |
| 参数构造难度 | 高(参数多且复杂) | 低(参数少而明确) |
| 描述清晰度 | 低(一个描述要覆盖多种功能) | 高(每个描述聚焦一个功能) |
| 错误处理 | 复杂(不同 action 的错误混在一起) | 简单(每个工具的错误独立) |
| 扩展性 | 差(加功能要改参数) | 好(加功能就加工具) |
我的经验是:优先细粒度,但控制工具总数。
细粒度工具对 Agent 更友好,因为每个工具的功能单一、描述清晰、参数简单。Agent 选择起来更容易,构造参数也更准确。
但工具总数不能太多。根据我的测试,当工具数量超过 15-20 个时,Agent 的选择准确率会明显下降。所以如果功能很多,需要做分组,或者用“路由工具”来引导 Agent。
5.2 什么时候该拆,什么时候该合
具体到操作层面,我通常用这几个标准来判断:
该拆的情况:
- 不同功能的参数差异很大
- 不同功能的返回结构不同
- 不同功能的使用场景明显不同
- 不同功能的错误处理方式不同
该合的情况:
- 功能之间高度相关,经常需要一起调用
- 参数高度重叠,只是行为略有不同
- 拆开后工具数量会超过阈值
举个例子。一个电商系统里,“查询订单”和“取消订单”应该拆开,因为参数不同(查询只需要订单号,取消还需要取消原因)、返回不同、错误处理不同。但“按订单号查询”和“按用户查询”可以考虑合并成一个query_order工具,通过可选参数来区分。
5.3 工具命名:让 Agent 一眼看懂
工具名称是 Agent 最先看到的信息。一个好的工具名称应该:
- 动词开头:表明这是一个动作,如
query_、create_、update_、delete_ - 名词在后:表明操作的对象,如
_order、_user、_product - 避免缩写:不要用
qry_ord这种缩写 - 保持一致性:所有工具用同一套命名规范
好的命名示例:
query_order_statuscreate_user_accountupdate_shipping_addresscancel_pending_order
差的命名示例:
getData(太泛)orderOp(含义不明)doStuff(完全不知道干什么)
6. 实战:从零设计一个高质量 MCP Tool
6.1 需求分析与工具拆分
假设我们要为一个任务管理系统设计 MCP Tool。系统支持:创建任务、查询任务、更新任务状态、删除任务、按条件搜索任务。
首先做工具拆分。按照细粒度原则,初步方案是 5 个工具:
create_task- 创建新任务query_task- 根据 ID 查询任务详情update_task_status- 更新任务状态delete_task- 删除任务search_tasks- 按条件搜索任务
5 个工具,数量合理。接下来逐个设计。
6.2 每个工具的描述与参数设计
工具 1:create_task
{ "name": "create_task", "description": "创建一个新的任务。当用户需要添加待办事项、新建任务、创建工单时使用此工具。注意:此工具仅用于创建新任务,不用于修改已有任务。", "parameters": { "title": { "type": "string", "description": "任务标题,简洁描述任务内容。例如:完成季度报告、修复登录bug。", "required": true }, "description": { "type": "string", "description": "任务的详细描述,可选。用于补充说明任务的具体要求。", "required": false }, "priority": { "type": "string", "enum": ["high", "medium", "low"], "description": "任务优先级。high表示紧急重要,medium表示常规,low表示不急。默认为medium。", "required": false }, "due_date": { "type": "string", "description": "任务截止日期,格式为YYYY-MM-DD。例如:2025-06-30。", "required": false } } }工具 2:query_task
{ "name": "query_task", "description": "根据任务ID查询任务的详细信息,包括标题、描述、状态、优先级、创建时间等。当用户询问某个具体任务的详情时使用。", "parameters": { "task_id": { "type": "string", "description": "任务的唯一标识符,通常是一串数字或字母组合。例如:TASK-001、12345。", "required": true } } }工具 3:update_task_status
{ "name": "update_task_status", "description": "更新指定任务的状态。当用户需要标记任务完成、开始任务、暂停任务时使用。注意:此工具仅修改状态,不修改任务的其他信息。", "parameters": { "task_id": { "type": "string", "description": "任务的唯一标识符。例如:TASK-001、12345。", "required": true }, "status": { "type": "string", "enum": ["pending", "in_progress", "completed", "cancelled"], "description": "新的任务状态。pending表示待处理,in_progress表示进行中,completed表示已完成,cancelled表示已取消。", "required": true } } }工具 4:delete_task
{ "name": "delete_task", "description": "永久删除指定的任务。此操作不可恢复。当用户明确要求删除任务时使用。注意:如果用户只是想取消任务,请使用update_task_status将状态改为cancelled。", "parameters": { "task_id": { "type": "string", "description": "要删除的任务的唯一标识符。", "required": true } } }工具 5:search_tasks
{ "name": "search_tasks", "description": "根据条件搜索任务列表。支持按状态、优先级、关键词等条件筛选。当用户需要查看任务列表、查找特定任务、了解任务概况时使用。", "parameters": { "keyword": { "type": "string", "description": "搜索关键词,匹配任务标题和描述。例如:报告、bug。", "required": false }, "status": { "type": "string", "enum": ["pending", "in_progress", "completed", "cancelled", "all"], "description": "按状态筛选。all表示不筛选状态。默认为all。", "required": false }, "priority": { "type": "string", "enum": ["high", "medium", "low", "all"], "description": "按优先级筛选。all表示不筛选优先级。默认为all。", "required": false }, "limit": { "type": "string", "description": "返回结果的最大数量,默认为10。例如:5、20。", "required": false } } }6.3 返回值与错误处理设计
每个工具的返回值都应该遵循统一的结构:
{ "success": true, "data": { ... }, "message": "操作成功的简要说明" }错误返回:
{ "success": false, "error_code": "TASK_NOT_FOUND", "message": "未找到ID为'TASK-999'的任务。请确认任务ID是否正确。", "suggestion": "可以使用search_tasks工具搜索任务,确认正确的任务ID。" }注意错误返回中的suggestion字段。这个字段告诉 Agent 下一步该怎么做,大大提升了 Agent 的自我修复能力。
6.4 测试与迭代:怎么验证工具设计是否合格
设计完工具后,必须经过实际测试。我的测试方法分三步:
第一步:单工具测试。对每个工具,构造 10-20 个不同的用户表达方式,看 Agent 是否能正确选择并调用这个工具。比如对于create_task,测试用例包括:“帮我创建一个任务”“添加一个待办”“新建工单”“我要记录一件事”等等。
第二步:多工具混淆测试。把所有工具放在一起,构造一些容易混淆的场景,看 Agent 是否能选对工具。比如“取消这个任务”应该用update_task_status而不是delete_task。
第三步:端到端测试。模拟真实的用户对话流程,看 Agent 能否在多轮对话中正确使用工具组合完成任务。
每次测试后,记录失败的 case,分析原因,然后迭代优化工具描述和参数设计。这个迭代过程通常需要 3-5 轮才能达到比较理想的效果。
7. 常见问题与排查技巧实录
7.1 Agent 选错工具怎么办
这是最常见的问题。排查思路:
检查工具描述是否有重叠。如果两个工具的描述都提到了相似的关键词,Agent 就容易混淆。解决方法是强化每个工具的“不适用场景”描述。
检查工具名称是否太相似。
query_order和search_order这种命名,Agent 很容易搞混。考虑改成get_order_detail和search_orders。检查是否有工具可以被合并。如果两个工具的功能高度重叠,考虑合并成一个,通过参数区分。
7.2 Agent 填错参数怎么办
参数填充错误通常有几种表现:
| 错误类型 | 典型表现 | 解决方法 |
|---|---|---|
| 格式错误 | 日期填成“明天”而不是“2025-06-30” | 在参数描述中明确格式要求并给示例 |
| 类型错误 | 数字填成字符串 | 考虑放宽类型约束,用 string + pattern |
| 语义错误 | 把“取消”理解成删除 | 在参数描述中明确枚举值的含义 |
| 缺失参数 | 忘记填必填参数 | 检查必填参数的描述是否足够醒目 |
7.3 Agent 陷入循环怎么办
Agent 反复调用同一个工具但解决不了问题,通常是因为:
- 错误信息不够明确,Agent 不知道该怎么修复
- 工具返回的结果没有提供足够的反馈
- 缺少“退出条件”,Agent 不知道什么时候该停止
解决方法是在错误返回中提供明确的suggestion,并且在工具描述中说明“如果连续失败 N 次,请停止重试并告知用户”。
7.4 工具数量太多导致选择困难
当工具超过 15 个时,Agent 的选择准确率会下降。解决方法:
- 分组:把工具按功能域分组,Agent 先选组再选工具
- 路由:设计一个“路由工具”,根据用户意图把请求转发到具体的工具
- 精简:定期审查工具列表,合并或删除不常用的工具
实操心得:我通常会维护一个“工具使用日志”,记录每个工具被调用的频率和成功率。调用频率极低(比如一个月不到 5 次)的工具,考虑下线。成功率低于 80% 的工具,必须优化。
7.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决措施 |
|---|---|---|---|
| Agent 不调用工具 | 描述缺少使用场景 | 检查描述是否包含“什么时候该用” | 补充使用场景描述 |
| Agent 调用错误的工具 | 工具描述重叠 | 对比相似工具的描述 | 强化负面边界,区分工具 |
| 参数格式错误 | 参数描述不清晰 | 检查参数描述是否有格式说明和示例 | 补充格式要求和示例 |
| 返回结果被误读 | 返回结构不稳定 | 检查不同调用的返回结构是否一致 | 统一返回格式 |
| 调用失败后不修复 | 错误信息不明确 | 检查错误返回是否包含 suggestion | 补充修复建议 |
| 反复调用同一工具 | 缺少退出条件 | 检查是否有重试次数限制 | 添加重试上限和终止提示 |
8. 进阶技巧:让 MCP Tool 更“聪明”
8.1 动态描述:根据上下文调整工具说明
MCP 协议本身不支持动态描述,但你可以在工具的实现层面做文章。比如,工具可以根据当前会话的上下文,返回不同的提示信息。
举个例子。一个搜索工具,如果 Agent 之前已经搜索过类似的关键词但没有找到结果,工具可以在返回中提示:“之前搜索过类似关键词'XXX',没有找到结果。建议尝试更宽泛的关键词,或者检查拼写。”
这种动态反馈能让 Agent 感觉工具是“活的”,而不是一个死板的接口。
8.2 工具链编排:让多个工具协同工作
有些任务需要多个工具配合完成。比如“帮我取消最近的那个订单”,需要先搜索订单,再取消订单。
你可以在工具描述中暗示这种编排关系。比如在cancel_order的描述中写:“如果不确定订单 ID,请先使用search_orders工具查找。”
更高级的做法是设计一个“复合工具”,内部自动完成多步操作。比如cancel_latest_order工具,内部先搜索再取消,对 Agent 来说只需要调用一次。
8.3 反馈闭环:让工具从调用中学习
这个思路比较前沿,但已经在一些系统中实践了。核心想法是:工具记录 Agent 的调用历史,分析哪些调用成功了、哪些失败了,然后自动优化工具描述和参数设计。
比如,如果发现 Agent 经常把date参数填成“明天”这种相对时间,工具可以自动在描述中增加“请使用绝对日期格式”的提示。
这种自适应优化能大大减少人工调优的工作量,但目前还需要比较多的工程投入。
8.4 多语言支持:让 Agent 用母语思考
如果你的 Agent 主要处理中文用户请求,工具描述也应该用中文写。虽然 Agent 能理解英文描述,但中文描述能减少一次“翻译”过程,提升理解准确率。
更重要的是,参数描述中的示例应该使用中文。比如city参数的示例应该是“北京、上海”,而不是“Beijing, Shanghai”。这样 Agent 在填充参数时,会倾向于使用中文值,减少格式转换的错误。
9. 我踩过的那些坑
说几个我实际踩过的坑,都是血泪教训。
第一个坑:把 REST API 直接套壳。早期我图省事,把已有的 REST API 直接转成 MCP Tool,参数名、返回结构都没改。结果 Agent 用得一塌糊涂。REST API 的参数设计是给程序员看的,有很多隐含约定,Agent 根本理解不了。后来全部重写,按照 Agent 的认知习惯重新设计,效果才上来。
第二个坑:工具描述写得太“技术化”。我一开始写描述的时候,用的是技术文档的风格,比如“调用此接口将返回指定资源的详细信息”。Agent 看到这种描述,理解起来很吃力。后来改成口语化的风格,比如“查询某个任务的详细信息,包括标题、状态、截止日期”,Agent 的理解准确率立刻上去了。
第三个坑:忽略了错误处理。最开始我只关注“成功路径”,错误返回就随便扔一个错误码。结果 Agent 遇到错误就懵了,要么反复重试,要么直接放弃。后来我在每个错误返回里都加了suggestion字段,告诉 Agent 下一步该怎么做,Agent 的自我修复能力大幅提升。
第四个坑:工具数量失控。一开始觉得每个功能都应该有独立的工具,结果工具数量膨胀到 30 多个。Agent 选择工具的时候经常选错。后来做了合并和精简,控制在 12 个以内,准确率明显改善。
第五个坑:没有做充分的测试。工具设计完后,我只测试了几个“标准”的调用场景,觉得没问题就上线了。结果真实用户的各种表达方式让 Agent 频繁出错。后来我建立了一个测试集,包含各种“非标准”的用户表达,每次修改工具设计后都跑一遍回归测试。
10. 工具设计检查清单
最后,分享一个我在每次设计 MCP Tool 时都会对照的检查清单。这个清单帮我避免了很多低级错误。
描述检查:
- 是否包含功能定义(这个工具做什么)
- 是否包含使用场景(什么时候该用)
- 是否包含负面边界(什么时候不该用)
- 描述长度是否在 100-300 字之间
- 语言是否口语化、易于理解
参数检查:
- 参数名是否完整、明确、无歧义
- 每个参数是否有详细的描述
- 每个参数是否有格式说明和示例
- 是否避免了不必要的选填参数
- 枚举值是否有明确的含义说明
返回值检查:
- 返回结构是否稳定统一
- 是否只包含必要的信息
- 错误返回是否包含修复建议
- 大数据量是否有截断或分页机制
测试检查:
- 是否测试了多种用户表达方式
- 是否测试了工具间的混淆场景
- 是否测试了错误处理和恢复
- 是否建立了回归测试集
这个清单看起来简单,但每次认真对照一遍,都能发现几个可以优化的点。工具设计这件事,细节决定成败。Agent 不像程序员那样会“猜”你的意图,它只能根据你给的信息做决策。你给的信息越清晰、越准确,Agent 的表现就越好。
我在实际项目中的体会是,设计 MCP Tool 的时间分配大概是:写代码 20%,写描述和参数 50%,测试和迭代 30%。很多人把 90% 的时间花在写代码上,结果工具功能没问题,但 Agent 就是用不好。记住,你的用户是 Agent,不是程序员。站在 Agent 的角度思考问题,才是设计高质量 MCP Tool 的关键。