别再裸调大模型了:用 60 行 Python 给 LLM 调用加上「重试 + 超时 + 降级」

别再裸调大模型了:用 60 行 Python 给 LLM 调用加上「重试 + 超时 + 降级」

关键词:LLM、Python、容错、重试、降级、生产可用

你有没有遇到过这种情况:本地跑得好好的 AI 功能,一上生产就各种报错------429 限流、偶发超时、某个模型半夜抽风。裸调一次 client.chat.completions.create(...) 的代码,放在 Demo 里没问题,放在生产里就是在埋雷。

本文用不到 60 行 Python,带你封装一层带重试、退避、降级的 LLM 调用壳。它不是框架,而是一套你可以直接抄进项目的「最小防护层」。


一、为什么需要防护层

大模型 API 最大的特点就是不稳定但可恢复

  • 限流(429):流量一上来就被掐,等一会儿就好;
  • 超时 / 5xx:网络抖动或服务端过载,重试大概率成功;
  • 模型波动:主模型偶尔质量下降或不可用时,换个小模型兜底也能给出可用结果。

裸调代码的问题是:它把「一次成功」当成「永远成功」。而生产环境的正确心智模型是------失败是常态,调用层要替业务方消化掉这些瞬时故障

我们的目标是:业务方只关心 llm.complete(prompt),至于重试几次、要不要降级,全部封装在底层。


二、设计目标

一个合格的 LLM 防护层,至少要做到这 4 点:

  1. 区分错误类型:429/超时可重试,401/400 不可重试(重试也是白重试);
  2. 指数退避 + 抖动:避免「重试风暴」把对方服务直接打挂;
  3. 主链路失败可降级:主模型挂了,自动切到兜底模型;
  4. 可观测:每次重试、每次降级都留痕,方便排查。

三、完整实现(核心 60 行)

python 复制代码
import time
import random
from typing import Callable, Optional


class LLMError(Exception):
    """所有 LLM 调用异常的基类。"""


class LLMTemporaryError(LLMError):
    """可重试的错误:限流(429)、超时、5xx 等。"""


class LLMPermanentError(LLMError):
    """不可重试的错误:鉴权失败(401)、参数错误(400) 等。"""


def is_retryable(err: Exception) -> bool:
    """默认策略:只有「临时错误」才重试,避免对 401/400 做无意义重试。"""
    return isinstance(err, LLMTemporaryError)


def backoff(attempt: int, base: float = 0.5, cap: float = 8.0) -> None:
    """指数退避 + 随机抖动,防止重试风暴。"""
    delay = min(cap, base * (2 ** (attempt - 1)))
    time.sleep(delay + random.uniform(0, delay * 0.2))


class ResilientLLM:
    """一个带重试 / 降级能力的 LLM 调用壳。"""

    def __init__(
        self,
        primary: Callable[[str], str],
        fallback: Optional[Callable[[str], str]] = None,
        max_retries: int = 3,
        is_retryable: Callable[[Exception], bool] = is_retryable,
        on_retry: Optional[Callable[[int, Exception], None]] = None,
    ):
        self.primary = primary
        self.fallback = fallback
        self.max_retries = max_retries
        self.is_retryable = is_retryable
        self.on_retry = on_retry

    def complete(self, prompt: str) -> str:
        last_err: Optional[Exception] = None
        for attempt in range(1, self.max_retries + 1):
            try:
                return self.primary(prompt)
            except Exception as e:
                last_err = e
                # 不可重试,或已是最后一次,直接跳出
                if not self.is_retryable(e) or attempt == self.max_retries:
                    break
                if self.on_retry:
                    self.on_retry(attempt, e)
                backoff(attempt)

        # 主链路失败,尝试降级
        if self.fallback is not None:
            try:
                return self.fallback(prompt)
            except Exception as e:
                last_err = e

        raise LLMError(f"主调用与降级均失败,最后错误: {last_err}")

关键点拆解:

  • is_retryable 决定了「该不该重试」。把不同错误翻译成 LLMTemporaryError / LLMPermanentError,重试逻辑就和业务解耦了;
  • backoff2^(n-1) 做指数增长,再用 random 加抖动,避免多个请求同时重试造成「惊群」;
  • fallback 在主链路彻底失败后兜底,降级是把「不可用」变成「勉强可用」,而不是「直接报错」。

四、接入真实 SDK(以 OpenAI 为例)

真实项目里,primaryfallback 就是两个不同模型的调用函数。注意一点:把底层异常翻译成我们自己的语义异常,这样重试判断才统一。

