AI Agent 错误处理与自愈机制:构建生产级可靠智能体

1 阅读1分钟

AI Agent 错误处理与自愈机制:构建生产级可靠智能体

"Reliable AI systems are designed for failure, not against it."

在 AI Agent 从 Demo 走向生产环境的道路上,错误处理是最容易被忽视却最具破坏力的环节。与传统软件不同,LLM API 具有独特的故障模式——Rate Limit、上下文溢出、内容策略拒绝、模型版本漂移——这些问题不是可以"修复"的 Bug,而是需要系统性应对的运营现实。

本文将深入解析 AI Agent 的错误处理与自愈机制,涵盖:错误分类体系、重试策略设计、熔断器模式、多层级降级方案、状态持久化与断点恢复,以及生产级可观测性设计。


一、错误分类:一切策略的起点

错误处理的第一步不是"重试",而是分类。不同类型的错误需要截然不同的处理策略——对瞬态错误重试是正确选择,对永久性错误重试则是浪费资源和时间。

1.1 LLM API 错误谱系

错误类型描述处理策略
Rate Limit (429)按 Token/分钟或请求/分钟配额超限指数退避重试
Timeout请求超时(通常 30-60 秒)重试
Server Error (5xx)提供商服务端问题重试
Context Overflow输入超过模型上下文窗口降级或截断
Content Filter安全策略拒绝降级或拒绝
Auth Error (401/403)认证失败立即失败,告警
Bad Request (400)请求格式错误立即失败,修 Bug

1.2 错误分类实现

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

class ErrorSeverity(Enum):
    TRANSIENT = "transient"      # 带退避重试
    PERMANENT = "permanent"      # 立即失败
    DEGRADED = "degraded"        # 切换到降级路径

def classify_llm_error(error: Exception) -> ErrorSeverity:
    """
    LLM API 错误分类器
    核心原则:宁可误分类为永久,也不应对认证错误盲目重试
    """
    # 瞬态错误 - 网络抖动、配额耗尽、服务端临时故障
    if isinstance(error, (RateLimitError, APITimeoutError, APIConnectionError)):
        return ErrorSeverity.TRANSIENT

    # 永久错误 - 认证失败
    if isinstance(error, AuthenticationError):
        return ErrorSeverity.PERMANENT

    # 请求错误 - 需要细分
    if isinstance(error, BadRequestError):
        error_msg = str(error).lower()
        if "context_length_exceeded" in error_msg:
            return ErrorSeverity.DEGRADED  # 上下文超限,需要降级处理
        return ErrorSeverity.PERMANENT    # 其他请求错误,立即失败

    # 内容策略拒绝 - 降级处理
    if isinstance(error, ContentFilterFinishReasonError):
        return ErrorSeverity.DEGRADED

    # 服务器错误 - 瞬态
    if isinstance(error, APIError) and error.status_code in (500, 502, 503, 504):
        return ErrorSeverity.TRANSIENT

    # 兜底 - 无法分类的视为永久错误,避免无限重试
    return ErrorSeverity.PERMANENT

关键设计原则

  • 宁可误判为永久,不盲目重试认证错误:重试 401 只会加速封禁
  • 上下文超限单独处理:不是失败,是需要降级的信号
  • 兜底为永久:未分类错误立即失败,避免资源浪费

二、重试策略:指数退避与抖动

2.1 为什么简单重试不够

# ❌ 错误示范:固定间隔重试
for i in range(3):
    try:
        return call_llm()
    except RateLimitError:
        time.sleep(1)  # 多实例同时等待1秒,同时恢复,同时触发限流...

固定间隔重试会导致雷鸣般的群体效应(Thundering Herd)——大量实例在限流恢复后同时发起请求,再次触发限流。

2.2 指数退避 + 随机抖动

from tenacity import (
    retry, retry_if_exception_type,
    stop_after_attempt, wait_exponential_jitter,
    before_sleep_log,
)
import openai

