先把 request id 拿到
出错时最先记下的不是错误文案,是 request id。多数实现把它放在响应头,常见名字是 x-request-id,网关也可能带自己的前缀,具体头名以实际响应为准。它能把一次失败请求和上游日志对上,报障时贴这一串比贴整段报错有用。
同时记下时间、模型名、分组、状态码和响应体前 500 字。这五项凑齐,多数问题自己就能判断,不用来回复述现象。
日志里把 request id 和业务单号打在同一行,出问题时可以从业务侧反查到底是哪一次调用。
401 与 403
401 是身份没通过:key 拼错、被删除、Authorization 头少了 Bearer 前缀、中间多了空格或换行。复制 key 时最常见的坑是尾部带了一个空格,肉眼看不出来。
403 是身份通过了但没权限:分组不支持这个模型、账户额度用完、IP 白名单拦了、key 的作用范围不含该接口。先看模型页列出的可用分组,比自己猜快。
分不清两者时,用同一个 key 请求 /v1/models:能列出模型说明身份没问题,问题在权限或分组;列不出来就是 key 本身无效。这一步能省掉大半排查时间。
404
404 基本是路径或名字的问题。base_url 要填到 /v1,请求路径是 /chat/completions,拼起来是 https://ai.idcidiot.com/v1/chat/completions。多一层或少一层 /v1 都会 404。
模型名要和 定价页 上完全一致,大小写和连字符都算。claude-sonnet-5 写成 claude-sonnet-5.0 不会自动纠正,模型清单可以对着 模型总览 核。
还有一种 404 来自反向代理:nginx 没配好路径转发,请求根本没到网关。用 curl -i 看响应头里的 server 字段,能区分是不是网关返回的。
429
429 是限流。两条线:请求数(RPM)和 token 数(TPM),长请求更容易先撞 TPM。响应里通常带 Retry-After,按它等比自己猜间隔准。
并发高时 429 会连锁:一次失败触发重试,重试又加剧拥堵。退避要加随机抖动,别让所有客户端在同一秒重发。
余额或配额不足有时也用 429 返回,具体看响应体里的 code 和 message,别只看状态码。
限流窗口一般按分钟滚动,等满一个窗口再发,比零散重试恢复得快。批量任务把请求摊到更长的时间段,比猛提并发更稳。
500 与 503
500 是上游或网关内部错误,重试一次通常能过。503 多是渠道暂时不可用或过载,换分组、换模型往往立刻恢复。
连续 500 不要死磕同一个模型,先切到备用型号,把失败样本记下来。重试会重新发一遍输入 token,claude-sonnet-5 在 Claude Max 分组输入 2.55 美元每百万 token,一次 4 万 token 的请求重试三次就是 0.306 美元,成本要算进预算。
流式请求更麻烦:HTTP 状态码已经是 200,错误藏在 SSE 的 error 事件里。客户端要监听数据流里的错误对象,只看状态码会漏掉半路失败。
一张对照表和重试封装
| 状态码 | 常见原因 | 先查什么 |
|---|---|---|
| 401 | key 错误或被删 | 头是否为 Bearer sk-...,尾部有无空格 |
| 403 | 分组无权限、额度或 IP 限制 | 模型页的可用分组、账户余额 |
| 404 | 路径或模型名写错 | base_url 是否含 /v1,模型名是否一致 |
| 429 | 触发 RPM 或 TPM 限流 | 响应体的 Retry-After 与错误码 |
| 500 | 上游异常或网关错误 | request id,隔几秒重试一次 |
| 503 | 渠道不可用或过载 | 换分组、换模型 |
import time
import requests
RETRYABLE = {429, 500, 502, 503, 504}
def post(payload, tries=4):
for i in range(tries):
r = requests.post(
"https://ai.idcidiot.com/v1/chat/completions",
headers={"Authorization": "Bearer sk-..."},
json=payload,
timeout=90,
)
rid = r.headers.get("x-request-id", "-")
if r.status_code < 400:
return r.json()
print(f"{r.status_code} request_id={rid} {r.text[:300]}")
if r.status_code not in RETRYABLE or i == tries - 1:
raise RuntimeError(f"{r.status_code} request_id={rid}")
wait = 1.5 ** i
retry_after = r.headers.get("retry-after", "")
if retry_after.isdigit():
wait = max(wait, int(retry_after))
time.sleep(wait + 0.4 * i)400 和 422 不要重试,它们的成因在请求体里,重发一百次结果一样。把这类错误单独打日志,按模型和字段聚合,能很快看出是哪段提示词或哪个参数不稳定。
重试次数设上限,总时长也设上限。一个请求卡住三分钟再失败,比立刻失败更伤,上游的资源已经占住了。