超时、错误、限流与重试
本节目标
把错误分为永久、暂时和未知三类,为自动重试设置上限、抖动、截止时间和副作用保护。
先分类再决定
- 通常不重试:认证失败、无权限、请求格式/schema 错误、超过上下文或明确的内容拒绝。应修改配置、权限或输入。
- 可能重试:连接中断、部分超时、速率限制和服务端暂时故障。优先遵循服务端
Retry-After或 SDK 明确策略。 - 未知:新错误类型、流中异常、结果状态不明。默认保守失败并保留请求 ID,不把未知当成功。
具体状态码与错误类以供应商当前文档为准。不要用错误消息字符串匹配。
| 结果证据 | 默认决策 | 自动化前提 |
|---|---|---|
| 401/403、请求/schema 不合法 | 永久失败 | 修复凭据、权限或请求;不消耗 retry budget |
| 429 速率过快、连接错误、部分 5xx | 候选暂时失败 | 结构化子类型确认为暂时、仍在 attempts/deadline 内、无重复副作用 |
| 429 额度/账单耗尽 | 配置性永久失败 | 额度或账户状态改变后由外部流程重新发起 |
| 已收到 partial 的流断开 | 不确定结果 | 旧 partial 标记失败;只有 Provider 明确恢复协议才续流,否则新建独立 attempt |
| 未知 code/type/terminal | 失败关闭 | 保存 raw type 与 request ID,升级 adapter 后再分类 |
同一个 HTTP 状态也可能包含不同语义。OpenAI 当前错误指南把“请求速率过快”和“额度/账单耗尽”都列为 429,但前者可能等待后恢复,后者需要额度或配置变化;Google 的 RESOURCE_EXHAUSTED 也要结合具体配额信息判断。应优先使用 SDK 异常类型和结构化 error.code/type,再结合状态码,而不是写成“所有 429 都重试”。
超时至少拆成连接、读取/流空闲、单次调用和业务截止时间。per-attempt timeout 防止一次网络调用无限等待;retry deadline 决定剩余时间是否足以再尝试。两者都需要,且要留出解析、校验与后续业务动作的时间。deadline 计算使用 monotonic clock;墙上时间只用于日志。用户取消或上游 deadline 到期应向 SDK/HTTP stream 传播并关闭资源,而不是等默认 timeout 自然耗尽。
指数退避与抖动
简单形式:第 n 次失败后的本地等待可取 min(cap, base × 2^(n-1)) 再加入有界随机抖动。若响应提供有效 Retry-After,把它视为服务端给出的最小等待;若等待会越过 retry deadline,则直接失败。RFC 9110 允许它是 delta-seconds 或 HTTP-date:解析失败就当未提供,不把负数、无限值或任意超大日期直接交给 sleeper;HTTP-date 需用受控当前时间换算并受本地最大等待约束。
抖动避免多个客户端同时重试形成“惊群”。还需限制最大尝试次数和总截止时间;排队已超过用户请求截止时间时,不应继续。测试应注入 clock、sleeper 与随机源,使 deadline、抖动和 Retry-After 在毫秒内可重复验证,而不真实等待。
429 重试本身也可能计入限额。并发控制、队列、令牌桶和逐步升流量通常比无脑重试更重要。读取响应中的限流头时,对缺失和语义变化做好兼容。
幂等性与副作用
纯文本生成可以重复计算,但请求周围的扣费、发信、写库和工具动作未必幂等。为一次业务操作生成稳定 operation_id,保存执行状态;若供应商支持幂等键,按其当前文档使用,但应用层仍要防止重复副作用。
超时后服务端可能已经完成请求,只是响应丢失。重试前要问:“再次执行会不会产生第二次动作?”将模型生成与外部副作用分成两个阶段,副作用由受信任代码按唯一键提交。
多层重试预算
SDK、网关、任务队列和应用都可能重试。指定唯一负责人或计算总上限,并记录实际 attempt。总 transport attempts 是各层“每次最多发送次数”的乘积,不是重试次数简单相加。比如 openai-python 2.46.0 默认初次发送加 2 次 retry,外层再允许 3 attempts,最坏是 3 × 3 = 9 次发送;该 SDK 默认 10 分钟 timeout,timeout 又可重试,所以不显式预算会远超交互请求 deadline。
流式响应一旦交付 partial,自动 retry 不能假装“从断点续写”:新 POST 可能生成不同内容和新 call ID。只有 API family 明确提供可验证的恢复游标时才按其协议恢复;否则关闭旧流、将 partial 标为失败,并以新 attempt 重新生成。熔断和降级应基于连续故障与业务风险,而不是把错误吞掉返回空结果或把某次 Provider 失败伪装成另一模型的同一响应。
练习与自测
为 401、429、500、连接重置、schema 失败和工具写入超时分别写处理策略。自测:一个外层 3 次、SDK 内层 2 次、队列 4 次的组合最多可能发出多少请求?你如何把总数限制在可接受范围?
掌握检查
- 我使用结构化错误类型/代码分类,不通过消息字符串或单一状态码猜测。
- 连接、读取/流空闲、单次调用和业务截止时间有独立边界。
- 重试同时受 attempts、retry deadline、
Retry-After、指数退避和抖动约束。 - SDK、网关、队列和应用只有一个明确重试负责人,最坏请求数已计算。
- 模型生成与发信、扣费、写库等副作用分离,副作用有稳定业务幂等键。
- 新错误类型默认保守失败并保留 request ID,不被当成成功或无限重试。
- 我能解析
Retry-After的两种 wire 形式,拒绝无效/无界等待,并用 monotonic deadline 决策。 - 流交付 partial 后不会用普通 POST retry 静默拼接;恢复只依赖明确的 Provider 协议。
下一步
进入 用量、可观测性与供应商适配。
参考资料
- RFC 9110:HTTP Semantics—Idempotent methods and Retry-After(访问于 2026-07-21)
- OpenAI:API errors(访问于 2026-07-21)
- OpenAI:Rate limits(访问于 2026-07-21)
- OpenAI:official Python SDK—Retries and timeouts(访问于 2026-07-21)
- Anthropic:API errors(访问于 2026-07-21)
- Anthropic:Rate limits(访问于 2026-07-21)
- Google:Troubleshooting guide(访问于 2026-07-21)
- Google:Gemini API rate limits(访问于 2026-07-21)