云计算百科
云计算领域专业知识百科平台

LLM 不是程序员:Agent 工具调用失败的 6 种死法与 7 层解药

文章目录

    • 开场:一个真实到让人头秃的场景
    • 先分类:工具调用失败的 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 + 时间),必须克制。

该重试(retryable=True)不该重试(retryable=False)
网络抖动 参数错误
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 自愈"的组合拳。

    三原则:

  • 能在本地拦住的错误,别发到下游(L1/L2 预防校验)
  • 能重试的别报错,能降级的别硬刚(L4/L5)
  • 能让 LLM 自己改的别转人工,改不了就诚实失败(L6/L7)

  • 附:自查清单(建议收藏)

    给正在做 Agent 的你逐条打勾:

    • 每个工具的 description 是否写清了参数格式和"不要做什么"?
    • 参数是否有 Pydantic 本地校验,避免无效下游调用?
    • 异常是否统一包装,并区分了 retryable / 非 retryable?
    • 重试上限是多少?会不会无限重试烧 token?
    • 是否为主工具设计了降级路径?
    • 错误信息是否回喂给了 LLM?是否包含"错在哪 + 怎么做"?
    • 最终失败时,Agent 是诚实回答,还是在瞎编?
    • 是否监控了自愈成功率这个关键指标?

    把这套七层防线装进你的 Agent,它就不再是"调工具的脆皮",而是一个能自己爬起来、也知道什么时候该求助的靠谱助手。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » LLM 不是程序员:Agent 工具调用失败的 6 种死法与 7 层解药
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!