课程 › 0基础Agent开发课 / 生产化部署 / AI应用的错误处理与重试策略
— 21 min read

AI应用的错误处理与重试策略

LLM API 在生产环境中面临多种失败场景:限流、超时、连接失败等。不同类型的错误需要不同的处理策略——盲目重试不仅无效,还会浪费配额、加剧服务器压力。本章系统介绍 LLM API 的错误分类、重试策略、超时配置、降级方案和熔断机制。

AI 应用的错误处理与重试策略

LLM API 在生产环境中面临多种失败场景:限流、超时、连接失败等。不同类型的错误需要不同的处理策略——盲目重试不仅无效,还会浪费配额、加剧服务器压力。本章系统介绍 LLM API 的错误分类、重试策略、超时配置、降级方案和熔断机制。


1.1 LLM API 的常见错误类型

是

否

LLM API 请求失败

错误类型分类

RateLimitError
HTTP 429 限流

APITimeoutError
超时

APIConnectionError
连接失败

AuthenticationError
HTTP 401 认证失败

InvalidRequestError
HTTP 400 请求无效

可重试

不可重试

指数退避 + 随机抖动
1s → 2s → 4s

重试成功?

恢复正常

达到最大重试次数
触发降级 / 熔断

立即报错

告警通知
人工介入

AI应用错误处理决策树——按错误类型分类,匹配重试、降级、熔断、告警策略

明确错误类型是制定处理策略的前提。

RateLimitError(限流错误),最常见。API 提供商对每个账号都有请求频率限制,请求太快就会触发。OpenAI 的报错是 429,通常过一会儿就能重试。解法:等一段时间再重试,不要立刻重发。

APITimeoutError(超时),API 响应时间过长,客户端主动断开。LLM 生成 token 本来就慢,加上网络延迟,这个问题不少见。解法:设置合理的 timeout,超时后重试。

APIConnectionError(连接失败),网络层面的问题,TCP 连接建立失败或中断。通常是瞬时的网络抖动。解法:重试几次,多数情况下能恢复。

AuthenticationError(认证失败),API Key 无效或过期,返回 401。这种错误不应该重试——重试也没用,Key 本身有问题。解法:立刻报错,告警通知,人工介入。

InvalidRequestError(请求无效),请求参数本身有问题,最常见的是 token 超限(context window 满了)。返回 400。这种错误重试也没用,需要先处理请求本身。解法:不重试,截断输入或分批处理。

归纳一下:可重试的是 429、超时、连接失败;不可重试的是认证失败和请求参数错误。


1.2 重试策略:指数退避加随机抖动

固定间隔重试存在"雷群效应"(Thundering Herd)问题:大量请求同时失败后,如果以固定间隔重试,这些请求会在同一时刻再次涌入,可能再次打垮服务器,形成失败-重试-失败的恶性循环。

正确的方案是指数退避(Exponential Backoff,每次重试等待时间翻倍:1秒、2秒、4秒……避免所有请求同时重试)加随机抖动(Jitter,在等待时间上加随机偏移,让不同请求错开重试时间):

  • 第 1 次失败等 1 秒
  • 第 2 次失败等 2 秒
  • 第 3 次失败等 4 秒
  • 每次等待时间再加一个随机的抖动值(比如 0-1 秒的随机数)

这样重试间隔越来越长,不同客户端的重试时间点也被打散,服务器压力分布均匀,能从故障中平稳恢复。

Python 中实现这个方案推荐使用 tenacity 库:

import os
import logging
from tenacity import (
    retry,
    stop_after_attempt,
    wait_exponential,
    retry_if_exception_type,
    before_sleep_log,
)
from openai import OpenAI, RateLimitError, APITimeoutError, APIConnectionError

logger = logging.getLogger(__name__)

client = OpenAI(base_url="https://api.deepseek.com", api_key=os.getenv("DEEPSEEK_API_KEY"))

@retry(
    # 只对这三类错误重试,认证失败和请求无效不重试
    retry=retry_if_exception_type((RateLimitError, APITimeoutError, APIConnectionError)),
    # 最多重试 4 次(加上第一次共 5 次)
    stop=stop_after_attempt(4),
    # 指数退避:最少等 1 秒,最多等 30 秒,乘数 2
    wait=wait_exponential(multiplier=1, min=1, max=30),
    # 每次重试前打日志,方便排查
    before_sleep=before_sleep_log(logger, logging.WARNING),
    # 重试全部失败后重新抛出原始异常,而不是 tenacity 的异常
    reraise=True,
)
def call_llm_with_retry(messages: list, model: str = "deepseek-chat") -> str:
    """带重试的 LLM 调用封装"""
    response = client.chat.completions.create(
        model=model,
        messages=messages,
        timeout=30,  # 单次调用超时 30 秒
    )
    return response.choices[0].message.content

