模型路由的降级链设计:别让一个超时拖垮整个产品
多模型路由不只是为了省钱,更是为了可用性。给出降级链的四级设计、超时预算分配、熔断参数取值与一个可直接抄用的路由配置模板。
一次真实的故障
某天下午,主模型供应商的一个区域节点限流,P99 延迟从 1.2 秒飙到 40 秒。你的产品没有做降级——因为”我们不希望质量下降”。结果:用户看到的是转圈,客服收到 200 条投诉,运营群里所有人问”是不是崩了”。
质量下降 10% 远好过一个功能完全不可用。 这是降级链的第一原则。
降级链的四级结构
不要设计成”A 模型 → B 模型”这么简单。完整的链应该分四层,每层解决不同问题:
| 层级 | 触发条件 | 动作 | 用户感知 |
|---|---|---|---|
| L0 主模型 | 正常 | 最强模型 + 完整上下文 | 最优 |
| L1 同级备选 | 主模型错误率 >5% 或延迟超标 | 换另一供应商同级模型,提示词适配层介入 | 几乎无感 |
| L2 降档模型 | L1 也不可用/超时 | 换小模型 + 更短上下文 | 略有质量下降 |
| L3 模板兜底 | 全部不可用或超总预算 | 返回预置模板 / 转人工 / 推迟处理 | 明确告知 |
第 4 层是最容易被忽略、也最重要的。必须有 L3,否则降级链只是把失败推迟了。
超时预算怎么分
假设产品级 SLA 是 3 秒返回首字:
- L0:1200ms(超过就放弃)
- L1:900ms
- L2:700ms
- 剩余 200ms 留给网络与渲染
注意两个细节:
- 超时要”预算感知”:已经花了 2.5 秒,就不要再试 L1(900ms 预算已不够),直接进 L2 或 L3。实现上用一个全局 deadline 传递,而不是每层各自设 timeout。
- 流式请求的判定不同:首 token 超时和总时长超时是两回事。首 token 超时应短(800ms),一旦开始输出,中断的代价很高——宁可让它说完。
熔断参数怎么取
别用默认值。推荐起点:
- 滑动窗口:10 秒
- 错误率阈值:50%(不是 5%——5% 会被正常抖动频繁触发)
- 最小请求数:20(样本太少不触发)
- 半开探测:熔断后每 30 秒放 3 个请求探活
- 恢复条件:3 个探测请求全部成功
对延迟敏感的场景,加一条独立规则:窗口内 P95 延迟 > 3× 基线 也触发熔断。供应商的”慢”比”错”更常见也更伤人。
L1 的隐藏成本:提示词适配层
换供应商最容易翻车的不是接口,是输出差异。必须在路由层和模型之间加一个薄适配层,处理:
- 格式差异:JSON 输出是否被 markdown 代码块包裹、字段名大小写
- 风格差异:另一个模型可能更啰嗦或更简洁,需按模型注入不同的风格约束片段
- 能力差异:备选模型可能不支持并行工具调用 → 适配层要把并行调用降级为串行
这意味着提示词不能硬编码在某一个模型上。正确做法是提示词分”通用核心 + 模型特定微调片段”,路由时组合。
一个路由配置模板
routes:
- name: primary
model: gpt-5
provider: openai
timeout_ms: 1200
prompt_profile: gpt
- name: secondary
model: claude-sonnet-5
provider: anthropic
timeout_ms: 900
prompt_profile: claude
- name: degraded
model: llama-4-70b
provider: self-hosted
timeout_ms: 700
prompt_profile: compact
- name: fallback
action: template
template: service_busy
breaker:
window_s: 10
error_rate: 0.5
min_requests: 20
latency_multiplier: 3
两件必须做的运维事
- 降级要打点:每次进入 L2/L3 都要记录事件和原因。每周看一次”降级率”,如果 L2 长期占 15%,说明主模型方案本身不匹配,该换的不是路由是主方案。
- 定期演练:每季度人为把主模型错误率调到 100%,看降级链是否按预期工作。不演练的降级链,大概率在真出事时才发现某个环节的提示词早就不兼容了。
别陷入的误区
- 不要为了省钱做降级:省钱应该用”前置路由”(先判断任务难度再选模型),这和”故障降级”是两套逻辑,混在一起会让代码难以维护。
- 不要在 L2 静默降质:面向用户的产品,质量降档应该有感知(如”当前为简化回答”),或者控制在用户难以察觉的范围内。
- 不要没有 L3:没有兜底的降级链,本质上是”更复杂的失败方式”。