重试写成了死循环,降级写成了静默失败——错误处理接入的三个翻车现场
先说三个真实感很强的失败现场
现场一:while True 式重试,把配额烧成灰
很多独立开发者第一次写重试的逻辑是这样的:
while True:
result = call_api(...)
if result.ok:
break「接口偶尔抽风,多试几次总行了吧?」——结果是对方网关短暂限流,你的进程以每秒几十次的频率疯狂重试,不仅没成功,还占满了本就不多的请求配额。凌晨一看日志,几十万次失败请求,问题没解决,配额先没了。
重试的本质不是「多试几次」,而是给失败一个有节制的、可退避的第二次机会。
现场二:捕获所有异常,然后什么都不做
第二种常见写法:
try:
result = call_api(...)
except Exception:
pass # 失败就失败吧看起来「程序不崩了」,实际上是静默失败。用户看到的是空白页面或卡死的加载态,你看到的是一片安静的日志。等到用户反馈过来,你连失败发生在哪一层都不知道。降级不等于吞异常,降级是失败时给用户一条仍然可用的路径。
现场三:重试和降级各写一套,互相打架
还有的团队重试逻辑写在业务代码里,降级逻辑写在网关配置里,两边不知道对方的存在。结果是小故障被网关降级兜住了,业务层却又发起重试,把一次降级放大成三倍流量——本来撑得住的服务,被自己的重试策略压垮了。
这三个现场的共同根因是:把重试和降级当成散落的补丁,而不是一套统一的策略。正确的路径是:统一接入一个网关层,重试用指数退避 + 抖动,降级要有明确的降级动作和日志留痕。
正确路径:五分钟接入 ThisToken.AI 网关
ThisToken.AI 提供统一的 API 网关,你可以把多个模型供应商的调用收敛到一个入口上,重试和降级在接入层统一配置,业务代码保持干净。下面走一遍从注册到跑通的全流程。
第一步:注册并获取 API Key
- 打开 https://api.thistoken.ai/register ,用邮箱注册账号(独立开发者个人邮箱即可,小团队建议用团队主账号)。
- 登录后进入控制台,在「API Keys」页面创建一个新 Key。
- 立即把 Key 保存到环境变量,不要硬编码进代码(这是另一个高频翻车点,此处不展开,但请务必做到):
export THISTOKEN_API_KEY="sk-xxxxxxxx"计费相关问题以官网价格页为准,本文不涉及具体数字。
第二步:安装依赖
pip install openai tenacity我们用官方 SDK 的 base_url 指向网关,用 tenacity 做重试策略,这样重试逻辑是声明式的、可审计的,而不是散落在业务代码里的 while True。
第三步:跑通第一段代码
import os
import logging
from openai import OpenAI, APIConnectionError, APIStatusError
from tenacity import retry, stop_after_attempt, wait_exponential_jitter, retry_if_exception_type
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("demo")
client = OpenAI(
api_key=os.environ["THISTOKEN_API_KEY"],
base_url="https://api.thistoken.ai/v1",
)
# 重试策略:指数退避 + 抖动,只对可重试的错误重试,最多 3 次
@retry(
retry=retry_if_exception_type((APIConnectionError,)), # 网络类错误才值得重试
wait=wait_exponential_jitter(initial=1, max=8), # 1s 起步,最长 8s,带随机抖动
stop=stop_after_attempt(3),
before_sleep=lambda rs: logger.warning(f"第 {rs.attempt_number} 次失败,准备退避重试"),
)
def ask_model(prompt: str) -> str:
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
)
return resp.choices[0].message.content
# 降级策略:主链路彻底失败时,给用户一条仍然可用的路径
def ask_with_fallback(prompt: str) -> str:
try:
return ask_model(prompt)
except (APIConnectionError, APIStatusError) as e:
# 关键:降级必须留痕,不能静默
logger.error(f"主链路失败,触发降级: {e!r}")
return "服务暂时繁忙,已为您切换到基础模式,这里是预置的兜底回复。"
if __name__ == "__main__":
print(ask_with_fallback("用一句话解释什么是指数退避"))跑起来后,正常情况你会得到一条模型回复;如果你临时断网测试,会看到日志里先出现两次重试警告,最后输出降级文案——而不是死循环烧配额,也不是静默吞掉异常。
这段代码做对了什么
对照前面的三个翻车现场逐条检查:
没有死循环重试。 重试上限是 3 次,等待时间指数增长且带随机抖动(jitter),避免多个客户端在同一时刻整齐地发起重试,形成「重试风暴」。且只对网络类错误重试——鉴权失败、参数错误这类问题重试一万次也不会成功,应该直接走降级。
没有静默失败。 降级分支里有 logger.error,失败原因、触发时机全部留痕。用户拿到兜底回复,开发者拿到排障线索,两头都不抓瞎。
重试和降级在一处协调。 重试是降级的「内层」:先给主链路几次有节制的机会,机会用尽才降级。这个层次关系写在同一个模块里,不会被网关配置和业务代码各改一半。接入统一网关后,供应商侧的切换、配额管理也收敛在同一个入口,业务层不再感知底层细节。
三个容易忽略的收尾建议
- 给降级动作分级。 降级不只有「返回兜底文案」一种,还可以是换用更便宜的模型、返回缓存的旧结果、把请求转入异步队列稍后处理。根据业务重要性分级,别让核心链路和边缘功能共用同一个兜底。
- 重试要设总预算。 除了单次请求的次数上限,还要考虑并发场景下的全局重试预算,避免高峰期重试流量叠加压垮下游。
- 监控降级触发率。 降级是安全网,不是常态。如果降级触发率持续偏高,说明主链路本身有问题,该修的是主链路,而不是给安全网加缓冲。
错误处理写得好不好,平时完全看不出来,出事的那一刻才见分晓。与其在事故现场补 try...except,不如花五分钟把统一的重试与降级策略接入到位。现在就到 https://api.thistoken.ai/register 注册一个账号,把上面的代码跑起来,亲眼看一次「重试两次后优雅降级」的完整过程,你会对这套策略有完全不同的体感。
---
不想折腾多家供应商的接入差异?在 https://api.thistoken.ai/register 注册,用一个 base_url 调用所有模型。