课程大纲 › 领域 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 系统中:
- 子 Agent 遇到暂时性错误时,先在本地处理(有限次数的退避重试、换一个等价数据源)。
- 本地无法解决的错误才上报协调者,并附带部分结果和已尝试的操作(例如"28 个来源中 25 个已处理;剩余 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 对不同错误的反应。
pip install mcp,保存下面的orders_server.py。- 在项目根目录创建
.mcp.json注册该服务器,然后启动claude。 - 分别询问"查一下订单 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 道题