Skip to content
🔗 分享本题
查看我的学习进度 →

24 模块 Q3 教学图:如何用 Python 实现健壮的 LLM 重试机制(含指数退避)?

🧠 图解记忆:为什么 LLM 调用需要重试;点击图片可查看原图。

💡 答案要点

为什么 LLM 调用需要重试?

LLM API 调用失败的两大原因:

  • 瞬时错误:网络抖动、API 限流(HTTP 429、500)
  • 长尾延迟:模型冷启动、超时
最佳重试策略:指数退避 + 抖动

指数退避原理:

策略重试间隔优点缺点
固定间隔1s, 1s, 1s简单可能打爆限流
线性增长1s, 2s, 3s温和收敛慢
指数退避1s, 2s, 4s, 8s收敛快可能过长
指数退避+抖动1±0.5s, 2±1s...平衡最优

生产级重试实现:

展开 Python 代码示例(86 行)
python
import asyncio
import random
from typing import TypeVar, Callable, Any
from functools import wraps
import logging

logger = logging.getLogger(__name__)

T = TypeVar('T')

class RetryError(Exception):
    """重试耗尽异常"""
    def __init__(self, attempts: int, last_error: Exception):
        self.attempts = attempts
        self.last_error = last_error
        super().__init__(f"重试 {attempts} 次后失败: {last_error}")


def async_retry(
    max_attempts: int = 3,
    base_delay: float = 1.0,
    max_delay: float = 60.0,
    exponential_base: float = 2.0,
    jitter: bool = True,
    retry_on: tuple = (Exception,),
):
    """
    异步重试装饰器(指数退避 + 抖动)
    
    Args:
        max_attempts: 最大尝试次数
        base_delay: 基础延迟(秒)
        max_delay: 最大延迟上限
        exponential_base: 指数基数
        jitter: 是否加随机抖动
        retry_on: 需要重试的异常类型
    """
    def decorator(func: Callable[..., T]) -> Callable[..., T]:
        @wraps(func)
        async def wrapper(*args, **kwargs) -> T:
            last_error = None
            
            for attempt in range(1, max_attempts + 1):
                try:
                    return await func(*args, **kwargs)
                except retry_on as e:
                    last_error = e
                    
                    if attempt == max_attempts:
                        raise RetryError(max_attempts, last_error) from e
                    
                    # 计算延迟
                    delay = min(base_delay * (exponential_base ** (attempt - 1)), max_delay)
                    
                    # 加抖动(避免多实例同时重试打爆服务)
                    if jitter:
                        delay = delay * (0.5 + random.random() * 0.5)
                    
                    # 根据错误类型调整延迟
                    if "429" in str(e) or "rate_limit" in str(e).lower():
                        delay = max(delay, 10)  # 限流错误至少等10秒
                    
                    logger.warning(
                        f"Attempt {attempt}/{max_attempts} failed for {func.__name__}: {e}. "
                        f"Retrying in {delay:.2f}s..."
                    )
                    await asyncio.sleep(delay)
            
            raise RetryError(max_attempts, last_error)
        
        return wrapper
    return decorator


# 使用示例
class LLMClient:
    def __init__(self):
        self.client = AsyncOpenAI()
    
    @async_retry(max_attempts=4, base_delay=1.5, retry_on=(RateLimitError, TimeoutError, APIError))
    async def chat(self, prompt: str) -> str:
        response = await self.client.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": prompt}]
        )
        return response.choices[0].message.content

带熔断器的重试(防止雪崩):

展开 Python 代码示例(49 行)
python
import time
from dataclasses import dataclass, field

@dataclass
class CircuitBreaker:
    """熔断器:连续失败 N 次后暂停服务"""
    failure_threshold: int = 5
    recovery_timeout: float = 60.0
    failures: int = field(default=0)
    last_failure_time: float = field(default=0)
    state: str = "closed"  # closed, open, half_open
    
    def record_success(self):
        self.failures = 0
        self.state = "closed"
    
    def record_failure(self):
        self.failures += 1
        self.last_failure_time = time.time()
        if self.failures >= self.failure_threshold:
            self.state = "open"
            logger.error(f"Circuit breaker opened after {self.failures} failures")
    
    def can_attempt(self) -> bool:
        if self.state == "closed":
            return True
        if self.state == "open":
            if time.time() - self.last_failure_time > self.recovery_timeout:
                self.state = "half_open"
                return True
            return False
        return True  # half_open


# 组合使用
circuit_breaker = CircuitBreaker()

@async_retry(max_attempts=3)
async def safe_chat(prompt: str) -> str:
    if not circuit_breaker.can_attempt():
        raise Exception("Circuit breaker is open")
    
    try:
        result = await llm_client.chat(prompt)
        circuit_breaker.record_success()
        return result
    except Exception as e:
        circuit_breaker.record_failure()
        raise

面试话术:

"LLM 重试不是失败后无条件重来。先区分可重试错误,例如部分 429、超时和临时 5xx,再使用带抖动的指数退避,避免多实例同步重试;设置总时限和最大次数,并配合并发限制、熔断与可观测性。工具调用等有副作用的操作还要使用幂等键或去重,具体参数应根据供应商响应头和业务 SLO 调整。"

📚 参考:Tenacity(Python 重试库官方文档)