跳转到主要内容

大模型 API 该重试还是切备用模型?先过这五道门

9 分钟阅读API 指南

故障短暂、调用可安全重放且预算还够,才重试同一路由;备用模型只有通过同一业务合同后,才可以自动切换。

大模型 API 故障依次经过故障归属、提交状态、恢复预算、备用等价性和路由健康度五道门,最终进入重试、切换、排队、降级或停止。

大模型 API 失败后,不要先数“这是第几次失败”。先回答五个问题:谁拥有故障、请求是否已经产生不可重复的结果、恢复预算还剩多少、备用模型是否真的等价、当前路由是否健康。只有故障可能短暂、调用可安全重放、预算仍足够时,才重试同一路由;只有备用路由已经通过同一业务验收,才允许切换。其他情况应排队、返回可控降级结果,或直接停止。

关卡必须确认的问题不满足时的动作
故障归属是临时网络/上游故障,而不是请求、权限、策略或预算问题吗?修正归属方或停止
提交状态再发一次不会重复 token、写入或外部动作吗?先对账或显式恢复
恢复预算总次数、总耗时、token、费用和用户耐心还够吗?排队、降级或停止
备用等价性备用路由通过了当前业务的 schema、工具、安全、数据和质量测试吗?禁止静默切换
路由健康度主路由值得有限探测,且每次尝试都可观测吗?熔断并进入已批准模式

“换一个模型再试”不是普通重试。它可能同时改变上下文窗口、结构化输出、工具调用、安全过滤、数据位置、价格和回答质量。把 fallback 当成另一次 retry,恰恰是生产事故容易被隐藏的地方。

同一个 429,可能属于两个完全不同的分支

状态码只能帮助定位,不能直接决定动作。OpenAI 当前的错误码说明把 429 分成两类:发送过快要减速;额度或月度支出耗尽要处理 credits/limits。对第二类做指数退避,并不会凭空产生预算。

生产日志至少要把下面几类拆开:

现象默认动作必须保留的证据
上游接收前的连接失败可重放时做一次受限重试连接阶段、客户端 trace、耗时
有短 retry-after 的限速按返回窗口等待并降低并发错误子类、限流 header
额度、余额或 spend limit 耗尽停止或等 owner 调整project、account、quota 类型
供应商证据明确归为瞬时故障的 5xx/过载有上限的抖动退避,再考虑已批准备用错误子类/消息、请求形状、request ID、路由健康度
400/schema/参数不支持修改请求,不原样重发validation error、request shape
401/403修复 credential 或 permissionkey owner、project、endpoint
安全/策略阻断按 policy 停止或人工复核安全类别,不记录敏感原文
流式输出半截或工具结果不确定先对账,不透明重放最后事件、operation ID

还要检查 SDK 是否已经替你重试。Anthropic 当前的错误参考说明,官方 SDK 默认会对连接错误、rate limit 和 5xx 做两次指数退避,并在存在时遵守 retry-after。Gemini 的排障指南也记录了官方 SDK 的自动重试。如果业务层再写“三次重试”,真实调用数往往不是三次。

把所有层的尝试装进一个恢复预算

恢复预算不是 maxRetries=3。它至少包含四个维度:

text
总尝试数 = 初始调用 + SDK 内部尝试 + 网关尝试 + 业务层重试 + 备用路由尝试 + 队列重新投递

同时设置最大总耗时、额外 token/费用和可接受质量变化。在线客服可能只允许一次短重试;夜间批处理可以等得更久,但不能无限消耗预算。

AWS 的超时、重试、退避与抖动解释了多层重试为什么危险:如果调用链每一层都各自重试,负载会成倍放大;上游本来过载时,重试还会拖慢恢复。应在一层统一决策,并使用有上限的指数退避和 jitter。

可以给每个业务工作流建立一张策略卡:

yaml
workflow: customer-support-answer max_total_attempts: 3 max_elapsed_ms: 9000 max_extra_cost_usd: 0.02 safe_replay_until: first_visible_token fallback_routes: - approved-support-backup queue_allowed: false stop_on: [auth, policy, unknown_side_effect, unapproved_route]

数字只是示例。正确数值来自用户可接受延迟、成本上限和故障演练,不来自博客里的固定答案。

超时不等于“上游什么都没做”

决定重放前,先找提交边界。

  • 上游还没有接收请求:通常最容易安全重放。
  • 上游已经接收,但客户端没有拿到结果:结果状态未知。
  • 用户已经看到第一个 token:透明切模型可能重复或矛盾。
  • 工具、数据库、Webhook、邮件或外部动作已经启动:超时不证明动作失败。

涉及写操作时,把“模型生成”与“业务执行”分开。每个外部动作使用应用自己的 operation ID 或 idempotency key,记录 requested、started、committed、acknowledged 四个状态。状态不确定时先查询或对账,不能让第二个模型再执行一次。

流式输出也要有明确产品规则:首个可见事件前可以清空状态,改用已批准路由;首个事件后应显示“响应中断”、让用户显式重新开始,或使用真正的 resume 协议。把另一个模型的新回答拼在半截输出后,不是无缝容灾。

备用模型要签“等价合同”

