多模型路由不是随机切换:质量、成本和延迟怎么一起决策
用三个任务和一份可运行的离线路由实验,拆解能力硬约束、质量门槛、成本、TTFT、健康度、缓存亲和与故障回退。

“接入三个模型,然后随机挑一个”叫负载分发,不叫多模型决策。
真正的模型路由至少要回答四个问题:
- 这个请求允许送到哪些模型?
- 哪些候选模型达到任务的最低质量要求?
- 在合格候选中,成本、首 Token 延迟和健康度怎么取舍?
- 首选模型失败后,能否安全回退到另一个模型?
最重要的顺序是:
先用硬约束淘汰不合格模型,再在剩余候选里优化质量、成本和延迟。
如果把所有指标直接塞进一个加权公式,一个不支持图片、上下文不够或不符合数据驻留要求的便宜模型,仍可能因为价格得分很高而胜出。这不是“性价比路由”,而是把不可违反的约束误当成偏好。
本文提供一份可运行的离线决策实验、三个任务的路由规则和一个错误反例。文中的模型名称与指标都是用于验证算法的合成 Fixture,不是线上模型 Benchmark,也没有伪造真实供应商延迟或质量数据。
先区分三类“路由”
很多系统把不同层级都叫 Model Routing,容易导致策略混乱。
| 层级 | 选择对象 | 典型依据 | 示例 |
|---|---|---|---|
| 部署负载均衡 | 同一模型的多个端点 | 权重、并发、RPM/TPM、延迟 | 两个区域的同一模型部署 |
| 模型选择 | 不同模型或模型层级 | 任务能力、质量、成本、TTFT | small / balanced / deep |
| 故障回退 | 主路径失败后的替代路径 | 错误类型、幂等性、兼容性 | 首 Token 前从主模型切到备选 |
LiteLLM 官方文档明确把同一 model_name 下的部署负载均衡与跨 model_name 的 fallback 分开;其部署策略包括 simple shuffle、least busy、usage、latency 和 cost based routing。Amazon Bedrock 的 Intelligent Prompt Routing 则是在同一模型家族内,根据 Prompt 预测响应质量并选择模型。
这两类能力都合理,但不能混为一个万能“智能路由器”。
正确的决策流水线
推荐把路由拆成五层:
请求分类
→ 硬约束过滤
→ 质量门槛
→ 多目标评分或规则排序
→ 同模型部署选择
→ 记录决策与回退链

第一层:硬约束
硬约束不应该参与加权,而应直接过滤:
- 是否支持 tools、vision、audio、JSON Schema;
- 上下文窗口是否足够;
- 指定区域与数据驻留;
- 租户是否有权访问;
- 模型是否允许处理该数据等级;
- 当前渠道是否启用且未触发用量上限;
- 请求协议能否保真转换;
- 是否要求固定模型版本或供应商。
形式上可以写成:
eligible(model, request) =
capability_match
AND context_fits
AND tenant_allowed
AND region_allowed
AND channel_healthy
任何一项为 false,就不进入下一轮。
第二层:质量门槛
质量不只是一个全局分数。摘要、代码迁移、票据 OCR 和工具调用应使用不同评测集。
建议给每类任务维护独立门槛:
support_triage.accuracy >= 0.85
invoice_extraction.schema_pass >= 0.98
code_migration.tests_pass >= 0.90
这些值必须来自自己的离线评测、灰度流量或人工验收,不能照抄模型厂商的综合榜单。没有评测数据时,先使用显式静态规则,而不是假装有准确的自动质量分。
第三层:多目标评分
只在通过约束与质量门槛的候选中评分。例如把所有值归一化到 0..1:
score =
wq × quality
+ wc × (1 - normalized_cost)
+ wl × (1 - normalized_ttft)
+ wh × health
+ wa × cache_affinity
在真实系统里,建议把“延迟”和“容量”继续拆开:
- TTFT 决定聊天、语音等交互场景的首字等待;
- 总完成时间包括完整生成、工具循环、验证与重试,更适合长报告和代码迁移;
- 限流余量应结合账户实际 RPM、TPM、并发和响应头,而不是硬编码公开示例;
- 429 可能表示瞬时容量不足,不等于模型质量下降或永久故障。
本文 Fixture 为保持可读性,只把 TTFT 和健康度放入公式;生产策略应按任务增加总完成时间和限流余量,并对输入快照做版本化。
权重必须按任务变化:
| 任务 | 质量 | 成本 | TTFT | 健康度 |
|---|---|---|---|---|
| 客服工单分类 | 0.25 | 0.35 | 0.30 | 0.10 |
| EU 票据图片抽取 | 0.45 | 0.15 | 0.20 | 0.20 |
| 大型代码迁移 | 0.60 | 0.10 | 0.10 | 0.20 |
注意:表中的权重是本文离线 Fixture 的策略输入,不是行业标准。
第四层:部署选择
先选模型层级,再在同一模型的可用部署中选渠道。部署选择可以使用:
- 优先级;
- 权重;
- least busy;
- RPM/TPM 剩余量;
- 实时或滑动窗口延迟;
- 地区;
- Prompt Cache 亲和;
- 渠道健康与冷却状态。
这一步不应重新把一个“不兼容的模型”塞回候选池。
三个任务怎么路由
离线实验定义了三个匿名模型层级:
| 模型层级 | Tools | Vision | Context | 区域 | 定位 |
|---|---|---|---|---|---|
fast-small | 是 | 否 | 32K | global | 低成本、低延迟 |
balanced | 是 | 是 | 128K | EU | 能力和成本折中 |
deep-reasoning | 是 | 是 | 256K | global | 高质量、高成本、高延迟 |
质量、成本、TTFT 和健康指标都是合成数据,只用来验证相同输入一定得到相同路由结果。

