模型调用与容错:让失败变成看不见的内部事件
一句话定位:调用层是 Agent 主循环与模型 API 之间唯一的通道,Harness 的「稳定」从这里开始——网络会断、限流会打回、流会卡死,容错体系把这些必然发生的失败消化成用户看不见的内部事件。前置:什么是 Harness。
一切稳定都从调用开始
你写了个小工具,每天调一次模型 API 总结新闻,几个月都没出过错。
换成真正的 Agent:一轮任务要调几十次 API、每次几千 token。这时 429(限流)、529(服务过载)、超时断流开始轮番出现——不是模型变笨了,是调用量放大了上千倍,而每一次失败都会让主循环当场中断。
生产级的 Harness 怎么让这层「看起来永远稳定」?
答案是一套容错体系:识别错误、决定等多久、服务过载到顶就换模型、流卡死就掐断重连。这一层在 Harness 里通常被封装成一个函数——Claude Code 里叫 queryLoop,本篇统一叫它调用层。
图:调用层全景——①②③④ 四个机构排成一条容错流水线,编号全文沿用
渐进拆解
类比:调用层的日常,像给客服热线打电话。两个系统的失败模式几乎一一对应:
- 响应 429 = 「坐席全忙」——你打得太频繁,被限流挡在门外(HTTP 标准状态码;这个限是你自己的配额,换条线也绕不开)
- 响应 529 = 「系统故障」——整个服务中心过载,谁打都打不通(这不是 HTTP 标准码,是 Anthropic 专有的 overloaded_error)
- 固定间隔重试 = 挂断立刻重拨——而且所有被拒的人都这样重拨,线路永远被挤占
- 指数退避 = 第一次挂断等 1 分钟,再占线等 2 分钟、4 分钟……越挫越退
- retryAfter = 语音提示「预计等待 5 分钟」——服务端直接告诉你等多久,比瞎猜准
- 降级 fallback = 热线打不通,改用 App 自助服务——换个能力弱些但可用的渠道,把事办了
- 看门狗 = 电话接通后对方长时间不出声——与其干等,掐断重打
例子:一次典型的限流风暴长这样——注意等待时间怎么翻倍:
图:重试时序——退避翻倍为摊薄重试频率;把各家时刻彼此错开,靠的是再加随机抖动(jitter)
为什么必须是指数退避,固定间隔不行? 假设服务过载,1000 个客户端同时被拒。固定间隔重试 = 1 秒后 1000 个请求又同时砸回来,服务刚喘口气又被压垮,永远满载。
指数退避让每轮重试的等待时间翻倍,把重试频率逐轮摊薄、给服务喘息——但注意,同时被拒的客户端若都用同样的退避表,重试时刻仍会整点集体砸回,只是间隔变大了;真正把各家的时刻彼此错开的是在退避之上再加抖动(jitter,随机化):每家的等待时长各乘一个随机系数,集体重砸散成细雨。AWS 的架构指南把这对分工讲得最直白:退避解决「频率」,抖动解决「扎堆」,两者缺一不可。这是保护服务,也是保护你自己下一次请求能通。
大白话:调用失败先问三个问题——能不能重试(限流、超时能;参数错、密钥错不能);能的话等多久(服务端说了算,没说就指数翻倍);一直不行怎么办(若是服务过载,换个模型兜底,别在一棵树上吊死;若是自己的限流,429 换条线绕不开,等或降速)。
技术定义:调用层是 Harness 中负责与模型 API 交换数据的模块,职责即全景图的 ①–④。一个容易忽略的细节:流式响应里,思考、文本、工具调用各是不同的 chunk 片段,调用层要分别路由。
另一个细节是 TTFT(time-to-first-token,从发出请求到吐出第一个字)——交互体验的硬指标,看门狗守的就是这条流不卡、不慢。
同一套「失败—纠错—重试」思想还延伸到两处:输出结构化校验失败时,把错误信息反喂给模型重试,几次不过就降级;测试场景用 VCR 机制录制一次真实响应、之后回放,让测试不依赖线上服务也稳定可复现。
动手:失败分流与 queryLoop
调用层的心脏是一张分流规则——每个错误进来,先定性质,再定处理:
图:错误分流——先问能不能重试,再问等多久,最后才是换人
伪代码实现这张分流图(编号呼应全景图):
def query_loop(messages, model="main"):
delay = 1 # ② 退避:起始 1 秒
while True:
try:
return stream_call(messages, model) # ④ 看门狗:流卡死即抛 Timeout
except RateLimited as e: # ① 429:自身配额,只等不换
wait = e.retry_after or delay # retryAfter 优先,否则退避
sleep(wait); delay *= 2 # 指数翻倍:1s → 2s → 4s
except Overloaded: # ① 529:服务端对该模型过载
sleep(delay); delay *= 2
if delay > MAX_DELAY: # ③ 529 重试到顶:换 fallback 模型
model = FALLBACK[model] # (Claude Code 官方行为;429 从不触发换模型)
delay = 1
except RetryableError:
if delay > MAX_DELAY: # ③ 其他可重试错误到顶:交还给人
raise Unrecoverable(result) # (不硬换模型——错误语义不合就别换)
sleep(delay); delay *= 2
错误速查(可重试性是唯一关键判断):
| 错误 | 含义 | 可重试 | 处理 |
|---|---|---|---|
| 429 | 限流(自身配额用太快) | ✓ | retryAfter 优先,指数退避——只等不换(Claude Code 口径:429 从不触发换模型) |
| 529 | 服务过载(Anthropic 专有码) | ✓ | 指数退避;重试到顶换 fallback 模型(官方 fallback 触发条件) |
| 超时 / 断流 | 网络或服务卡死 | ✓ | 看门狗掐断,按超时重试 |
| 400 / 401 类 | 参数或鉴权错误 | ✗ | 直接抛给人修 |
同一概念的叫法对照:
| 本篇用词 | 文献中的其他叫法 | 深挖去处 |
|---|---|---|
| 调用层 | queryLoop · LLM Gateway · Provider 抽象层 | 可观测性 |
| 退避重试 | 指数退避 + 抖动(jitter)· retryAfter 优先 | 本篇 |
| 降级 | fallback 模型 · 模型路由与降级 | 生产化 |
| 流量回放 | VCR 录制回放 · Replay | 可观测性 |
边界与取舍:容错不是越多越好
退避重试只对「可重试错误」有意义
参数错、密钥过期这类 4xx 错误,重试一百次结果一样——只会白烧时间和 token。分流的第一步永远是定性。
重试有延迟代价
指数退避救的是后台批任务;交互式场景(用户盯着屏幕等第一个字)等不了 4s+8s+16s——重试次数和上限要按场景收紧,必要时宁可快速报错。
降级是拿质量换可用,要透明
fallback 模型通常更弱,答案质量会下降——调用层该做的是降级后明确标注(「当前由备用模型回答」),让用户知情,而不是悄悄换掉装作没事。
多模型路由已经标准化
Provider 抽象层把「调 Claude 还是调 OpenAI 兼容模型」收敛成统一接口,LLM Gateway(LiteLLM 这类网关)则把路由、fallback、成本追踪做成开箱即用的配置——自建调用层前先看看网关是不是已经够用。
路由换模型有两种触发,别混为一谈。被动降级是 ③ 的延续:服务过载(529)重试到顶才换,拿质量换可用,换是不得已;主动路由是任务分发时就按性质选模型——重推理给旗舰档、扫描粗筛给轻量档,图的是成本与延迟。后者已走进产品默认行为:2026 年 9 月的 Codex 允许给每个 subagent 单独指定模型与推理档位(默认继承主会话的配置),官方文档并给出分工建议——重推理 gpt-5.6、快速扫描 gpt-5.6-terra、高频小任务 gpt-5.6-luna——「哪个任务派哪个模型」从网关配置升级成了 Agent 编排时的一等参数,派活侧的取舍见多 Agent 编排。
自测
合上文章先自己答,再展开对照:
为什么必须指数退避而不是固定间隔重试?
固定间隔让所有被拒客户端在同一时刻集体重试,形成重试风暴——服务刚喘口气又被压垮,谁也进不来。指数退避让每轮等待翻倍,把重试频率逐轮摊薄;但退避表相同的客户端仍会在同一时刻集体重试,把时刻真正彼此错开的是再加抖动(jitter,随机化系数)。若服务端返回 retryAfter,按它给的时长等更准。三件套各管一事:退避管频率、抖动管扎堆、retryAfter 管准头。
Claude Code 遇到 429 时,决定「等」还是「换模型」的依据是什么?
依据是错误语义,不是等待时长。429 是你自己的配额限流——换通道绕不开(fallback 模型同一个账号配额),所以正解只有「等」:retryAfter 优先、指数退避,Claude Code 的官方行为里 429 从不触发换模型。「换」留给语义不同的场景:529(服务端对该模型过载)或模型不可用重试到顶,才按官方 fallback 机制切换模型兜底。一个易混点:「重试到顶就换模型」是 LiteLLM 这类网关可以配置的通用策略,不是 Claude Code 对 429 的行为——判断「等还是换」先看这个错误是谁的问题:自己的配额(429)等,服务端的过载(529)等到顶再换。
参考与延伸
- Anthropic《Building Effective Agents》(2024-12):Workflow 与 Agent 的形态分界出自此文 — anthropic.com/engineering/building-effective-agents
- MDN:HTTP 429 状态码与 Retry-After 响应头的标准语义 — developer.mozilla.org
- DeepSeek Harness 开源仓库(2026-08):「everything-is-a-plugin」的公开实现 — github.com/deepseek-ai/deepseek-harness
- LiteLLM 文档:模型路由、fallback、成本追踪的标准化实现 — docs.litellm.ai
- ChatGPT Learn 官方文档《Subagents》(2026-09-15 查证):Codex 为 subagent 单独配置模型与推理档位、默认继承主会话配置的官方口径 — learn.chatgpt.com/docs/agent-configuration/subagents
本系列的后续学习路线(推荐顺序):上下文压缩 → 状态机与 Checkpoint → 工具执行 → 权限机制 → 记忆机制 · Skill 机制 → 多 Agent 编排。