@retry(
    retry=retry_if_exception_type((
        openai.RateLimitError,
        openai.APITimeoutError,
        openai.APIConnectionError,
    )),
    wait=wait_exponential_jitter(
        initial=1,    # 第一次等待 1 秒
        max=60,       # 最大等待 60 秒
        jitter=5,     # 添加最多 ±5 秒随机抖动
    ),
    stop=stop_after_attempt(5),  # 最多尝试 5 次
    before_sleep=before_sleep_log(logger, logging.WARNING),
    reraise=True,
)
def call_llm_with_retry(client: openai.OpenAI, messages: list[dict], **kwargs):
    """
    生产级 LLM 调用封装

    重试延迟计算示例:
    - 第1次重试: 1s + random(0~5)s = 1~6s
    - 第2次重试: 2s + random(0~5)s = 2~7s
    - 第3次重试: 4s + random(0~5)s = 4~9s
    - ...
    - 最多 60s 上限
    """
    return client.chat.completions.create(
        model="gpt-4",
        messages=messages,
        timeout=30,  # 单次请求硬超时
        **kwargs,
    )

抖动退避的核心价值

  • 分散请求峰值:多实例不会同时重试
  • 自适应:失败越多,等待越久
  • 有上限:不会无限等待

2.3 异步重试实现

from tenacity import AsyncRetrying, retry_if_exception_type, stop_after_attempt, wait_exponential_jitter

async def call_llm_async(client: openai.AsyncOpenAI, messages: list[dict], **kwargs):
    async for attempt in AsyncRetrying(
        retry=retry_if_exception_type((
            openai.RateLimitError,
            openai.APITimeoutError,
            openai.APIConnectionError,
        )),
        wait=wait_exponential_jitter(initial=1, max=60, jitter=5),
        stop=stop_after_attempt(5),
        reraise=True,
    ):
        with attempt:
            return await client.chat.completions.create(
                model="gpt-4",
                messages=messages,
                timeout=30,
                **kwargs,
            )

三、熔断器模式:防止级联故障

3.1 什么是熔断器

熔断器(Circuit Breaker)借鉴自电路保护机制,其核心思想是:当一个组件持续失败时,"熔断"后续请求,快速失败而不是无谓等待

┌─────────────────────────────────────────────────────────┐
│                    Circuit Breaker                      │
│                                                         │
│   CLOSED ──(连续失败达到阈值)──▶ OPEN                   │
│     ▲                              │                    │
│     │                              │ (reset_timeout)   │
│     │     (2次成功)                ↓                    │
│     ◀────────────────────────── HALF-OPEN              │
│                                  │                      │
│                                  │ (测试请求失败)       │
│                                  ▼                      │
│                               (保持OPEN)                │
└─────────────────────────────────────────────────────────┘
状态行为何时进入
CLOSED请求正常通过初始状态;HALF-OPEN 成功后
OPEN立即拒绝,抛出 CircuitBreakerError连续失败达到 fail_max
HALF-OPEN允许一个测试请求reset_timeout 超时后

3.2 每个提供者独立熔断器

import pybreaker

# ❌ 错误:共享熔断器
shared_breaker = pybreaker.CircuitBreaker(fail_max=5)
# OpenAI 宕机 → Anthropic 请求也被拒绝

# ✅ 正确:每个提供者独立熔断器
openai_breaker = pybreaker.CircuitBreaker(
    fail_max=5,           # 5 次连续失败后开放
    reset_timeout=60,     # 60 秒后尝试恢复
    success_threshold=2, # 需要 2 次成功才完全关闭
    name="openai-gpt4",
)

anthropic_breaker = pybreaker.CircuitBreaker(
    fail_max=5,
    reset_timeout=60,
    success_threshold=2,
    name="anthropic-claude",
)

关键设计要点

  • 独立熔断:OpenAI 宕机不应阻塞 Anthropic 调用
  • Success Threshold > 1:防止不稳定提供者过早恢复
  • 独立命名:便于监控和问题定位

3.3 装饰器风格集成

