错误体格式与读取顺序
所有失败响应都是同一个结构:
{"error":{"code":"...","message":"..."}}
读取顺序固定三步:
- HTTP 状态码,决定大类;
error.code,决定具体原因,程序里用它做分支;error.message,给人看,写进日志,不要用它做字符串匹配。
先想清楚一个分界:能靠重试解决的(429、503,以及网络超时)和必须改动才能解决的(其余)。把两类混在一起,是线上事故最常见的根源,比如对 402 不停重试,结果每秒打几十个无效请求。
日志里建议固定记录四个字段:时间、状态码、error.code、本次请求的 prompt_tokens 估值。出问题时,一眼就能看出是某一类错误突然增多,还是整体失败。再配一条告警规则:402 只要出现一次就立刻通知,因为它意味着服务已经对用户不可用;429 则看比例,偶发无需处理,持续出现说明并发设计有问题。
状态码速查
| 状态码 | error.code | 含义 | 重试? |
|---|---|---|---|
| 400 | — | 请求不合法,如 prompt 加 max_tokens 超过 100k | 否 |
| 401 | — | key 无效或缺失 | 否 |
| 402 | no_credit | 余额用尽或试用过期 | 否 |
| 403 | content_blocked | 内容被拦截 | 否 |
| 404 | — | 端点不存在 | 否 |
| 429 | — | 触发限流 | 是,退避 |
| 503 | upstream_busy | 服务暂时繁忙 | 是,几秒后 |
表中“—”表示没有需要你特别处理的固定 code 字符串,按状态码分支即可。
4xx:改请求,别重试
400 请求不合法
- 成因一:JSON 格式错误,常见于手拼字符串时引号没转义。
- 成因二:prompt 加 max_tokens 超过 100,000。长对话最容易触发。
- 成因三:请求体超过 8 MB。
- 修法:用 JSON 库序列化;发送前估算 token,超限就裁剪历史或调小 max_tokens;见长上下文实战。
401 密钥问题
- Header 缺失,或缺了
Bearer前缀。 - 重新生成过 key,旧 key 立刻失效,而某台机器还在用旧值。
- 环境变量没带进容器或定时任务。
402 no_credit
余额用完,或 7 天试用已过期。去账户页充值预付费余额即可恢复。建议你自己的服务监控余额,别等用户报错才发现。
403 content_blocked
内容被拦截。合法的成人向内容、虚构和争议话题不会被拒绝,但涉及未成年人的性内容一律拦截,包括小说和角色扮演。遇到 403,应该检查输入和历史里是否有这类内容,而不是换措辞重试。
404 端点不存在
只有两个端点:POST /v1/chat/completions 和 GET /v1/models。路径漏了 /v1、多了斜杠、拼错,或者请求了不支持的 embeddings、图片等接口,都会 404。
400 里还有一类容易误判:长对话的 token 随轮次线性增长,白天测试一切正常,跑到几十轮后突然开始 400。这不是接口不稳定,而是上下文到顶了。解决办法是给历史设上限,超过阈值就丢最旧的轮次,或者把旧内容压缩成摘要。别等报错再处理,在发请求前就估算好。
401 的排查有个小技巧:把 key 的前后各四位打印到日志里,不要打印完整内容,和账户页里显示的比对。这样可以确认进程里读到的到底是不是你以为的那把,尤其是容器和定时任务环境里,环境变量缺失或读到旧值是最常见的原因。
429 与 503:该重试的两类
429 限流
每把 key 每分钟 300 次请求。批量任务、多实例共用一把 key、重试风暴,都会触发。修法有两层:客户端先限速(见后面的代码),再对 429 做退避重试。
503 upstream_busy
服务暂时繁忙,几秒后重试即可。别立刻连发,也别在一秒内重试十次,那只会让情况更糟。
网络超时
长文本生成耗时较长,timeout 不要设得太短,示例用 120 秒。超时后重试要注意:上一次请求可能已经在服务端执行,重复调用可能重复消耗额度,所以对生成类请求,超时重试次数要少,最好配合流式输出缩短单次等待。
import os
import random
import time
import requests
URL = "https://api.wushenchaapi.com/v1/chat/completions"
HEADERS = {
"Authorization": "Bearer " + os.environ["API_KEY"],
"Content-Type": "application/json",
}
RETRYABLE = {429, 503} # 只重试这两类
FATAL = {400, 401, 402, 403, 404}
class ApiError(Exception):
def __init__(self, status, code, message):
super().__init__(f"{status} {code}: {message}")
self.status, self.code = status, code
def call(payload, max_retries=5, base=1.0, cap=30.0):
for attempt in range(max_retries + 1):
try:
r = requests.post(URL, headers=HEADERS, json=payload, timeout=120)
except (requests.ConnectionError, requests.Timeout):
r = None # 网络层失败,按可重试处理
if r is not None and r.ok:
return r.json()
if r is not None:
try:
err = r.json().get("error", {})
except ValueError:
err = {}
if r.status_code in FATAL or r.status_code not in RETRYABLE:
raise ApiError(r.status_code, err.get("code"), err.get("message"))
if attempt == max_retries:
raise ApiError(r.status_code if r is not None else 0, "retry_exhausted", "重试次数用尽")
delay = min(cap, base * (2 ** attempt)) * random.uniform(0.5, 1.0)
time.sleep(delay)
if __name__ == "__main__":
out = call({"model": "uncensored", "max_tokens": 100,
"messages": [{"role": "user", "content": "回复一个字:好"}]})
print(out["choices"][0]["message"]["content"])要点:退避公式是 min(cap, base × 2^n) × 随机系数,随机抖动避免多个客户端同时重试撞在一起;FATAL 集合里的状态码一次都不重试。
客户端限速与诊断请求
与其等 429 再退避,不如事先限速。下面的小工具保证一分钟内请求数低于设定值,多线程安全:
import threading
import time
class RateGate:
"""简单的客户端限速:保证一分钟内请求数不超过 limit。"""
def __init__(self, limit=240): # 留出余量,低于 300/分钟
self.limit, self.stamps, self.lock = limit, [], threading.Lock()
def wait(self):
while True:
with self.lock:
now = time.time()
self.stamps = [t for t in self.stamps if now - t < 60]
if len(self.stamps) < self.limit:
self.stamps.append(now)
return
sleep_for = 60 - (now - self.stamps[0])
time.sleep(max(sleep_for, 0.05))排查时,最干净的办法是脱离业务代码,用 curl 发一个最小请求,-i 能同时看到状态行和响应头:
curl -i https://api.wushenchaapi.com/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"uncensored","max_tokens":20,"messages":[{"role":"user","content":"ping"}]}'curl 能通而业务代码不通,问题在你的代码或环境(代理、环境变量、编码);curl 也不通,看 key、余额和网络。
限速值为什么设 240 而不是 300:多实例部署时,各实例各自限速,加起来仍可能超过总额;再加上重试会额外占用配额。留出两成余量,是比较稳妥的做法。如果你有多个实例,把每个实例的额度按总数平分,或者用一个共享的计数器统一调度。
排障清单
- 读 body 里的
error.code,不要只看状态码。 - 401:key 前缀、环境变量、是否重新生成过。
- 400:JSON 是否合法,prompt + max_tokens 是否超 100,000,请求体是否超 8 MB。
- 402:余额与试用有效期。
- 403:输入和历史里是否含被禁内容。
- 404:路径是否为 /v1/chat/completions 或 /v1/models。
- 429:是否多实例共用一把 key,是否做了客户端限速。
- 503:是否做了退避重试,间隔是否至少几秒。
- 超时:timeout 是否足够,是否可以改流式。
- 以上都排除:用 curl 最小请求复现。
清单用法:出问题时从上往下逐项排除,不要跳着看。大多数“诡异”故障,最终都落在前四项里。
补充一条经验:把每次排障结论回写到团队文档,下次同样的报错就能直接对号入座。