上周我在用 OpenClaw 跑批量代码生成任务,跑到一半控制台疯狂刷红:Error: API rate limit reached。一开始以为是网络问题,重启了两次才反应过来——请求频率打满了。搜了一圈发现不少人也踩过这个坑,最近 OpenClaw 热度暴涨,百度 APP 都接入了,用的人一多限流就更狠。折腾了大半天,把试过的方案整理出来,直接说哪个好使。
先说结论
OpenClaw API 报 rate limit reached,本质是单位时间内请求次数超了账户配额。解决路线有三条:
| 方案 | 原理 | 难度 | 适用场景 | 效果 |
|---|---|---|---|---|
| 指数退避重试 | 遇到限流自动等待后重试 | ⭐ | 偶发限流、请求量不大 | 能用,但慢 |
| 请求队列 + 令牌桶限速 | 主动控制发送频率 | ⭐⭐ | 批量任务、高并发场景 | 稳定,不浪费配额 |
| 聚合 API 多通道分流 | 请求分散到多个供应商 | ⭐ | 生产环境、不想改业务逻辑 | 最省事,基本告别限流 |
我最后选了方案三搭配方案一,跑了三天没再报错。下面一个一个说。
搞清楚 Rate Limit 的触发机制
先别急着写代码,得知道限流是怎么算的。OpenClaw 的 rate limit 通常有两层:
- RPM(Requests Per Minute):每分钟请求次数上限
- TPM(Tokens Per Minute):每分钟 Token 消耗上限
免费账户和付费账户的配额差距很大,不同模型的限额也不一样,Claude Opus 4.6 这种热门模型通常限得更紧。
触发限流时,API 会返回 HTTP 429 状态码,响应头里通常带 retry-after 或 x-ratelimit-reset 字段,告诉你多久后可以重试。很多人忽略了这个信息,直接无脑重试,反而被限得更久。
graph TD
A[发送 API 请求] --> B{HTTP 状态码?}
B -->|200 OK| C[正常处理响应]
B -->|429 Rate Limited| D[读取 retry-after 头]
D --> E{有 retry-after?}
E -->|有| F[等待指定秒数]
E -->|没有| G[指数退避等待]
F --> H{重试次数 < 上限?}
G --> H
H -->|是| A
H -->|否| I[抛出异常 / 进入降级逻辑]
B -->|500/502| J[服务端错误, 直接重试]
J --> H
方案一:指数退避重试(Exponential Backoff)
最基础的方案,遇到 429 就等一会儿再试,每次等待时间翻倍。适合请求量不大、偶尔触发限流的场景。
import time
import random
from openai import OpenAI, RateLimitError
client = OpenAI(
api_key="your-api-key",
base_url="https://api.example.com/v1" # 替换为你的 API 地址
)
def chat_with_retry(messages, model="gpt-5", max_retries=5):
"""带指数退避的 API 调用"""
for attempt in range(max_retries):
try:
response = client.chat.completions.create(
model=model,
messages=messages,
timeout=30
)
return response
except RateLimitError as e:
if attempt == max_retries - 1:
raise e
# 尝试从错误信息中提取等待时间
wait_time = (2 ** attempt) + random.uniform(0, 1)
# 如果响应头有 retry-after,优先用它
if hasattr(e, 'response') and e.response is not None:
retry_after = e.response.headers.get('retry-after')
if retry_after:
wait_time = float(retry_after) + random.uniform(0, 0.5)
print(f"[Rate Limited] 第 {attempt + 1} 次重试,等待 {wait_time:.1f}s...")
time.sleep(wait_time)
raise Exception("重试次数耗尽")
# 使用
resp = chat_with_retry(
messages=[{"role": "user", "content": "解释一下 Python 的 GIL"}],
model="gpt-5"
)
print(resp.choices[0].message.content)
一开始我没加随机抖动(jitter),结果多个并发请求同时触发限流、同时重试,又同时被限——这叫 thundering herd 问题。加个 random.uniform(0, 1) 让每个请求的重试时间错开就好了。
这个方案的问题是:如果本身就是批量任务,几百个请求排着队等重试,整体耗时会很感人。
方案二:令牌桶限速 + 异步队列
与其被动等限流再重试,不如主动控制发送频率,从根上不触发 429。用 asyncio 加简单令牌桶写了个异步版本:
import asyncio
import time
from openai import AsyncOpenAI, RateLimitError
class RateLimiter:
"""简易令牌桶限速器"""
def __init__(self, rpm=50):
self.rpm = rpm
self.interval = 60.0 / rpm # 每个请求最小间隔
self.last_request_time = 0
self._lock = asyncio.Lock()
async def acquire(self):
async with self._lock:
now = time.monotonic()
wait_time = self.last_request_time + self.interval - now
if wait_time > 0:
await asyncio.sleep(wait_time)
self.last_request_time = time.monotonic()
class BatchProcessor:
def __init__(self, api_key, base_url, rpm=50):
self.client = AsyncOpenAI(
api_key=api_key,
base_url=base_url
)
self.limiter = RateLimiter(rpm=rpm)
self.results = []
async def single_request(self, messages, model="gpt-5", idx=0):
await self.limiter.acquire()
for attempt in range(3):
try:
resp = await self.client.chat.completions.create(
model=model,
messages=messages,
timeout=30
)
content = resp.choices[0].message.content
print(f"[{idx}] 完成")
return {"index": idx, "content": content, "status": "ok"}
except RateLimitError:
wait = (2 ** attempt) + 1
print(f"[{idx}] 限流了,等 {wait}s 重试...")
await asyncio.sleep(wait)
return {"index": idx, "content": None, "status": "failed"}
async def run_batch(self, task_list, model="gpt-5", concurrency=10):
"""并发执行批量请求"""
sem = asyncio.Semaphore(concurrency)
async def bounded_request(messages, idx):
async with sem:
return await self.single_request(messages, model, idx)
tasks = [
bounded_request(msgs, i)
for i, msgs in enumerate(task_list)
]
self.results = await asyncio.gather(*tasks)
ok = sum(1 for r in self.results if r["status"] == "ok")
print(f"\n完成: {ok}/{len(task_list)} 成功")
return self.results
# 使用示例
async def main():
processor = BatchProcessor(
api_key="your-key",
base_url="https://api.example.com/v1",
rpm=50 # 根据你的账户配额设置
)
# 构造 100 个任务
tasks = [
[{"role": "user", "content": f"用一句话解释概念 #{i}"}]
for i in range(100)
]
results = await processor.run_batch(tasks, concurrency=10)
asyncio.run(main())
拿 100 个请求测过,rpm 设 50,总耗时 2 分钟出头,零 429。代价是得准确知道自己的配额上限,设低了浪费时间,设高了还是会限流。
有个地方容易搞混:asyncio.Semaphore 和 RateLimiter 是两回事。Semaphore 控制并发数(同时在飞的请求数量),RateLimiter 控制发送频率(每分钟发几个)。两个都要用,少一个都可能出问题。
方案三:聚合 API 多通道分流
前两个方案我都用了,但真正让我安心的是换了个思路——不在一棵树上吊着。
单个 API 端点有限流,把请求分散到多个供应商通道不就完了?手动维护多个 API Key 太麻烦,我后来用了 ofox.ai 的聚合接口。ofox.ai 是一个 AI 模型聚合平台,一个 API Key 可以调用 GPT-5、Claude Opus 4.6、Gemini 3、DeepSeek V3 等 50+ 模型,后端有多供应商冗余备份(Azure、Bedrock、阿里云、火山引擎),单个供应商限流了会自动切换通道。
代码改动极小,就改个 base_url:
from openai import OpenAI
# 只需要改这两行
client = OpenAI(
api_key="your-ofox-key",
base_url="https://api.ofox.ai/v1" # 聚合接口,多通道自动分流
)
# 业务代码完全不用动
response = client.chat.completions.create(
model="gpt-5",
messages=[
{"role": "user", "content": "写一个快速排序"}
]
)
print(response.choices[0].message.content)
# 切模型也是改个参数的事
response_claude = client.chat.completions.create(
model="claude-opus-4-6", # 无缝切换到 Claude Opus 4.6
messages=[
{"role": "user", "content": "review 这段代码的性能问题"}
]
)
配合方案一的指数退避重试一起用,相当于双保险。跑了三天批量任务,零 429。
踩坑记录
坑 1:并发数开太大,429 变 503
异步并发开了 50,以为令牌桶能兜住。结果令牌桶只管发送频率,50 个请求几乎同时飞出去,服务端直接返回 503。后来并发降到 10,配合 RPM 限速才稳住。
坑 2:retry-after 的单位问题
有的 API 返回的 retry-after 是秒数(比如 2),有的是 HTTP 日期格式(比如 Thu, 20 Mar 2026 12:00:00 GMT)。一开始统一按秒数处理,碰到日期格式直接 float() 报错。记得做个判断:
import email.utils
from datetime import datetime, timezone
def parse_retry_after(value):
try:
return float(value)
except ValueError:
# 尝试解析为 HTTP 日期
dt = email.utils.parsedate_to_datetime(value)
return max(0, (dt - datetime.now(timezone.utc)).total_seconds())
坑 3:Streaming 模式下的限流更隐蔽
用 stream=True 的时候,429 错误不是在连接时抛出的,而是在迭代 chunk 的过程中才报。如果重试逻辑只包在 create() 外面,stream 中途断了是抓不到的。要把整个迭代过程都包进 try-catch:
def stream_with_retry(messages, max_retries=3):
for attempt in range(max_retries):
try:
stream = client.chat.completions.create(
model="gpt-5",
messages=messages,
stream=True
)
full_content = ""
for chunk in stream:
if chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
full_content += content
print(content, end="", flush=True)
print()
return full_content
except Exception as e:
if "rate" in str(e).lower() or "429" in str(e):
wait = 2 ** attempt
print(f"\nStream 中断,等 {wait}s 重试...")
time.sleep(wait)
else:
raise
raise Exception("Stream 重试耗尽")
小结
Rate limit 就三个层次:被动应对用指数退避重试,简单粗暴,个人项目够用;批量任务上令牌桶限速,主动把频率压在配额以内;不想折腾的生产环境直接上聚合 API 多通道分流。
方案一几行代码的事,必须加。然后根据场景选二或三——批量任务就二加三都上,日常开发一加三就够了。
最近 OpenClaw 用的人越来越多,限流只会更频繁。代码都贴了,直接复制能跑,有问题评论区聊。