@openai_breaker
def call_openai(client: openai.OpenAI, messages: list[dict], **kwargs):
    return client.chat.completions.create(
        model="gpt-4",
        messages=messages,
        timeout=30,
        **kwargs,
    )

@anthropic_breaker
def call_anthropic(client, messages: list[dict], **kwargs):
    return client.messages.create(
        model="claude-sonnet-4",
        messages=messages,
        max_tokens=4096,
        timeout=30,
    )

# 使用示例
def resilient_call(messages: list[dict], **kwargs):
    try:
        return call_openai(openai_client, messages, **kwargs)
    except pybreaker.CircuitBreakerError:
        logger.warning("OpenAI 熔断器开放,切换到 Anthropic")
        return call_anthropic(anthropic_client, messages, **kwargs)

四、多层级降级策略

4.1 降级金字塔

                    ┌─────────────────┐
                    │   静态 Fallback  │  ← 最后防线
                    ├─────────────────┤
                    │   缓存响应       │  ← 相似查询缓存
                    ├─────────────────┤
                    │   降级模型       │  ← 小模型/开源模型
                    ├─────────────────┤
                    │   备用提供者     │  ← 跨提供商 Fallback
                    ├─────────────────┤
                    │   重试          │  ← 瞬态错误恢复
                    └─────────────────┘
                         主请求路径

4.2 Fallback 链实现

from dataclasses import dataclass

@dataclass
class ModelConfig:
    provider: str
    model: str
    max_tokens: int

class LLMFallbackChain:
    """
    多提供者 Fallback 链
    按优先级尝试每个提供者,失败后自动切换
    """

    def __init__(self, models: list[ModelConfig]):
        self.models = models
        self._clients = {}  # 延迟初始化

    def _get_client(self, provider: str):
        if provider not in self._clients:
            if provider == "openai":
                self._clients[provider] = openai.OpenAI()
            elif provider == "anthropic":
                self._clients[provider] = anthropic.Anthropic()
        return self._clients[provider]

    def call(self, messages: list[dict], **kwargs) -> dict:
        errors = []

        for config in self.models:
            severity = None
            try:
                client = self._get_client(config.provider)

                if config.provider == "openai":
                    resp = client.chat.completions.create(
                        model=config.model,
                        messages=messages,
                        timeout=30,
                        **kwargs,
                    )
                    return {"content": resp.choices[0].message.content, "provider": "openai"}

                elif config.provider == "anthropic":
                    resp = client.messages.create(
                        model=config.model,
                        messages=messages,
                        max_tokens=config.max_tokens,
                        timeout=30,
                    )
                    return {"content": resp.content[0].text, "provider": "anthropic"}

            except Exception as e:
                severity = classify_llm_error(e)
                errors.append({"model": config.model, "error": str(e), "severity": severity.value})

                # 永久错误不重试
                if severity == ErrorSeverity.PERMANENT:
                    raise RuntimeError(f"永久错误,停止 Fallback: {e}")

                # 继续尝试下一个提供者
                continue

        raise RuntimeError(f"所有 {len(self.models)} 个提供者都失败了: {errors}")

# 使用示例
chain = LLMFallbackChain([
    ModelConfig("openai", "gpt-4", 4096),
    ModelConfig("anthropic", "claude-sonnet-4-20250514", 4096),
    ModelConfig("openai", "gpt-4o-mini", 4096),  # 降级模型
])

4.3 完整降级路径

class DegradationTier(Enum):
    FULL = "full"        # 主模型完整能力
    REDUCED = "reduced"  # 降级模型
    CACHED = "cached"    # 缓存响应
    STATIC = "static"    # 静态模板

