避免重试风暴:AI 接口限流处理要点
文章区分临时速率限制、并发超限与额度耗尽,强调根据接口文档、错误码和响应头决定处理方式。针对可恢复的限流,应遵守服务端等待时间、控制并发并设置重试预算,避免同步重试放大流量。
AI 深度解读
50 个 AI 请求撞上限流,所有 worker 都等 1 秒再试,下一秒又一起撞上去。如果新请求还在进入,重试会和正常流量争抢容量,一次小突发就可能滚成重试风暴。
作者给出的结论是:处理 HTTP 429,不能只加循环和指数退避。客户端还得辨别错误、遵守服务端等待时间,并给整个操作设预算。这套思路值得前端和全栈开发者读几分钟,因为它直接影响 AI 接口拥堵时,用户究竟是在等待恢复,还是陪系统重复撞墙。
收到 429,先判断它能不能重试
HTTP 429 的含义是 Too Many Requests,但状态码没有说明撞上了哪种限制。有些 API 也用它表示额度耗尽或计费限制,继续等待未必能解决。
作者建议结合响应头、文档定义的错误码和消息分类,别靠英文消息里的某个子串猜原因。401、403、无效模型名、请求格式错误,都需要各自处理。一个「任何错误都重试」的公共函数,很容易把配置问题藏进漫长的等待里。
经过网关时,还要确认拒绝来自哪一层。应用收到 429,并不能直接证明模型供应商拒绝了请求;网关自身也可能限流。能拿到时间戳和 request ID,就把两侧记录关联起来。
只有确认属于临时限制,才该进入这套重试策略。
这也是后面代码的使用前提:计算等待时间的函数,不负责决定错误是否值得重试。
等多久,要同时听服务端和本地策略
Retry-After 有两种格式。Retry-After: 5 表示至少等 5 秒;Retry-After: Tue, 10 Nov 2026 09:00:00 GMT 则指定了重试时间,客户端时钟需要足够准确。
这里的「至少」很关键。本地退避上限即使是 8 秒,服务端要求等 60 秒,也不能擅自压成 8 秒。操作预算装不下,就停止或延期。供应商特有的 reset 响应头,还可能使用不同单位或时间戳,必须按对应文档解析。
只遵守等待时间,也可能让所有 worker 在同一刻醒来。作者因此加入随机抖动(jitter):指数窗口依次为 0.5、1、2、4 秒,每次在窗口内随机选值;存在 Retry-After 时,把随机值加在服务端最低等待时间之后。
下面是原文用于 Python 3.10+ 的计时函数。它不发网络请求,也不判断错误类型:
import math
import random
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
def retry_delay(retry_number: int, retry_after: str | None) -> float:
"""retry_number starts at 0; call only for a retryable response."""
minimum_wait = 0.0
if retry_after:
value = retry_after.strip()
if value.isascii() and value.isdecimal():
minimum_wait = float(value)
else:
try:
retry_at = parsedate_to_datetime(value)
if retry_at.tzinfo is None:
retry_at = retry_at.replace(tzinfo=timezone.utc)
minimum_wait = max(
0.0,
(retry_at - datetime.now(timezone.utc)).total_seconds(),
)
except (TypeError, ValueError, OverflowError):
pass # Invalid header: use local backoff.
if not math.isfinite(minimum_wait):
return math.inf # Caller should stop rather than sleep forever.
window = min(8.0, 0.5 * 2 ** min(retry_number, 10))
return minimum_wait + random.uniform(0.0, window)
重点看最后两行:8 秒只限制本地随机窗口,服务端要求的等待时间仍完整保留。retry_number 从 0 开始,第 2 次重试的窗口为 2 秒;搭配 Retry-After: 5,实际等待为 5 到 7 秒,没有有效响应头时则为 0 到 2 秒。
格式错误的响应头会回退到本地退避;大到无法表示为有限数值的数字会返回 math.inf,调用方应停止重试。这个辅助函数不能直接当成完整的请求封装。
重试预算要覆盖整条链路,还得限制入口
单次请求超时,管不住「请求、等待、再请求」的总时长。作者给交互请求举了一组起始策略:包含首次请求共 4 次尝试,整个操作限时 30 秒,只处理文档明确的临时 429;剩余时间必须容纳等待和下一次有效尝试,调用方放弃时立即取消。
这些数字是示例,不是通用默认值。聊天消息和后台任务的等待预算通常不同。
总耗时应使用单调时钟计算,避免系统时间调整干扰预算。睡眠前先检查剩余时间,服务端要求等得太久,就返回限流结果或重新调度后台任务。异步 Python 要使用可取消的异步等待,并在整个操作外约束期限;把阻塞的 time.sleep 塞进异步处理器,会拖住其他工作。
还要排查 SDK 和网关是否已经重试。原文用这组配置说明放大效应:
Application: up to 3 attempts
SDK: up to 3 attempts per application attempt
Gateway: up to 3 upstream attempts per SDK attempt
要注意每层的 3 attempts:如果每层都对同一次失败重试,一个逻辑操作最坏会触发 27 次上游尝试。实际数量取决于各层处理哪些错误,但预算必须有人统一负责。
退避只能缓解重试压力,入口控制才能约束持续进入的工作。
信号量限制同时执行的请求数,速率限制器控制请求启动频率,有界队列限制积压量,三者不能互相替代。假设额度为每分钟 60 次,一次性发出 60 次,仍可能撞上更短的突发窗口。Token 又是独立约束,少量长提示词也能耗尽预算。
作者还划出了一条边界:连接超时、流中断不能直接套用临时 429 策略,因为服务端可能已经执行。用户收到部分答案后,静默重放可能产生重复或矛盾内容;agent 工具调用更要单独去重,避免重复发邮件、下单、写记录。幂等键必须遵守 API 文档里的作用范围和保留期限,自创一个请求头不会自动获得幂等能力。
我的判断
这篇最有价值的地方,是把重试从局部代码问题拉回了系统容量问题。相比固定等待 1 秒或无差别重试,它考虑了错误语义、时间预算和流量放大,逻辑更完整。
但原文没有给出压测数据、线上恢复率或策略对照实验。开头 50 个请求的场景,以及每分钟 60 次的额度,都是说明问题的例子;4 次尝试、30 秒期限也只是建议。不能据此宣称这套参数能带来多少收益。
作者从事 YW Code API(ywcode)网关工作,提供的是客户端设计经验。它成立的前提之一,是供应商给出了足够明确的错误分类和响应头文档;文档含糊时,原文没有提供万能识别办法。
那段 Python 代码适合拿来理解时间计算,生产接入仍缺错误分类、取消、总期限和入口调度。如果 SDK 已有重试机制,我会先核对它的行为和配置,再决定是否添加外层逻辑,避免把一次失败放大成多层请求。
能拿来做什么
- 给 Node AI 接口梳理错误分类,区分临时限流、额度耗尽和请求错误,同时核查 SDK、网关的默认重试行为,明确哪一层负责总预算。
- 给交互请求设置整体期限,并把用户取消传递到等待和后续尝试;流式输出中断后明确提示,让用户主动选择重新生成。
- 给批量任务分别设置并发限制、启动速率和队列容量。供应商未公开容量时,从低并发逐步增加,同时观察排队时间、429 频率和端到端延迟。
- 上线前验证临时 429 后成功、额度错误立即停止、等待超过期限、等待中取消、输出后断流这些确定性场景。日志记录逻辑操作 ID、尝试次数、错误码、等待时间和剩余预算,默认不记录完整提示词与响应内容。