文章目录
-
- 开场:一个真实到让人头秃的场景
- 先分类:工具调用失败的 6 种"死法"
- 七层防御体系
-
- L1 预防层:把工具说明书写到 LLM 不犯错
- L2 校验层:参数门禁(Pydantic)
- L3 捕获层:统一异常包装
- L4 重试层:只重试"该重试的"
- L5 降级层:Fallback 工具链
- L6 自愈层(Agent 独有,全文最关键):把错误喂回去
- L7 兜底层:诚实失败
- 一个完整可跑的例子
- 监控与可观测
- 总结:一张图 + 三句话
- 附:自查清单(建议收藏)
传统程序里,异常是 Bug;Agent 里,异常是正常输入。
开场:一个真实到让人头秃的场景
假设你在做一个订单查询客服 Agent,工具定义很简单:
def get_order(order_id: str) –> dict:
"""查订单详情"""
...
用户说:“帮我看看张三的订单到哪了。”
LLM 把 order_id 传成了 "张三" —— 因为它觉得"张三"就是要查的东西。工具抛了 OrderNotFound。此时 Agent 通常有两种死法:
- 死法 A:整个流程崩了,返回 500,用户一脸懵;
- 死法 B:LLM 为了"完成任务",编造了一个订单号,返回一个不存在的"已签收"。
第二种最可怕。它表面成功,实则撒谎,比报错危害大一百倍。
为什么传统程序没这个问题?因为你写 requests.get() 报错,是你写错了,改代码就行。但 Agent 里,是 LLM 动态决定调哪个工具、传什么参数——你永远无法穷举它会传什么。
所以,Agent 的异常处理,思路要从"修 Bug"切换到"设计容错系统"。下面这套七层防线,就是解决这个问题的完整方案。
先分类:工具调用失败的 6 种"死法"