class ResilientAgent:
    """
    完整弹性 Agent 实现
    自动按降级路径处理各类故障
    """

    def __init__(self, chain: LLMFallbackChain, cache: dict):
        self.chain = chain
        self.cache = cache

    def invoke(self, messages: list[dict]) -> tuple[dict, DegradationTier]:
        # Tier 1: 完整质量 (通过 Fallback 链)
        try:
            result = self.chain.call(messages)
            # 缓存成功响应
            cache_key = messages[-1]["content"][:100]
            self.cache[cache_key] = result
            return result, DegradationTier.FULL

        except RuntimeError:
            pass

        # Tier 2: 缓存响应
        cache_key = messages[-1]["content"][:100]
        if cache_key in self.cache:
            logger.warning("主路径失败,提供缓存响应")
            return self.cache[cache_key], DegradationTier.CACHED

        # Tier 3: 静态模板
        logger.error("所有降级路径耗尽,返回静态响应")
        return {
            "content": "抱歉,我遇到了暂时性技术问题,请在几分钟后重试。",
            "provider": "static",
        }, DegradationTier.STATIC

五、状态持久化与断点恢复

5.1 LangGraph 检查点机制

对于复杂的多步骤 Agent,状态持久化是断点恢复会话保持的基础。

from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, END

# 创建检查点管理器
checkpointer = MemorySaver()  # 开发环境用内存存储

# 生产环境使用 PostgreSQL
# from langgraph.checkpoint.postgres import PostgresSaver
# checkpointer = PostgresSaver.from_conn_string("postgresql://user:pass@host/db")
# checkpointer.setup()

# 创建 Agent 图
graph = StateGraph(AgentState)
graph.add_node("agent", agent_node)
graph.add_node("tools", tool_node)
graph.add_edge("agent", "tools")
graph.add_edge("agent", END)

# 编译时绑定检查点
app = graph.compile(checkpointer=checkpointer)

# 调用时会自动保存状态
config = {"configurable": {"thread_id": "user-session-123"}}
result = app.invoke({"messages": ["帮我分析销售数据"]}, config)

5.2 状态恢复与续跑

# 获取历史状态
config = {"configurable": {"thread_id": "user-session-123"}}
history = list(app.get_state_history(config))
print(f"已保存 {len(history)} 个检查点")

# 从断点恢复并继续
# 场景:Agent 在执行过程中失败,重启后继续
config = {"configurable": {"thread_id": "user-session-123", "checkpoint_id": "xxx"}}
app.invoke(None, config)  # None = 使用保存的状态,不传入新输入

# 更新状态后继续
app.update_state(config, {"messages": [...new_message...]})
app.invoke(None, config)

5.3 检查点存储对比

存储适用场景持久性并发
MemorySaver开发/测试进程结束丢失单进程
SqliteSaver小规模生产磁盘持久化单进程
PostgresSaver大规模生产数据库持久化多进程/多机器

六、工具调用容错

6.1 工具级重试装饰器

from functools import wraps
import logging

logger = logging.getLogger(__name__)

