第06章 错误处理与容错(Error Handling & Resilience)

分享

「任务助手 Agent」获得了"韧性"——从第04章的"能跑就行",进化为"出错也能优雅恢复"。 一个生产级 Agent 与玩具 Demo 的分水岭,不在于"功能多",而在于出错时会发生什么

TL;DR

30 秒速读:Agent 容错的核心是区分"可重试错误"(超时、限流)和"永久错误"(认证失败、参数错误),前者用指数退避重试,后者立即失败,工具异常则反馈给 Agent 让它自我纠正。

如果只记一件事:工具抛异常时不要崩溃,把错误信息以 role="tool" 反馈给 Agent,它会自己换工具或调整参数,这叫"自我纠正",是 Agent 比普通程序强大的关键。


本章目标

学完本章,你将理解:

  1. 为什么错误处理要在早期教:失败是系统设计问题,不是事后补丁
  2. 四大容错机制:指数退避重试、工具异常自我纠正、幻觉工具检测、错误分类
  3. 降级策略:工具失败时不是崩溃,而是反馈给 Agent 让它换思路
  4. 护栏基础:输入/输出校验,把"垃圾进垃圾出"挡在门外
  5. 反模式:裸 except 吞错误、无上限重试、不区分错误类型

为什么错误处理要在早期教

很多教程把"错误处理"放到最后一章,当作"高级技巧"。这是个昂贵的误解。

Anthropic 的工程共识是:失败是系统设计问题,不是事后补丁。

原因有三:

  1. Agent 比普通程序更容易出错。Agent 由 LLM + 工具 + 网络 + 外部数据源组成,链条上任何一环都可能失败:API 限流、工具超时、模型幻觉一个不存在的工具名、JSON 解析失败……如果你等到上线才处理,每个错误都会变成用户投诉。

  2. 早期不教,会养成"裸 except"习惯。新手写 Agent 最常见的"反模式"就是 try: ... except: pass——把所有错误吞掉。结果 Agent 在生产环境静默失败,你连日志都没有。这种习惯一旦养成,很难纠正。

  3. 容错机制是 Agent"可用性"的核心。一个"功能 100 分但一出错就崩"的 Agent,实际可用性远低于"功能 70 分但出错能恢复"的 Agent。后者才是用户愿意用的。

💡 心智模型:把 Agent 想象成一个实习生。好实习生不是"从不犯错"(那不可能),而是"犯了错知道怎么补救"——重试一次、换个方法、或者诚实报告"我搞不定"。本章教你如何让 Agent 具备这种"补救能力"。


常见失败模式

在写容错代码之前,先认清 Agent 会以哪些方式失败。对症下药,才能写对 except

失败模式 典型表现 根本原因 可重试?
API 超时 APITimeoutError / 连接超时 网络抖动、服务端慢 ✅ 是
API 限流 RateLimitError(HTTP 429) 短时间请求过多 ✅ 是(等一会儿)
连接错误 APIConnectionError DNS 失败、网络断 ✅ 是
认证错误 AuthenticationError(HTTP 401) 密钥无效/过期 ❌ 否(重试也没用)
参数错误 BadRequestError(HTTP 400) 请求格式错、模型不支持 ❌ 否(得改代码)
工具异常 工具函数抛 ValueError/KeyError 输入不合法、外部服务挂 ⚠️ 看 Agent 决策
JSON 解析失败 json.JSONDecodeError 模型返回的 arguments 不是合法 JSON ⚠️ 可重试(让模型重新生成)
幻觉工具调用 模型调了不存在的工具名 模型"编造"了一个工具 ⚠️ 告知后可纠正

关键区分:可重试错误 vs 永久错误。

  • 可重试错误(超时、限流、连接错误):再试一次可能成功 → 重试 + 退避
  • 永久错误(认证、参数错误):再试 100 次也一样 → 立即失败,别浪费时间

把这两类混为一谈,是新手最常见的错误:要么"什么错误都重试"(认证错误重试 3 次纯属浪费),要么"什么错误都不重试"(一次网络抖动就让 Agent 挂掉)。


机制 1:指数退避重试(Exponential Backoff)

面对可重试错误(超时、限流、连接错误),正确做法不是"立刻重试",而是等一会儿再试,每次等更久