拿到问题先分类,才能对症下药。绝大多数失败都能归进这张表:
| 1 | 参数幻觉 | LLM 编造不存在的字段名、传错格式 | 工具描述写得烂 | L1/L2 预防+校验 |
| 2 | 业务异常 | 订单不存在、余额不足、无权限 | 下游业务规则 | L3 捕获 |
| 3 | 网络/超时 | 500、ConnectionError、read timeout | 下游不稳定 | L4 重试 |
| 4 | 返回格式脏 | 返回了 HTML 错误页、JSON 混注释 | 下游不规范 | L3 捕获 |
| 5 | 决策错误 | 该查天气,却调了计算器 | 工具选择混淆 | L1 写好描述 |
| 6 | 资源耗尽 | 配额、限流、熔断 | 外部依赖雪崩 | L4/L5 重试+降级 |
一句话记忆:前 2 类靠"写好工具"解决,中间 2 类靠"工程兜底"解决,最后 2 类靠"系统设计"解决。
七层防御体系
先用一张图建立全局认知,后面每一层逐一展开:
#mermaid-svg-NymIWboKu5Aepg2A{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NymIWboKu5Aepg2A .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NymIWboKu5Aepg2A .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NymIWboKu5Aepg2A .error-icon{fill:#552222;}#mermaid-svg-NymIWboKu5Aepg2A .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NymIWboKu5Aepg2A .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NymIWboKu5Aepg2A .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NymIWboKu5Aepg2A .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NymIWboKu5Aepg2A .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NymIWboKu5Aepg2A .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NymIWboKu5Aepg2A .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NymIWboKu5Aepg2A .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NymIWboKu5Aepg2A .marker.cross{stroke:#333333;}#mermaid-svg-NymIWboKu5Aepg2A svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NymIWboKu5Aepg2A p{margin:0;}#mermaid-svg-NymIWboKu5Aepg2A .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-NymIWboKu5Aepg2A .cluster-label text{fill:#333;}#mermaid-svg-NymIWboKu5Aepg2A .cluster-label span{color:#333;}#mermaid-svg-NymIWboKu5Aepg2A .cluster-label span p{background-color:transparent;}#mermaid-svg-NymIWboKu5Aepg2A .label text,#mermaid-svg-NymIWboKu5Aepg2A span{fill:#333;color:#333;}#mermaid-svg-NymIWboKu5Aepg2A .node rect,#mermaid-svg-NymIWboKu5Aepg2A .node circle,#mermaid-svg-NymIWboKu5Aepg2A .node ellipse,#mermaid-svg-NymIWboKu5Aepg2A .node polygon,#mermaid-svg-NymIWboKu5Aepg2A .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NymIWboKu5Aepg2A .rough-node .label text,#mermaid-svg-NymIWboKu5Aepg2A .node .label text,#mermaid-svg-NymIWboKu5Aepg2A .image-shape .label,#mermaid-svg-NymIWboKu5Aepg2A .icon-shape .label{text-anchor:middle;}#mermaid-svg-NymIWboKu5Aepg2A .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NymIWboKu5Aepg2A .rough-node .label,#mermaid-svg-NymIWboKu5Aepg2A .node .label,#mermaid-svg-NymIWboKu5Aepg2A .image-shape .label,#mermaid-svg-NymIWboKu5Aepg2A .icon-shape .label{text-align:center;}#mermaid-svg-NymIWboKu5Aepg2A .node.clickable{cursor:pointer;}#mermaid-svg-NymIWboKu5Aepg2A .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NymIWboKu5Aepg2A .arrowheadPath{fill:#333333;}#mermaid-svg-NymIWboKu5Aepg2A .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NymIWboKu5Aepg2A .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NymIWboKu5Aepg2A .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NymIWboKu5Aepg2A .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NymIWboKu5Aepg2A .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NymIWboKu5Aepg2A .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NymIWboKu5Aepg2A .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NymIWboKu5Aepg2A .cluster text{fill:#333;}#mermaid-svg-NymIWboKu5Aepg2A .cluster span{color:#333;}#mermaid-svg-NymIWboKu5Aepg2A div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NymIWboKu5Aepg2A .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NymIWboKu5Aepg2A rect.text{fill:none;stroke-width:0;}#mermaid-svg-NymIWboKu5Aepg2A .icon-shape,#mermaid-svg-NymIWboKu5Aepg2A .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NymIWboKu5Aepg2A .icon-shape p,#mermaid-svg-NymIWboKu5Aepg2A .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NymIWboKu5Aepg2A .icon-shape .label rect,#mermaid-svg-NymIWboKu5Aepg2A .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NymIWboKu5Aepg2A .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NymIWboKu5Aepg2A .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NymIWboKu5Aepg2A :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
是
否
有
无
能
不能
LLM 决策调用工具
L1 工具描述与 Schema 门禁
L2 参数 Pydantic 校验
L3 try-except 统一包装
L4 可重试?
指数退避重试
L5 有 Fallback?
降级到备用工具
L6 错误信息回喂 LLM
LLM 能否自我修正?
L7 兜底: 转人工 / 诚实回答
L1 预防层:把工具说明书写到 LLM 不犯错
预防永远优于补救。 大部分参数幻觉,是工具描述写得不够清楚造成的。
❌ 反例:
def get_order(order_id: str):
"""查订单"""
✅ 正例:主动写"不要做什么",把格式约束写死:
def get_order(order_id: str):
"""
根据订单号查询订单详情。
注意:
– order_id 必须是 18 位字符,以 ORD 开头(如 ORD20241020000123);
– 不要传用户姓名、手机号或 ID;
– 如果对话里没有订单号,先向用户索要,不要猜测。
"""
经验:工具描述里"不要做什么",比只写"做什么"更管用。LLM 是概率模型,你把歧义空间缩小,它犯错概率就下降。
L2 校验层:参数门禁(Pydantic)
即使描述写得好,也要在本地做硬校验。LLM 传错参数,我们在发起下游调用之前就拦住,把校验错误回喂给它:
from pydantic import BaseModel, Field
class GetOrderInput(BaseModel):
order_id: str = Field(
...,
pattern=r"^ORD\\d{15}$",
description="订单号格式: ORD + 15位数字",
)
这样 get_order(order_id="张三") 在进入函数前就被 Pydantic 拦截,返回校验错误。下游一个请求都没发出,就不存在"烧钱失败"的问题。
L3 捕获层:统一异常包装

