2242 字
约 7 分钟
19
S21-A18 · MCP 工具的粒度:一个工具做一件事

一个团队给 Agent 接了一个"订单服务"的工具,参数有十二个:操作类型、订单号、金额、状态、备注、时间范围……调用成功率只有六成,而且失败时很难判断是参数错了还是服务拒绝了。后来拆成六个单一职责的小工具,成功率升到九成五。

MCP 工具的粒度问题不是"接口设计风格",而是模型能不能正确调用的问题。参数越多、语义越含糊,模型越容易填错;而错误一旦发生,工具本身不会告诉你"我理解错了你的意思"。


1. 粒度粗的四种代价

代价 说明
参数填错 十二个参数里选错一个的概率显著上升
语义歧义 "操作类型"这种参数需要模型自己映射到枚举
错误不可归因 失败时不知道是哪一步理解错了
描述难写 一个工具的描述要覆盖十二种行为

粒度还会影响可测试性:一个十二参数的工具,测试组合几乎无法覆盖;六个单参数的工具,每个都能被明确测试。工具层的缺陷往往不在服务端,而在"模型按错误的方式调用了它"——这类缺陷只有逐工具测试才抓得住。

第二项最典型:枚举型参数把"选择"变成了模型的负担,而模型对枚举值的理解往往来自名字,而不是文档。

2. 拆分的三个判据

判据 说明
动词单一 一个工具只做一件动作(创建/查询/取消)
必填可少 必填参数不超过三个
失败可区分 失败时能定位到具体原因

命名还要避免"抽象动词":handle、process、manage 这类词对模型没有信息量,它们不构成意图提示。工具名本身就是给模型的提示,应当直接体现它解决的问题。

第一项是核心:工具名应当是一个动词。create_order、query_order、cancel_order 天然互斥,而 manage_order 需要模型先做一次意图分类。

// 粗粒度:一个工具承担多种意图
- name: manage_order
  params: [operation, order_id, amount, status, note, time_range, ...]
  # 模型需要先决定 operation 是什么,再做参数填充

// 细粒度:动词单一,参数少
- name: query_order
  params: [order_id]
- name: cancel_order
  params: [order_id, reason]
- name: create_order
  params: [user_id, amount, items]

3. 参数设计的三个约束

约束 原因
必填尽量少 每个必填都是一次填错机会
类型强约束(枚举、格式) 减少自由文本
语义互补而非重叠 避免"两个参数含义相近"

参数描述里给出一个真实示例,收益远高于一段解释:

// 描述里带示例:模型对示例的模仿远好于对规则的理解
order_id:
  type: string
  description: "订单号,形如 O-7781(字母 O,短横线,四位以上数字)"
  example: "O-7781"

第三项常见于"开始时间/结束时间/时间范围"三者并存——重叠的参数会让模型随机选一个,而结果看起来都对。

{
  "name": "query_order",
  "description": "按订单号查询单个订单的状态与金额。不返回订单明细,明细请用 query_order_items。",
  "inputSchema": {
    "type": "object",
    "properties": {
      "order_id": { "type": "string", "pattern": "^O-[0-9]{4,}$" }
    },
    "required": ["order_id"]
  }
}

注意 description 里写了两件事:做什么,以及不做什么。后者决定了模型会不会把它用在错误的场景。

4. 工具数量与可发现性

工具数 问题
< 5 通常可控
5–15 需要清晰命名与描述
15–40 需要分组或分层(先选领域再选动作)
> 40 模型选择准确率明显下降

分层还带来一个副作用要处理:模型的上下文里同时只有一小组工具,这意味着上一轮用过的工具可能"看不见"了。可行做法是把用过的工具固定在上下文顶部,或允许按名字直接调用。

第三档需要"工具集"的概念:先让模型选一个领域(订单、用户、库存),再暴露该领域的具体工具。它把一次四十选一变成两次小范围选择。

// 分层暴露:按领域分组,避免一次给出全部工具
step1: list_domains() → ["order", "user", "inventory"]
step2: list_tools(domain="order") → [query_order, cancel_order, create_order]
step3: call_tool("query_order", { order_id })
// 代价:多一轮交互;收益:选择准确率与可维护性

5. 错误信息也是接口的一部分

错误内容 模型能否据此修正
"参数错误" 否
"order_id 格式不匹配 ^O-[0-9]{4,}$" 能
"订单不存在:O-9999" 能(换参数或告知用户)
"服务内部错误" 否(应重试或上报)

错误返回还要区分"工具层的错"与"业务的否":

// 两类结果必须分开,否则模型会去修一个不需要修的东西
TOOL_ERROR   :参数不合法、权限不足、下游超时   → 模型可修正或重试
BUSINESS_NO  :余额不足、状态不允许、重复操作   → 模型应转述给用户
// 把业务否误报成工具错,会让 Agent 陷入"改参数重试"的死循环

第二项与第三项的差别在于是否可行动。工具的错误返回应当让模型能决定"改参数、换工具、还是告诉用户",而不是只能重试。

// 错误返回:给出可行动的字段,而不是一句 message
return ToolResult.error(Map.of(
    "code", "INVALID_ARGUMENT",
    "field", "order_id",
    "expected", "^O-[0-9]{4,}$",
    "retryable", false));

工具粒度的调整还有个现实约束:每次调整都要重新验证模型的实际调用行为。纸面上更清晰的拆分,可能让模型的调用分布变得更分散,所以调整后应当用一批真实任务跑一遍,对比成功率与错误类型。

6. 排错:五种"工具调不对"

现象 成因 处理方向
参数被填成默认值 必填过多、描述模糊 减少必填、写清示例
选错工具 工具名语义重叠 改名为动词、加"不做什么"
反复重试同一错误 错误不可行动 返回字段级原因
工具太多选不准 一次暴露过多 分层暴露
工具描述与行为不符 描述未随实现更新 把描述纳入测试

第五项很实际:描述漂移是工具层的隐性缺陷。做法是把"描述里的承诺"写成一条测试——比如描述说"不返回明细",就用一个断言验证返回体里没有明细字段。

7. 边界:什么情况粗粒度更好

场景 为什么 做法
操作天然是一个事务(转账) 拆开会出现中间态 保留一个工具
参数本来就是一批同质数据 拆开只是重复 用一个接受列表的工具
工具数极少(< 3) 选择压力小 不必拆
内部脚本调用(非模型调用) 无理解问题 按工程习惯设计

还有一条与演进有关的边界:工具是接口,一旦发布就有调用方(模型与它的提示词)。改名、改参数语义都会让既有提示失效,所以它应当像对外 API 一样有版本与兼容期,而不是"随手改一下更清楚"。

判断口径一句话:这个工具的参数需不需要模型做"意图分类"。需要,就该拆;不需要,粗粒度反而是优点(一次调用完成一件事)。


8. 自测

  1. 粒度粗的四种代价中,哪一项把"选择"变成了模型的负担?
  2. 拆分的三个判据是什么?为什么"工具名应当是一个动词"是核心?
  3. "开始时间/结束时间/时间范围"三者并存会引发什么问题?
  4. 工具数超过 15 后为什么需要分层暴露?分层带来什么代价?
  5. 什么样的错误返回能让模型自行修正?"服务内部错误"为什么不行?
  6. 判断"该粗还是该细"的那个问题是什么?
S21-A18 · MCP 工具的粒度:一个工具做一件事
https://www.clxhxhhr.top/posts/6436/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。