为什么不能"立刻重试"

如果 API 限流是因为"你请求太频繁",立刻重试只会让限流更严重。你需要退让,让服务端喘口气。

指数退避的公式

attempt 0: 失败 → 等 2^0 = 1 秒
attempt 1: 失败 → 等 2^1 = 2 秒
attempt 2: 失败 → 等 2^2 = 4 秒
attempt 3: 放弃(达到 max_retries=3)

每次等待时间翻倍——这就是"指数"的含义。它比"固定间隔"更智能:前几次快速重试(也许只是瞬时抖动),后面拉长间隔(给服务端恢复时间)。

实现代码骨架

import time

MAX_RETRIES = 3

def call_llm_with_retry(messages, tools):
    for attempt in range(MAX_RETRIES):
        try:
            return client.chat.completions.create(
                model=cfg.model, messages=messages, tools=tools,
            )
        except (APITimeoutError, APIConnectionError, RateLimitError) as e:
            # 只重试"可重试错误"
            if attempt == MAX_RETRIES - 1:
                raise  # 最后一次也失败了,抛出去
            wait = 2 ** attempt  # 1s, 2s, 4s
            print(f"第 {attempt+1} 次失败,{wait}s 后重试:{e}")
            time.sleep(wait)

⚠️ 只重试可重试错误except 子句必须精确列出可重试的异常类型。绝不能写裸 except Exception——那会把认证错误也重试,纯属浪费。

生产级增强(了解即可)

真实生产环境还会加:

  • 抖动(jitter):在等待时间上加随机量(如 wait + random()),避免多个客户端同时重试造成"惊群"
  • 上限封顶(cap)min(wait, 60) 防止等待时间无限增长
  • 遵守 Retry-After:HTTP 429 响应常带这个头,告诉你"多久后再试"

本章先用最朴素的 2 ** attempt,这些增强留到第15章讲。


机制 2:工具异常 + Agent 自我纠正

工具执行时抛异常,不应该让整个 Agent 崩溃。正确做法是:把错误信息以 role="tool" 追加到 messages,让 Agent 自己决定怎么办

为什么"告诉 Agent 出错了"比"崩溃"好

Agent 是有推理能力的。如果你告诉它"get_weather 失败了:城市名拼写错误",它能:

  1. 换工具:既然天气查不到,改用 search_wiki 查这个城市
  2. 调整参数:发现"Beijing"拼错了,改成"北京"再试
  3. 诚实回答:告诉用户"我查不到这个城市的天气"

这就是 Agent 自我纠正(self-correction)——错误不是终点,而是新的"观察",驱动 Agent 调整下一步。

实现代码骨架

for tc in assistant_msg.tool_calls:
    func_name = tc.function.name
    args = json.loads(tc.function.arguments)
    func = TOOL_FUNCTIONS.get(func_name)

    try:
        result = func(**args)
    except Exception as e:
        # ❌ 不要崩溃,也不要吞掉错误
        # ✅ 把错误"翻译"成 Agent 能理解的语言
        result = f"[工具执行失败] {func_name} 抛出异常:{type(e).__name__}: {e}"

    # 无论成功还是失败,都把结果反馈给 Agent
    messages.append({
        "role": "tool",
        "tool_call_id": tc.id,
        "content": str(result),
    })

下一轮循环,Agent 会"看到"这条错误信息,然后重新决策。这比"工具一抛异常就整个崩溃"优雅得多。

💡 错误信息要"可操作"。不要只写 "工具失败",要写 "get_weather 抛出 ValueError:城市名 'xyz' 不在数据库中。可用的城市有:北京、上海、深圳"。信息越具体,Agent 越容易纠正。


机制 3:幻觉工具名检测

LLM 有时会"编造"一个不存在的工具名——这在业内叫幻觉(hallucination)

比如你只提供了 get_weathercalculatesearch_wiki 三个工具,模型却调了 get_stock_price(你以为它知道,其实它在瞎编)。

检测 + 纠正

VALID_TOOL_NAMES = set(TOOL_FUNCTIONS.keys())  # {"get_weather", "calculate", "search_wiki"}