wait_exponential 默认已经内置了随机抖动,不需要额外处理。

retry_if_exception_type 指定哪些异常才触发重试,这一条非常重要。如果不加限制,AuthenticationError 也会重试 4 次,毫无意义还浪费时间。


1.3 生产级 LLM API 错误处理完整模板

以下是经过生产验证的完整错误处理模板,涵盖所有常见故障场景:

import asyncio
import logging
import os
import time
from typing import Optional
from openai import AsyncOpenAI, RateLimitError, APIStatusError, APIConnectionError

logger = logging.getLogger(__name__)

class LLMAPIError(Exception):
    """LLM API 调用失败的统一异常"""
    def __init__(self, message: str, status_code: Optional[int] = None, retryable: bool = False):
        super().__init__(message)
        self.status_code = status_code
        self.retryable = retryable

async def call_llm_with_retry(
    client: AsyncOpenAI,
    messages: list,
    model: str = "deepseek-chat",
    max_tokens: int = 1024,
    max_retries: int = 3,
    base_delay: float = 1.0,
) -> str:
    """
    带完整错误处理和指数退避重试的 LLM 调用。

    错误分类:
    - 429 限流:可重试,等待更长时间
    - 500/529 服务器错误:可重试
    - 400 请求错误:不可重试(参数有问题)
    - 401 认证失败:不可重试(检查 API Key)
    - 超时:可重试
    """
    last_exception = None

    for attempt in range(max_retries):
        try:
            response = await asyncio.wait_for(
                client.chat.completions.create(
                    model=model,
                    max_tokens=max_tokens,
                    messages=messages,
                ),
                timeout=30.0  # 30秒超时
            )

            # 验证响应不为空
            if not response.choices:
                raise LLMAPIError("API 返回空响应", retryable=True)

            return response.choices[0].message.content

        except asyncio.TimeoutError:
            last_exception = LLMAPIError("API 调用超时(30s)", retryable=True)
            logger.warning(f"LLM 调用超时,第 {attempt + 1}/{max_retries} 次尝试")

        except RateLimitError as e:
            # 429:限流,需要等待更长时间
            last_exception = LLMAPIError(f"API 限流: {e}", status_code=429, retryable=True)
            wait_time = base_delay * (2 ** attempt) + 5  # 额外等5秒
            logger.warning(f"API 限流,等待 {wait_time:.1f}s 后重试")
            await asyncio.sleep(wait_time)
            continue

        except APIStatusError as e:
            if e.status_code in (500, 529):
                # 服务器错误,可重试
                last_exception = LLMAPIError(f"API 服务器错误: {e}", status_code=e.status_code, retryable=True)
                logger.warning(f"API 服务器错误 {e.status_code},第 {attempt + 1}/{max_retries} 次尝试")
            elif e.status_code == 400:
                # 请求参数错误,不可重试
                logger.error(f"API 请求参数错误: {e}")
                raise LLMAPIError(f"请求参数错误: {e}", status_code=400, retryable=False)
            elif e.status_code == 401:
                # 认证失败,不可重试
                logger.error("API Key 无效或已过期")
                raise LLMAPIError("API Key 无效,请检查配置", status_code=401, retryable=False)
            else:
                last_exception = LLMAPIError(f"API 错误 {e.status_code}: {e}", status_code=e.status_code, retryable=True)

        except APIConnectionError as e:
            # 网络连接问题,可重试
            last_exception = LLMAPIError(f"网络连接失败: {e}", retryable=True)
            logger.warning(f"网络连接失败,第 {attempt + 1}/{max_retries} 次尝试")

        # 指数退避:1s, 2s, 4s...
        if attempt < max_retries - 1:
            wait_time = base_delay * (2 ** attempt)
            logger.info(f"等待 {wait_time}s 后重试...")
            await asyncio.sleep(wait_time)

    # 所有重试都失败
    logger.error(f"LLM 调用在 {max_retries} 次重试后失败: {last_exception}")
    raise last_exception or LLMAPIError("未知错误")


