本节摘要:工具执行会失败——这是工程现实。本节讲透 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 响应 |
| 语义清晰 | 模型看到明确的错误标记 |
| 支持任意异常类型 | ValueError、PermissionError、自定义异常都行 |
有时你想更精细控制错误的结构(比如带错误码、分类),可以主动返回带错误信息的文本:
@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 # 系统故障(模型该放弃)
不同异常类型传递不同严重程度,帮模型决策。
isError: true,推荐首选。isError 标记让模型零成本识别错误。错误处理清楚了,最后一节讲如何用 pydantic.Field 精化约束,把契约写到「模型几乎不会调错」。