任务一:客服工单分类
需求:
- 8K 上下文;
- 需要调用工单工具;
- 不需要图片;
- 用户等待在线结果;
- 单次价值较低,量大。
规则:
必须支持 tools
质量达到分类门槛
成本和 TTFT 权重高于最高质量
离线结果:
| 候选 | 分数 |
|---|---|
fast-small | 0.8410 |
balanced | 0.6845 |
deep-reasoning | 0.4265 |
选择 fast-small。这不是因为“小模型永远适合客服”,而是在已经通过此任务质量门槛之后,它的成本和延迟优势才有意义。
任务二:EU 票据图片抽取
需求:
- Vision;
- 20K 上下文;
- 数据必须留在 EU;
- 输出进入财务流程。
硬约束先过滤:
fast-small:缺少 Vision,且不满足 EU;deep-reasoning:不满足 EU;balanced:唯一合格。
选择 balanced,分数为 0.7835。即使其他模型在某个综合榜单更强或更便宜,也不应参与排序。
任务三:大型代码迁移
需求:
- 100K 上下文;
- 需要工具;
- 质量权重最高;
- 允许较高延迟,但仍控制成本。
fast-small 因 32K 上下文被淘汰。剩余结果:
| 候选 | 分数 |
|---|---|
balanced | 0.8250 |
deep-reasoning | 0.7900 |
这说明“质量权重最高”也不等于永远选最昂贵模型。balanced 的合成质量略低,但在这个 Fixture 中,它的成本和 TTFT 优势仍让总分更高。
如果真实评测显示 balanced 无法通过迁移测试门槛,它会在第二层被淘汰,届时才选择 deep-reasoning。
错误反例:全局选择最便宜模型
错误实现:
selected = min(models, key=lambda model: model.cost)
在本文 Fixture 中,它永远选择 fast-small,结果是:
- 客服分类:可能合适;
- EU 票据:没有 Vision,也不满足区域约束;
- 代码迁移:32K 上下文装不下 100K 请求。