不要把原始的 Traceback 直接甩给 LLM——它读不懂,而且原始错误文本还可能被注入利用。正确做法是把异常结构化:
class ToolError(Exception):
def __init__(self, code: str, message: str, retryable: bool):
super().__init__(message)
self.code = code # 例如 ORDER_NOT_FOUND / TIMEOUT / UPSTREAM
self.message = message # 给人 / 给 LLM 看的安全文案
self.retryable = retryable # 是否值得重试
核心设计:给每个错误打上 retryable 标签。这是后面重试、降级、自愈决策的基础。
L4 重试层:只重试"该重试的"

不是所有错误都值得重试。重试要花钱(token + 时间),必须克制。
| 网络抖动 | 参数错误 |
| 5xx 服务端错误 | 404 不存在 |
| 超时 | 业务拒绝 |
| 限流 429 | 鉴权失败 |
指数退避 + 抖动,避免所有客户端同时重试打崩下游:
import random, time
def retry_with_backoff(fn, max_attempts=3):
for attempt in range(max_attempts):
try:
return fn()
except ToolError as e:
if not e.retryable or attempt == max_attempts – 1:
raise
sleep = min(2 ** attempt, 30) + random.uniform(0, 1)
time.sleep(sleep)
铁律:最多重试 2~3 次。别让 Agent 无限重试把预算烧光。
L5 降级层:Fallback 工具链
Agent 不该因为一个下游挂掉就整个瘫痪。设计备用路径:
- 主订单库超时 → 查只读副本 / 缓存
- 实时天气 API 挂了 → 返回昨日缓存 + 提示"数据非实时"
- 内部 RAG 检索失败 → 退回通用 LLM 能力回答
def search_catalog(query):
try:
return internal_rag(query)
except ToolError:
# 降级:退到基础 LLM 知识回答,并明确告知用户
return {"source": "llm_fallback", "answer": llm_ask(query)}
关键:降级时必须诚实标注来源,别让用户误以为看到了实时权威数据。
L6 自愈层(Agent 独有,全文最关键):把错误喂回去
这是 Agent 区别于传统程序的本质差异。传统程序报错就停了;Agent 报错,要把错误作为 observation 回喂给 LLM,让它自己改。
LangChain 中的实现——把错误包装成 ToolMessage 塞回 ReAct 循环:
result = agent_executor.invoke({
"input": "查一下 ORD20241020000123 这个订单到哪了",
"chat_history": [],
})
当 LLM 第一次传错,工具返回的不是崩溃,而是:
System(observation):
工具调用失败[ORDER_NOT_FOUND]: 订单号格式应为 ORD+15位数字,
你传入的是用户姓名"张三"。请从对话历史中提取正确的订单号,
如果没有就向用户确认,不要猜测。
错误信息里必须包含三要素,LLM 才能真正自我修正:
经验:错误信息写得越好,自愈成功率越高。这本质上是"给 LLM 写更好的 Prompt"。
L7 兜底层:诚实失败
重试 3 次、降级也失败、LLM 也改不对?那就放弃,但不要编答案。
if state["attempts"] >= 3:
return {
"messages": [
AIMessage(
content="我暂时无法查询到这个订单的状态。"
"可能是系统繁忙或订单号有误,"
"需要我帮你转接人工客服吗?"
)
]
}
这是信任问题:一次瞎编订单号的代价,远大于坦诚说"查不到"。宁可转人工,不可给假答案。
一个完整可跑的例子
把 L2~L7 串起来,看一次真实的"失败 → 自愈 → 成功"闭环:
import time
from langgraph.graph import StateGraph
from langchain_core.messages import AIMessage, ToolMessage
@tool(args_schema=GetOrderInput)
def get_order(order_id: str) –> dict:
try:
resp = requests.get(f"https://api.shop.com/orders/{order_id}", timeout=5)
resp.raise_for_status()
return resp.json()
except requests.Timeout:
raise ToolError("TIMEOUT", "订单接口超时", retryable=True)
except requests.HTTPError as e:
if e.response.status_code == 404:
raise ToolError("NOT_FOUND", "订单不存在", retryable=False)
raise ToolError("UPSTREAM", f"上游错误: {e}", retryable=True)
def call_tool_node(state):
"""LangGraph 中的工具调用节点,内置重试 + 错误回喂"""
for attempt in range(3):
try:
result = get_order.invoke(state["tool_input"])
return {"messages": [ToolMessage(content=str(result))]}
except ToolError as e:
if not e.retryable or attempt == 2:
# 结构化错误回喂给 LLM,让它自己修正
return {"messages": [ToolMessage(content=(
f"工具调用失败[{e.code}]: {e.message}. "
f"请修正参数或向用户确认,不要猜测。"
))]}
time.sleep(2 ** attempt)
return {"messages": [ToolMessage(content="重试耗尽")]}
# 把 call_tool_node 接进 StateGraph,配上 LLM 决策节点即可
运行日志(体现完整闭环):
[1] LLM: 调用 get_order(order_id="张三")
[2] 工具: [ORDER_NOT_FOUND] 格式应为 ORD+15位数字, 传入的是姓名
[3] LLM: 重新解析用户问题… 从对话历史提取订单号 ORD20241020000123
[4] 工具: 返回订单状态 "已发货, 预计明天到达"
[5] 完成 ✅
从"传错"到"改对",全程没有人工介入——这就是 Agent 自愈能力的价值。
监控与可观测