# 使用示例
async def main():
    client = AsyncOpenAI(
        base_url="https://api.deepseek.com",
        api_key=os.environ.get("DEEPSEEK_API_KEY"),
    )
    try:
        result = await call_llm_with_retry(
            client=client,
            messages=[{"role": "user", "content": "你好"}],
        )
        print(result)
    except LLMAPIError as e:
        if e.retryable:
            print(f"临时错误,请稍后重试: {e}")
        else:
            print(f"配置错误,请检查设置: {e}")

关键设计决策说明:

错误类型 HTTP状态码 是否重试 原因
限流 429 重试+额外等待 等待配额恢复
服务器错误 500/529 重试 临时故障
超时 - 重试 网络抖动
请求错误 400 不重试 参数有问题,重试无意义
认证失败 401 不重试 需要修复配置
空响应 - 重试 偶发异常

与前面 tenacity 方案的区别:这个模板直接控制每种错误的处理逻辑,不依赖第三方库,更适合对错误行为有精确要求的场景;tenacity 方案代码更简洁,适合快速集成。两种方式可以根据项目情况选择。


1.4 超时设置:两个维度

超时有两种语义,要分开处理。

连接超时:建立 TCP 连接的等待时间。如果服务器根本没响应,这个超时决定等待多久才放弃。通常设 5-10 秒够了。

读取超时:连接建立之后,等待服务器返回数据的时间。LLM 生成内容慢,这个值要设大一些,建议 30-60 秒。

from openai import OpenAI
import httpx

# 分别设置连接超时和读取超时
client = OpenAI(
    base_url="https://api.deepseek.com",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    timeout=httpx.Timeout(
        connect=5.0,    # 建立连接最多等 5 秒
        read=60.0,      # 等待响应最多等 60 秒
        write=5.0,
        pool=5.0,
    )
)

流式输出的超时是另一个需要注意的场景。流式输出时,第一个 token 可能要等 3-5 秒,但之后每个 token 很快。处理流式超时的思路是:分开控制"首个 token 等待超时"和"token 间隔超时"。

import asyncio
from openai import AsyncOpenAI

async_client = AsyncOpenAI(base_url="https://api.deepseek.com", api_key=os.getenv("DEEPSEEK_API_KEY"))

async def stream_with_timeout(prompt: str, first_token_timeout: float = 10.0):
    """带首 token 超时的流式输出"""
    stream = await async_client.chat.completions.create(
        model="deepseek-chat",
        messages=[{"role": "user", "content": prompt}],
        stream=True,
        timeout=60.0,
    )

    first_token_received = False
    full_content = []

    try:
        async for chunk in asyncio.wait_for(
            _iterate_stream(stream, full_content),
            timeout=first_token_timeout if not first_token_received else None,
        ):
            first_token_received = True
            yield chunk
    except asyncio.TimeoutError:
        raise LLMAPIError("等待首个 token 超时", retryable=True)

async def _iterate_stream(stream, collector: list):
    async for chunk in stream:
        delta = chunk.choices[0].delta.content
        if delta:
            collector.append(delta)
            yield delta

1.5 降级策略:主模型挂了用备用模型

单一模型依赖是一个风险点。当主模型出现区域性故障时,如果没有备用方案,整个服务就停了。

设计思路:主模型失败时,降级到备用模型。备用模型通常是更小、更便宜的版本,或者来自不同提供商。

import os
from openai import OpenAI, APIError

openai_client = OpenAI(base_url="https://api.deepseek.com", api_key=os.getenv("DEEPSEEK_API_KEY"))
deepseek_client = OpenAI(
    base_url="https://api.deepseek.com",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
)

def call_with_fallback(user_message: str) -> dict:
    """
    主模型:GPT-4o
    降级一:GPT-4o-mini
    降级二:DeepSeek
    """
    strategies = [
        ("gpt-4o", _call_openai),
        ("gpt-4o-mini", _call_openai),
        ("deepseek-chat", _call_deepseek),
    ]

    last_error = None
    for model_name, call_fn in strategies:
        try:
            result = call_fn(user_message, model_name)
            return {"content": result, "model_used": model_name}
        except Exception as e:
            logger.warning(f"模型 {model_name} 失败: {type(e).__name__}: {e}")
            last_error = e
            continue

    # 全部失败,返回友好错误,不暴露技术细节
    logger.error(f"所有模型均失败,最后一个错误: {last_error}")
    return {
        "content": "服务暂时不可用,请稍后再试。",
        "model_used": None,
        "error": True,
    }