def resilient_tool(max_retries: int = 2, fallback_return=None):
    """
    Agent 工具容错装饰器
    自动重试 + fallback 返回值
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_error = None

            for attempt in range(max_retries + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_error = e
                    if attempt < max_retries:
                        logger.warning(
                            f"工具 '{func.__name__}' 第 {attempt + 1} 次失败: {e}"
                        )
                        continue

            # 所有重试耗尽
            if fallback_return is not None:
                logger.error(f"工具 '{func.__name__}' 使用 fallback 返回值")
                return fallback_return

            raise last_error

        return wrapper
    return decorator

# 使用示例
@resilient_tool(max_retries=3, fallback_return=[])
def search_documents(query: str) -> list[dict]:
    """
    搜索向量数据库
    失败时返回空列表而不是崩溃
    """
    results = vector_db.similarity_search(query, k=5)
    return [{"content": r.page_content, "score": r.score} for r in results]

6.2 工具调用超时控制

import asyncio
from asyncio.timeout import timeout as async_timeout

async def tool_with_timeout(tool_fn, timeout_seconds: int = 10, **kwargs):
    """
    带超时的工具调用
    防止工具执行时间过长导致 Agent 卡死
    """
    try:
        async with async_timeout(timeout_seconds):
            return await tool_fn(**kwargs)
    except asyncio.TimeoutError:
        logger.error(f"工具 '{tool_fn.__name__}' 执行超时 ({timeout_seconds}s)")
        raise

七、生产级可观测性

7.1 关键监控指标

指标计算方式告警阈值含义
重试率重试次数 / 总请求数> 20%临时故障频率过高
Fallback 激活率Fallback 调用 / 总请求数> 10%主提供者健康问题
熔断器状态变化状态变更次数每次频繁切换 = 不稳定
P95 延迟延迟分布 95 百分位基准 + 50%重试对延迟的影响
降级分布各降级层级占比-系统整体健康度

7.2 结构化日志设计

import structlog

logger = structlog.get_logger()

async def call_with_logging(messages: list[dict], **kwargs):
    request_id = str(uuid.uuid4())

    logger.info(
        "llm_request_start",
        request_id=request_id,
        model=kwargs.get("model"),
        message_count=len(messages),
    )

    try:
        start = time.monotonic()
        result = await call_llm_async(client, messages, **kwargs)
        duration = time.monotonic() - start

        logger.info(
            "llm_request_success",
            request_id=request_id,
            duration_ms=duration * 1000,
            provider=result.get("provider"),
        )
        return result

    except Exception as e:
        logger.error(
            "llm_request_failed",
            request_id=request_id,
            error_type=type(e).__name__,
            error_message=str(e),
            severity=classify_llm_error(e).value,
        )
        raise

八、架构总览:纵深防御

┌──────────────────────────────────────────────────────────────────┐
│                         Agent / 业务逻辑                          │
├──────────────────────────────────────────────────────────────────┤
│                    响应验证 (Pydantic Schema)                      │  ← 捕获格式错误
├──────────────────────────────────────────────────────────────────┤
│                       可观测性 (日志/指标)                         │  ← 问题发现
├──────────────────────────────────────────────────────────────────┤
│                   熔断器 (每个提供者独立)                          │  ← 故障隔离
├──────────────────────────────────────────────────────────────────┤
│                     Fallback 链 (多提供者)                       │  ← 降级处理
├──────────────────────────────────────────────────────────────────┤
│                   指数退避 + 抖动重试                              │  ← 瞬态恢复
├──────────────────────────────────────────────────────────────────┤
│                      工具级容错装饰器                              │  ← 细粒度保护
├──────────────────────────────────────────────────────────────────┤
│                      LLM Provider API                             │
└──────────────────────────────────────────────────────────────────┘

每层职责

  • 重试:处理瞬态故障(网络抖动、偶发限流)
  • Fallback:处理提供者级别故障(宕机、内容拒绝)
  • 熔断器:快速失败,防止资源浪费和级联故障
  • 降级:保证核心功能可用,而不是完全崩溃
  • 检查点:支持断点恢复,保证会话连续性

九、总结与最佳实践

核心原则

  1. 错误分类先行:不同错误类型需要不同策略,盲目重试是万恶之源
  2. 每个提供者独立保护:OpenAI 宕机不应影响 Claude 调用
  3. 降级优于崩溃:用户更能容忍降级能力,而不是"系统错误"
  4. 检查点保证连续性:长时任务必须有状态持久化
  5. 可观测性是基础:没有监控的错误处理是盲目的

检查清单

  • 错误分类完整,无未分类异常泄露
  • 每个提供者独立重试策略(含指数退避和抖动)
  • 每个提供者独立熔断器
  • Fallback 链端到端测试覆盖
  • 优雅降级路径(缓存/静态响应)
  • 所有外部工具调用有重试和 fallback
  • 可观测性就位(日志、指标、告警)
  • 每个外部调用设置明确超时
  • 故障注入负载测试验证

参考框架

  • LangChain/LangGraphRetryPolicycheckpointer
  • Tenacity:Python 重试库
  • PyBreaker/aiobreaker:熔断器实现
  • LiteLLM:统一的多模型调用和 Fallback
  • structlog:结构化日志

记住:在生产环境中,错误不是意外,而是常态。设计系统时要假设任何组件都可能失败,然后确保系统在面对这些失败时仍然能够优雅地降级和恢复。这才是真正的可靠性工程。