Groq API 错误码逐条排查:从 401 到 499 怎么定位怎么修
同样是「调不通」,401 和 429 的处理方向几乎相反。401 说明配置错了,重试一万次也不会突然变对,得回去改代码或改环境变量;429 说明配置全对,只是打得太快,这时唯一该做的是等一会儿再来,改代码反而没用。我见过太多人把这两类混在一个 except Exception: retry() 里,结果 key 拼错了还在那儿指数退避,眼睁睁看着脚本空转十分钟才报错退出。
所以排查 Groq 的报错,第一步不是查那条 message 什么意思,而是先按状态码把问题分到「改代码」还是「等一等」两个桶里。这篇按 Groq 官方错误码清单逐条过一遍,每条说清三件事:什么原因、怎么定位、怎么修。
先看清错误响应长什么样
Groq 的错误响应是 JSON,里面有一个 error 对象,包含两个关键字段:
message:人读的描述文本type:分类,例如invalid_request_error
写代码时请记住一条纪律:判断逻辑只依赖 HTTP 状态码和 type,不要去正则匹配 message 里的文本。message 是给人看的,措辞随时可能被平台改掉,今天匹配得上明天就匹配不上,而且这种失效是静默的——你的重试分支会悄悄走到 else 里去,线上跑一周才发现某类错误从来没被正确处理过。状态码和 type 是契约的一部分,稳定得多。
message 当然要读,但读它的场景是「人在排查」,不是「代码在判断」。日志里把整段 error 对象原样打出来就行。
逐条过错误码
400 Bad Request:语法非法
请求在语法层面就过不去。最常见的是 JSON 拼错了——少个逗号、多个尾随逗号、body 编码不对,或者根本没设 Content-Type。
定位办法很土但有效:把你实际发出去的 body 打印出来(不是你以为发出去的那个),扔进任意一个 JSON 校验器过一遍。用 SDK 时这类错误少,用 requests 手拼 body 时特别多。修法就是把请求体拼对,重试没有任何意义。
401 Unauthorized:缺失或无效凭据
字面意思是凭据没给或者给错了。但实际排查时它其实是两个完全不同的问题,混在同一个码里:
情况一:key 压根没读到。 Groq 的 key 走环境变量 GROQ_API_KEY 配置。你在终端里 export 了,但 IDE 里跑的进程没继承到;或者 .env 文件放错了目录、忘了调加载函数;或者容器起来的时候环境变量没注入。这种情况下客户端拿到的是空字符串或者 None,发出去的自然是无效凭据。
情况二:key 本身失效了。 被轮换、被删除、或者复制的时候少了尾字符。
分辨这两者只要一行代码——在发请求之前,先把 key 的长度和前后几位打印出来(绝不要打印完整 key):
import os
k = os.environ.get("GROQ_API_KEY")
print("key loaded:", bool(k), "len:", len(k) if k else 0, "tail:", k[-4:] if k else "")
长度是 0 或者变量根本不存在,那是环境变量问题,去查加载链路;长度正常、尾部也对得上控制台里那条 key,那就是 key 本身的问题,去控制台重新签发一把。这一步能省掉至少一半的瞎猜时间。401 的通用处理思路可以再看下 401 鉴权失败排查。
403 Forbidden:权限不足
凭据是真的、也认出来了,但这把 key 或者这个组织没有权限做你要做的事。和 401 的区别在于:401 是「你是谁我不知道」,403 是「我知道你是谁,但你不能干这个」。
定位方向是去控制台确认这把 key 所属组织的权限范围,以及你调的这个能力是不是需要额外开通。改代码里的重试次数对 403 一点用没有。
404 Not Found:资源不存在
这条在 Groq 上有一个高频陷阱:base_url 写错,表现出来非常像 404。
Groq 的 OpenAI 兼容端点是 https://api.groq.com/openai/v1。少写了后面的 /openai/v1,或者只写到 https://api.groq.com,请求打到的路径就不是 chat completions 那个位置,返回的东西自然对不上。
所以看到 404,先别急着怀疑模型 ID 打错了,按这个顺序查:
- base_url 是不是精确等于
https://api.groq.com/openai/v1(注意别多一个尾斜杠、别少/v1) - SDK 是不是会在 base_url 后面自动再拼一次
/v1,导致最终 URL 变成两个 v1 - 最后才去查模型 ID 拼写
第 2 点特别阴——某些 OpenAI 兼容客户端默认会追加版本段,你配了完整路径反而错了。判断方法是打开客户端的调试日志,看真实请求 URL。base_url 这块的通用坑可以参考 base_url 配置与常见错法。
413 Request Entity Too Large:请求体过大
你塞进去的东西超过了服务端能接收的请求体上限。做长文档摘要、把整个 RAG 检索结果一股脑拼进 messages、或者传大音频文件时容易撞上。
定位方法:在发送前打印一下序列化后 body 的字节数。修法只有一个方向——把请求变小:切分文档分批处理、裁剪检索结果条数、压缩或分段处理音频。
注意 413 是请求体大小的问题,跟上下文窗口是两回事,虽然它们经常同时被触发。
422 Unprocessable Entity:格式合法但语义错误
这条是最容易和 400 混的,讲清楚它们的区别其实很简单:
- 400 是「我看不懂你在说什么」——JSON 都解析不出来,语法层面就废了。
- 422 是「我看懂了你在说什么,但你说的这事不成立」——JSON 完全合法,字段名也对,但值不合理。
举几个落到 422 这一侧的典型:某个参数超出了允许区间、两个参数互相冲突、结构上要求成对出现的字段只给了一半、枚举值给了个不存在的选项。请求在语法上无可挑剔,只是语义上说不通。
定位就靠读 error.message——422 的 message 通常会点名是哪个字段有问题,这是人工排查时 message 最有价值的场景。修法是改参数,同样不该重试。
顺便提醒一句:Groq 的 OpenAI 兼容层有一份明确的不支持参数清单,包括 logprobs、logit_bias、top_logprobs、messages[].name,N 若传必须等于 1,音频格式不支持 vtt 和 srt。另外 temperature 传 0 会被转换成 1e-8。从 OpenAI 那边直接搬代码过来的时候,如果原代码里带了这些参数,是排查参数类报错时第一个该看的地方。
424 Failed Dependency:Remote MCP 鉴权问题
这个码别家平台基本见不到,值得单独说。Groq 文档里明确它出现在 Remote MCP 鉴权出问题的场景。
它的语义是「你的请求没毛病,但我依赖的那个外部东西出问题了」。所以看到 424,排查方向不在你这边的请求体,而在你配置的那个远端 MCP:它的凭据是不是过期了、是不是配错了、是不是本身就拒绝了这次连接。
实践里 424 最大的价值是它帮你省时间——它明确告诉你「别在自己的 payload 上浪费精力了」。如果你的应用没接 Remote MCP 却收到 424,那说明配置里挂了一个你不知道的依赖,去翻配置。
429 Too Many Requests:不是你算错了额度
429 表示单位时间内打得太多了。但 Groq 这里有两个官方说明,直接解释了「为什么我明明算过额度还是撞了」:
第一,速率限制适用于组织级别,不适用于个别用户。 你自己那个脚本算得再准也没用——同一个组织下的其他人、其他服务、CI 里那个定时任务,全都在啃同一份配额。本地测得好好的,一上生产就 429,十有八九是这个原因。
第二,限速是多维的,先撞到哪个阈值哪个就生效。 Groq 免费层的限速表是逐模型列出 RPM、RPD、TPM、TPD 四个维度的。你可能请求数离 RPM 上限还很远,但因为每次请求都塞了很长的上下文,TPM 先满了;也可能 RPM、TPM 都宽裕,但跑了一整天,RPD 那个日上限先到顶——RPD 撞顶最难受,退避多久都没用,得等日窗口滚过去。
所以撞到 429,先确认是哪个维度爆的,再决定动作:RPM 爆了就退避加限速;TPM 爆了就缩短 prompt,光退避没用,因为每次重试的体积一点没变;RPD 爆了就别硬刚了,排到下一个窗口或者换配额。退避与限速的具体写法见 429 限流与指数退避。
498 与 499:两个 Groq 自定义码
这两个是 Groq 自己定义的,标准 HTTP 状态码里没有:
- 498:Flex Tier 容量超限
- 499:调用方取消请求
它们的麻烦之处不在于难懂,而在于通用 HTTP 库和 SDK 往往不认识它们。很多客户端的错误映射表是按标准状态码写的,碰到 498/499 会落进「未知错误」的兜底分支,或者干脆抛一个语焉不详的异常。你的重试逻辑里如果只列了 429 和 5xx,这两个码就会走到「不重试、直接失败」那一支——或者更糟,被兜底逻辑当成可重试的一直重试。
所以要在自己的错误处理里显式列出 498 和 499。
498 意味着 Flex Tier 那侧的容量满了,跟你请求写得对不对无关,退避后重试是合理动作。499 是调用方取消请求,先去查你自己这边:客户端超时设太短了?上游把连接掐了?用户点了取消?如果是超时设置太紧导致的自我取消,重试只是在重复制造同一个 499,该调的是超时值——这块可以看 超时设置。
5xx:500 / 502 / 503,而且不计费
- 500 Internal Server Error:服务端内部错误
- 502 Bad Gateway:网关错误
- 503 Service Unavailable:维护或过载
这三个都是服务端侧的问题,你的请求本身通常没毛病。有一条官方事实直接影响策略:Groq 的 5xx 响应不计费。
这意味着 5xx 的重试可以更积极——失败的那次不产生费用,重试成本只有时间。当然「更积极」不等于无脑打,退避该有还得有,不然你只是在给一个已经过载的服务继续加压。
206 Partial Content:不是错误
清单里还有一个 206,属于成功类,表示带 range 头时的分段返回。写错误处理时别把它归到失败里去——有些手写状态码判断的代码会写成 if status != 200: raise,那就会把 206 误伤。
一段可以直接抄的错误分流
把上面这些整理成代码,核心就是一张表:哪些码重试有用,哪些码重试纯属浪费。
import random
import time
# 重试有意义:服务端问题、容量问题、限速
RETRIABLE = {429, 498, 500, 502, 503}
# 重试是浪费:不改代码/配置,重试一万次也一样
FATAL = {400, 401, 403, 404, 413, 422, 424}
def call_with_retry(do_request, max_attempts=5, base=1.0, cap=30.0):
for attempt in range(max_attempts):
resp = do_request()
if resp.status_code < 300: # 200、206 都在这里放行
return resp
code = resp.status_code
err = (resp.json() or {}).get("error", {})
# 只记录 message,不拿它做判断
print(f"[groq] status={code} type={err.get('type')} message={err.get('message')}")
if code in FATAL:
raise RuntimeError(f"不可重试,请修配置或请求体: {code}")
if code == 499:
# 调用方取消:先确认是不是自己的超时太紧,
# 确认非本地原因后再纳入重试,否则只是重复制造 499
raise RuntimeError("请求被调用方取消,检查客户端超时与连接")
if code not in RETRIABLE:
raise RuntimeError(f"未预期的状态码: {code}")
sleep = min(cap, base * (2 ** attempt))
sleep = sleep * (0.5 + random.random() / 2) # 抖动,避免同时重试
time.sleep(sleep)
raise RuntimeError("重试次数耗尽")
几点说明。第一,FATAL 那一组遇到就立刻抛,不要客气——早失败早发现,比在日志里静默退避十分钟强。第二,抖动别省,多个 worker 一起撞 429 的时候,没有抖动它们会同步退避、同步重来,然后一起再撞一次。第三,429 撞的如果是日维度,指数退避那点秒数根本救不了,成熟一点的做法是给日配额单独记一个本地计数,撞顶后直接停批而不是继续重试。
排查顺序清单
按这个顺序走,从最省事的开始,大部分问题在前三步就能收掉:
- 先看状态码,分桶。 落在 400/401/403/404/413/422/424 里的,直接判定「要改东西」,别浪费时间在重试上;落在 429/498/5xx 里的,判定「等一等」。
- 把整段
error对象打出来看。type告诉你分类,message告诉你细节,尤其 422 的 message 通常直接点名出问题的字段。 - 验 key 加载。 打印 key 的长度和末四位,区分「环境变量没读到」和「key 本身失效」,一行代码的事。
- 核对 base_url。 确认精确等于
https://api.groq.com/openai/v1,并打开客户端调试日志看真实请求 URL,防止 SDK 自动追加版本段。 - 对照不支持参数清单。 从 OpenAI 迁过来的代码,重点看有没有带
logprobs、logit_bias、top_logprobs、messages[].name,以及N是否等于 1。 - 429 定位到具体维度。 分清是 RPM、TPM 还是 RPD 撞顶,并记住限速是组织级的——别只盯着自己那个脚本算账。
- 最后才怀疑平台。 确认是 5xx 且请求本身没问题,那就交给重试逻辑,反正 5xx 不计费。
真正难查的报错其实很少,大部分时间浪费在「用错了方向」——对着一个 401 反复调退避参数,或者对着一个 TPM 超限反复缩短重试间隔。先分对桶,剩下的都是体力活。