codecamp

Grok 错误码该不该重试

「加个重试就好了」是生产事故的常见起点。400 重试一百次还是 400,只是把一次失败拖成一百次;429 立刻重试,等于在一堵墙上反复撞;最危险的是超时后重试——第一次请求可能已经计费、已经生成,第二次又完整来一遍。

重试不是「加个循环」,是按错误类型分三类动作。

一、问题不是「重不重试」,而是「这次重试会改变什么」

判断一个错误该不该重试,只问两个问题:

  • 重试能让结果变好吗? 参数错了、密钥无效,重试一万次也不会变。
  • 重试会产生副作用吗? 对可能已计费的生成请求,重试可能造成重复计费与重复输出。

按这两个问题,状态码可以分成三类:

  • 不重试:400、401、403、404、413、415、422——请求本身有问题,重试无意义。
  • 退避重试:408、429、5xx——临时状态;429 读 Retry-After,5xx 用指数退避。
  • 谨慎重试:超时、连接中断——可能已产生结果,重试前先确认是否计费。

二、最小可运行示例:重试决策与退避

下面的脚本不联网、不需要真实密钥。它按状态码给出动作与等待时间,429 优先用 Retry-After,5xx 用指数退避加抖动。

"""重试决策(mock):三类动作 + 指数退避 + 抖动。"""

import random

NO_RETRY = {400, 401, 403, 404, 413, 415, 422}

def decide(status: int, attempt: int, retry_after: float | None = None,
           seed: int = 0, timed_out: bool = False) -> tuple[str, float]:
    """返回 (动作, 等待秒数)。attempt 从 0 开始。"""
    if timed_out or status == 0:
        return "cautious_retry", 0.0      # 先确认是否已计费,再决定重发
    if status in NO_RETRY:
        return "no_retry", 0.0
    if status in (408, 429):
        wait = retry_after if retry_after is not None else min(2 ** attempt, 30)
        return "backoff", float(wait)
    if 500 <= status < 600:
        base = min(2 ** attempt, 30)
        jitter = random.Random(seed).uniform(0, 0.5)
        return "backoff", round(base + jitter, 2)
    return "no_retry", 0.0

def offline_check() -> None:
    cases = [
        ("400 参数错", 400, 0, None, False),
        ("401 密钥错", 401, 0, None, False),
        ("413 请求体过大", 413, 0, None, False),
        ("408 请求超时", 408, 0, None, False),
        ("429 带 Retry-After", 429, 0, 3.0, False),
        ("429 无 Retry-After", 429, 3, None, False),
        ("503 服务端错", 503, 2, None, False),
        ("网络超时", 0, 0, None, True),
    ]
    for label, status, attempt, ra, to in cases:
        action, wait = decide(status, attempt, ra, timed_out=to)
        print(f"{label}:{action} {wait}")

if __name__ == "__main__":
    offline_check()

真正上线时,attempt 从 0 开始计数,每次重试前检查「是否超过最大次数」;seed 在生产里换成随机源,测试里固定以便复现。

三、三类动作的触发与边界

类别 状态码 动作 注意
不重试 400 / 401 / 403 / 404 / 413 / 415 / 422 直接失败并记录 401 先查密钥读取时机
退避重试 408 退避后重试 与「超时」区分:408 是服务端明说超时
退避重试 429 读 Retry-After,无则指数退避 别用固定间隔猛撞
退避重试 5xx 指数退避 + 抖动 设最大次数与总时长
谨慎重试 超时 / 连接断 先确认是否已计费 可能重复生成

关于 429,还有个容易被忽略的层次:它不是笼统的「你太快了」,而是两个独立维度各自触发——每秒请求数(RPS)与每分钟 token 数(TPM)。RPS 由每分钟配额推出来(RPM ÷ 48,且下限为 2),所以「把一分钟的额度在一秒内打完」必然被拦。TPM 那边有个反直觉的点:命中缓存的那部分 token 同样计入 TPM,只是单价更低——省的是钱,不是配额。

