课程大纲 › 领域 2:工具设计与 MCP

2.2 实现 MCP 工具的结构化错误响应

タスクステートメント2.2:MCPツールの構造化されたエラーレスポンスを実装する

官方大纲原文(日文):知识 4 条 · 技能 4 条

需要掌握的知识

  • ツールの失敗をエージェントに伝えるための、MCPのisErrorフラグパターン
  • 一時的なエラー(タイムアウト、サービスの利用不可)、検証エラー(無効な入力)、ビジネスエラー(ポリシー違反)、権限エラーの違い
  • 一律のエラーレスポンス(汎用的な"Operation failed")では、エージェントが適切な復旧の判断を下せなくなる理由
  • リトライ可能なエラーとリトライ不可能なエラーの違い、および構造化されたメタデータを返すことで無駄なリトライを防ぐ仕組み

需要能做到的事

  • errorCategory(transient/validation/permission)、ブール値のisRetryable、人間が読める説明を含む、構造化されたエラーメタデータを返す
  • エージェントが適切に伝えられるように、ビジネスルール違反に対してretriable: falseフラグと顧客にわかりやすい説明を含める
  • 一時的な失敗についてはサブエージェント内でローカルにエラー復旧を実装し、ローカルで解決できないエラーのみを、部分的な結果および試行した内容とともにコーディネーターに伝播する
  • アクセスの失敗(リトライの判断が必要)と、有効な空の結果(一致するものがなかった成功したクエリを表す)を区別する

课程讲解 · AI 草稿,未经人工审核 以官方文档为准

这一条考什么

考你如何让 MCP 工具在失败时"说清楚发生了什么",从而让 Agent 做出正确的恢复决策。你需要知道 MCP 的 isError 标志用法,能区分暂时性、校验、业务、权限四类错误,理解为什么统一的"Operation failed"会让 Agent 无所适从,并能设计包含错误类别、是否可重试、人类可读说明的结构化错误。还会考:子 Agent 何时本地恢复、何时上报协调者,以及"访问失败"与"成功但无结果"的区别。

核心概念

MCP 的 isError 模式

MCP 区分两类错误:

  • 协议级错误(JSON-RPC error):例如调用了不存在的工具、请求格式非法。这类错误通常不会以可推理的形式交给模型。
  • 工具执行错误:工具被正确调用,但执行时失败(后端超时、订单不存在、违反政策)。应在工具结果中返回错误内容,并设置 isError: true。
{
  "content": [{"type": "text", "text": "{\"errorCategory\":\"transient\",\"isRetryable\":true,\"message\":\"订单数据库超时,稍后重试通常可以成功\"}"}],
  "isError": true
}

在 Claude API 层面,这对应 tool_result 块上的 is_error: true。这样模型能"看到"失败并据此决定下一步。反模式包括:抛异常让框架吞掉、返回空结果"避免混淆模型"、返回成功响应但在某个字段里藏一个错误状态。

四类错误

  • 暂时性错误(transient):超时、服务暂不可用、限流。重试很可能成功。
  • 校验错误(validation):输入非法,例如日期格式错、必填字段缺失、订单号格式不对。原样重试必然失败,需要修正输入或向用户索取信息。
  • 业务错误(business):请求合法但违反业务规则,例如超过退款期限、超出金额上限。重试无意义,需要向客户解释或走例外流程。
  • 权限错误(permission):调用方无权执行该操作。重试无用,需要升级或换用有权限的路径。

为什么统一错误信息有害

如果所有失败都返回"Operation failed",Agent 无法区分"再试一次就好"和"永远不可能成功"。结果往往是:对业务规则违规反复重试、对暂时性故障过早放弃、对校验错误不知道该改哪个参数、对客户给出含糊甚至错误的解释。错误信息是 Agent 的决策输入,信息越少,决策越差。

可重试 vs. 不可重试与结构化元数据

在错误中显式携带"能否重试"的信号,可以避免浪费轮次和 token 的无效重试:

{
  "errorCategory": "business",
  "isRetryable": false,
  "message": "订单 A-7781 已超过 30 天退货期(下单于 2026-08-30)。",
  "customerMessage": "很抱歉,该订单已超过 30 天退货期,无法直接退款。我可以为您申请例外审批。",
  "suggestedAction": "offer_exception_or_escalate"
}

要点:

  • errorCategory:transient / validation / business / permission。
  • isRetryable(考题中也写作 retriable):布尔值,明确告诉 Agent 是否值得再试。
  • message:给 Agent 的技术说明,包括原因和可能的下一步(例如"订单未找到——可尝试用 get_customer 按电话号码搜索")。
  • 对业务规则违规,额外提供面向客户的说明,让 Agent 可以准确转述而不暴露内部细节。

本地恢复与向上传播

在多 Agent 系统中:

  1. 子 Agent 遇到暂时性错误时,先在本地处理(有限次数的退避重试、换一个等价数据源)。
  2. 本地无法解决的错误才上报协调者,并附带部分结果和已尝试的操作(例如"28 个来源中 25 个已处理;剩余 3 个均超时,已各重试 3 次")。
  3. 协调者据此决定:接受部分结果、换子 Agent、调整范围或上报用户。

