别再裸调大模型了:用 60 行 Python 给 LLM 调用加上「重试 + 超时 + 降级」
关键词:LLM、Python、容错、重试、降级、生产可用
你有没有遇到过这种情况:本地跑得好好的 AI 功能,一上生产就各种报错------429 限流、偶发超时、某个模型半夜抽风。裸调一次 client.chat.completions.create(...) 的代码,放在 Demo 里没问题,放在生产里就是在埋雷。
本文用不到 60 行 Python,带你封装一层带重试、退避、降级的 LLM 调用壳。它不是框架,而是一套你可以直接抄进项目的「最小防护层」。
一、为什么需要防护层
大模型 API 最大的特点就是不稳定但可恢复:
- 限流(429):流量一上来就被掐,等一会儿就好;
- 超时 / 5xx:网络抖动或服务端过载,重试大概率成功;
- 模型波动:主模型偶尔质量下降或不可用时,换个小模型兜底也能给出可用结果。
裸调代码的问题是:它把「一次成功」当成「永远成功」。而生产环境的正确心智模型是------失败是常态,调用层要替业务方消化掉这些瞬时故障。
我们的目标是:业务方只关心 llm.complete(prompt),至于重试几次、要不要降级,全部封装在底层。
二、设计目标
一个合格的 LLM 防护层,至少要做到这 4 点:
- 区分错误类型:429/超时可重试,401/400 不可重试(重试也是白重试);
- 指数退避 + 抖动:避免「重试风暴」把对方服务直接打挂;
- 主链路失败可降级:主模型挂了,自动切到兜底模型;
- 可观测:每次重试、每次降级都留痕,方便排查。
三、完整实现(核心 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,重试逻辑就和业务解耦了;backoff用2^(n-1)做指数增长,再用random加抖动,避免多个请求同时重试造成「惊群」;fallback在主链路彻底失败后兜底,降级是把「不可用」变成「勉强可用」,而不是「直接报错」。
四、接入真实 SDK(以 OpenAI 为例)
真实项目里,primary 和 fallback 就是两个不同模型的调用函数。注意一点:把底层异常翻译成我们自己的语义异常,这样重试判断才统一。
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 时直接复用。
如果你有更好的容错策略(比如基于令牌桶的限流、基于语义的降级),欢迎在评论区交流 👇
如果这篇文章对你有帮助,点个赞或者收藏,是我持续输出的最大动力。 ❤️