并发请求与 API 速率限制处理实战指南

并发请求与 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 压根不出现。

令牌桶算法是目前最常用的主动限流方案。它的工作方式:

  1. 一个桶,最多能装 capacity 个令牌
  2. 以固定速率往桶里加令牌(比如每秒加 2 个)
  3. 每次请求消耗 1 个令牌
  4. 令牌不够就等待,直到桶里有令牌

类比:桶就像你的零钱罐,每个月固定往里存钱(速率),每次花钱(请求)消耗一枚硬币。偶尔有大额开销(突发流量),只要罐子里的积蓄够用就行。

自己实现一个简单的令牌桶:

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 服务端排队等待或冷启动缓起

排查技巧

  1. 看状态码:429 是限流,5xx 是服务端问题,两者都值得重试;4xx(除 429 外)是客户端问题,重试没用
  2. 看错误信息 :不同的 429 可能对应不同的限流维度,信息里通常会带关键词(如 RPMTPMburst
  3. Retry-After :服务端建议的等待时间是最可靠的参考
  4. 看时间分布:如果限流集中在启动瞬间,多半是 RPS 或 Burst 超限;如果运行一段时间后才出现,多半是 RPM 或 TPM 超限

九、生产环境高可用请求队列设计技巧

把前面的知识点串起来,设计一个生产可用的请求队列。

核心组件

  1. 并发控制 :用 asyncio.Semaphore 控制同时进行的请求数量
  2. 速率限制:用令牌桶控制请求频率,从源头避免 429
  3. 重试机制 :用 tenacity 实现指数退避 + Retry-After 解析
  4. 熔断降级:错误率过高时快速失败,避免雪崩

完整实现:

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 分批处理

十、合规调用最佳实践与性能优化总结

最后,把经验浓缩成几条可以贴在工位上的原则:

关于限流处理

  1. 主动限流优于被动重试:用令牌桶在客户端控速,让 429 尽量不出现
  2. 尊重 Retry-After:服务端让你等多久就等多久,这是最有效的策略
  3. 指数退避 + 抖动:避免惊群效应,分散重试压力
  4. 区分可重试和不可重试错误:429 和 5xx 重试,4xx(除 429 外)直接失败

关于并发控制

  1. 信号量控制并发数:建议 3~10,根据 API 限制调整
  2. 超时一定要设:连接超时 5 秒,读取超时 30 秒是合理起点
  3. 分批处理大批量请求:不要一次性提交成千上万个任务

关于生产环境

  1. 配置外部化:重试次数、超时时间、速率限制参数都放在配置文件里,方便线上调优
  2. 加监控和告警:记录 429 频率、重试次数、平均响应时间,设置告警阈值
  3. 做好降级:主模型不可用时自动切换备用模型,或者返回缓存结果

WEB项目地址:演示地址

安卓APP下载地址:演示地址

大模型 API 的速率限制不是障碍,而是你需要理解和共舞的规则。把主动限流和被动重试结合起来,你的服务才能在各种流量波动中保持稳定。