def _call_openai(message: str, model: str) -> str:
    response = openai_client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": message}],
        timeout=30,
    )
    return response.choices[0].message.content

def _call_deepseek(message: str, model: str) -> str:
    response = deepseek_client.chat.completions.create(
        model=model,
        max_tokens=1024,
        messages=[{"role": "user", "content": message}],
        timeout=30,
    )
    return response.choices[0].message.content

对用户暴露的错误信息应使用友好提示("服务暂时不可用"),而非技术细节(APIConnectionError: Connection refused to api.openai.com)。技术细节暴露给用户没有意义,还可能引发安全问题。


1.6 熔断器:防止雪崩

重试和降级处理的是单次失败。熔断(Circuit Breaker)处理的是持续性故障。

当 API 提供商持续返回 500 时,若每次请求都要等 30 秒超时再重试 4 次,大量并发请求会耗尽线程池,导致服务自身也挂掉。熔断器的逻辑是:

  • 关闭状态(正常):请求正常放行
  • 开启状态(熔断):连续失败 N 次后,直接拒绝后续请求,不再发出 API 调用
  • 半开状态(探测):隔一段时间放一个请求进去探测,如果成功则恢复正常,失败则继续熔断
import time
from threading import Lock
from enum import Enum

class CircuitState(Enum):
    CLOSED = "closed"       # 正常
    OPEN = "open"           # 熔断中
    HALF_OPEN = "half_open" # 探测中

class CircuitBreaker:
    def __init__(
        self,
        failure_threshold: int = 5,     # 连续失败几次触发熔断
        recovery_timeout: float = 60.0, # 熔断后多少秒开始探测
        success_threshold: int = 2,     # 探测成功几次才恢复正常
    ):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.success_threshold = success_threshold

        self.state = CircuitState.CLOSED
        self.failure_count = 0
        self.success_count = 0
        self.last_failure_time = None
        self._lock = Lock()

    def call(self, func, *args, **kwargs):
        with self._lock:
            if self.state == CircuitState.OPEN:
                if time.time() - self.last_failure_time >= self.recovery_timeout:
                    self.state = CircuitState.HALF_OPEN
                    self.success_count = 0
                    logger.info("熔断器进入半开状态,开始探测")
                else:
                    raise RuntimeError("服务熔断中,请稍后重试")

        try:
            result = func(*args, **kwargs)
            self._on_success()
            return result
        except Exception as e:
            self._on_failure()
            raise

    def _on_success(self):
        with self._lock:
            if self.state == CircuitState.HALF_OPEN:
                self.success_count += 1
                if self.success_count >= self.success_threshold:
                    self.state = CircuitState.CLOSED
                    self.failure_count = 0
                    logger.info("熔断器恢复正常状态")
            elif self.state == CircuitState.CLOSED:
                self.failure_count = 0

    def _on_failure(self):
        with self._lock:
            self.failure_count += 1
            self.last_failure_time = time.time()
            if (self.state in (CircuitState.CLOSED, CircuitState.HALF_OPEN)
                    and self.failure_count >= self.failure_threshold):
                self.state = CircuitState.OPEN
                logger.warning(f"熔断器开启,连续失败 {self.failure_count} 次")


# 使用示例
breaker = CircuitBreaker(failure_threshold=5, recovery_timeout=60)

def safe_call_llm(message: str) -> str:
    try:
        return breaker.call(call_llm_with_retry, [{"role": "user", "content": message}])
    except RuntimeError as e:
        # 熔断状态,快速失败
        return "服务暂时不可用,请稍后再试。"

熔断器和重试配合使用:重试处理偶发错误,熔断处理持续故障。有了这两层,AI 应用在 API 出问题时能做到快速失败,而不是慢性等待。


1.7 上线前的验证清单

LLM API 本身比普通 HTTP 服务脆弱得多:响应慢、偶发超时、有限流配额、价格敏感。这些问题在生产环境中每天都会出现。

上线前至少要验证以下场景:

  • API Key 故障时用户看到的是什么
  • 限流触发时用户等待多久
  • 主模型挂了备用模型能否正常接管
  • 连续故障时熔断器是否生效

在 staging 环境模拟这些场景,能在用户发现问题之前先行发现并修复。

目录