GPT-6 Luna 调用报错时 / 先分清鉴权、限流还是参数不合法

文章导读
调用 GPT-6 Luna 失败时,如果只拿到一个笼统提示,先不要急着改代码。按三步取证:把原始错误体完整打印出来、用最小请求回放一次、看失败在时间上的分布。这三步做完,原因通常能落到鉴权、限流、参数不合法三类里,再决定是换密钥、降频率还是改参数。
📋 目录
  1. 壹 把原始错误体完整打印出来
  2. 贰 用一条最小请求排除参数干扰
  3. 叁 换密钥或换环境复现一次
  4. 肆 按时间分布判断是不是限流
  5. 伍 写一个错误分类分支
A A

调用 GPT-6 Luna 失败时,如果只拿到一个笼统提示,先不要急着改代码。按三步取证:把原始错误体完整打印出来、用最小请求回放一次、看失败在时间上的分布。这三步做完,原因通常能落到鉴权、限流、参数不合法三类里,再决定是换密钥、降频率还是改参数。

报错文本只说明现象,不说明原因。先拿到状态码和错误体原文,再用只含 model 与单条 messages 的最小请求回放;换一把有效密钥就恢复的偏鉴权,失败在时间上聚集、与并发峰值同相的偏限流,最小请求能成功、加回某个字段才失败的偏参数不合法。鉴权类和参数类错误不要重试,只对限流和临时性错误做有限次退避重试。

把原始错误体完整打印出来

先解决“看不见”的问题。多数 SDK 或业务封装会把异常压成一句“请求失败”,而真正能区分原因的状态码、错误类型、错误信息原文都藏在 HTTP 响应里。建议把打印动作放在最外层 HTTP 客户端拦截的位置,而不是业务代码的 try/except 里,避免被二次封装吞掉关键字段。

日志中建议保留这些字段:请求时间(含毫秒与时区)、endpoint、model 名、HTTP 状态码、request-id / trace-id、错误类型字段、错误信息原文、重试次数、单次耗时。响应体原文通常值得留存,但若里面带用户输入,需要脱敏或截断后再写日志。

status = resp.status_code
rid = resp.headers.get("request-id")
body = resp.text
log.error("luna_call_failed status=%s request_id=%s body=%s", status, rid, body[:2000])

拿到这些字段后,可以先按下表做初判,再到对应小节验证。错误码的具体含义以官方文档为准,这里只给出常见的归类方向。

观察点鉴权问题限流参数不合法
HTTP 状态码多为 401 / 403多为 429多为 400 / 422
换一把有效密钥恢复通常无变化无变化
换成最小请求仍失败可能成功,也可能仍被限成功,加回某字段后失败
时间分布稳定复现聚集成簇,与并发同相稳定复现
重试是否有用无用退避后可能有用无用

用一条最小请求排除参数干扰

这一步的目的,是判断问题出在请求本身,还是出在参数组合上。先把线上那份请求体复制一份,删到只剩必需字段,发出去看结果。

{
  "model": "<你的模型标识>",
  "messages": [
    {"role": "user", "content": "ping"}
  ]
}

model 名以你实际接入的标识为准,上面只是骨架。最小请求通过后,再逐项把参数加回来:先加回 max_tokens,再加 temperature,然后依次加 system 提示、多轮 messages、tools / 结构化输出、stream 等。一次只加一项,每加一项发一次请求,哪一项加回去就复现失败,问题就在那一项上。如果最小请求本身就失败,说明问题不在参数组合,回头去看鉴权和时间分布。

换密钥或换环境复现一次

鉴权问题最容易被“密钥没问题”这种假设掩盖。检查清单建议按这个顺序走:密钥从哪来(环境变量、.env 文件、配置文件、代码常量、容器 secret、CI 变量)、这些来源同时存在时谁覆盖谁、加载顺序是否符合预期;密钥字符串前后是否混入空格、引号、换行或 BOM;这把密钥对应的项目或组织是否正确、额度是否已用尽、是否已被禁用或轮换。

GPT-6 Luna 调用报错时 / 先分清鉴权、限流还是参数不合法

然后做对照:用同一把密钥在另一台机器或另一个网络出口,绕开业务代码的加载逻辑,直接用脚本或命令行发一次最小请求;再换一把确认有效的密钥,用同样的最小请求再发一次。前者失败后者成功,基本可判为密钥或加载链路问题;两把密钥都报同样的错误、且错误体指向某个字段,则更可能是参数问题。这里只讲机制,具体密钥内容不要写进日志和代码仓库。

按时间分布判断是不是限流

参数错误稳定复现,限流往往与并发和流量峰值相关,所以时间分布是最省事的区分手段。记录每次失败的时间戳(精确到秒或毫秒)、当时的并发数或正在进行的请求数、是否处于重试、以及状态码。并发数可以来自你的工作线程数、连接池占用数或网关侧的请求指标。

grep luna_call app.log | awk '{print $1, $2}' | cut -c1-16 | sort | uniq -c | sort -rn | head

上面这条只是把日志按分钟聚合成失败次数,字段位置需要按你自己的日志格式调整。判读时关注:失败是否集中在某几个时间窗内、是否与并发升高同相、失败之间是否交替出现成功请求、重试后是否延迟成功。如果符合这些特征,通常更接近限流或配额耗尽;如果同样的请求体在任何时间点都失败、换低并发也一样,那更可能是参数或鉴权问题。

写一个错误分类分支

分类的意义在于让重试只发生在该重试的错误上。下面是一段通用骨架,状态码到类别的映射需要按你接入方的文档确认后再调整。

RETRYABLE = {408, 409, 429, 500, 502, 503, 504}

def classify(status):
    if status in (401, 403):
        return "auth"          # 鉴权:不重试
    if status == 429:
        return "rate_limit"    # 限流:退避后重试
    if status in (400, 422):
        return "bad_request"   # 参数:不重试
    if status in RETRYABLE:
        return "transient"     # 临时性:可重试
    return "fatal"             # 其他:不重试

def call(payload, max_attempts=3):
    for attempt in range(1, max_attempts + 1):
        status, rid, body = call_once(payload)
        kind = classify(status)
        log.warning("kind=%s status=%s request_id=%s attempt=%s",
                    kind, status, rid, attempt)
        if kind in ("auth", "bad_request", "fatal"):
            raise LunaError(kind, body)      # 直接抛出
        if attempt == max_attempts:
            raise LunaError(kind, body)
        time.sleep(backoff(attempt) + random.uniform(0, 0.3))

需要直接抛出、不再重试的,通常是鉴权失败、参数不合法、模型标识不存在、内容被策略拒绝、请求体超出大小限制这几类——重试只会重复消耗时间,并不会改变结果。适合重试的是限流和临时性服务端错误,重试次数建议设上限,退避里加一点随机抖动,避免多实例同时重试把压力又叠回去。重试次数、退避曲线这些取值要结合你的并发规模和接入方的配额来定,先用小并发验证分类是否准确,再放到真实流量里。