大模型 API 429 报错:原因、重试与并发调优

429 不一定是「你请求太多了」。这篇拆开 RPM、TPM、并发数和免费档限流四种成因,给出带抖动的退避重试实现、并发池写法与降级链路,附监控指标清单。

跑批量任务时突然一片 429,日志里只有一行 rate limit exceeded。多数人的第一反应是「加个 sleep 重试」,结果流量高峰时 sleep 完继续撞墙,任务队列越堆越长。

429 的本质是「服务端认为你这一秒的请求超过了约定配额」。但配额有四种,撞的是哪一种,解法完全不同。

四种配额,四种解法

配额类型含义典型报错信息解法
RPM每分钟请求数requests per minute控制请求节奏,排队发送
TPM每分钟 token 数tokens per minute裁剪上下文、拆长文本
并发限制同时在飞的请求数concurrent requests信号量限流
免费档限额每日/每分钟次数上限daily limit换付费模型或降级

先看响应体和响应头,别猜。多数服务会在 x-ratelimit-remaining-* 里告诉你还剩多少,在 retry-after 里告诉你等几秒。

curl -i https://api.zaimod.com/v1/chat/completions \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}'

返回头里如果有 retry-after: 3,那就等 3 秒;没有的话从 1 秒起步做指数退避。

先修代码:带抖动的退避重试

最容易被忽略的是「抖动」:十个 worker 同时 429、同时等 1 秒、同时重发,等于把一次拥塞变成十次。加上随机抖动才能把请求打散。

import random, time
from openai import OpenAI, RateLimitError, APIConnectionError

client = OpenAI(api_key="sk-xxx", base_url="https://api.zaimod.com/v1")

def chat(messages, model="gpt-4o-mini", max_retries=5):
    for attempt in range(max_retries):
        try:
            return client.chat.completions.create(
                model=model, messages=messages, max_tokens=800,
            )
        except RateLimitError:
            if attempt == max_retries - 1:
                raise
            # 指数退避 + 抖动:1s, 2s, 4s, 8s 附近随机
            wait = min(2 ** attempt, 20) + random.uniform(0, 1)
            time.sleep(wait)
        except APIConnectionError:
            time.sleep(2 + random.uniform(0, 1))
    raise RuntimeError("retries exhausted")

三个细节决定这段代码好不好用:

  • 只重试可重试的错误。429、5xx、连接超时重试;400(参数错)、401(Key 错)重试一万次也没用
  • 指数退避要封顶。退避到 30 秒就够了,再长不如直接失败让上层知道
  • 记录重试次数。重试率持续高于 5%,说明配额已经不够用,加 sleep 只是把问题往后推

再修架构:并发池

单条请求重试解决不了「一次要跑一万条」。真正有效的是把并发压到一个稳定值,让服务端看到的是匀速流量。

import asyncio
from openai import AsyncOpenAI

client = AsyncOpenAI(api_key="sk-xxx", base_url="https://api.zaimod.com/v1")
SEM = asyncio.Semaphore(8)          # 同时在飞 8 个

async def one(text):
    async with SEM:
        for attempt in range(5):
            try:
                r = await client.chat.completions.create(
                    model="gpt-4o-mini",
                    messages=[{"role": "user", "content": text}],
                    max_tokens=300,
                )
                return r.choices[0].message.content
            except Exception:
                await asyncio.sleep(min(2 ** attempt, 20) + random.uniform(0, 1))
        return None

async def run(texts):
    return await asyncio.gather(*(one(t) for t in texts))

并发数怎么定?从 4 开始,每次加 4,观察 429 比例:低于 1% 可以继续加,超过 5% 就退回来。不同模型、不同分组的限额不一样,同一个并发数在 gpt-4o-mini 上没事,换成 claude-opus-4-7 可能立刻撞墙。

降级链路:让失败有出口

批量任务里最贵的不是 429,是「因为 429 整批失败」。准备一条降级链,比无限重试划算:

  1. 主力模型 429 → 重试两次
  2. 仍然 429 → 换同价位备份模型(如 gpt-4o-mini → gemini-3.1-flash-lite-preview)
  3. 仍然失败 → 走免费档或小模型出「粗略结果」,标记 degraded: true
  4. 全部失败 → 落盘重试队列,下一轮补跑

关键是第 3 步要在数据里留标记,否则你不知道哪批结果是降级产出的。这条经验来自踩过的坑:某次批处理跑了 6 小时,第二天才发现三成摘要是小模型生成的,格式全不对。

从源头减流量:三个立刻见效的改动

  • 裁上下文:只发最近 6 轮对话 + 一段摘要,别把整个历史塞进每次请求。长会话的成本是线性增长的,TPM 撞墙多半是这个原因
  • 合并请求:十条短文本拼成一个 JSON 数组让模型一次处理,比十次请求省 60% 以上的 token(系统提示只发一次)
  • 限制输出:max_tokens 不设上限等于放弃预算控制,一个「继续」就能生成两千字

监控这几个指标

上线前把四个数记进日志,出问题三十秒能定位:

  • 429 比例:分模型、分 Key 统计,超过 1% 就该调整
  • P95 延迟:429 之前通常先出现延迟抬升
  • 重试次数分布:重试 3 次以上的请求占比
  • token 用量曲线:TPM 撞墙前会有明显尖峰

流式请求要单独处理:SSE 连接建立后再断,客户端感知不到错误码,得靠首字节超时判断。细节见 流式输出 SSE 原理与调试。

常见问题

Q:加了重试还是 429,是不是被限流了?

先确认请求是不是「瞬间爆发」。把并发降到 1 单独跑一条,如果还 429,说明配额或账号级别被限制,需要换分组或联系服务方;如果单条能过,就是并发问题。

Q:429 会扣费吗?

不计费的失败请求通常不产生 token 账单,但会消耗 RPM 配额。所以「重试」本身也在消耗你的限流额度,这就是为什么要退避而不是立刻重发。

Q:为什么白天没事,凌晨批量任务全是 429?

批量任务通常是并发打满,而白天是零星请求。同一份额度,突发流量更容易撞墙。

Q:不同模型的限流是独立的吗?

取决于平台实现。多数平台按「模型 + 分组」分别计算,但账号级别往往还有一层总限额。跨模型分散请求能缓解 TPM,但缓解不了账号总并发。

延伸阅读

#api 429 报错#大模型 api 限流#rate limit 解决#api 并发调优
免费创建 API Key

相关阅读

Claude API 和 OpenAI API 有什么区别

Claude 的 messages 和 OpenAI 的 chat/completions 看着像,实际有 6 处硬性差异:system 位置、max_tokens 必填、content 块数组、工具调用结构、多模态格式、流式事件名。这篇逐项对照并给出同一需求的两套写法,附迁移检查表。

Function Calling 工具调用入门指南

让大模型查天气、下单、读数据库,靠的是 Function Calling 而不是更长的提示词。这篇讲清工具调用的完整循环:定义 tools、模型输出 tool_calls、本地执行、结果回传,附可直接运行的 Python 代码与常见坑。

流式输出 SSE 原理与调试:从 curl 到前端

大模型 API 的流式输出本质是一条 SSE 长连接。本文拆解 text/event-stream 报文格式、stream:true 的分片结构、curl 与前端 ReadableStream 的调试方法,以及代理缓冲、[DONE]、usage 丢失等高频坑位。