多模型路由:按质量、延迟和价格选择模型
刚开始做 LLM 应用时,大家都喜欢把模型名字直接写进代码:这里调用模型 A,那里调用模型 B,某个 Prompt 失败了,就在业务函数里加一个 if 换模型。这样做很快,甚至能让第一个 Demo 提前几天上线。
真正的麻烦通常在产品已经有人使用以后才出现。模型 A 今天延迟突然升高,模型 B 的价格调整了,某个任务需要更长上下文,另一个任务只需要简单分类,生产环境还要求敏感数据不能离开指定区域。此时你会发现,模型选择不是一个常量,而是一项持续变化的策略。
我后来把模型路由理解成“给每个任务分配合适的计算资源”。不是所有问题都该发给最强、最贵的模型;也不是所有请求都能交给最便宜的模型。路由系统要根据任务能力、上下文、延迟、价格、数据边界和当前健康状态作决定,而且每一次决定都应该可以解释和复盘。
一、先把模型调用从业务代码里拿出来
业务层需要的是“完成摘要”“抽取订单字段”或“回答知识库问题”,不应该知道某个供应商的消息格式、错误码和流式事件。第一步是定义自己的统一接口:
type LlmRequest = {
task: 'classify' | 'extract' | 'answer' | 'reason'
messages: Array<{
role: 'system' | 'user' | 'assistant'
content: string
}>
maxTokens?: number
responseFormat?: 'text' | 'json'
sensitivity: 'public' | 'internal' | 'restricted'
deadlineAt?: string
}
type LlmResponse = {
text: string
model: string
inputTokens: number
outputTokens: number
latencyMs: number
finishReason: 'stop' | 'length' | 'error'
requestId: string
}
interface LlmProvider {
readonly name: string
complete(request: LlmRequest): Promise<LlmResponse>
health(): Promise<{ healthy: boolean; latencyMs: number }>
}
适配器负责把统一请求转换成不同供应商的格式,并把响应和错误转换回统一类型。以后换模型、加供应商或在本地部署开源模型,业务代码都不需要跟着改。
这一步看起来像普通抽象,实际上决定了后续能否做可靠路由。如果业务代码到处直接调用厂商 SDK,路由逻辑就会散落在几十个文件里,质量和成本也无法统一统计。
二、先定义“什么叫合适”
路由不是简单的价格排序。一个模型是否适合当前请求,至少要看六个维度:
- 能力:是否能完成当前任务,是否支持工具、JSON 或多模态输入。
- 质量:在这个具体任务上的准确率、引用正确率和拒答表现。
- 延迟:当前健康状态下的 P50、P95 和超时率。
- 价格:输入、输出和额外工具调用的真实成本。
- 上下文:窗口大小、语言覆盖和对长输入的实际处理能力。
- 数据边界:数据是否允许发送到该供应商或区域。
这意味着模型的“强弱”不是全局排名,而是任务相关的能力矩阵:
分类 抽取 长文问答 推理 工具调用
模型 A(便宜) 高 高 中 低 中
模型 B(平衡) 高 高 高 中 高
模型 C(强) 高 高 高 高 高
本地模型 中 中 中 低 中
矩阵不是一次测出来就永远有效。模型版本、Prompt、数据分布和供应商服务都会变化,所以能力数据需要持续评测和时间戳。没有实测数据的路由策略,只是把个人感觉写成了代码。
三、路由策略从简单规则开始
第一版不需要机器学习路由器。可解释的规则已经能覆盖很多真实需求:
type RouteContext = {
task: LlmRequest['task']
estimatedInputTokens: number
requiresJson: boolean
requiresReasoning: boolean
sensitivity: LlmRequest['sensitivity']
remainingBudgetCents: number
}
function chooseProvider(context: RouteContext, providers: LlmProvider[]) {
const candidates = providers.filter((provider) =>
supports(provider, context),
)
if (context.sensitivity === 'restricted') {
return candidates.find((provider) => isApprovedForRestricted(provider))
}
if (context.requiresReasoning) {
return candidates.find((provider) => hasBestReasoningScore(provider))
}
if (context.remainingBudgetCents < 1) {
return candidates.find((provider) => hasLowestCost(provider))
}
return candidates.find((provider) => hasBestLatency(provider))
}
规则顺序就是策略优先级。先过滤不满足数据边界和能力要求的模型,再在剩下的候选中考虑质量、延迟和成本。不要先选最便宜的,再发现它不支持 JSON 或无法处理当前语言。
四、加权评分适合处理多个目标
当候选不止一个都合格时,可以用归一化后的指标做评分:
routeScore
= 质量分 × 质量权重
+ 延迟分 × 延迟权重
+ 成本分 × 成本权重
+ 健康分 × 健康权重
质量分和延迟分需要统一量纲。成本越低不一定分越高,应该结合任务价值和预算。高风险任务可以把质量权重设得很高,后台批处理则可以优先成本。
type ProviderStats = {
quality: number
p95LatencyMs: number
costPerTask: number
errorRate: number
}
function scoreProvider(stats: ProviderStats, weights: {
quality: number
latency: number
cost: number
health: number
}) {
const latencyScore = 1 / Math.max(stats.p95LatencyMs, 1)
const costScore = 1 / Math.max(stats.costPerTask, 0.0001)
const healthScore = 1 - stats.errorRate
return (
stats.quality * weights.quality +
latencyScore * weights.latency +
costScore * weights.cost +
healthScore * weights.health
)
}
不要直接把原始毫秒和质量分相加。先做合理归一化、设置上下限,再用离线和线上数据校准权重。评分公式的优点是可解释,缺点是指标之间可能相互影响,且历史平均值会掩盖当前突发故障。
五、故障时的降级不是“随便换一个模型”
当首选模型失败,系统要区分错误类型:参数错误和权限错误换模型也解决不了;限流、超时和供应商暂时不可用,才可能切换到备用模型。错误应携带是否可重试和是否可降级的信息:
type ProviderError = {
code: 'RATE_LIMITED' | 'TIMEOUT' | 'INVALID_REQUEST' | 'UNAVAILABLE' | 'UNKNOWN'
retryable: boolean
fallbackAllowed: boolean
requestId: string
}
一个可靠的降级链路通常是:
首选模型
↓ 暂时性错误
备用模型
↓ 仍然失败
缓存 / 模板 / 人工队列
↓
诚实说明当前无法完成
降级后的答案要标记来源和能力变化。不能因为模型换了,就假装质量、上下文长度和工具能力完全一样。某些任务应该直接停止,而不是用不支持关键约束的模型生成一个貌似正常的结果。
重试和切换也必须有预算。一次请求最多尝试几次、总等待多久、允许增加多少成本,都要在任务上下文中明确。否则所有供应商都故障时,系统会把一条请求变成一串重复调用。
六、熔断器保护整个服务
如果某个供应商连续超时,所有请求仍然轮流尝试它,队列会越来越长。熔断器可以在错误率或延迟超过阈值时暂时摘除供应商:
type CircuitState = 'closed' | 'open' | 'half_open'
type CircuitBreaker = {
state: CircuitState
failures: number
openedAt?: number
failureThreshold: number
recoveryMs: number
}
function canSend(breaker: CircuitBreaker, now: number) {
if (breaker.state === 'closed') return true
if (
breaker.state === 'open' &&
breaker.openedAt &&
now - breaker.openedAt >= breaker.recoveryMs
) {
breaker.state = 'half_open'
return true
}
return breaker.state === 'half_open'
}
半开状态只放少量探测请求,成功后恢复,失败则继续熔断。阈值不能只看单个错误,最好结合时间窗口、请求量和分任务类型统计。某个模型可能只在长上下文任务上变慢,不能因为简单分类正常就把所有任务都标记为健康。
七、质量路由需要真实评测数据
路由系统最容易犯的错误,是用模型供应商的通用榜单替代自己的任务评测。你的数据、Prompt、语言和业务约束都可能不同。应该为每个任务建立黄金样本,并定期跑出模型能力矩阵:
type EvaluationResult = {
model: string
task: string
datasetVersion: string
successRate: number
schemaPassRate: number
citationAccuracy: number
p95LatencyMs: number
costPerTask: number
}
路由上线前要做离线回放:同一批请求分别交给候选模型,比较质量、延迟和成本;上线后可以对低风险样本做小比例影子评测或 A/B 测试。模型路由改变后,必须看分任务指标,不能只看全站平均满意度。
还要保留路由决策本身:为什么这次选择了模型 B,过滤掉了哪些候选,使用了哪一版能力矩阵和权重。没有决策日志,路由出错时只能猜测。
八、成本控制要看“每个成功任务”
便宜的模型如果经常失败并触发重试,可能比一次成功的强模型更贵。成本比较应该使用“每个成功任务的成本”,而不是单次调用价格:
成功任务成本
= 单次调用成本 × 平均尝试次数
+ 失败重试成本
+ 验证 / 解析成本
+ 人工接管成本
对于结构化抽取,便宜模型的格式失败率可能直接增加解析和人工修复;对于复杂推理,强模型一次答对可能比多个弱模型重复尝试更省。成本优化必须和质量、延迟一起看。
可以给不同任务设置预算档位:实时交互严格限制延迟,后台批处理优先成本,高价值决策优先质量。预算耗尽时,系统应切换到明确的降级行为或请求人工,而不是悄悄突破上限。
九、数据边界应该在路由前执行
敏感数据不能先发给模型,再由路由器决定是否允许。路由上下文里要带数据等级,先过滤不符合政策的供应商,再从合规候选中选择。
原始请求
↓
识别数据等级与租户策略
↓
过滤不合规模型
↓
按质量、延迟、成本选路由
↓
调用并记录审计信息
必要时先脱敏、分段或在本地模型完成敏感字段处理。路由日志也不能包含完整秘密。模型供应商、区域、数据处理协议和留存策略,应该成为供应商注册信息的一部分,而不是上线后才补的文档。
十、流式响应让路由更难一点
流式输出时,首 Token 延迟、总生成时间和中途断流都要记录。切换供应商不能简单地在半截文字后把另一个模型的结果拼接上去,否则用户会看到重复、冲突或格式损坏。
如果首选模型已经输出了一部分内容,通常有三种策略:继续等待并在超时后结束、取消并重新生成完整答案、返回已有片段并明确说明未完成。选择哪种取决于任务类型。聊天闲聊可以容忍片段,JSON、代码和执行计划则必须保证整体完整。
type StreamState = {
provider: string
text: string
receivedTokens: number
finishReason?: 'stop' | 'timeout' | 'disconnect'
canRestart: boolean
}
流式协议的错误、取消和计量也要统一,否则不同供应商的行为会渗透到业务层,最终形成一堆特殊分支。
十一、不要过早训练一个“智能路由器”
当模型数量和任务规模变大,团队可能想训练一个分类器或小模型来预测应该调用谁。这有价值,但前提是已经有足够的历史数据、稳定的质量标签和明确的约束。
早期使用规则的好处是可解释、容易回滚、容易发现指标缺口。等你积累了任务特征、模型响应、用户反馈和真实成本,再让一个路由模型学习复杂模式。即使使用机器学习,也要保留硬约束:敏感数据、能力不支持和预算超限不能被预测结果绕过。
路由模型本身也会退化。模型版本变化、业务分布变化和供应商价格变化,都可能让过去的策略失效。它需要独立评测、灰度发布和版本回滚,不能因为“只是选模型”就跳过质量门禁。
十二、我的总结:路由系统是在管理取舍
多模型路由的本质,不是把请求平均分给更多模型,而是在质量、延迟、成本、能力和风险之间持续做取舍。最强模型应该留给真正需要它的任务,最便宜模型也应该只接它能够稳定完成的任务。
我现在设计路由时,会先做四件事:建立统一接口,建立任务级评测集,记录每次决策和结果,再给故障和成本设置明确边界。规则从简单开始,数据足够以后再逐步引入评分和学习;任何自动化策略都保留硬性的权限、预算和回滚护栏。
如果一次模型调用失败,系统应该知道是输入错、权限错、模型能力不够、供应商故障,还是路由策略判断错。只有错误被分清,路由才不是一串“再试另一个模型”的 if-else,而是一套能够解释、监控和改进的基础设施。
2024 年的大模型应用开始真正面对工程现实:模型会变,价格会变,流量会变,用户问题也会变。把模型名从业务代码里解耦出来,只是第一步;更重要的是建立一条证据链,证明每次选择为什么合适、失败后如何收敛、成本是否值得。能把这些取舍管理好,模型数量增加不会让系统失控,反而会让应用拥有更有弹性的能力边界。