同类错误还有:
- 先按平均延迟选择,再检查模型是否支持工具;
- 用全站统一权重处理所有任务;
- 把 429 的模型直接换成语义不兼容的便宜模型;
- 把模型质量榜单分数当作自己的任务成功率;
- 忽略缓存亲和,在多个部署间随机打散长前缀;
- 把失败后的模型切换算作同一次“负载均衡”,不记录降级。
可运行的离线实验
实验脚本只使用 Python 标准库,核心结构如下:
def eligible(model, task):
reasons = []
if task["tools"] and not model.tools:
reasons.append("tools")
if task["vision"] and not model.vision:
reasons.append("vision")
if model.context_k < task["context_k"]:
reasons.append("context")
if task["region"] and model.region != task["region"]:
reasons.append("region")
return reasons
def score(model, weights):
return (
weights["quality"] * model.quality
+ weights["cost"] * (1 - model.cost)
+ weights["latency"] * (1 - model.ttft)
+ weights["health"] * model.health
)
真实上线时,应把合成字段替换为:
- 每类任务版本化的离线评测结果;
- 实际 usage 与供应商价格换算的成本;
- p50/p95 TTFT 与总生成时间;
- 429、5xx、超时和中断后的健康统计;
- 可用上下文、工具、模态与区域能力目录;
- Prompt Cache 的命中和亲和信息。
每次决策至少记录:
{
"policy_version": "router-2026-07-20",
"task_class": "invoice-vision-eu",
"eligible_models": ["balanced"],
"rejected": {
"fast-small": ["vision", "region"],
"deep-reasoning": ["region"]
},
"selected_model": "balanced",
"selected_channel": 17,
"fallback_chain": ["balanced-backup"],
"decision_reason": "only candidate satisfying vision+EU"
}
这样才能回答“为什么这次没有走更便宜的模型”。
静态规则、加权评分、级联分别什么时候用
静态规则
适合:
- 任务类型清楚;
- 合规要求严格;
- 缺少可靠质量数据;
- 模型数量较少;
- 希望决策容易审计。
例如:
EU invoice + vision → eu-vision-balanced
code migration > 64K → long-context-code pool
support classification → fast-tool pool
静态规则并不落后。错误的“智能评分”通常不如诚实的规则表。
加权评分
适合:
- 候选都已满足硬约束;
- 有稳定、同口径的质量和性能数据;
- 需要连续调整成本/延迟权衡;
- 能做影子评估与回放。
风险包括归一化漂移、权重互相补偿和指标延迟。必须版本化权重和输入快照。
级联路由
级联不是故障重试,而是:
先调用低成本模型
→ 用确定性验证器检查结果
→ 不合格才升级到更强模型
适合有强验证器的任务:JSON Schema、单元测试、规则校验、OCR 字段完整性。若验证器只是另一个模糊的 LLM Judge,升级决策本身也需要评测。
级联总成本为:
cheap_cost + escalation_rate × expensive_cost
不要只展示首个模型的价格。
回退策略不能只看失败
模型或渠道回退至少分三类:
| 类型 | 例子 | 建议 |
|---|---|---|
| 同模型、同协议部署切换 | 某区域连接失败 | 首 Token 前可切换,风险较低 |
| 同能力模型切换 | 主模型 429,备选支持同样 tools/schema | 先验证协议与质量门槛,记录降级 |
| 跨能力降级 | Vision 模型切到纯文本模型 | 默认禁止,除非业务显式允许 |
LiteLLM 把部署重试、cooldown 和跨模型 fallback 分开,是一个有用的边界。上一篇LLM Gateway 与普通 API Gateway 的对比也说明:流式首 Token 已发送后,透明重放可能造成重复文本、损坏工具参数和重复计费。
更详细的 429、退避与故障切换状态机会在 #107 单独做故障复盘,本篇不把“路由”扩张成所有可靠性问题。
Prompt Cache 会改变最优路径
如果两个渠道都提供同一模型,最短延迟的渠道不一定是总成本最低的渠道。稳定前缀已经在渠道 A 建立缓存时,下一次请求随机切到 B,可能失去缓存读取折扣和 TTFT 收益。
因此可以把缓存亲和作为:
- 软偏好:A 健康时提高分数;
- 有限硬约束:短 TTL 会话窗口内固定渠道;
- 降级条件:A 错误或用量上限后清除亲和并切换。
Nbility 当前代码已经有渠道亲和:先尝试缓存中的 preferred channel,确认渠道启用、路径、模型和分组仍可用;成功请求后记录亲和。候选失效时是否保留、清理或禁止重试由配置决定。
这不等于 Nbility 当前已经实现本文的质量/成本/TTFT 自动加权模型选择。当前代码可验证的能力是:
- Token 模型权限和分组准入;
ModelMapping;- 渠道启用与用量上限过滤;
- 优先级分层;
- 同优先级按权重随机选择;
- Retry 时下降到下一优先级;
- 渠道亲和;
- 记录请求模型、上游模型、渠道与计费。
质量评分器和按实时 TTFT/成本自动选不同模型,应当作为上层策略服务或明确配置,而不能写成现有产品能力。
Nbility 的优先级与权重到底表示什么
当前 GetRandomSatisfiedChannel 的实际顺序是:
- 按 group、model 和 request path 获取渠道;
- 跳过达到用量上限的渠道;
- 收集可用优先级并从高到低排序;
- 首次请求选择最高优先级;
- 同一优先级按 weight 做加权随机;
- Retry Index 增加时进入下一优先级。
所以可以用:
- 优先级表达主渠道与备用渠道;
- 权重表达同一层渠道的流量比例;
- Model Mapping让应用别名映射到上游真实模型;
- 分组表达价格、权限或服务等级边界;
- 亲和维持会话或缓存路径。
但不要把 weight=80 解读为“质量占 80%”。它是同优先级候选的流量权重,不是多目标决策权重。
上线前的门禁
- 先定义任务分类,而不是先选模型
- 能力、上下文、权限、区域作为硬约束
- 每类任务有独立质量评测和最低门槛
- 成本使用实际输入/输出/缓存结构估算
- 延迟同时看 TTFT 与总生成时间
- 健康度区分 429、额度不足、5xx 和超时
- 同模型部署均衡与跨模型选择分开
- 回退前验证 tools、schema、模态和上下文兼容
- 首 Token 后不透明重放完整流
- 缓存亲和有清除和故障旁路策略
- 每次决策保存 Policy Version 与拒绝原因
- 新策略先做离线回放、影子流量和小比例灰度
- 业务成功率变差时能快速回滚
结论
多模型路由的核心不是“随机切换三家模型”,也不是把质量、成本和延迟放进一个看起来精确的公式。
正确顺序是:
- 用能力、上下文、权限和合规做硬过滤;
- 用任务级评测设置质量门槛;
- 只在合格候选中优化成本、TTFT、健康和缓存亲和;
- 选定模型后再做同模型渠道负载均衡;
- 把故障重试与跨模型降级分开;
- 记录每次决策,持续用真实结果更新策略。
如果暂时没有质量数据,先从 Nbility 的模型别名、分组、优先级、权重和渠道亲和构建可解释的静态策略,而不是伪造一个“智能路由分”。随着真实日志与任务评测积累,再把评分器放到稳定模型入口之前。可在 Nbility 查看统一模型接入能力,或通过工单中心申请小额测试额度。