容错系统不做监控,等于没做。你无法知道"自愈到底成功了多少次"。
必须记录每次工具调用的:
- tool_name / input / output
- latency(延迟)
- error_code(错误码)
- attempts(重试次数)
核心指标:
| 工具成功率 | 调用成功比例 | 突降 |
| 重试率 | 触发重试的比例 | 持续升高 → 下游不稳 |
| 自愈成功率 | LLM 收到错误后是否改对 | 降低 → 错误信息写得差 |
| 兜底率 | 最终转人工/失败比例 | 升高 → 预警 |
告警误区:某工具失败率突然 > 20%,多半是下游挂了,不是 LLM 变笨了。先查依赖服务,别急着调 Prompt。
链路追踪用 LangSmith / Langfuse,能看到完整 ReAct 轨迹和每次自愈的来龙去脉。
总结:一张图 + 三句话
核心一句话:
Agent 的异常处理不是 try-catch,而是"工程容错 + LLM 自愈"的组合拳。
三原则:
附:自查清单(建议收藏)
给正在做 Agent 的你逐条打勾:
- 每个工具的 description 是否写清了参数格式和"不要做什么"?
- 参数是否有 Pydantic 本地校验,避免无效下游调用?
- 异常是否统一包装,并区分了 retryable / 非 retryable?
- 重试上限是多少?会不会无限重试烧 token?
- 是否为主工具设计了降级路径?
- 错误信息是否回喂给了 LLM?是否包含"错在哪 + 怎么做"?
- 最终失败时,Agent 是诚实回答,还是在瞎编?
- 是否监控了自愈成功率这个关键指标?
把这套七层防线装进你的 Agent,它就不再是"调工具的脆皮",而是一个能自己爬起来、也知道什么时候该求助的靠谱助手。
网硕互联帮助中心




评论前必须登录!
注册