两个维度的上限都随团队档位放大,而档位看的是累计消费:$50 / $250 / $1,000 / $5,000 四道门槛,达到即升级,且只升不降。这解释了一个常见困惑:同一份代码在 A 团队跑得稳、在 B 团队频繁 429——往往不是代码问题,是档位问题。图像与视频(Imagine)的额度不在自助档位里,要单独找商务申请。

三件要强调的事:

  • 重试要设总预算。不只看次数,还要看总耗时;一个退避 30 秒的循环跑五次,用户已经走了。
  • 抖动不是装饰。多个客户端同时重试会形成新的尖峰;加随机抖动能把重试摊开。
  • 超时是最难的一类。连接断了不代表请求没执行;对生成类请求,重试前先查「是否已有结果」,或者用幂等键去重(若平台提供,以官方页为准)。

四、预期输出与异常分支

运行决策器,预期输出:

400 参数错:no_retry 0.0
401 密钥错:no_retry 0.0
413 请求体过大:no_retry 0.0
408 请求超时:backoff 1.0
429 带 Retry-After:backoff 3.0
429 无 Retry-After:backoff 8.0
503 服务端错:backoff 4.42
网络超时:cautious_retry 0.0

换成真实流量时,若出现异常,按下面顺序定位:

现象 优先怀疑 第一步动作
重试风暴 没有抖动或上限 加抖动、设最大次数
401 一直重试 把认证错误当临时故障 归入不重试
账单出现成对请求 超时后直接重发 先确认是否已计费
等待时间过长 退避翻倍无上限 封顶并设总时长
429 频繁但并发不高 TPM 触顶(含缓存 token) 按 token 量算每分钟消耗,别只数请求数

最后一行尤其重要:429 的处理不是「等更久」,而是「按服务端给的节奏等」。忽略 Retry-After,重试越勤奋越糟。

五、判据卡

判断项 该这么做 不该这么做
400/401/403/404/422 不重试,直接记录 一律包进重试循环
429 读 Retry-After,并先分清 RPS 还是 TPM 触顶 一律当成「等久一点」
5xx 指数退避 + 抖动 线性重试
超时 先确认是否已计费 直接重发
上限 次数 + 总时长双限 只限次数

这张卡的重点是先分类再重试。五条里有四条在说:不同错误需要不同动作,混成一类就等于没有策略。

六、来源与核验

  • 官方 API 参考:https://docs.x.ai/developers/rest-api-reference(核验日期 2026-11-04)。错误响应的整体结构以该页为准;具体状态码语义遵循 HTTP 通用约定。
  • 官方 Streaming 页:https://docs.x.ai/developers/model-capabilities/text/streaming(核验日期 2026-11-04)。推理模型流式输出建议覆盖超时的提示,是「超时不等于失败」的一个具体来源。
  • 官方限流页:https://docs.x.ai/developers/rate-limits(核验日期 2026-11-04)。RPS 由 RPM ÷ 48 推导(下限 2)、按档位的 RPS/TPM 上限、档位门槛与只升不降、缓存 token 计入 TPM、Imagine 额度需单独申请,均以该页为准(F58–F60)。
  • 三类动作划分与退避实现为本书原创的工程框架;第三方重试文章中的相似提法仅用于确认这是共性问题,本节不转述其内容。平台是否提供幂等键,以官方页为准。

七、动手试

  1. 给你现有代码里每个重试点标注类别(不重试 / 退避 / 谨慎),找出「400 也在重试」的地方。
  2. 把 decide() 改成带总预算的版本:超过总时长直接返回 give_up,并说明为什么这比「再试一次」更稳。
  3. 设计一条超时请求的对账流程:先查什么、再决定重发还是放弃,写出三个检查点。
Grok Voice 3.0 与转写模型
Grok 契约测试与 CI mock
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }