并发请求与 API 速率限制处理实战指南
调用大模型 API 时遇到 429 报错,几乎是每个开发者都会经历的"成人礼"。限流不是 API 厂商故意为难你,而是为了保护服务端不被冲垮。理解并处理好速率限制,是从"能调通"到"能调稳"的必经之路。
本文从最基础的限流概念讲起,手把手带你搭建一套完整的并发请求与限流处理方案。
一、速率限制核心概念与生活化类比解析
先搞清楚 API 厂商到底在限制什么。
大模型 API 的速率限制通常有以下几个维度:
- RPM(每分钟请求数) :一分钟内最多能发多少次请求
- TPM(每分钟 Token 数) :一分钟内最多能消耗多少 Token
- RPS(每秒请求数) :一秒钟内最多能发多少次请求
- Traffic Burst(增速限制) :短时间内请求量激增会触发
生活化类比:限流就像奶茶店排队。
- RPM 是"每分钟最多接待 60 个顾客"------你排在第 61 个就得等下一分钟
- TPM 是"每分钟最多卖出 1000 杯"------有人一次点 50 杯,很快就占满额度
- RPS 是"每秒最多接待 2 个顾客"------你不能在那一秒同时挤进去 10 个人
- Traffic Burst 是"不能突然冲进来 50 个人"------店长会怀疑你是来砸场的
理解这些维度之后,你会明白:单纯"遇错重试"效果有限,因为限流可能同时受请求数和 Token 量两个维度约束。你需要的是组合拳------主动限流 + 被动重试。
二、开发环境搭建与测试工具快速部署
创建一个干净的虚拟环境,把需要的依赖一次性装好。
bash
# 创建并激活虚拟环境
python -m venv ratelimit-env
source ratelimit-env/bin/activate # Windows 用 ratelimit-env\Scripts\activate
# 安装依赖
pip install requests tenacity aiohttp python-dotenv
各依赖的用途:
requests:同步 HTTP 请求,用来演示最基础的调用tenacity:Python 最成熟的重试库,支持指数退避aiohttp:异步 HTTP 客户端,用来做高并发请求python-dotenv:管理 API Key,别把密钥写死在代码里
创建 .env 文件:
API_KEY=sk-xxxxxxxxxxxxxxxx
BASE_URL=https://api.openai.com/v1
三、基础同步请求触发限流复现步骤
先写一个简单的同步请求脚本,手动制造限流场景,看看 429 长什么样。
python
import os
import time
import requests
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv("API_KEY")
BASE_URL = os.getenv("BASE_URL", "https://api.openai.com/v1")
def call_api(prompt: str):
headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
payload = {
"model": "gpt-4o",
"messages": [{"role": "user", "content": prompt}]
}
response = requests.post(
f"{BASE_URL}/chat/completions",
headers=headers,
json=payload,
timeout=30
)
return response
# 模拟短时间内密集请求------触发限流
if __name__ == "__main__":
for i in range(50): # 连续发 50 个请求
resp = call_api(f"第{i}次请求")
print(f"请求{i}: {resp.status_code}")
if resp.status_code == 429:
print("触发限流!响应头:", resp.headers.get("Retry-After"))
# 不设间隔,很快就能看到 429
跑这个脚本,你会看到一片 200 中间零星夹杂着 429。这就是限流在向你打招呼。
四、异步并发请求代码实现与压力测试
同步请求效率太低,实际生产环境通常会使用异步并发。但异步并发如果不加控制,限流来得更快。
下面是一个基础的异步并发请求示例:
python
import asyncio
import aiohttp
import os
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv("API_KEY")
BASE_URL = os.getenv("BASE_URL")
async def fetch(session, prompt: str, idx: int):
headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
payload = {
"model": "gpt-4o",
"messages": [{"role": "user", "content": prompt}]
}
async with session.post(f"{BASE_URL}/chat/completions", headers=headers, json=payload) as resp:
return idx, resp.status
async def main():
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, f"测试请求{i}", i) for i in range(100)]
results = await asyncio.gather(*tasks)
for idx, status in results:
print(f"请求{idx}: {status}")
asyncio.run(main())
压力测试观察点:
- 前几个请求可能都成功(200)
- 后面会密集出现 429------因为 100 个请求几乎同时发出,瞬间打满配额
- 这就是典型的 RPS 超限场景
关键结论:异步并发虽然快,但不加控制就是把压力从"均匀分布"变成了"瞬间爆炸"。
五、指数退避算法自动重试机制编写
遇到 429 怎么办?等。等多久?指数退避。
指数退避的核心思想:每次重试的等待时间逐渐拉长------第 1 次等 1 秒,第 2 次等 2 秒,第 3 次等 4 秒,以此类推。
用 tenacity 实现非常简单:
python
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception
import requests
def is_retryable(exception):
"""只对限流和服务端错误重试"""
if isinstance(exception, requests.exceptions.HTTPError):
return exception.response.status_code in [429, 500, 502, 503, 504]
return isinstance(exception, (requests.exceptions.Timeout, requests.exceptions.ConnectionError))
@retry(
stop=stop_after_attempt(5), # 最多重试 5 次
wait=wait_exponential(multiplier=1, min=1, max=60), # 1s, 2s, 4s... 最大 60s
retry=retry_if_exception(is_retryable)
)
def call_with_retry(prompt: str):
response = requests.post(...)
response.raise_for_status()
return response.json()
wait_exponential(multiplier=1, min=1, max=60) 的含义是:等待时间 = 1 * (2 ^ 重试次数),最小 1 秒,最大 60 秒。第 1 次重试等 2 秒,第 2 次等 4 秒,第 3 次等 8 秒......到第 5 次等 32 秒。
但是,如果所有客户端都用相同的退避策略,它们会在同一时间同时重试------这就是"惊群效应"。解决办法是加抖动(Jitter):
python
from tenacity import wait_random
@retry(
stop=stop_after_attempt(5),
wait=wait_exponential(multiplier=1, min=1, max=60) + wait_random(0, 1),
# 实际等待 = 指数退避时间 + 0~1 秒随机值
retry=retry_if_exception(is_retryable)
)
def call_with_jitter(prompt: str):
pass
六、令牌桶算法本地限流器集成方案
指数退避是被动应对------被限流了才等。更好的思路是主动限流:在客户端控制请求速率,让 429 压根不出现。
令牌桶算法是目前最常用的主动限流方案。它的工作方式:
- 一个桶,最多能装
capacity个令牌 - 以固定速率往桶里加令牌(比如每秒加 2 个)
- 每次请求消耗 1 个令牌
- 令牌不够就等待,直到桶里有令牌
类比:桶就像你的零钱罐,每个月固定往里存钱(速率),每次花钱(请求)消耗一枚硬币。偶尔有大额开销(突发流量),只要罐子里的积蓄够用就行。
自己实现一个简单的令牌桶:
python
import time
import threading
class TokenBucket:
def __init__(self, rate: float, capacity: int):
"""
rate: 每秒生成的令牌数(长期速率)
capacity: 桶的最大容量(允许的突发大小)
"""
self.rate = rate
self.capacity = capacity
self.tokens = capacity # 初始装满
self.last_refill = time.time()
self.lock = threading.Lock()
def _refill(self):
now = time.time()
elapsed = now - self.last_refill
new_tokens = elapsed * self.rate
self.tokens = min(self.capacity, self.tokens + new_tokens)
self.last_refill = now
def acquire(self, block: bool = True) -> bool:
"""获取一个令牌,block=True 时阻塞等待"""
with self.lock:
self._refill()
if self.tokens >= 1:
self.tokens -= 1
return True
if not block:
return False
# 计算需要等多久
needed = 1 - self.tokens
wait_time = needed / self.rate
time.sleep(wait_time)
# 递归获取
return self.acquire(block=True)
# 使用示例:每秒最多 10 个请求,突发容量 20
limiter = TokenBucket(rate=10, capacity=20)
for i in range(100):
limiter.acquire() # 阻塞直到有令牌
# 执行实际的 API 请求
response = call_api(f"请求{i}")
这样配置的含义是:长期来看每秒最多 10 个请求,但允许瞬间最多 20 个请求的突发。
对于大模型 API,如果同时受 RPM 和 TPM 两个维度限制,可以考虑用双重令牌桶------分别控制请求数和 Token 数。
七、HTTP 响应头信息解析与动态等待策略
被限流时,服务端返回的 429 响应里通常会带一个 Retry-After 头,告诉你具体要等多少秒。
服务端说等多久,你就等多久------这是最基本的礼貌,也是最有效的策略。
解析 Retry-After 的完整实现:
python
import time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
def parse_retry_after(headers):
"""
解析 Retry-After 响应头,返回需要等待的秒数
支持两种格式:
- 数字秒数: "Retry-After: 30"
- HTTP 日期: "Retry-After: Wed, 21 Oct 2015 07:28:00 GMT"
"""
retry_after = headers.get("Retry-After")
if not retry_after:
return None
# 尝试解析为数字(秒数)
try:
return max(0.0, float(retry_after))
except ValueError:
pass
# 尝试解析为 HTTP 日期
try:
reset_dt = parsedate_to_datetime(retry_after)
now = datetime.now(timezone.utc)
return max(0.0, (reset_dt - now).total_seconds())
except Exception:
return None
结合退避策略和 Retry-After,计算等待时间的完整函数:
python
import random
def calculate_wait(response, attempt: int, base: float = 1.0, max_delay: float = 60.0):
"""
计算重试前需要等待的时间
优先使用服务端返回的 Retry-After,否则用指数退避 + 抖动
"""
# 优先听从服务端的建议
server_wait = parse_retry_after(response.headers)
if server_wait is not None:
return server_wait
# 指数退避 + 抖动
delay = min(base * (2 ** attempt), max_delay)
jitter = random.uniform(0, delay * 0.1) # 抖动范围:0 ~ 10% 的延迟
return delay + jitter
八、常见限流报错代码识别与精准排查
不同云厂商返回的限流错误码略有差异,但核心逻辑相通:
| 错误码 | 含义 | 触发维度 | 应对策略 |
|---|---|---|---|
| 429 / limit_requests | 请求频率超限 | RPM 超限 | 令牌桶控制请求配额 |
| 429 / limit_requests | 请求频率超限 | RPS 超限 | 信号量控制并发 + 平滑限速 |
| 429 / insufficient_quota | Token 用量超限 | TPM 超限 | 双重令牌桶(同时控 RPM 和 TPM) |
| 429 / limit_burst_rate | 流量增速超限 | Traffic Burst | 服务端排队等待或冷启动缓起 |
排查技巧:
- 看状态码:429 是限流,5xx 是服务端问题,两者都值得重试;4xx(除 429 外)是客户端问题,重试没用
- 看错误信息 :不同的 429 可能对应不同的限流维度,信息里通常会带关键词(如
RPM、TPM、burst) - 看
Retry-After:服务端建议的等待时间是最可靠的参考 - 看时间分布:如果限流集中在启动瞬间,多半是 RPS 或 Burst 超限;如果运行一段时间后才出现,多半是 RPM 或 TPM 超限
九、生产环境高可用请求队列设计技巧
把前面的知识点串起来,设计一个生产可用的请求队列。
核心组件:
- 并发控制 :用
asyncio.Semaphore控制同时进行的请求数量 - 速率限制:用令牌桶控制请求频率,从源头避免 429
- 重试机制 :用 tenacity 实现指数退避 +
Retry-After解析 - 熔断降级:错误率过高时快速失败,避免雪崩
完整实现:
python
import asyncio
import aiohttp
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception
class APIClient:
def __init__(self, api_key: str, base_url: str,
max_concurrent: int = 10, # 最大并发数
rate: float = 10, # 每秒请求数
burst: int = 20): # 突发容量
self.api_key = api_key
self.base_url = base_url
self.semaphore = asyncio.Semaphore(max_concurrent)
self.token_bucket = TokenBucket(rate, burst) # 前面实现的令牌桶
def _is_retryable(self, exception):
if isinstance(exception, aiohttp.ClientResponseError):
return exception.status in [429, 500, 502, 503, 504]
return isinstance(exception, (aiohttp.ClientConnectionError, asyncio.TimeoutError))
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=10),
retry=retry_if_exception(_is_retryable)
)
async def _do_request(self, session, prompt: str):
# 令牌桶限流------先拿令牌,拿不到就阻塞等待
self.token_bucket.acquire()
# 并发控制------获取信号量
async with self.semaphore:
headers = {"Authorization": f"Bearer {self.api_key}"}
payload = {"model": "gpt-4o", "messages": [{"role": "user", "content": prompt}]}
async with session.post(f"{self.base_url}/chat/completions",
headers=headers, json=payload) as resp:
if resp.status == 429:
# 解析 Retry-After,tenacity 会自动处理
pass
resp.raise_for_status()
return await resp.json()
async def call(self, prompt: str):
async with aiohttp.ClientSession() as session:
return await self._do_request(session, prompt)
async def call_many(self, prompts: list):
"""批量调用,自动控制并发和速率"""
tasks = [self.call(p) for p in prompts]
return await asyncio.gather(*tasks, return_exceptions=True)
队列设计的关键原则:
- 并发数设置:建议 3~10 之间是比较稳妥的范围。太小浪费性能,太大容易触发限流
- 超时时间:建议 10~15 秒,太短会造成频繁误判超时
- 令牌桶参数:参考 API 文档的 RPM 和 RPS 限制来设置
- 分批提交 :如果有大量请求,不要一次性全部提交,用
asyncio.gather分批处理
十、合规调用最佳实践与性能优化总结
最后,把经验浓缩成几条可以贴在工位上的原则:
关于限流处理:
- 主动限流优于被动重试:用令牌桶在客户端控速,让 429 尽量不出现
- 尊重 Retry-After:服务端让你等多久就等多久,这是最有效的策略
- 指数退避 + 抖动:避免惊群效应,分散重试压力
- 区分可重试和不可重试错误:429 和 5xx 重试,4xx(除 429 外)直接失败
关于并发控制:
- 信号量控制并发数:建议 3~10,根据 API 限制调整
- 超时一定要设:连接超时 5 秒,读取超时 30 秒是合理起点
- 分批处理大批量请求:不要一次性提交成千上万个任务
关于生产环境:
- 配置外部化:重试次数、超时时间、速率限制参数都放在配置文件里,方便线上调优
- 加监控和告警:记录 429 频率、重试次数、平均响应时间,设置告警阈值
- 做好降级:主模型不可用时自动切换备用模型,或者返回缓存结果
WEB项目地址:演示地址
安卓APP下载地址:演示地址
大模型 API 的速率限制不是障碍,而是你需要理解和共舞的规则。把主动限流和被动重试结合起来,你的服务才能在各种流量波动中保持稳定。