把每个小故障都上抛会让协调者被噪声淹没;吞掉所有错误、只返回成功部分又会让协调者误以为结果完整。

访问失败 vs. 有效的空结果

"查询成功但没有匹配记录"与"查询根本没执行成功"是两回事:

  • 有效空结果:isError: false,内容如 {"results": [], "note": "查询成功,未找到匹配订单"}。Agent 可以据此得出"该客户名下没有这笔订单"。
  • 访问失败:isError: true,errorCategory: "transient"。Agent 不能据此下任何"不存在"的结论,而应重试或说明无法确认。

把超时伪装成空列表,会让 Agent 自信地告诉用户"没有找到您的订单",这是典型的错误传播事故。

正确做法与反模式

  • 正确:工具执行失败时在结果中返回错误内容并设置 isError: true。反模式:抛出异常、返回空结果,或返回"成功"但用状态字段表示失败。
  • 正确:返回 errorCategory、isRetryable、人类可读说明组成的结构化元数据。反模式:所有失败一律返回"Operation failed"。
  • 正确:业务规则违规时 isRetryable: false 并附面向客户的说明。反模式:让 Agent 解析错误文本自己猜是否可重试。
  • 正确:子 Agent 本地处理暂时性错误,无法解决时连同部分结果与已尝试操作上报。反模式:任何错误都立刻上抛,或全部吞掉只报成功部分。
  • 正确:明确区分"成功但为空"与"访问失败"。反模式:超时时返回空列表。

常见陷阱

  • "在 MCP 服务器中对所有错误做指数退避重试,用尽后再返回"——对校验、业务、权限错误重试毫无意义,还延长了响应时间。
  • "新增一个 analyze_error 工具,每次失败后调用来判断类别"——类别信息本该由出错的工具直接给出,多一次调用纯属浪费。
  • "添加 few-shot 示例教 Agent 解析错误文本判断是否可重试"——依赖脆弱的文本解析,正确做法是结构化字段。
  • "记录服务端日志并返回空结果以免困扰模型"——把失败伪装成"没有数据",导致错误结论。
  • "抛出异常让框架捕获"——模型看不到可推理的错误信息,无法自行恢复。

实操练习

目标:用官方 MCP Python SDK 写一个返回结构化错误的 lookup_order 工具,并在 Claude Code 中观察 Agent 对不同错误的反应。

  1. pip install mcp,保存下面的 orders_server.py。
  2. 在项目根目录创建 .mcp.json 注册该服务器,然后启动 claude。
  3. 分别询问"查一下订单 A-1""查一下订单 A-TIMEOUT""查一下订单 abc""查一下订单 A-404",观察 Agent 的不同处理。
import json
from mcp.server.fastmcp import FastMCP
from mcp.types import CallToolResult, TextContent

mcp = FastMCP("orders")

def err(category, retryable, message, customer_message=None):
    body = {"errorCategory": category, "isRetryable": retryable, "message": message}
    if customer_message:
        body["customerMessage"] = customer_message
    return CallToolResult(content=[TextContent(type="text", text=json.dumps(body, ensure_ascii=False))],
                          isError=True)

@mcp.tool()
def lookup_order(order_id: str) -> CallToolResult:
    """按订单号查询订单。order_id 形如 A-1234。未找到时返回空 results(不是错误)。"""
    if not order_id.startswith("A-"):
        return err("validation", False, "order_id 格式应为 A-数字,可先用 get_customer 查询客户订单。")
    if order_id == "A-TIMEOUT":
        return err("transient", True, "订单数据库超时,稍后重试通常可以成功。")
    if order_id == "A-404":
        return CallToolResult(content=[TextContent(type="text",
                              text=json.dumps({"results": [], "note": "查询成功,未找到匹配订单"}, ensure_ascii=False))])
    if order_id == "A-999":
        return err("business", False, "订单已超过 30 天退货期。",
                   "该订单已超过 30 天退货期,我可以为您申请例外审批。")
    return CallToolResult(content=[TextContent(type="text",
                          text=json.dumps({"results": [{"order_id": order_id, "status": "delivered"}]}))])

if __name__ == "__main__":
    mcp.run()
{
  "mcpServers": {
    "orders": {"command": "python", "args": ["orders_server.py"]}
  }
}

检查结果:暂时性错误时 Agent 应尝试重试;校验错误时会请你确认订单号;A-404 时会明确说"没有找到"而不是"系统出错";业务错误时会用 customerMessage 的口径解释并且不重试。

自测清单

  • MCP 中的协议级错误与工具执行错误有何不同?后者应如何返回?
  • 暂时性、校验、业务、权限四类错误分别该如何恢复?哪些可以重试?
  • 为什么统一返回"Operation failed"会让 Agent 行为不稳定?
  • 一个结构化错误响应应包含哪些字段?业务违规时还要多给什么?
  • 子 Agent 何时应本地恢复?上报协调者时应附带哪些信息?
  • 如何区分"查询成功但无结果"与"访问失败"?混淆二者会造成什么后果?

对应题目(4)

题目归类由 AI 判断,仅供参考。

练习这 4 道题