VicroCode
让代码创造价值
VicroCode是一个轻量级代码在线发布、交易平台,开箱即用,免部署、免服务器、免备案,支持接入智能体、通用管理系统、游戏等多种项目
稍候片刻,您可先学习AI编程手册
正在加载中...

AI MARKET GUIDE

API调用失败重试逻辑怎么写?我记了一周失败日志才搞明白

OpenAI 网页进不去、20x 账号降智限流、中转站还爆出安全事故,这一周我算是被 API 失败折磨透了。与其守着官方状态页刷新,不如把「调用会失败」当成默认前提去写代码。这篇复盘我怎么用 Python 加指数退避、用 SQLite 把每次失败原因和重试次数记下来,再分清哪些错该重试、哪些该立刻放弃,顺便聊聊为什么稳定性靠的不是换更强的模型。

那天我盯着状态页刷了半小时,一行代码没改

上周有个晚上,我做的一个小工具突然大面积报错。用户在群里问我是不是挂了,我第一反应是打开 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 会崩、模型会降智、限流是常态——这些你改变不了。

你能改变的,是让自己的代码在这些事发生时,还能体面地扛住。

把失败当成默认前提去设计,这是我这一周最大的收获。