for tc in assistant_msg.tool_calls:
    func_name = tc.function.name
    if func_name not in VALID_TOOL_NAMES:
        # 幻觉!告知 Agent 正确的工具列表
        result = (
            f"[错误] 工具 '{func_name}' 不存在。"
            f"可用的工具有:{', '.join(sorted(VALID_TOOL_NAMES))}。"
            f"请从上述列表中选择一个。"
        )
        messages.append({
            "role": "tool", "tool_call_id": tc.id, "content": result,
        })
        continue
    # ... 正常执行 ...

Agent 看到"这个工具不存在,这是正确的工具列表"后,通常会在下一步选一个合法的工具。这就是用反馈而非崩溃来纠正幻觉


机制 4:区分可重试错误 vs 永久错误

这是本章的"元技能"——它决定了前三个机制如何组合。

分类逻辑

from openai import (
    APITimeoutError, APIConnectionError, RateLimitError,
    AuthenticationError, BadRequestError, APIError,
)

def is_retryable(error: Exception) -> bool:
    """判断一个错误是否值得重试。"""
    if isinstance(error, (APITimeoutError, APIConnectionError, RateLimitError)):
        return True   # 瞬时故障,再试可能成功
    if isinstance(error, APIError) and getattr(error, "status_code", 0) >= 500:
        return True   # 服务端 5xx 错误,通常瞬时
    return False      # 认证错误、参数错误等 → 永久错误

# 使用:
try:
    response = call_with_retry(...)  # 内部只重试 is_retryable 的
except AuthenticationError:
    raise SystemExit("密钥无效,请检查 .env")  # 永久错误,直接退出
except BadRequestError as e:
    raise SystemExit(f"请求格式错误:{e}")      # 永久错误,得改代码

错误分类表(快速参考)

错误类型 可重试? 处理方式
APITimeoutError 退避重试
APIConnectionError 退避重试
RateLimitError(429) 退避重试(等久点)
服务端 APIError(5xx) 退避重试
AuthenticationError(401) 立即退出,提示检查密钥
BadRequestError(400) 立即退出,提示改请求
工具异常 ⚠️ 反馈给 Agent(机制 2)
JSONDecodeError ⚠️ 反馈给 Agent 让它重生成

降级策略(Graceful Degradation)

"降级"指:当首选方案失败时,退而求其次,而不是崩溃。

在 Agent 语境下,降级有三种层次:

层次 1:工具级降级(反馈给 Agent)

工具 A 失败 → 把错误告诉 Agent → Agent 改用工具 B。这是机制 2 的延伸——Agent 自己做降级决策

Agent: 调 get_weather("火星")
工具:  [失败] 火星不在数据库
Agent: 那我改用 search_wiki("火星") 查点信息
工具:  火星是太阳系第四颗行星...
Agent: 我查不到火星的天气,但查到了百科信息:...

层次 2:API 级降级(换模型)

主模型(如 GPT-4o)API 挂了 → 切换到备用模型(如 deepseek-chat)。本章不深入,留到第15章可观测性讲。

层次 3:功能级降级(拒绝服务)

所有重试失败 → 返回"抱歉,服务暂时不可用",而不是让用户对着一个转圈圈等到天荒地老。

💡 降级的核心原则:永远给用户一个"可接受的次优结果",而不是"没有结果"。一个说"我现在查不到,但你可以试试 XXX"的 Agent,远好过一个卡死的 Agent。


护栏基础(Guardrails)

护栏指"在输入和输出两端做校验,把不合法的内容挡在门外"。本章只讲基础(输入/输出校验),深度安全护栏留到第17章。

输入护栏

在把用户输入交给 Agent 之前,先校验:

  • 长度限制:防止超长输入耗尽 token 预算
  • 字符过滤:过滤明显的注入攻击(如"忽略以上所有指令")
  • 敏感词检测:拦截违法/有害内容
def validate_input(user_message: str) -> str:
    if len(user_message) > 10_000:
        raise ValueError("输入过长(超过 10000 字符),请精简后重试")
    if "ignore previous instructions" in user_message.lower():
        raise ValueError("检测到疑似 prompt 注入,已拒绝")
    return user_message

输出护栏

在把 Agent 的回答返回给用户之前,先校验:

  • 格式校验:如果要求 JSON,检查是否真的是合法 JSON
  • 内容校验:过滤有害/不当内容
  • 长度截断:防止 Agent 啰嗦到 token 爆表
def validate_output(answer: str) -> str:
    if len(answer) > 5000:
        return answer[:5000] + "\n\n(回答过长,已截断)"
    return answer

⚠️ 本章只讲基础。真正的安全护栏(prompt 注入防御、越狱检测、PII 脱敏)是第17章的主题,涉及更复杂的攻防。这里只建立"校验"的意识。


反模式(什么不该做)

❌ 裸 except 吞错误

# 坏:吞掉所有错误,连日志都没有
try:
    result = tool(**args)
except:
    result = None  # 然后呢?Agent 不知道出错了,继续往下走

后果:Agent 静默失败,调试时你完全不知道哪里错了。

正确:捕获具体异常类型,并把错误信息反馈给 Agent(机制 2)。

❌ 重试无上限

# 坏:无限重试,万一永久错误就死循环
while True:
    try:
        return call_api()
    except Exception:
        continue  # 永远不放弃...但也永远不成功

后果:认证错误也会无限重试,浪费资源 + 可能触发更严厉的封禁。

正确max_retries=3,最后一次失败就抛出去。

❌ 不区分可重试错误与永久错误

# 坏:对 AuthenticationError 也重试 3 次,纯属浪费
try:
    call_api()
except Exception as e:
    for _ in range(3):
        try:
            call_api()
        except Exception:
            pass

正确:用 is_retryable(error) 判断,只重试瞬时错误。

❌ 工具异常直接崩溃

# 坏:工具一抛异常,整个 Agent 循环崩溃
result = func(**args)  # 万一这里抛异常?
messages.append({"role": "tool", "content": result})

正确try/except 包住工具执行,把错误反馈给 Agent(机制 2)。

❌ 幻觉工具名当"正常情况"

# 坏:假设模型永远不会调错工具
func = TOOL_FUNCTIONS[func_name]  # KeyError 直接崩

正确:先检查 func_name in VALID_TOOL_NAMES,不存在就告知 Agent(机制 3)。

常见错误

概念懂了,实际写代码还是会踩坑。这些是初学者最常犯的错误。

错误 症状 解决
except 吞掉所有错误 Agent 静默失败,日志里什么都看不到 捕获具体异常类型,把错误信息反馈给 Agent
对认证错误也重试 401 错误重试 3 次,浪费时间和资源 is_retryable() 判断,认证/参数错误立即失败
重试时没加退避 限流后立刻重试,触发更严厉的封禁 2 ** attempt 等待,每次翻倍
幻觉工具名直接 KeyError 模型编造了不存在的工具名,代码崩溃 先检查 func_name in VALID_TOOL_NAMES,不存在就告知 Agent

本章的四大机制如何组合

这四个机制不是孤立的,它们在 Agent 循环里协同工作:

agent_loop():
    for step in range(MAX_STEPS):
        try:
            # 机制 1 + 4:带退避的重试,只针对可重试错误
            response = call_llm_with_retry(messages, tools)
        except AuthenticationError:
            raise  # 永久错误,直接退出(机制 4)

        assistant_msg = response.choices[0].message
        if not assistant_msg.tool_calls:
            return validate_output(assistant_msg.content)  # 输出护栏

        messages.append(assistant_msg)
        for tc in assistant_msg.tool_calls:
            func_name = tc.function.name
            # 机制 3:幻觉工具检测
            if func_name not in VALID_TOOL_NAMES:
                result = f"工具 '{func_name}' 不存在,可用:{...}"
            else:
                # 机制 2:工具异常 → 反馈给 Agent
                try:
                    result = TOOL_FUNCTIONS[func_name](**args)
                except Exception as e:
                    result = f"[工具失败] {e}"
            messages.append({"role": "tool", "content": result, ...})

每个机制负责一层防御,层层叠加,Agent 才能在出错时"软着陆"。


运行示例

# Python
cd ai-agent/06-error-handling
python3 python/main.py

# TypeScript
cd ai-agent/06-error-handling
npx tsx typescript/main.ts

代码会先用真实 API 尝试(占位符密钥会失败),然后自动降级为离线 mock 演示,100% 可靠地展示四大容错机制:

  • Demo A:指数退避重试(mock 一个"前 2 次失败、第 3 次成功"的 API)
  • Demo B:工具自我纠正(mock get_weather 第一次抛异常,Agent 换工具)
  • Demo C:幻觉工具检测(mock 模型调不存在的工具,告知后纠正)
  • Demo D:错误分类(展示可重试 vs 永久错误的判断逻辑)

输出用 OUT: 前缀标记,方便 grep 过滤 dotenvx 横幅。


兼容性注意

  • .env 是占位符密钥OPENAI_API_KEY=sk-REPLACE-ME)→ 真实 API 调用会 401。 代码捕获 AuthenticationError 并降级为离线 mock,依然展示完整的容错逻辑。
  • Ollama 不支持 tools API(返回 400)→ 同样降级为离线 mock。

下一步

本章你让「任务助手 Agent」获得了韧性——它能重试、能纠正、能区分错误、能优雅降级。

但到目前为止,我们的 Agent 都是"一次性"的——每次 agent_loop 调用,模型都从零开始,不记得上一次对话。

第07章「ReAct 推理」会让 Agent 学会显式思考——把推理过程(Thought → Action → Observation)写出来,而不是隐式地藏在 tools API 里。这让 Agent 的决策更透明、更可控,也为复杂任务(如多步研究)打下基础。

💡 容错 + 推理是 Agent 的两条腿:容错让它"走得更稳"(不出错能恢复),推理让它"走得更远"(能处理复杂任务)。缺一不可。


代码

阅读更多

AI Agent 进阶教程 — 从零到生产(Python + TypeScript)

一句话:从最原始的 API 调用出发,自己从零造一个 mini Agent 框架, 再切换到现代框架——学完后,无论流行什么框架你都能快速上手,知识不过时。 全程 Python + TypeScript 双语言并列,OpenAI 兼容接口可一键切换提供商。 为什么有这个教程 市面上的 Agent 教程大多有两个毛病: "先工具后原理":上来就教你调 LangChain 的 AgentExecutor,一行代码跑起来很爽, 但出 bug 时完全不知道哪里错了。框架一旦过时,你的知识也跟着报废。 玩具示例:清一色 "Hello World Agent",看完不会写真实的 Agent。 本教程反其道而行: 🧭 先原理后工具 先看清原理,再选择工具。 原理是不变的,工具是流动的。

作者:zhangsheng

第01章 · LLM 基础 —— 与模型对话的第一步

目标:理解 LLM API 的核心概念,能用 Python/TypeScript 发出第一个请求并读懂返回值。 这是整个教程的地基——后续 16 章的 Agent 循环、工具调用、记忆系统全都建立在此之上。 TL;DR 30 秒速读:LLM API 就是一个 HTTP 接口,你发 messages 数组,它返回补全文本,每次调用完全独立,模型不会记住任何历史。 如果只记一件事:messages 数组里的 role(system/user/assistant)决定了"谁在说话",system prompt 是你控制模型行为的最强杠杆。 1. LLM API 是什么?

作者:zhangsheng

第02章 Prompt 工程 — 让 LLM 精准听话

本章是「任务助手 Agent」的第一次能力升级:从"随便聊聊"变成"精准执行任务"。 我们会用原生字符串模板(f-string / 模板字符串)手写 4 种核心 Prompt 技术, 不依赖任何第三方抽象库。 TL;DR 30 秒速读:Prompt 工程的四种核心技术是 System Prompt(定身份)、Few-shot(给示例教模式)、Chain-of-Thought(引导逐步推理)、结构化输出(强制 JSON),它们是所有 Agent 指令设计的基础。 如果只记一件事:system prompt 的优先级高于 user message,

作者:zhangsheng

第03章 工具调用(Tool Use / Function Calling)

「任务助手 Agent」获得了"手"——它能调用工具查天气、做计算、搜百科,不再只是空谈。 TL;DR 30 秒速读:LLM 本身不能执行任何代码,所谓"工具调用"是模型输出一段 JSON 指令,由你的客户端解析并执行,再把结果反馈给模型。 如果只记一件事:工具的 description 写得越清晰,模型选择工具越准确;写"处理数据"不如写"查询指定城市的当前天气,返回温度和天气状况"。 本章目标 学完本章,你将理解: 工具调用的本质:LLM 不直接执行工具,而是输出结构化指令(JSON),由客户端执行 JSON Schema

作者:zhangsheng