python 复制代码
from openai import OpenAI

client = OpenAI()


def call_gpt4o(prompt: str) -> str:
    try:
        resp = client.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": prompt}],
            timeout=15,
        )
        return resp.choices[0].message.content or ""
    except Exception as e:
        msg = str(e).lower()
        # 限流 / 超时 → 临时错误(可重试)
        if "429" in str(e) or "timeout" in msg:
            raise LLMTemporaryError(str(e)) from e
        # 其他 → 永久错误(不重试)
        raise LLMPermanentError(str(e)) from e


def call_gpt4o_mini(prompt: str) -> str:
    try:
        resp = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}],
            timeout=10,
        )
        return resp.choices[0].message.content or ""
    except Exception as e:
        msg = str(e).lower()
        if "429" in str(e) or "timeout" in msg:
            raise LLMTemporaryError(str(e)) from e
        raise LLMPermanentError(str(e)) from e


llm = ResilientLLM(
    primary=call_gpt4o,        # 主模型:质量高
    fallback=call_gpt4o_mini,  # 降级模型:便宜、快
    max_retries=3,
    on_retry=lambda n, e: print(f"[重试] 第{n}次失败: {e}"),
)

print(llm.complete("用一句话解释什么是向量数据库"))

业务方从此只调用 llm.complete(prompt),重试和降级全在底层消化。


五、一个可直接运行的演示

没有 API Key 也能验证重试逻辑。下面是一个「70% 概率失败」的假模型:

python 复制代码
import random

def flaky(prompt: str) -> str:
    if random.random() < 0.7:
        raise LLMTemporaryError("429 Too Many Requests")
    return f"✅ 成功响应: {prompt[:20]}..."

demo = ResilientLLM(primary=flaky, max_retries=5)
print(demo.complete("你好"))

多跑几次你会发现:前几次大概率失败并重试,最终成功返回------这就是防护层在替你「扛波动」。把 max_retries 调到 1,它就会立刻把错误抛给你,对比非常直观。


六、进阶:还能加什么

防护层打好地基后,可以按需往上叠能力:

  • 熔断(Circuit Breaker):连续失败 N 次后,短时间内直接拒绝,给下游喘息机会;
  • 结果缓存:相同 prompt 命中缓存直接返回,省钱又降延迟;
  • 结构化输出 :用 Pydantic + function calling 把「文本」变成「对象」,调用方拿到的是 dataclass 而不是要自己正则解析的字符串;
  • 限流(Rate Limiter):在客户端侧主动限速,从源头避免 429。

这些是「锦上添花」,但重试 + 降级是「雪中送炭」,优先级最高。


七、小结

裸调大模型最大的坑,是把「Demo 里的一次成功」当成「生产里的常态」。真正生产可用的调用层,应该帮业务方消化掉瞬时故障:

主模型挂了 → 重试(带退避)→ 还不行 → 降级到兜底模型 → 再不行才报错。

上面那 60 行代码,就是这套心智模型的极简实现。建议把它存进你的工具库,下次接 LLM 时直接复用。

如果你有更好的容错策略(比如基于令牌桶的限流、基于语义的降级),欢迎在评论区交流 👇


如果这篇文章对你有帮助,点个赞或者收藏,是我持续输出的最大动力。 ❤️

相关推荐
菜冻鱼1 小时前
Python-sklearn-评估指标
开发语言·人工智能·python·机器学习·numpy·pandas·sklearn
guyiICtestsocket1 小时前
国内支持定制的手机LPDDR芯片测试座工厂多种结构
人工智能·python·智能手机
for_ever_love__1 小时前
python基础语法学习: 变量, 输入输出, 运算符
网络·python·学习
努力搬砖的咸鱼1 小时前
AI Agent测试全景图:它到底改变了什么
人工智能·python·ai·集成测试·pytest·agent·ai编程
张小殊.2 小时前
LoongForge TAOT 训练方案,解决MoE EP不均衡问题
人工智能·python·深度学习·机器学习·ai
玫幽倩2 小时前
2026黄河流域公安院校-电子物证单项赛(程序逆向分析+服务器取证)
运维·服务器·python·电子取证·逆向·程序分析·服务器取证
北斗落凡尘2 小时前
LangGraph 入门实战(4)
python·langchain
liwulin05063 小时前
【PYTHON】使用Selenium + ChromeDriver以及XPATH语法
开发语言·python·selenium
copyer_xyf3 小时前
Agentic RAG 实战:PostgreSQL + LangGraph 一条链路
python·postgresql·agent