Mistral Large 4 返回 429 时,HTTP 状态码本身只说明「你被限流了」,不区分是发得太快还是额度用完。可操作的判断顺序是:先把这次响应的头部和响应体完整留下来,再对照账户页面的用量与余额,最后才决定是降并发还是补额度。多数情况下两者会给出不同信号——速率限制通常在 Retry-After 或 X-RateLimit-* 里带具体数值,配额耗尽则更多体现在错误消息和账户的余额、billing 状态上。
先做一次带 -i 的 curl,把 429 的响应头和响应体都留档。头部出现 Retry-After、X-RateLimit-Remaining 接近 0,偏向速率/并发限流;错误消息写 insufficient quota、余额不足,或账户页显示额度为 0,偏向配额耗尽。两者可能同时成立,所以重试逻辑要能读 Retry-After,批处理要能降并发;而配额问题是重试解决不了的,只能调整计划或充值。
查看 429 响应头中的限流信息
很多 SDK 会把异常包装掉,只抛出「429 Too Many Requests」,看不到头部。排查阶段建议先用 curl 直接打一次接口,把 -i 打开,把头部和响应体都打印出来。
# 放在能访问外网的机器上执行,替换 BASE_URL 和 API_KEY
curl -i -X POST "$BASE_URL/v1/chat/completions" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @payload.json
响应头里常见的字段含义如下,字段名各服务商不完全一致,以你实际收到的为准:
- Retry-After:服务端建议你等待多少秒再重试。这个值优先于你自己算的退避时间,有就按它睡。
- X-RateLimit-Limit-*:当前窗口允许的上限,常见维度有 requests(RPM)和 tokens(TPM)。
- X-RateLimit-Remaining-*:当前窗口还剩多少额度。这个值接近 0 却还在报 429,说明你踩的是速率上限。
- X-RateLimit-Reset-*:窗口重置时间。用来判断还要等多久,不要用固定 sleep 硬扛。
一个示意的响应头(数值用占位表示):
HTTP/1.1 429 Too Many Requests
Retry-After: <秒数>
X-RateLimit-Limit-Requests: <上限>
X-RateLimit-Remaining-Requests: <剩余>
X-RateLimit-Reset-Requests: <重置时间戳>
Content-Type: application/json
验证方式:连续发几次相同请求,看 Remaining 是否单调递减到 0。如果是,那就是速率问题;如果 Remaining 一直很充足却仍被拒,或者头部根本没这些字段,就要转到下一节看错误消息和账户状态。
区分并发限制与配额限制
头部之外,响应体里的 error 字段是更直接的线索。结构示意如下,字段名和文案以实际返回为准:
// 偏向速率/并发超限
{
"error": {
"type": "rate_limit_exceeded",
"message": "requests per minute limit exceeded"
}
}
// 偏配额/余额问题
{
"error": {
"type": "insufficient_quota",
"message": "you exceeded your current quota, please check your plan and billing details"
}
}
判断路径可以这样走:
- 错误消息里出现
rate limit、too many requests、RPM、TPM一类词,且 Retry-After 有值,按速率限制处理,走退避重试和降并发。 - 错误消息里出现
quota、billing、credit、balance一类词,先去控制台的用量或计费页面确认当前额度与账单状态。 - 如果同时命中,先修配额。额度为 0 时,重试多少次都是 429,指数退避只会让你更慢地拿到同一个错误。
注意:部分服务对「免费额度用尽」也返回 429 而不是 402,所以不能只看状态码。账户页面的用量曲线是唯一能确认配额的手段,日志和头部只能给出倾向性判断。
在代码中实现指数退避重试
临时限流适合用重试消化,但必须满足两点:尊重 Retry-After,以及加抖动。下面是一个通用骨架,fn 换成你自己的调用函数即可,返回值需要有 status_code 和 headers。
import time
import random
def call_with_backoff(fn, max_retries=5, base=1.0, cap=30.0):
resp = None
for attempt in range(max_retries):
resp = fn()
if resp.status_code != 429:
return resp
retry_after = resp.headers.get('Retry-After')
if retry_after:
wait = float(retry_after) # 服务端说了算
else:
wait = min(cap, base * (2 ** attempt))
wait += random.uniform(0, wait * 0.3) # 抖动,避免多个线程同时重试
time.sleep(wait)
return resp # 重试耗尽,交给上层决定是否降级
替换项与验证:base 从 1 秒起步通常够用,cap 控制单次最长等待,避免线程被长时间挂住。max_retries 不要设太大,重试耗尽后应该有明确的失败返回,而不是无限循环。验证方式是在本地人为压低触发条件,观察日志里每次重试的间隔是否成倍增长、是否读到了 Retry-After。另外,重试只对幂等或可容忍重复的调用安全,带副作用的写操作需要先确认是否可以重复提交。
用日志记录请求时间与频率
要判断是不是「发得太快」,得先有一份能按分钟统计的日志。关键是把时间戳、请求 ID、状态码都打在同一行上。
import logging, time, uuid
log = logging.getLogger('mistral')
logging.basicConfig(
format='%(asctime)s %(levelname)s %(message)s',
level=logging.INFO,
)
def tracked_call(fn):
req_id = str(uuid.uuid4())[:8]
start = time.monotonic()
resp = None
try:
resp = fn()
return resp
finally:
log.info('req_id=%s status=%s cost=%.3fs',
req_id,
getattr(resp, 'status_code', 'n/a'),
time.monotonic() - start)
有了日志,就能按分钟聚合请求数(注意 substr 的长度要和你的时间格式对齐):
# 统计每分钟的总请求数
grep 'req_id=' app.log | awk '{print substr($1,1,16)}' | sort | uniq -c
# 只看 429 出现在哪些分钟
grep 'req_id=' app.log | grep 'status=429' | awk '{print substr($1,1,16)}' | sort | uniq -c
验证方式:如果 429 集中在某几个分钟、其余分钟很干净,基本是突发并发;如果 429 从某个时间点开始持续出现且请求量没变,更像是配额或账户状态变化。请求 ID 还能帮你把重试和原始请求对应起来,避免把重试也算成新请求而误判频率。
调整批处理或降低并发数
退避重试是止血,降低发送速率才是从源头减少 429。第一步是把并发数写成显式的小值,而不是依赖线程池或框架的默认行为。
from concurrent.futures import ThreadPoolExecutor
# max_workers 按你的 RPM/TPM 上限反推,先取一个小值再慢慢往上调
with ThreadPoolExecutor(max_workers=4) as pool:
results = list(pool.map(send_one, batch))
如果并发不好控制,或者上游限制是按分钟计的,用队列串行发送更稳:
import queue, threading, time
q = queue.Queue()
for item in batch:
q.put(item)
def worker(interval):
while True:
try:
item = q.get_nowait()
except queue.Empty:
return
send_one(item)
time.sleep(interval) # 控制发送节奏,按实际限流值调整
q.task_done()
threads = [threading.Thread(target=worker, args=(0.2,)) for _ in range(2)]
for t in threads:
t.start()
取舍上:max_workers 和队列线程数决定了峰值 RPM,interval 决定了长期平均速率。两者都调小可以明显减少 429,代价是总耗时变长,批量任务需要重新排期。批处理还可以按大小拆分成多轮提交,轮与轮之间留出间隔。
验证方式:改成串行或小并发后,重跑同一批任务,对照日志里每分钟的请求数和 429 出现次数。如果 429 消失但吞吐明显下降,说明你之前确实踩在速率上限附近;如果降到单线程仍然 429,就该回到第二节,检查是不是配额问题。