实际 LLM 应用中,很多团队会遇到一个共同问题:所有请求都调用同一个最强模型,效果虽然稳定,成本却随着调用量上升迅速失控。AI model router 就是用来解决这类问题的一种中间层,它根据请求类型、模型能力、价格和当前负载,把请求分发到合适的 LLM,用较低成本满足大部分需求。这篇文章以一次成本优化项目为背景,完整梳理模型路由器的设计思路、最小实现、成本验证和生产落地要点。
需要先说明一点:标题里的 94% 成本下降来自特定业务场景,不一定能在所有系统上复现。文章给出的代码和配置是用于讲解实现思路,落地时要以实际模型 API、定价策略和业务数据为准。
1. 先理解 AI 模型路由器的价值边界
1.1 模型路由要解决什么问题
模型路由器的核心问题只有一个:当前这个请求,真的需要最强的模型吗?
在自然语言处理项目中,并不是所有请求都要求高推理能力。用户问“今天天气怎么样”“这个接口返回的字段是什么意思”,和用户提交一份包含数学推理、代码调试、多步规划的复杂问题,对模型能力的需求完全不同。如果统一调用大参数、高价格模型,简单请求会支付超额成本;如果统一调用小模型,复杂请求会频繁失败或质量下降。
AI model router 就是在用户请求和底层 LLM 之间增加一层决策逻辑。它把请求分类,估算需要的输出长度,读取每个模型的能力和价格,然后决定把请求交给哪个模型。本质上是在“效果、延迟、成本”三者之间做权衡。
1.2 为什么路由会带来成本下降
LLM 的成本结构通常由输入 token 数、输出 token 数和不同模型单价决定。大部分业务场景里,高频请求其实是短文本问答、摘要、格式转换、意图识别这类任务,它们不需要顶级模型的全部能力。
一个典型例子:
- 1000 次请求全部走大模型,每次输入约 500 token,输出约 200 token,成本按大模型单价计算。
- 其中 800 次请求实际是小模型就能完成的,只有 200 次真正需要大模型。
如果路由逻辑能把 800 次简单请求切到小模型,成本变化会非常明显。这不是通过压缩模型参数量实现的,而是通过减少高单价模型调用次数实现的。94% 这个数字虽然来自特定案例,但背后的逻辑是一致的:大部分成本浪费来自“模型能力和请求需求不匹配”。
1.3 路由器与 API 网关的差异
有人会把模型路由器当成 API 网关,两者职责并不一样。
API 网关关心的是路由转发、鉴权、限流、协议转换、负载均衡,它不关心模型能力是否匹配。模型路由器关心的是业务语义和模型能力:任务属于哪个类别、输出质量要求多高、延迟预算多少、当前模型能不能处理。
实际项目中,两者可以配合:API 网关负责把外部请求接入系统,模型路由器位于网关之后,在内部完成模型选择。不要把模型路由逻辑直接写进网关,否则后续调整模型策略会受制于网关发布和团队职责边界。
2. 从案例出发确定路由目标和判断维度
2.1 一次成本优化项目的背景
以一次成本优化项目为例。团队运营一个企业内部知识库助手,主要包含三类请求:
- 文档内容问答:用户针对某段文档提问,问题通常短、答案可以从上下文里提取。
- 代码与命令生成:用户要求生成脚本、调试命令,对步骤准确性要求高。
- 综合总结与规划:用户提交多段材料,要求生成结构化报告或方案。
项目启动前,所有请求都调用同一个高价模型。监控显示,每月 token 消耗中约 20% 的请求属于代码和复杂规划类,剩下 80% 是文档问答和短文本处理。把 80% 的简单请求拆到低成本模型,是成本优化的主要切入点。
目标并不是让所有请求都用最便宜的模型,而是保证每个请求都使用“够用且不浪费”的模型。还需要约束延迟和错误率,否则成本降下来但用户体验变差,优化没有意义。
2.2 决策维度一览
模型路由决策不是一个单一的 if else,而是多维度打分的组合。常用维度如下:
| 维度 | 说明 | 影响 |
|---|---|---|
| 任务类型 | 问答、摘要、代码、数学、推理、生成等 | 决定是否需要强模型 |
| 输入长度 | 段落长度、上下文大小 | 影响输入成本和延迟 |
| 预期输出长度 | 回复长度估算 | 影响输出成本和模型输出上限 |
| 质量要求 | 是否存在标准答案、是否会被人工审查 | 低质量场景可接受较小模型 |
| 延迟预算 | 用户能接受的等待时间 | 大模型延迟高,小模型延迟低 |
| 成本上限 | 单次请求允许的最大成本 | 超预算请求必须切模型或拒绝 |
在最小实现里,任务类型往往是最强信号。代码、数学、复杂推理优先分给强模型;文档问答、简单摘要、意图识别分给小模型。但要避免只按任务类型判断,因为同一个任务类型内部复杂度也可能差别很大。
2.3 示例价格模型
为了演示成本计算,需要先定义模型和价格。以下价格不是任何平台的实时报价,仅作为示例:
| 模型标识 | 输入价格(美元/1K token) | 输出价格(美元/1K token) | 能力倾向 |
|---|---|---|---|
| llm-large | 0.015 | 0.075 | 高推理、代码、长文生成 |
| llm-small | 0.0015 | 0.006 | 短问答、摘要、格式转换 |
| llm-medium | 0.003 | 0.015 | 中等复杂度解释、分类 |
把这组价格写进配置,路由服务才能按实际 token 用量计算成本。注意,采购或接入新模型时,价格要单独确认并测试 tokenizer 的切分规则,不同模型对同一段文本的 token 计费可能不同。
3. 最小可运行的模型路由服务
3.1 项目结构与数据模型
下面用一个最小示例说明模型路由器的实现思路。项目结构如下:
llm-router/ ├── config/ │ └── routing.yaml ├── core/ │ ├── scheme.py │ ├── estimator.py │ ├── router.py │ └── client.py ├── logs/ │ └── router.log └── main.py首先定义模型配置。用ModelConfig保存模型名称、价格、上下文窗口和是否支持复杂推理:
# core/scheme.py from dataclasses import dataclass from typing import Optional @dataclass class ModelConfig: name: str input_price: float output_price: float context_window: int = 8192 max_output_tokens: int = 4096 supports_reasoning: bool = False @dataclass class RouteRequest: task_type: str prompt: str expected_output_tokens: int = 512 max_cost_usd: Optional[float] = None max_latency_seconds: Optional[float] = NoneRouteRequest是路由器的统一入参。在线服务中,它通常由上游服务组装:从请求体里读取用户输入,用分类模型或规则识别任务类型,再传给路由器。
3.2 统一请求响应结构
路由器返回时不能只告诉上层“选择了哪个模型”,还要返回内容、token 用量和成本。这样才能在后面做成本统计和质量回归。
@dataclass class RouteResponse: model: str content: str input_tokens: int output_tokens: int cost_usd: float统一响应结构有两个作用:
- 上层业务代码不关心底层模型差异,只需要处理一个响应对象。
- 成本统计模块可以从响应对象中提取 token 和费用,不需要额外去查调用链。
3.3 基于规则的路由实现
先实现一个 token 估算器。真实项目应该使用各模型对应的 tokenizer,但最小示例可以用字符数近似:
# core/estimator.py def estimate_tokens(text: str, bytes_per_token: float = 4.0) -> int: if not text: return 0 return max(1, int(len(text.encode("utf-8")) / bytes_per_token))这个函数的价值是:在真正调用模型前,先得到输入 token 数量,用于成本估算和模型选择。生产环境里,建议在每个 provider adapter 中调用官方 tokenizer,避免估算偏差影响路由判断。
接下来实现规则路由器:
# core/router.py from core.scheme import ModelConfig, RouteRequest from core.estimator import estimate_tokens class RuleRouter: def __init__(self, models: dict[str, ModelConfig]): self.models = models def route(self, req: RouteRequest) -> str: input_tokens = estimate_tokens(req.prompt) if input_tokens > self.models["llm-large"].context_window * 0.8: return "llm-large" if req.task_type in {"code", "math", "reasoning", "multi-step-plan"}: return "llm-large" if req.task_type in {"qa-extractive", "summarize-short", "intent"}: return "llm-small" return "llm-medium"这个路由规则包含三层判断:
- 超长输入优先走大模型,因为小模型上下文窗口不足。
- 高推理任务直接走大模型,避免小模型反复生成错误内容。
- 简单任务走小模型,其余走中等模型。
这里要强调:规则路由器适合快速验证思路,但不适合长期维护。规则会随着任务类型增加而膨胀,最终出现“优先级冲突”和“难以解释”的问题。更可靠的方式是引入打分函数。
3.4 本地验证与预期输出
在main.py里模拟一次调用:
from core.scheme import ModelConfig, RouteRequest from core.router import RuleRouter models = { "llm-large": ModelConfig( name="llm-large", input_price=0.015, output_price=0.075, context_window=128000, supports_reasoning=True, ), "llm-medium": ModelConfig( name="llm-medium", input_price=0.003, output_price=0.015, context_window=32000, ), "llm-small": ModelConfig( name="llm-small", input_price=0.0015, output_price=0.006, context_window=16000, ), } router = RuleRouter(models) req = RouteRequest(task_type="qa-extractive", prompt="后端返回的 error_code 是什么意思?") print(router.route(req))预期输出是llm-small。用同样的方法构造一个task_type="code"的请求,预期输出是llm-large。这就是最小闭环:输入请求,输出模型选择结果。之后再把模型选择结果接入真实客户端,替换返回的content字段。
4. 让路由决策更可靠
4.1 从规则路由升级为打分路由
规则路由的缺点是“一刀切”。同样是代码任务,修改一行配置和编写一个完整微服务,复杂度完全不同。打分路由的思路是:为每个候选模型计算综合分数,选择分数最高的模型。
打分函数可以包含三个子分数:成本分、延迟分、质量分。
def score_model( model: ModelConfig, input_tokens: int, expected_output_tokens: int, task_quality_need: float, latency_budget: float, ) -> float: cost = input_tokens / 1000 * model.input_price + expected_output_tokens / 1000 * model.output_price cost_score = 1.0 / (1.0 + cost * 100) quality_score = model.supports_reasoning * task_quality_need + (1.0 - model.supports_reasoning) * (1.0 - task_quality_need) latency_score = max(0.0, 1.0 - latency_budget / 10.0) return cost_score * 0.5 + quality_score * 0.4 + latency_score * 0.1这里的参数不能直接复制到生产环境。它只是说明打分路由的构造方式:每一项都应该是可量化的,并且权重需要根据业务反馈调整。真实项目中,成本分应该使用真实单价,质量分应该来自离线评测或线上效果指标,而不是简单用supports_reasoning代替。
4.2 回退机制设计
路由器需要处理小模型失败的情况。常见失败包括:内容被过滤、输出格式不符合要求、捕获到异常、质量分过低。
回退机制可以这样实现:
def route_with_fallback(req: RouteRequest, client, router) -> str: selected = router.route(req) response = client.complete(model=selected, prompt=req.prompt) if not response.is_valid: return client.complete(model="llm-large", prompt=req.prompt) return response回退机制的关键是:不要无限制重试。第一跳失败后切到更大模型,最多重试两次;如果仍然失败,直接进入错误处理流程。否则,成本没有降下来,延迟反而翻倍。
4.3 动态调节路由策略
路由器的配置不应该写死在代码里。推荐把模型价格、任务类型映射、权重参数放到配置中心或独立配置文件,运行时可以动态更新。
# config/routing.yaml default_model: llm-medium rules: - task_type: qa-extractive model: llm-small - task_type: code model: llm-large - task_type: reasoning model: llm-large fallback: enabled: true max_retry: 2 fallback_model: llm-large更新配置后,要记录版本号和生效时间。模型价格调整或新模型接入时,先在小流量环境验证,再全量发布。不要直接修改生产配置后不做任何验证,否则可能出现“所有请求都路由到同一个模型”的问题。
5. 成本统计与收益验证
5.1 按模型聚合成本
成本优化是否有效,不能只看总账单。要建立按模型、按任务类型、按天聚合的成本视图。
可以写一个简单的统计函数:
def aggregate_cost(responses: list[RouteResponse]) -> dict[str, float]: result = {} for resp in responses: key = resp.model result[key] = result.get(key, 0.0) + resp.cost_usd return result更完整的方案是把每次调用的RouteResponse写入日志表,字段包括请求 ID、模型、输入 token、输出 token、任务类型、耗时、成本。后续做分析时,只需要按模型聚合,就能看到成本分布。
5.2 验证收益时要排除缓存干扰
很多 LLM 产品提供 prompt 缓存。如果缓存命中,token 成本可能大幅下降。但这部分收益来自缓存,不是完全来自模型路由。
验证路由器收益时,建议先关闭缓存,或者分别统计“缓存命中”和“未命中缓存”的两组数据。否则,两种优化混在一起,无法判断路由策略是否真的合适。
另一个容易忽略的问题是 token 估算误差。假如estimate_tokens低估了实际 token 数,成本统计就会偏乐观。生产环境里,成本统计必须以实际扣费回执为准,不要用估算值做对账。
5.3 在测试环境做质量回归
路由优化不能只看钱,还要看效果。建议建立一个小规模测试集,里面包含三类请求:
- 小模型应该能处理的简单请求。
- 必须由大模型处理的高难度请求。
- 边界请求,比如超长输入、低质量 prompt、多语言混合内容。
每次调整路由策略后,跑一遍测试集,记录模型选择结果和质量评分。如果小模型在简单请求上出现明显下降,需要调整路由规则或质量评分权重。
6. 常见问题与排查路径
6.1 路由结果不稳定
现象:同一个请求在不同时间被路由到不同模型。
排查路径:
- 确认请求参数是否一致,特别是
task_type是否由上游传递。 - 检查输入长度是否接近阈值,
estimate_tokens可能因为字符集不同产生偏差。 - 检查配置是否被动态更新,规则表是否被中途修改。
解决方案是把路由决策结果连同输入摘要写入日志。出现问题时,可以回放当时的请求和配置版本。
6.2 成本没有下降反而上升
现象:接入路由器后总成本更高。
常见原因:
- 小模型需要多次重试才能达到效果,重试导致 token 消耗增加。
- 回退机制过于宽松,大量请求第一次失败后都走了大模型。
- 上游请求经过路由后,输出长度估算不准,大模型输出了更多 token。
- 缓存策略变化,原本命中的缓存失效。
排查时先看“模型调用次数”和“平均输出 token 数”两个指标。如果小模型调用次数高,但失败后重试次数也高,说明小模型选择边界可能太激进。
6.3 小模型抢占高价值请求
现象:代码生成或复杂推理请求被路由到小模型,用户反馈答案质量差。
原因通常是任务类型识别不准。比如用户用“帮我看看这个命令有没有问题”表达代码调试需求,但上游把这句话分类成了普通问答。
解决方式:
- 提升任务分类准确率。
- 在路由规则中设置关键词扩展。
- 引入质量反馈机制,用户“不满意”或“重新生成”的请求下次直接走大模型。
6.4 日志和监控维度缺失
现象:优化上线后,只能看到总账单,无法定位是哪类请求贡献了大部分成本。
这是最需要提前预防的问题。从第一天起就记录路由决策、token 用量、模型名称、任务类型、耗时和错误状态。没有这些数据,任何成本异常都无法快速定位。
7. 生产环境落地的关键点
7.1 配置外置与动态更新
模型路由的配置变化非常频繁。新模型发布、价格调整、业务任务类型变化,都会影响路由策略。配置应该放在独立配置中心,而不是编译进代码。
生产环境至少需要:
- 配置版本管理。
- 灰度发布能力。
- 配置变更后自动加载。
- 变更审计日志。
7.2 超时、重试与熔断
每个模型调用的超时时间不同。小模型响应快,大模型在长上下文下可能很慢。路由器需要为不同模型设置不同的超时时间,不能共用同一个值。
重试需要考虑两个层面:
- 网络或服务端错误,例如 429、5xx,可以重试。
- 模型输出质量不合格,只能走回退逻辑。
如果某个模型连续失败,比如连续 5 次超时或 5 次返回空内容,应该触发熔断,暂时把请求切到其他模型,并发送告警。
7.3 安全、权限与合规
模型路由器会暴露模型调用能力,需要严格控制访问权限。
- 内部服务通过 API Key 或服务账号访问路由器。
- 外部用户请求不能直接指定模型名称。
- 需要记录输入内容,区分哪些字段可以进入模型、哪些字段必须脱敏。
- 对于涉及个人信息的请求,路由日志不能完整保存原文。
模型路由虽然能降低成本,但不应成为数据合规的例外点。日志中保存最小必要字段,并在保存前进行敏感信息过滤。
7.4 生产部署前检查清单
上线前可以按下面这个清单逐项确认:
| 检查项 | 确认内容 |
|---|---|
| 模型可用性 | 候选模型 API 是否可用,是否有配额限制 |
| 单价确认 | 输入输出单价是否与合同或控制台一致 |
| Tokenizer 验证 | 估算 token 与实际 tokenizer 的误差是否在可接受范围 |
| 超时配置 | 每个模型是否设置了独立超时时间 |
| 回退策略 | 回退重试次数是否限制,是否会触发告警 |
| 日志字段 | 是否记录了请求 ID、模型、token、成本、错误信息 |
| 质量测试集 | 简单请求和高难度请求是否都覆盖 |
| 灰度计划 | 能否先切 10% 流量,观察后再放量 |
| 回滚方案 | 配置是否可以快速一键回滚 |
| 成本监控 | 是否按模型和任务类型设置成本告警 |
这张清单同样适用于学习环境之外的所有阶段。不要在没有质量测试集和日志指标的情况下直接全量发布。
8. 从路由器到更细粒度的成本治理
模型路由器不是成本优化的终点。它是一套机制的入口:请求分类、能力评估、成本计算、质量反馈、灰度实验、指标监控,这些能力组合起来,才构成完整的成本治理体系。
对新手来说,最有价值的练习不是直接搭一个生产级路由器,而是先用最小规则路由跑通流程,记录 1000 条真实请求,统计其中高频任务类型和成本分布。有了真实数据,再逐步把规则改成打分路由,把小模型的选择边界调整到合理范围。
这篇文章主要聚焦在模型选择层。后续可以继续深入三个方向:一是基于 embedding 的语义路由,把请求向量化和历史效果做匹配;二是基于强化学习的路由策略,让模型选择随质量反馈自动优化;三是把路由器接入 MCP 或 Agent 工具链,在多个工具和模型之间统一调度。每个方向都需要稳定的日志和评估体系支撑,否则再复杂的路由算法也缺少判断依据。