“都兼容 Chat Completions”远远不够。每个备用路由必须针对具体工作流通过八项验收:

  1. 输入:上下文长度、图片/文件、系统指令、语言和请求上限。
  2. 输出:JSON Schema、拒绝格式、finish reason、引用和截断。
  3. 工具:工具名、参数 schema、并行调用、tool result 回传和幂等。
  4. 安全:内容过滤、高风险任务、人工升级和停止规则。
  5. 数据:保留策略、区域、租户隔离、允许的数据类别。
  6. 运维:延迟分布、流式行为、request ID、状态可见性。
  7. 经济:计价单位、缓存、重试费用、quota owner、最大支出。
  8. 质量:用同一批 fixture 跑评测,并达到该工作流阈值。

一个轻量模型可以胜任标签分类,但未必适合给客户解释退款政策;另一个供应商可能更可用,却不支持当前工具 schema 或数据边界。没有生成式备用通过时,缓存、确定性规则或“稍后重试”往往比未经验证的答案更安全。

把决策写成状态机,而不是散落的 catch

下面的 TypeScript 展示核心边界。provider adapter 负责把错误归一化;策略层只决定动作。

ts
type Action = "retry" | "fallback" | "queue" | "degrade" | "fail_closed"; type Failure = { owner: "transient" | "rate" | "quota" | "request" | "auth" | "policy" | "unknown"; commitState: "none" | "committed" | "unknown"; retryAfterMs?: number; }; function decide( f: Failure, budget: { attemptsLeft: number; elapsedMsLeft: number; costLeft: number }, estimate: { retryMs: number; retryCost: number; fallbackMs: number; fallbackCost: number }, routes: { primary: "healthy" | "degraded" | "open"; fallback: "healthy" | "unhealthy" }, fallbackApproved: boolean, queueAllowed: boolean, degradeApproved: boolean ): Action { if (["request", "auth", "policy"].includes(f.owner)) return "fail_closed"; if (f.commitState !== "none") return "fail_closed"; if (f.owner === "unknown") return queueAllowed ? "queue" : "fail_closed"; const canRetry = budget.attemptsLeft > 0 && budget.elapsedMsLeft >= (f.retryAfterMs ?? 0) + estimate.retryMs && budget.costLeft >= estimate.retryCost; const canFallback = budget.attemptsLeft > 0 && budget.elapsedMsLeft >= estimate.fallbackMs && budget.costLeft >= estimate.fallbackCost; if (canRetry && ["transient", "rate"].includes(f.owner) && routes.primary !== "open") { return "retry"; } if ( canFallback && ["transient", "rate", "quota"].includes(f.owner) && fallbackApproved && routes.fallback === "healthy" ) return "fallback"; if (queueAllowed) return "queue"; return degradeApproved ? "degrade" : "fail_closed"; }

retryAfterMs 只约束主路由重试;备用分支使用自己的预计耗时与费用,且 fallbackApproved 已包含备用路由独立的额度检查。主路由要求长时间等待时,只要健康备用仍能落在 workflow SLO 内,就不会被错误拦截。committedunknown 必须在自动分发器之外先对账,或使用已经验证的 resume/idempotency 协议。每次新网络调用前都先持久化 attempt;只有供应商错误子类、消息与请求阶段共同证明它是瞬时故障时才退避。HTTP 500 也可能来自请求本身,例如 Gemini 当前排障文档把输入上下文过长列为一种原因,因此不能只凭状态码把 owner 设为 transient

用故障注入验收,不用 200 骗自己

在 staging 至少注入这些情况:

  • retry-after 的 429:只等待一次,并发不升高。
  • 主路由 retry-after 超过剩余 SLO,但已批准的健康备用能在自身时间与费用预算内完成:选择 fallback,不是排队。
  • insufficient_quota:不原样重试,不暗中切换另一个预算。
  • 首 token 前的 503:预算内重试,再进入已批准备用。
  • 错误 tool schema:返回可修复错误,不切模型掩盖。
  • 工具已提交后的 timeout:先对账,不重复执行。
  • 已输出 token 后断流:进入明确中断状态,不拼接新回答。
  • 备用结果缺少必填字段:output validator 拒绝。
  • 熔断打开:普通流量绕开,只有有限 half-open 探测恢复。

每次尝试记录 workflow ID、attempt index、requested route、selected route、provider request ID、错误类、等待、token、费用、提交状态、fallback 原因和最终动作。指标也要拆成 primary success、retry recovery、fallback recovery、degraded response 与 fail closed。否则“最终成功率”会把已经坏掉的主路由藏起来。

从具体错误进入正确下一步

如果你现在拿到的是某一家供应商的 429,先使用对应 owner:OpenAI API rate limitClaude API 限流Gemini API rate limit。先证明故障归属,再让通用策略接管。

如果需要用一个兼容入口测试多个已经批准的模型,可以在 staging 对照当前 LaoZhang AI API 文档验证。统一 endpoint 能减少 adapter 工作,但不会自动证明模型等价,更不会替你定义预算、日志和停止规则。

完成标准不是“最后返回 200”,而是每类故障只进入一个可解释动作、所有重试共享一份预算、每个备用都通过同一业务合同,并且最终成功不会抹掉此前的失败证据。

#大模型 API#重试策略#备用模型#API 稳定性
分享文章: