4.4 错误处理:抛异常即回传错误


4.4 错误处理:抛异常即回传错误

本节摘要:工具执行会失败——这是工程现实。本节讲透 MCP 工具的错误处理机制,核心是两条把错误告知模型的路径:抛异常(SDK 自动转成 isError: true 的错误响应)与主动返回错误内容(你控制返回结构)。我们会讲清两条路径的差别、模型如何读懂错误并据此重试或换策略,以及一个反模式警告——吞掉异常是最糟糕的做法。读完本节,你能让工具失败时被模型正确理解,而不是让整个对话跑偏。

一、为什么错误处理在工具里特别重要

先建立直觉:工具调用的失败,不是终点,而是模型决策的输入

传统编程的错误观 异常 = 失败 = 终止 MCP 工具的错误观 错误 = 信息 = 模型据此决策下一步

模型调用工具失败后,它不会「崩溃」,而是读懂错误、决定怎么办——可能是:

  • 「参数不对,我换个值重试」
  • 「这个工具暂时不可用,我换个工具」
  • 「这个操作被拒绝了,我向用户解释」

所以工具的错误处理,核心不是「怎么让工具不失败」,而是「失败时怎么把信息清楚地告诉模型」。

二、路径一:抛异常(自动转错误响应)

最简单也最推荐的方式——直接抛异常,SDK 捕获后转成模型能读懂的错误响应:

@mcp.tool() def divide(a: int, b: int) -> float: """Divide a by b.""" if b == 0: raise ValueError("除数不能为零,请提供非零的 b") return a / b

模型收到的响应大致是:

{ "isError": true, "content": [{"type": "text", "text": "除数不能为零,请提供非零的 b"}] }

模型看到 isError: true,明确知道这是错误,然后读错误文本决定下一步:「哦,除数不能为零,我换一个值或问用户」。

抛异常的优势:

优势 说明
写法简单 该抛就抛,不用构造错误响应对象
自动转换 SDK 替你转成 isError 响应
语义清晰 模型看到明确的错误标记
支持任意异常类型 ValueErrorPermissionError、自定义异常都行

三、路径二:主动返回错误内容

有时你想更精细控制错误的结构(比如带错误码、分类),可以主动返回带错误信息的文本:

@mcp.tool() def divide(a: int, b: int) -> dict: """Divide a by b.""" if b == 0: return {"error": "DIV_BY_ZERO", "message": "除数不能为零"} return {"result": a / b}

这种方式下,模型收到的是正常响应(没有 isError 标记),但内容里包含错误信息。模型需要自己从文本判断这是错误

两条路径的差别:

维度 抛异常 主动返回错误内容
模型看到的标记 isError: true(明确) 无标记(自己判断)
错误结构 文本为主 你控制(可带错误码等)
适合 真正的失败 「软失败」(可恢复、要解释)
模型识别难度 低(有明确标记) 中(要从内容判断)

💡 技巧:优先用抛异常。它的 isError 标记让模型零成本识别错误,最不容易出问题。只有当你需要返回结构化错误信息(带错误码、分类),且不希望它被当作「硬失败」时,才用主动返回。

四、模型如何读懂错误并决策

这是错误处理最关键的部分——模型不只是「收到」错误,它会「用」错误。几种典型决策:

场景:模型调 divide(10, 0),收到错误「除数不能为零」 模型可能的决策: 1. 换值重试:「除数不能为零,我用 1 试试」→ divide(10, 1) 2. 换工具:「这个工具不行,我换计算器工具」 3. 问用户:「除数不能为零,请问你想用哪个除数?」 4. 放弃解释:「我无法完成这个除法,因为除数为零」

模型能做出这些决策,前提是错误信息足够清楚。「除数不能为零」比「错误」或「失败」有用得多——前者告诉模型「问题在除数」,后者只说「有问题」。所以写错误信息时,要说清问题在哪、该怎么修

五、反模式:吞掉异常

最糟糕的做法是吞掉异常——try/except 把错误吃掉,返回一个看似正常的结果:

# ❌ 反模式:吞掉异常 @mcp.tool() def divide(a: int, b: int) -> float: try: return a / b except ZeroDivisionError: return 0 # 吞掉错误,返回 0

为什么这是灾难?

模型调 divide(10, 0) │ ▼ 收到 0(没错误标记) 模型:「结果是 0,继续推理」 │ ▼ 基于错误结果继续 模型:「10 除以 0 等于 0,所以...」 ← 完全跑偏

模型不知道发生了错误,基于错误结果继续推理,导致整个对话被污染。所以铁律是:

该抛就抛,绝不吞异常。 让模型知道「这一步失败了」,比让它基于假结果推理安全得多。

六、错误处理的实践模式

几个实战中的常用模式:

模式一:验证前置(抛清晰的参数错误)

@mcp.tool() def transfer(amount: float) -> bool: if amount <= 0: raise ValueError(f"金额必须为正,收到 {amount}") if amount > 10000: raise PermissionError("单笔转账上限 10000") # 执行转账

清晰的条件检查 + 具体的错误信息,让模型能据此调整。

模式二:外部失败用错误码(主动返回)

@mcp.tool() def call_api(endpoint: str) -> dict: status, body = make_request(endpoint) if status != 200: return {"error": "API_FAILED", "status": status, "body": body} return {"data": body}

外部 API 失败时,返回带状态码的结构化错误,让模型判断是否重试。

模式三:区分「业务错误」与「系统错误」

class BusinessError(Exception): pass # 业务规则违反(模型可调整) class SystemError(Exception): pass # 系统故障(模型该放弃)

不同异常类型传递不同严重程度,帮模型决策。

本节要点回顾

  1. 工具的错误是模型决策的输入,不是终点——模型据此重试、换工具、问用户。
  2. 路径一:抛异常,SDK 自动转 isError: true,推荐首选。
  3. 路径二:主动返回错误内容,你控制结构,适合软失败。
  4. 优先抛异常,它的 isError 标记让模型零成本识别错误。
  5. 错误信息要说清问题在哪、该怎么修,「除数不能为零」比「错误」有用。
  6. 反模式:吞掉异常是灾难,模型会基于错误结果继续推理,污染对话。
  7. 实践模式:验证前置抛清晰错误、外部失败用错误码、区分业务/系统错误。

错误处理清楚了,最后一节讲如何用 pydantic.Field 精化约束,把契约写到「模型几乎不会调错」。


作者与出处
原作者: 灏天文库
来源:modelcontextprotocol
许可证:MIT
整理: 灏天文库整理
由灏天文库结构化整理,提供目录导航、全文检索与在线阅读,便于系统化学习
发布者: 作者: 灏天文库 转发
评论区 (0)
U