LLM API 备用路由:生产故障切换实战手册
LLM API 的备用路由听起来很简单,直到第一次真正的事故发生:捕获错误、切换模型、然后重试。在生产环境中,这条规则可能会把一个提供商问题变成重复的工具调用、损坏的 JSON、混杂的流式输出、失控的重试流量,或者一个在技术上成功但已经不再满足产品契约的响应。
更安全的设计会把备用机制视为一个有边界的状态机,而不是一串备用模型名称。每个请求都会经过一小组决策:
- 这个失败是可重试的吗?
- 重复这个请求安全吗?
- 下一次尝试应该使用相同的目标还是不同的目标?
- 备用方案能否保留所需的契约?
- 这个请求是否已经产生了输出或副作用?
- 端到端延迟和尝试预算是否已经耗尽?
本手册将这些问题转化为错误矩阵、路由策略、TypeScript 控制器、测试计划,以及面向多提供商 LLM 应用的上线检查清单。
可靠的 LLM API 备用路由背后的四种动作
不要把所有错误都送进同一个重试循环。生产级路由器需要四种不同的动作。
| 动作 | 适用场景 | 典型示例 |
|---|---|---|
| 重试相同目标 | 失败看起来是暂时性的,并且当前部署可能会在请求截止时间内恢复 | 在响应头之前连接被重置、孤立超时、短暂限流等待 |
| 故障切换到等价目标 | 提供商、区域、部署或账户状态不健康,但其他地方仍提供相同的模型契约 | 区域性故障、部署配额耗尽、重复的 5xx 响应 |
| 降级到另一模型 | 经过评估的备用模型可以保留应用的最低能力和输出契约 | 主模型不可用,而经过测试的次级模型支持相同的工具和 schema |
| 停止并上报错误 | 重复请求不会解决问题、可能产生副作用,或无法保留契约 | 身份验证无效、请求格式错误、不支持的参数、策略拦截、部分流 |
故障切换和备用降级之间的区别很重要。故障切换会保持逻辑模型契约不变,只改变基础设施。备用降级则会更换模型或能力层级。故障切换通常是风险更低的选择。
如果你需要围绕别名、健康评分、计费和可观测性的更完整请求路径设计,请先阅读 AI API 网关架构指南。本文聚焦于在目标已选定之后运行的控制器。
在编写重试代码之前,先构建错误到动作矩阵
提供商 SDK 会暴露不同的异常类和响应体,但路由器应当把它们归一化为一个小型内部分类体系。
| 归一化故障 | 重试同一目标? | 等效故障切换? | 跨模型备用? | 说明 |
|---|---|---|---|---|
| 请求被接受前连接失败 | 是,一次 | 是 | 也许 | 保持在同一个端到端截止时间内 |
| 响应头到达前超时 | 也许 | 是 | 也许 | 只重复那些可以安全重放的请求 |
429 限流 |
在有上限的延迟后 | 是 | 也许 | 在可用时遵守服务端指引;不要制造重试风暴 |
提供方 5xx 或过载 |
最多一次 | 是 | 也许 | 在达到预定义故障阈值后打开熔断器 |
| 认证或权限错误 | 否 | 否 | 否 | 修复凭据或策略;切换模型无济于事 |
| 请求格式错误或参数不受支持 | 否 | 否 | 否 | 修正客户端契约 |
| 上下文长度超限 | 不要盲目重试 | 否 | 仅在明确做了适配时 | 截断、摘要,或更大上下文的路由都会改变请求 |
| 安全或策略拒绝 | 不要盲目重试 | 否 | 通常不行 | 通过切换提供方来规避策略决定不是可靠性策略 |
| 输出 schema 校验失败 | 也许,可修复 | 否 | 仅在经过评估后 | 将 schema 修复与传输重试分开处理 |
| 流在第一个 token 之前失败 | 也许 | 是 | 也许 | 此时还没有任何用户可见输出 |
| 流在输出开始后失败 | 不自动切换 | 否 | 不自动切换 | 不要把两个模型的响应拼接在一起 |
| 工具调用可能已经执行 | 不要盲目重试 | 否 | 不要盲目重试 | 需要幂等键或工具级去重 |
官方提供方文档进一步说明了为什么需要归一化。Anthropic 记录了不同的限流、API 和过载错误,并指出流式请求即使在初始响应成功后也可能失败。OpenAI 也将无效请求、限流和服务端失败明确区分开来。你的应用应将提供方特定信号转换为稳定的内部决策,而不是在业务逻辑中到处嵌入提供方名称。
为整个请求设置一个重试预算
重试通常会同时存在于多个层级:HTTP 客户端、提供商 SDK、网关、后台任务以及应用服务。如果每一层都执行三次尝试,那么一次用户操作就可能放大成远超团队预期的上游调用次数。
更安全的模式是:
- 选择一层来负责 LLM 重试和备用路由。
- 为用户请求或任务设置一个端到端的截止时间。
- 设置上游最大尝试次数。
- 为备用目标预留一部分截止时间。
- 对瞬时故障使用带抖动的指数退避。
- 当剩余时间不足以支持下一次有意义的尝试时就停止。
AWS 关于超时、重试、退避和抖动的指导说明了重试如何放大过载,并建议采用有边界的行为,而不是持续立即重复。同样的原则也适用于模型 API,因为在负载之下,提供商最难承受同步发生的重试流量。
一个实用的交互式预算可以用策略来表达,而不是把睡眠时间硬编码进去:
type RetryBudget = {
deadlineMs: number;
maxAttempts: number;
maxSameTargetAttempts: number;
reserveForFallbackMs: number;
};
具体数值取决于产品。聊天界面、代码代理、批处理评测和异步视频工作流不应共享同一套预算。
使用熔断器阻止路由到已知故障
熔断器可以防止每个新请求都重新踩中同一个故障。
标准状态如下:
- 关闭:请求正常流转,同时路由器测量失败和延迟。
- 打开:由于近期行为超过阈值,目标暂时不可用。
- 半开:少量探测请求用于测试目标是否已恢复。
Azure 的熔断器模式描述了这种关闭/打开/半开生命周期。对于 LLM 路由,熔断器键应足够具体,以隔离发生故障的表面。可用的维度包括提供商、模型、区域、部署、账户和能力。文本补全部署可能是健康的,而工具调用路由或区域端点却可能正在故障。
不要因为每一种客户端错误就打开熔断器。身份验证无效、请求格式错误、上下文溢出以及策略拒绝,通常更多反映的是请求本身而不是提供商健康状况。熔断器应主要响应连接失败、超时、过载和服务器错误等瞬态基础设施信号。
在不同模型之间保持能力契约
备用模型并不只是因为它接受 OpenAI 兼容请求就算安全。请为每个路由别名定义最低契约。
route: support-agent-v3
requires:
modalities: [text]
streaming: true
tools: true
parallel_tool_calls: false
structured_output: json_schema
context_window_min: 64000
max_output_tokens_min: 4000
quality_gates:
task_success_rate_min: 0.94
schema_valid_rate_min: 0.995
policy:
same_model_failover_first: true
cross_model_fallback_allowed: true
在将目标加入备用集之前,至少测试:
- 支持的请求参数
- 工具定义和工具调用行为
- 结构化输出有效性
- 流式事件形状
- 上下文和输出限制
- 适合该应用的安全行为
- 成本控制使用的令牌统计字段
- 代表性提示词上的延迟和质量
这种以契约为先的方法对于跨模态的工作流尤为重要。多模态代理路由指南涵盖了文本、图像、音频和视频路由的额外检查。
TypeScript 备用控制器
下面的示例刻意保持与提供商无关。它假设上游适配器会在路由层看到错误和响应之前对它们进行规范化。
type FailureKind =
| "connect"
| "timeout"
| "rate_limit"
| "overloaded"
| "server_error"
| "invalid_request"
| "auth"
| "policy"
| "context_overflow"
| "partial_stream"
| "unknown";
type Target = {
id: string;
contractId: string;
healthy: boolean;
circuit: "closed" | "open" | "half_open";
};
type RequestState = {
attempt: number;
sameTargetAttempts: number;
deadlineAt: number;
outputStarted: boolean;
sideEffectsPossible: boolean;
};
function canReplay(state: RequestState): boolean {
return !state.outputStarted && !state.sideEffectsPossible;
}
function isTransient(kind: FailureKind): boolean {
return [
"connect",
"timeout",
"rate_limit",
"overloaded",
"server_error",
].includes(kind);
}
function chooseNextAction(
kind: FailureKind,
state: RequestState,
current: Target,
equivalent: Target | undefined,
fallback: Target | undefined,
) {
if (!canReplay(state) || kind === "partial_stream") return { type: "stop" };
if (!isTransient(kind)) return { type: "stop" };
if (state.attempt >= 3 || Date.now() >= state.deadlineAt) {
return { type: "stop" };
}
if (
state.sameTargetAttempts < 1 &&
current.circuit === "closed" &&
current.healthy
) {
return { type: "retry", target: current };
}
if (equivalent?.healthy && equivalent.circuit !== "open") {
return { type: "failover", target: equivalent };
}
if (
fallback?.healthy &&
fallback.circuit !== "open" &&
fallback.contractId === current.contractId
) {
return { type: "fallback", target: fallback };
}
return { type: "stop" };
}
生产代码还需要带抖动的延迟、取消传播、请求 ID、熔断器更新、遥测以及适配器特定的错误解析。关键属性是:在选择另一个目标之前,会先检查重放安全性和契约兼容性。
将流式备用视为一种独立协议
流式传输会创建一条硬边界:一旦内容到达客户端,网关就不能再假装这次尝试从未发生过。
如果上游在第一个事件转发之前就失败,重试或备用路由仍可能是透明的。一旦第一个 token、工具增量、图像事件或音频片段已经送达,自动模型切换就有可能把两段不兼容的响应混在一起。
请使用以下明确策略之一:
- 清晰地让流失败。 返回一个稳定的错误事件,带上请求 ID,让客户端提供重试选项。
- 先缓冲再释放。 对于短的结构化响应,在向下游发送之前先验证完整结果。这会牺牲首 token 时间。
- 实现应用层恢复。 用明确上下文开启一个新轮次,说明上一段响应已被中断。把它视为一次新的模型生成,而不是同一字节流的延续。
不要静默地把两个模型的输出拼接起来。
将工具调用可靠性与模型调用可靠性分开
一次 LLM 请求可能可以重放,而它所选择的工具却未必可以。支付、邮件、部署、数据库写入或工单创建,可能会在模型连接失败、但应用尚未来得及记录结果之前就已经成功执行。
请用以下方式保护写入型工具:
- 基于用户操作而不是提供方尝试生成的幂等键
- 持久化的工具执行记录
- 在工具边界进行去重
- 明确区分
planned、started、succeeded和unknown - 对不确定但影响重大的副作用进行人工审核
如果可能产生副作用且其结果未知,就停止自动备用路由。先协调工具状态。
将备用路由作为产品结果进行观测
较低的提供方错误率并不能证明备用路由有效。要跟踪完整的路由结果。
| 指标 | 它揭示了什么 |
|---|---|
| 主目标成功率 | 基线提供方或部署健康状况 |
| 重试恢复率 | 同目标重试是否有用 |
| 等价故障切换恢复率 | 冗余部署或区域的价值 |
| 跨模型备用恢复率 | 替代模型集的价值 |
| 契约拒绝率 | 候选目标有多频繁未通过资格检查 |
| 备用后 schema 有效性 | “成功”响应是否仍然可用 |
| 备用后任务成功率 | 用户是否仍能完成预期工作 |
| 新增备用延迟 | 用户为可靠性付出的成本 |
| 备用成本差值 | 恢复路径对计费的影响 |
| 断路器开启时长与探测成功率 | 断路阈值与恢复时机是否合理 |
为每次尝试记录路由原因:所选目标、标准化错误、重试延迟、断路器状态、备用原因、请求截止时间剩余量,以及最终结果。除非产品的数据政策明确允许,否则避免记录敏感提示或输出。
在启用自动备用路由之前测试故障路径
先在预发布环境中进行故障注入,然后在生产环境中以金丝雀方式推出该策略。
传输和提供方测试
- 在响应头之前断开连接。
- 返回重复的速率限制错误,并且分别带有和不带有重试指引。
- 模拟过载和服务器错误。
- 将主路由延迟到请求截止时间几乎耗尽。
- 打开目标熔断器,并验证流量移动到一个可用路由。
- 恢复目标,并验证半开探测不会过早恢复全部流量。
契约测试
- 从备用适配器中移除一个必需工具。
- 返回无效的结构化输出。
- 更改流式事件的形状。
- 超出上下文或输出限制。
- 在固定评估集上比较备用方案质量。
回放安全测试
- 在第一个流式事件之前和之后失败。
- 在写入侧工具开始后失败。
- 重复相同的幂等键。
- 在备用尝试挂起时取消客户端请求。
只有当路由器选择了预期操作并记录原因时,测试才算通过。
Flatkey 的定位
Flatkey 为受支持的模型提供一个 API 密钥和一个与 OpenAI 兼容的基础 URL,并集中管理用量和计费。这为多模型访问和路由创建了稳定的集成边界。
应用团队仍应负责本实战手册中描述的路由契约:哪些错误可以重试,哪些目标等价,是否允许跨模型备用,如何对工具去重,以及恢复后的响应必须达到什么质量阈值。
如果想要最短的集成路径,请使用 Flatkey 集成入门。如果你正在迁移现有客户端,OpenAI 兼容 API 网关清单涵盖了基础 URL、参数、流式传输以及错误形状验证。
生产上线检查清单
- 将提供商错误规范化为稳定的内部分类体系。
- 定义重试、等价故障切换、跨模型备用和停止动作。
- 指定一个组件来负责重试预算。
- 强制使用一个端到端截止时间和最大尝试次数。
- 为瞬时故障添加带抖动的指数退避。
- 按最小有用故障域来划分熔断器键。
- 为每个路由别名定义版本化能力契约。
- 在部分输出开始后阻止自动切换。
- 为写入侧工具添加幂等性和对账。
- 记录路由原因和最终任务结果。
- 注入传输、过载、契约、流式和副作用故障。
- 在启用跨模型备用之前,先对等价故障切换进行金丝雀发布。
- 为每个目标和备用策略添加开关。
FAQ
什么是 LLM API 的备用路由?
LLM API 的备用路由是一种可靠性策略:当首选路由无法完成请求时,它会选择另一个可用的模型或提供商。在切换之前,安全备用会检查回放安全性、能力兼容性、熔断器健康状况、延迟预算和输出状态。
LLM 重试和备用之间有什么区别?
重试是在相同目标上重复请求。故障切换则迁移到等效基础设施,同时保留逻辑模型契约。跨模型备用会改变模型,因此需要更强的兼容性和质量测试。
LLM API 是否应该对每个 429 或 5xx 错误都重试?
不应该。重试应受端到端截止时间、尝试次数上限、退避策略、熔断状态以及重放安全检查的约束。相较于反复调用一个不健康的目标,等效故障切换可能更好。
LLM 路由器可以在流式输出过程中切换模型吗?
在输出已经到达客户端之后,不能无缝切换。安全的默认做法是明确中止该流,或者开启一个新的应用级轮次。将来自不同模型的部分输出拼接起来可能会破坏响应契约。
什么时候应该禁用跨模型备用?
当备用模型无法保留所需工具、结构化输出、上下文限制、安全行为、质量阈值或副作用保证时,应禁用它。如果在部分输出后或工具执行结果不确定时,也应禁用自动重放。
一个 LLM 请求应该进行多少次备用尝试?
没有统一的数字。应使用最小且有上限的尝试次数,并与产品的延迟预算和测试证据相匹配。当剩余截止时间不足以支持下一次有效尝试时,路由器应停止。
可靠的备用并不意味着“把所有办法都试一遍”。它意味着让下一步动作明确、兼容、可安全重放、可观测,并且易于停止。



