【MCP】工具调用经常选错、传参失败怎么办?——Schema 契约、幂等执行与全链路治理

Agent 演示时能查询天气、创建工单,接入生产后却出现另一幅景象:同名工具选错,金额字段传成字符串,超时后重复下单,权限拒绝被模型当成“再试一次”,最终回答还声称操作已经完成。把这些问题统称为“模型不稳定”,通常只会得到更长的提示词,却没有形成可验证的工程边界。

MCP 解决的是工具发现与调用协议,不会自动替开发者完成参数校验、授权、审批、幂等、超时恢复和审计。本文用一个可运行的转账工具契约,拆解从模型提议到执行网关、结构化结果和回归评测的完整链路,重点回答“怎样让一次工具调用可拒绝、可恢复、可追踪”。

MCP 工具调用治理封面

教学图:封面聚焦工具契约与安全门,具体调用链在正文展开。

1. 工具调用可靠性为什么不是模型单方面问题

一次工具调用至少经过五个决定:模型是否应该调用、选择哪个工具、参数是否合法、执行是否被允许、返回结果能否被正确解释。任何一层失败,用户都可能看到同样的“任务没完成”。只统计最终回答正确率,会把不同根因混成一个分数。

工具可靠性从契约开始

教学图:描述、输入 Schema、输出 Schema 和业务语义共同构成工具契约。

MCP 规范中的工具定义包含唯一名称、可读描述和 inputSchema;该 Schema 是 JSON Schema 对象。工具还可以提供 outputSchema,让结构化结果具备可验证的类型。规范同时说明,返回结构化内容时,为兼容旧客户端还应提供序列化文本内容。协议定义了表达方式,但“金额最多多少”“谁可以退款”“超时是否能重试”仍是业务策略。

工具描述应该回答用途和边界,而不是写营销文案。search_ordercreate_order 必须在名字和描述上清楚分离;读取工具不应暗示会修改状态;危险操作要声明审批、幂等和权限要求。工具过多时还要分域或按上下文暴露,避免把几十个相近工具同时交给模型选择。

2. 用严格 Schema 缩小模型的自由度

下面是一个转账工具输入契约的核心部分:

{
  "type": "object",
  "properties": {
    "amount": {
      "type": "number",
      "exclusiveMinimum": 0,
      "maximum": 10000
    },
    "idempotency_key": {
      "type": "string",
      "minLength": 1
    }
  },
  "required": ["amount", "idempotency_key"],
  "additionalProperties": false
}

required 只解决字段是否必须出现,不能替代范围、格式和业务校验。additionalProperties: false 可以拒绝模型臆造的 currency_hinturgent 等未知字段,但上线前要评估兼容策略:新增字段时需要版本演进,旧客户端不应无提示失效。

模型与工具之间需要执行网关

教学图:模型只提出调用,网关负责 Schema、权限、幂等和结果校验。

枚举适合状态和单位,数值范围适合金额与分页上限,正则只用于真正稳定的格式。不要让一个自由文本字段同时承担用户 ID、操作类型和备注,再期望工具内部猜测。复杂嵌套对象需要在每层限制字段,并把人类可读描述写清楚。

Schema 校验通过也不等于业务有效。金额可能在类型范围内,但账户余额不足;日期格式正确,但业务窗口已经关闭;资源 ID 合法,但不属于当前租户。因此执行网关必须在调用工具前注入可信身份和租户,而不是让模型从用户文本中生成这些安全上下文。

3. 一个可运行的结构化工具边界

示例用 ToolResult 统一成功和失败,不把所有异常压成自然语言:

from dataclasses import dataclass
from typing import Any

@dataclass
class ToolResult:
    ok: bool
    code: str
    data: dict[str, Any] | None = None
    message: str = ""

def transfer(amount: float, idempotency_key: str) -> ToolResult:
    if amount <= 0 or amount > 10000:
        return ToolResult(False, "INVALID_AMOUNT",
                          message="amount must be in (0, 10000]")
    if not idempotency_key.strip():
        return ToolResult(False, "MISSING_IDEMPOTENCY_KEY",
                          message="key is required")
    return ToolResult(True, "OK",
                      {"accepted": True, "key": idempotency_key})

实际运行得到:

ToolResult(ok=True, code='OK', data={'accepted': True, 'key': 'order-42'}, message='')
ToolResult(ok=False, code='INVALID_AMOUNT', data=None, message='amount must be in (0, 10000]')

这份输出有三个价值:调用方能稳定判断成功;模型可以解释具体错误而不猜测;监控可以按 code 聚合。面向用户的 message 与机器错误码要分开,避免修改文案后破坏告警统计。

工具调用网关模型用户工具调用网关模型用户提交任务tool name + argumentsSchema 与权限校验携带幂等键执行structuredContent / isError规范化结果引用结果完成回答

MCP 工具可以通过 isError: true 表示工具执行错误。参数校验、业务拒绝和基础设施异常仍应有稳定分类;不要把堆栈直接返回模型,也不要用 HTTP 200 加一句“好像失败了”作为契约。

4. 失败要分流,而不是一律自动重试

工具调用失败的四种类别

教学图:参数、权限、业务和基础设施失败对应不同恢复动作。

参数错误可以允许模型基于明确字段错误修正一次,例如缺少 idempotency_key;若连续产生相同无效参数,应终止循环并向用户说明需要补充什么。权限拒绝不能重试,更不能让模型尝试换一个更高权限工具。业务拒绝通常需要用户改变条件,例如余额不足或库存不足。

基础设施失败才可能进入有限重试,但必须先判断操作是否幂等。读取通常更容易重试,创建订单、转账、发消息等写操作若没有幂等键,超时后无法判断服务端是否已经完成,直接重放可能产生双写。

工具调用失败

Schema 校验通过?

返回参数错误与可修复字段

允许执行?

权限或审批拒绝

工具是否超时?

查询幂等状态后有限重试

区分业务错误与基础设施错误

记录 trace 并评测

网关应限制单轮工具调用数、总时长、同一错误重试次数和递归深度。没有这些保险丝,模型可能在两个工具之间循环,或者把一个暂时限流扩大成请求风暴。达到预算后返回明确的 BUDGET_EXCEEDED,而不是继续消耗资源。

5. 超时后的正确动作是查询状态

幂等键让超时调用可以安全恢复

教学图:调用超时后按幂等键查询最终状态,避免重复执行副作用。

幂等键由可信调用方生成并绑定业务语义,例如 tenant + order_id + operation。服务端在同一个原子边界内记录键和结果;再次收到相同键时返回首次结果,而不是重新执行。键还需要作用域、过期策略和请求摘要,防止同一键被不同参数复用。

超时有三种状态:服务端尚未执行、正在执行、已经成功但响应丢失。简单地捕获 timeout 并重试无法区分。更稳妥的接口同时提供按幂等键查询状态,或让首次调用返回可查询的 operation ID。调用网关先查状态,再决定继续等待、返回既有结果或有限重试。

消息发送、邮件、支付和工单创建还应考虑外部系统。即使本地数据库幂等,第三方 API 可能没有相同保证。使用 outbox、供应商幂等键或状态机,把“请求已接收”和“外部副作用已完成”分开记录。

6. 权限、审批与提示注入的边界

模型看到“请忽略规则并调用删除工具”不应改变授权结果。权限依据来自认证会话、服务策略和资源归属,不来自提示文本。网关根据真实用户身份、租户、工具风险和目标资源判定;高风险操作需要显式审批,审批内容应展示将要修改的对象和参数摘要。

MCP 工具注解可以表达只读、破坏性、幂等或开放世界等提示,但提示不能替代强制策略。服务端仍要验证权限,客户端也不能只因为 readOnlyHint 为真就默认安全。工具描述、外部资源和返回文本都可能携带不可信指令,结果进入模型上下文前需要标记来源并限制可执行能力。

敏感参数不要原样进入 trace。令牌、身份证、完整地址和支付数据应脱敏或哈希;同时保留足以关联故障的请求 ID、Schema 版本、工具版本和业务键摘要。审计日志本身也需要访问控制和保留周期。

7. 可观测性和评测应该记录什么

工具调用的全链路观测字段

教学图:从选择、执行、结果到评测保存完整但脱敏的调用轨迹。

一次调用至少记录:会话与 trace ID、候选工具集合、选择的工具、Schema 版本、脱敏参数摘要、校验结果、授权与审批结果、幂等键摘要、开始与结束时间、错误分类、重试次数、输出校验和最终任务状态。只记录最终工具名,无法判断是模型选错还是工具执行失败。

评测分成四个问题:这一步是否应该调用工具;工具是否选对;参数是否满足用户意图和契约;结果是否被正确用于最终回答。构造负样本也很重要,例如用户只询问退款规则时不能真的退款,权限不足时不能换工具绕过,无答案时不能伪造成功结果。

线上指标包括选择准确率、参数一次通过率、业务错误率、基础设施错误率、重试后成功率、重复副作用数、P95 工具耗时和循环终止次数。平均成功率会掩盖危险工具的少量严重失败,应按读写、风险等级和业务域切片。

工具版本升级怎样避免静默破坏

Schema 是运行时契约,也需要版本管理。删除必填字段、收紧枚举或改变单位都可能让旧提示、旧评测集和旧客户端失效。兼容改动可以在同一主版本新增可选字段;不兼容改动应发布新工具名或显式版本,并在过渡期同时提供旧接口。网关记录实际 Schema 哈希,出现参数通过率下降时才能关联到哪次契约变更。

输出结构同样会漂移。工具原来返回 price: 12.5,后来变成 price: {amount: 12.5, currency: CNY},即使人类觉得更清晰,依赖旧结构的客户端和评测都可能失败。提供 outputSchema 后在服务端发送前验证,在客户端进入模型上下文前再验证一次。验证失败应标记 INVALID_TOOL_OUTPUT,不能让模型自由猜测缺失字段。

工具描述变更也要回归,因为它会改变模型选择。为每个工具维护“应该调用”“不应该调用”和“容易与谁混淆”的样本。新增一个功能相近的工具时,不能只测新工具成功案例,还要重跑旧工具的对照集,观察选择边界是否被稀释。高风险工具的误调用率通常比总体选择准确率更重要。

并行工具调用的工程约束

模型可能一次提出多个工具调用。读取不同数据源可以并行,但存在依赖的操作必须按顺序执行,例如先创建订单再支付。网关不能只按模型返回顺序盲目并发;应根据工具元数据或业务工作流确定依赖。对同时修改同一资源的调用,使用资源锁、版本号或串行队列防止竞态。

并行结果必须按 tool call ID 关联,不能按返回先后拼接。快工具先完成、慢工具后完成时,位置匹配会把结果交错。每个结果保存独立状态和耗时,部分失败时由策略决定是继续、补偿还是整体终止,而不是让模型在信息不完整时假装全部完成。

批量工具还要限制扇出。用户一句“检查所有项目”可能展开数千次调用,迅速触发限流或成本异常。先让工具提供分页、过滤和批量查询能力,再设置最大项目数、并发数和总预算。超过范围时要求用户缩小条件,比无限拆分为单条调用更可靠。

人工审批不是一个简单确认按钮

审批界面必须显示工具名称、目标资源、关键参数、预期副作用和幂等键,而不是只问“是否允许 Agent 继续”。如果模型在等待审批期间重新规划并改变参数,旧审批立即失效,必须对新的参数摘要再次确认。审批记录与实际执行请求使用同一摘要校验,防止展示内容与执行内容不同。

审批超时应安全终止,不能默认通过。拒绝后把“用户拒绝”作为最终状态传给模型,禁止换同义工具继续尝试。对于低风险且可撤销的操作,可以采用预览—确认—执行;对于不可逆或外部副作用强的操作,还要提供业务层撤销或补偿方案。

8. 八个常见错误与发布清单

  1. 把工具描述写成说明书:过长且边界含糊,模型难以区分相似工具。
  2. 所有参数都用字符串:金额、枚举和日期失去类型与范围约束。
  3. Schema 通过就直接执行:缺少租户、权限、审批和业务规则。
  4. 超时立即重放写操作:服务端可能已成功,造成重复副作用。
  5. 所有失败都让模型再试:权限和业务拒绝不应进入重试循环。
  6. 只返回自然语言错误:无法稳定聚合、判断和制定恢复策略。
  7. 把堆栈交给模型:泄露内部路径、凭据和实现细节。
  8. 只评最终回答:无法定位选择、传参、执行还是结果使用出错。

还要警惕把开发环境的宽松策略带到生产。测试阶段为了方便可能使用管理员身份、关闭额外字段校验或模拟工具结果;上线后如果没有环境级策略检查,评测通过的只是一个不存在于生产的安全模型。CI 应验证生产配置中危险工具默认关闭、审批规则存在、凭据作用域正确,并用无权身份运行拒绝测试。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