那天我盯着状态页刷了半小时,一行代码没改
上周有个晚上,我做的一个小工具突然大面积报错。用户在群里问我是不是挂了,我第一反应是打开 OpenAI 网页看看——结果网页也进不去,codex 客户端点历史会话直接弹「无法加载此对话」。
当时论坛里一片哀嚎,都在问「GPT 现在是崩了吗」。
我承认,那半小时我什么正事没干,就是刷状态页、刷论坛、等它自己好。刷到最后我突然意识到一个特别蠢的事实:我这半小时的焦虑,一行代码都没帮我省下来。
等官方恢复,是我唯一的策略。而这个策略,烂透了。
更糟的是,模型这边最近本来就不太平。有人观测到 OAI 大封杀、限制 20x 订阅之后账号开始「降智」和限流,静置申诉后大概只有五分之一能恢复,还怀疑同一个 IP 调用太频繁会「连坐」。
这意味着什么?意味着你的 API 调用失败,很可能不是一次性的偶发,而是一段时间内反复出现的常态。
那次之后我干了件事:把接下来一周里所有失败的调用,全都记下来。不是凭感觉,是真的落库。
记完我才搞明白,重试逻辑到底该怎么写。
先别急着写重试,先搞清楚你在跟什么打交道
我最早的重试代码特别天真,就是 `try...except` 里套个循环,失败就立刻再来一次,最多三次。跑了两天我就发现不对劲。
有些错误你重试一百次也没用。比如 401、403 这种鉴权问题,key 过期了或者被限制了,重试只是在浪费时间;再比如 400 参数错误,是你自己请求体写错了,重试永远是同一个错。
这些属于「立刻放弃」那一类,甚至应该直接抛给上层去报警。
真正值得重试的是另一批:网络超时、连接被重置、429 限流、还有 5xx 这种服务端临时抽风。前面提到的 GPT 网页进不去、账号降智限流,落到代码层面基本就是这几种。
它们的共同点是——过一会儿再试,很可能就好了。
所以重试的第一步根本不是写循环,而是给错误分类。我后来是这么分的:
- **该立刻放弃**:4xx 里除了 429 的大部分(鉴权、参数、余额不足),这些重试无意义;
- **该重试**:429、超时、连接错误、5xx;
- **拿不准的**:先归到「记录但不重试」,等日志攒够了再决定。
第三类特别重要。我一开始觉得自己能靠经验分好,结果一周日志看下来,好几个我以为该放弃的错,其实重试一次就过了。
所以别信经验,信数据。
指数退避:别再用固定间隔傻等了
分好类之后是退避策略。我第一版是失败了就 sleep 2 秒再试,结果撞上限流的时候特别惨——大家都在同一时刻重试,等于集体去捶服务端,429 更严重了。
正确的做法是指数退避加随机抖动。逻辑很简单:第一次失败等 1 秒,第二次等 2 秒,第三次等 4 秒,每次翻倍,再叠一个随机的小抖动,避免所有请求卡在同一个整点重试。
代码骨架大概长这样:
import time
import random
def call_with_retry(fn, max_retries=5, base=1.0, cap=30.0):
for attempt in range(max_retries):
try:
return fn()
except RetryableError as e:
if attempt == max_retries - 1:
raise # 重试次数用完了,老实抛出去
delay = min(cap, base * (2 ** attempt))
delay += random.uniform(0, delay * 0.3) # 加抖动
log_failure(e, attempt, delay) # 关键:把这次失败记下来
time.sleep(delay)
except FatalError:
raise # 该放弃的直接放弃,不进重试注意那个 `cap`,退避间隔要封顶,不然翻几次就等到几分钟去了,用户早跑了。这段逻辑不复杂,你可以直接在Python在线运行里贴一个假的失败函数跑几轮,看看退避间隔是不是按预期在涨,比在本地反复改环境省事多了。
这里我想说句可能不讨喜的话:稳定性从来不是靠换更强的模型换来的。模型再强,网络抖一下、服务端限个流,一样给你 500。
真正兜底的是你这套「失败了怎么办」的代码。
把每次失败都记进数据库,一周后我才看清全貌
光重试还不够。我最想搞明白的是:我的失败到底长什么样?
是集中在某个时段?某类错误?
还是某个模型?
光靠打日志到控制台完全不够,翻起来要命。我干脆建了张表,每次失败都往里写一行:时间戳、调用的哪个接口、错误码、错误类型、是第几次重试、最后成没成功。
表结构大概就这几列:
CREATE TABLE api_failures (
id INTEGER PRIMARY KEY,
ts TEXT,
endpoint TEXT,
error_code TEXT,
error_type TEXT,
attempt INTEGER,
final_success INTEGER
);我用的是 SQLite,理由很简单:一个文件搞定,不用起服务,做这种小工具的失败记录刚刚好。跑了一周,我用SQLite编辑器直接打开表按错误类型分组数了一下,结论挺打脸的:
我一直以为主要问题是超时。结果数据显示,超过一半的失败是 429 限流,跟那批「账号降智限流」的观测对得上;真正的超时其实没几次。
这意味着我该优化的根本不是超时时间,而是控制自己的请求节奏、把重试间隔拉长。
这就是我说「信数据别信经验」的原因。要是没这张表,我可能到现在还在瞎调超时参数。
顺便说说中转站:省下来的钱,可能是最贵的
复盘的时候还有个信号我没法忽略。为了绕开官方限流,很多人转头去用各种中转站、聚合网关。
技术上这事不新鲜,社区里就有人拆过一个网关怎么把 Chat、Responses、Anthropic Messages 三种协议来回转换,做得相当细。
但同一周还冒出来一条挺吓人的消息(这条我标**待核实**):有安全研究者称从某些 LLM 路由中转站买到了数据集,里面混着开发者发过去的 SSH key、VPN 配置、云厂商密钥、GitLab token,还说部分中转站在偷偷注入恶意的工具调用、甚至有客户的钱包被抽干。这份报告我没法独立验证真伪,但它至少提醒了一件事:
你把 key 和请求交给一个不透明的第三方,等于把安全边界交出去了。为了省点限流的麻烦,可能把更贵的东西赔进去。
所以我的建议是,重试和调度这层逻辑,尽量握在自己手里。你可以调用平台已经接入的模型能力,但失败重试、日志落库、密钥管理这些,别外包给来路不明的中转站。
这方面基础不太扎实的话,翻翻AI编程相关的资料补一补密钥管理和异常处理,比省那点限流成本划算得多。
一个你今晚就能做的小动作
如果你手上也有个天天调 API 的小工具,别等下次崩了再抓瞎。今晚花二十分钟做三件事:
第一,把你的错误分成「该重试」和「该放弃」两类,先粗糙分也行;第二,给重试加上指数退避和抖动,间隔封个顶;第三,建一张最简单的失败记录表,哪怕只有时间和错误码两列。
跑上三五天,你再回头看这张表,大概率会发现真正的问题跟你想的不一样。 API 会崩、模型会降智、限流是常态——这些你改变不了。
你能改变的,是让自己的代码在这些事发生时,还能体面地扛住。
把失败当成默认前提去设计,这是我这一周最大的收获。