RouteLLM 实战:从偏好数据到生产路由
更新于 2026-04-16
上一篇 路由分类器 讲了四种分类器路由的原理 — “是什么”和”为什么”。本篇转向实操:“怎么做”。以 RouteLLM 框架为载体,我们将走完从偏好数据准备、MF Router 训练、阈值校准到 OpenAI-compatible API 部署的完整流程。读完本文,你应该能从零跑通整个 RouteLLM 训练部署链路。
1 RouteLLM 架构总览
RouteLLM 的设计分为三层,每层职责清晰:
1.1 Router 层
抽象基类 Router 定义了唯一的核心方法:
# 以下为简化伪代码
class Router(ABC):
def calculate_strong_win_rate(self, prompt) -> float:
"""返回 ∈ [0,1],表示强模型的预期胜率"""
...
def route(self, prompt, threshold, routed_pair):
if self.calculate_strong_win_rate(prompt) >= threshold:
return routed_pair.strong
else:
return routed_pair.weak
语义很直接:calculate_strong_win_rate 返回一个 之间的浮点数,如果 threshold 就走强模型,否则走弱模型。框架内置了 5 种实现:
| Router | 类名 | 核心思路 |
|---|---|---|
| MF | MatrixFactorizationRouter | 矩阵分解,从偏好数据学习 query-model 匹配 |
| BERT | BERTRouter | 3-class 分类器,本地推理 |
| SW-Ranking | SWRankingRouter | 相似度加权 Elo,无需训练 |
| Causal LM | CausalLLMRouter | Llama-3-8B 评分,语义理解最深 |
| Random | RandomRouter | 随机基线,用于对比实验 |
1.2 Controller 层
Controller 是编排核心。它管理 ModelPair(strong/weak 两个模型名称),持有一组已加载的 router 实例,并提供 completion() / acompletion() 接口。
Model name 编码协议是 Controller 的关键设计:客户端通过形如 router-mf-0.7 的 model name 同时指定 router 类型和阈值。Controller 的 _parse_model_name 用 model.split("-", 2) 解析出三部分 — 前缀 router、router 名称 mf、阈值 0.7。路由完成后,Controller 通过 LiteLLM 将请求转发到实际的模型 API。
1.3 OpenAI Server 层
openai_server.py 是一个标准的 FastAPI 应用,启动时初始化 Controller(加载指定的 router),暴露 POST /v1/chat/completions 端点,完全兼容 OpenAI Chat API 格式。客户端只需改 base_url 和 model 字段即可无缝接入。
上面的流程图展示了一个请求的完整生命周期:进入 server → Controller 解析 model name → Router 计算 strong_win_rate → 与 threshold 比较 → 选择 strong/weak model → LiteLLM 转发 → 返回结果。
接下来,我们以 MF Router 为主线,走完整个训练部署流程。
2 数据准备:Chatbot Arena 偏好数据
2.1 数据来源与结构
RouteLLM 的训练数据来自 lmsys/lmsys-arena-human-preference-55k,这是 Chatbot Arena 平台的人类偏好对比数据集。每条记录包含以下核心字段:
prompt:用户 query,JSON 格式的对话历史(通常取json.loads(prompt)[0]得到第一轮对话文本)model_a/model_b:对战的两个模型名称winner:"model_a"|"model_b"|"tie"|"tie (bothbad)"
2.2 数据清洗
训练脚本(train_matrix_factorization.py)对原始数据做了两步过滤:
# 以下为简化伪代码,对应源码过滤逻辑
filtered_data = [
sample for sample in data
if sample["winner"] in ["model_a", "model_b"] # 去除 tie
and sample["model_a"] != sample["model_b"] # 去除同模型对战
]
去除 tie 和 tie (bothbad):MF Router 的训练需要明确的 winner/loser 关系,tie 无法提供这种信号。去除同模型对战:model_a 和 model_b 相同时,胜负没有可区分性。
2.3 自定义数据集
如果要用自己的模型对和数据训练 router,最小 JSON 格式如下:
{
"prompt": "用户的 query 文本",
"model_a": "模型A名称",
"model_b": "模型B名称",
"winner": "model_a"
}
两种构造方式:
- LLM-as-Judge 自动标注:同一 prompt 分别让两个候选模型回答,用 GPT-4 做 pairwise 比较判定 winner。RouteLLM 自身就用此方法做了数据增强 —
routellm/gpt4_judge_battles数据集包含约 25K 条 GPT-4 judge 生成的偏好对。 - 人工标注:成本高但质量最可靠,适合对路由精度要求极高的场景。
数据量参考:RouteLLM 论文使用约 55K 人类偏好对 + 约 25K GPT-4 judge 增强数据(Ong et al., 2024)。实践中几千条可以训出可用的 MF router,但覆盖度和泛化能力会受限。
MODEL_IDS 扩展:model.py 中的 MODEL_IDS 字典包含 64 个 Chatbot Arena 中的模型,每个模型映射到一个整数 ID(用于 nn.Embedding 索引)。如果你的候选模型不在其中,需要在 MODEL_IDS 中新增 entry 并重新训练 — 因为模型 embedding 矩阵 P 的大小由 num_models 决定。
2.4 Prompt Embedding 生成
MF Router 的训练需要预计算所有 prompt 的 embedding。流程是:
- 用 OpenAI
text-embedding-3-small模型(输出 1536 维向量)对每条 prompt 做 embedding - 将结果存为
.npy文件 - 训练时通过 frozen
nn.Embedding加载:Q = nn.Embedding(num_prompts, 1536).requires_grad_(False)
关键约束:训练和推理必须使用同一个 embedding 模型。MF 推理模型 MFModel 的 forward 方法在运行时调用 OPENAI_CLIENT.embeddings.create(input=[prompt], model="text-embedding-3-small"),如果训练时用了不同的 embedding 模型,向量空间不一致会导致路由失效。
MF Router 数据准备流水线
点击各阶段查看详情
3 MF Router 训练深度剖析
3.1 模型结构
MF 训练模型 MFModel_Train 包含四个组件:
| 组件 | 定义 | 说明 |
|---|---|---|
P | nn.Embedding(num_models, dim) | model embedding,可训练。num_models=64, dim=128 |
Q | nn.Embedding(num_prompts, text_dim) | prompt embedding,frozen。从 .npy 加载,text_dim=1536 |
text_proj | nn.Linear(text_dim, dim, bias=False) | 将 1536 维 prompt embedding 投影到 128 维,与 model embedding 对齐 |
classifier | nn.Linear(dim, num_classes, bias=False) | 输出标量 logit。num_classes=1 |
注意 text_proj 和 classifier 都没有 bias — 源码中明确标注 bias=False(classifier 的注释写道 “bias should be False!”)。
3.2 Forward 过程
以下为简化伪代码,展示一次 forward pass 的核心逻辑:
logit = classifier(
(normalize(P[model_win]) - normalize(P[model_loss])) * text_proj(Q[prompt])
)
逐步拆解:
- Model Embedding 查找与归一化:分别查找 winner 和 loser 的 model embedding,L2 归一化到单位球面
- 差值计算: — 编码”赢家比输家强在哪”,这是 Bradley-Terry 偏好模型的核心思想
- Prompt Embedding 投影:将 1536 维的 prompt embedding 通过
text_proj投影到 128 维 - Element-wise Product:差值向量与 prompt 向量逐元素相乘 — 含义是”在这个 query 的语义上下文中,强多少”
- Classifier 压缩:128 维结果通过线性层压缩为标量 logit
直觉理解:如果 在某些维度上正值大,说明赢家在那些”能力维度”上更强;乘以 prompt embedding 后,只有与当前 query 相关的维度会被放大。最终 logit 越大,说明在这个 query 上,“赢家确实该赢”的信念越强。
3.3 Loss 函数
训练使用 BCEWithLogitsLoss(Binary Cross-Entropy with Logits),label 恒为 1.0。
为什么 label 永远是 1?因为 PairwiseDataset.__getitem__ 已经做了数据重排:
# 以下为简化伪代码
def __getitem__(self, index):
if self.winners[index] == "model_a":
return models_a[index], models_b[index], prompt_id[index]
else:
return models_b[index], models_a[index], prompt_id[index]
无论原始数据中谁赢了,__getitem__ 总是把 winner 放在第一个位置(model_win)、loser 放在第二个位置(model_loss)。因此,forward 计算的 logit 语义是”win 确实赢了的置信度”,正确答案始终是”是的”(label=1)。模型需要学的是:对于每个 (winner, loser, prompt) 三元组,输出足够大的正 logit。
3.4 训练噪声
if not test:
prompt_embed += torch.randn_like(prompt_embed) * alpha
对 frozen 的 prompt embedding 加高斯噪声做正则化。因为 Q 是冻结的,模型可能会过拟合到特定 embedding 的精确数值 — 加噪声迫使模型学习对 prompt embedding 小扰动具有鲁棒性的路由决策。
注意 alpha 的两个值:函数签名中 alpha 默认值是 0.05,但训练脚本示例代码中传入的是 alpha=0.1。推理时(test=True)跳过噪声注入。
3.5 训练 vs 推理的模型差异
RouteLLM 有两个不同的 MF 模型类,这是一个容易混淆的设计:
训练用 MFModel_Train:
- 包含
Qembedding 矩阵,通过nn.Embedding预加载全量.npy - Forward 接收
(model_win, model_loss, prompt_idx),按 index 查找 prompt embedding - 支持 batch 处理,高效训练数万条数据
推理用 MFModel(继承 PyTorchModelHubMixin,可直接从 HuggingFace Hub 加载):
- 没有 Q embedding 矩阵
- Forward 接收
(model_id, prompt_text),实时调用 OpenAI embedding API 获取 prompt 向量 - 通过
pred_win_rate方法同时计算两个模型的 logit,取sigmoid(logit_a - logit_b)作为 A 的胜率
这个设计的原因:训练时需要高效地 batch 处理数万条数据,每条都调 API 不现实;推理时只处理单条 query,调一次 API 可接受(约 50ms)。
3.6 超参配置
以下是训练脚本示例中的超参(来自 train_matrix_factorization.py):
| 超参 | 值 | 说明 |
|---|---|---|
dim | 128 | model embedding 维度,也是投影后的 prompt embedding 维度 |
text_dim | 1536 | prompt embedding 原始维度(text-embedding-3-small 输出) |
lr | 3e-4 | Adam 学习率 |
weight_decay | 1e-5 | L2 正则化系数 |
alpha | 0.1 | prompt embedding 噪声强度(函数签名默认 0.05) |
batch_size | 64 | 训练 batch 大小 |
num_epochs | 100 | 训练轮数 |
| train/test split | 95/5 | 随机划分比例 |
4 BERT Router 训练
BERT Router 与 MF Router 的设计哲学差异显著。MF 基于 pairwise 偏好对,BERT 基于分类标签 — 两者对数据的要求不同,训练流程也完全不同。
4.1 模型结构与数据
BERT Router 使用 AutoModelForSequenceClassification 加载预训练 BERT,配置 3-class 分类头(num_labels=3):
- Class 0:强模型赢
- Class 1:平局(tie)
- Class 2:弱模型赢
输入是纯文本 prompt(经过 tokenizer 处理),不需要预计算 embedding,也不需要 model pair 信息。
4.2 推理逻辑
BERTRouter.calculate_strong_win_rate 的推理流程:
# 以下为简化伪代码,对应 BERTRouter 源码
outputs = model(tokenized_prompt)
logits = outputs.logits # shape: [1, 3]
# 手动 softmax(源码用 numpy 实现)
softmax_scores = exp(logits - max(logits)) / sum(exp(logits - max(logits)))
# 取后两类的概率之和:P(tie) + P(weak wins)
binary_prob = sum(softmax_scores[-2:])
# 返回强模型胜率
return 1 - binary_prob
关键设计:将 3-class 输出转为 binary 路由决策时,把 tie 和 weak wins 归为一类(不需要强模型),只有 class 0(强模型赢)的概率越高,strong_win_rate 越高。
4.3 与 MF 的关键差异
| 维度 | MF Router | BERT Router |
|---|---|---|
| 训练数据 | pairwise 偏好对 (winner/loser) | 3-class 分类标签 |
| 外部 API 依赖 | 推理时需要 OpenAI Embedding API | 无,完全本地推理 |
| 推理延迟 | 约 50ms(含 API 调用) | 约 15ms(CPU) |
| 上下文窗口 | 无限制(embedding 压缩) | 512 tokens(BERT 限制) |
| 预训练 checkpoint | routellm/mf_gpt4_augmented | routellm/bert_gpt4_augmented |
BERT Router 的最大优势是零外部依赖 — 不需要调 OpenAI API,不需要网络连接,完全本地推理。这在对延迟敏感或需要离线部署的场景中极有价值。劣势是 512 token 的上下文限制,长 query 会被截断。
5 SW-Ranking 与 Causal LM Router 简述
5.1 SW-Ranking Router
Similarity-Weighted Ranking Router 是唯一不需要训练的 router。其原理是对每个新 query,先计算其 embedding 与 Arena 历史数据中所有 query 的 cosine 相似度,然后用相似度作为权重重新计算 Elo MLE(Maximum Likelihood Estimation)。
推理过程:
- 用
text-embedding-3-small计算新 query 的 embedding - 与全量 Arena battle embedding(数万条)做 cosine 相似度
- 将相似度转换为权重:(指数缩放,使高相似度的 battle 权重远大于低相似度的)
- 用加权样本重新跑 Elo MLE,得到在”类似 query”上各模型的评分
- 从 Elo 评分计算 strong vs weak 的胜率:
优势:无需训练,直接利用 Arena 数据。劣势:推理最重 — 每次需全量计算加权 Elo(约 200-500ms),且依赖 OpenAI Embedding API。适合离线评估或对延迟不敏感的场景。
5.2 Causal LM Router
Causal LM Router 使用 Llama-3-8B(meta-llama/Meta-Llama-3-8B)对 query 做 5 分制评分。模型通过 special tokens [[1]] 到 [[5]] 输出评分,其中高分表示弱模型有更高概率能胜任。
推理过程:
- 将 prompt 格式化为包含 system message 和 classifier message 的 OpenAI 格式消息
- 模型输出 5 个 special token 的 logits,转为概率分布
score_threshold(默认 4)将评分转为 binary:- 返回
1 - binary_prob作为strong_win_rate - 如果模型输出无效(解码失败),fallback 返回 1(路由到强模型)
独特优势:如果弱模型本身就是某个小语言模型,路由判断可以”顺便完成” — 这就是所谓的 zero-marginal-cost routing。劣势:需要 GPU,加载完整 LLM。
5.3 四种 Router 对比
| MF | BERT | SW-Ranking | Causal LM | |
|---|---|---|---|---|
| 训练数据 | 偏好对 | 分类标签 | 无需训练 | 偏好对 |
| 外部依赖 | OpenAI Embedding API | 无 | OpenAI Embedding API | GPU |
| 推理延迟 | 约 50ms | 约 15ms | 约 200-500ms | 约 50-100ms |
| 适用场景 | 通用线上路由 | 低延迟 / 离线部署 | 离线评估 | 小模型即弱模型 |
6 阈值校准
训练完 router 后,下一步是确定 threshold — 这个值直接控制强弱模型的流量分配比例。RouteLLM 提供了 calibrate_threshold.py 工具完成这一步。
6.1 工作流程
Generate 阶段:对验证集(默认使用 Arena 55K 数据)中每条 prompt,用指定 router 计算 strong_win_rate,结果存入 HuggingFace dataset。
python -m routellm.calibrate_threshold \
--task generate \
--routers mf \
--config config.yaml
Calibrate 阶段:给定目标 --strong-model-pct(例如 0.5 表示 50% 流量走强模型),取 strong_win_rate 分布的 分位数作为阈值。
python -m routellm.calibrate_threshold \
--task calibrate \
--routers mf \
--strong-model-pct 0.5
6.2 阈值的含义
核心公式:
直觉理解:strong_win_rate 越高的 query 越”需要”强模型。如果我们希望 50% 的流量走强模型,就取中位数作为阈值 — 只有 strong_win_rate 高于这个值(排名前 50%)的 query 才会被路由到强模型。
阈值越高 → 越少 query 走强模型 → 成本越低,但质量风险越大 阈值越低 → 越多 query 走强模型 → 质量越高,但成本越高
路由阈值校准模拟器
高阈值 = 更多走弱模型 = 省钱但质量略降
7 部署 OpenAI-compatible Server
7.1 Server 架构
openai_server.py 基于 FastAPI 构建:
- 启动时:通过 lifespan context manager 初始化
Controller,加载指定的 router(可以同时加载多个) - 运行时:
POST /v1/chat/completions端点接收标准 OpenAI Chat API 格式请求,从model字段解析 router 类型和阈值,委托 Controller 完成路由和转发 - 健康检查:
GET /health返回{"status": "online"}
7.2 启动命令
python -m routellm.openai_server \
--routers mf \
--strong-model gpt-4-1106-preview \
--weak-model anyscale/mistralai/Mixtral-8x7B-Instruct-v0.1 \
--port 6060
主要参数:
--routers:加载的 router 列表(可多个,如--routers mf bert)--strong-model/--weak-model:模型对,对应 LiteLLM 的模型标识符--config:可选,指定 YAML 配置文件(包含 router checkpoint 路径等)--base-url/--api-key:可选,指定 LLM API 的 base URL 和 key
如果不指定 --config,Controller 会使用内置的 GPT_4_AUGMENTED_CONFIG,自动从 HuggingFace Hub 下载预训练 checkpoint(如 routellm/mf_gpt4_augmented)。
7.3 客户端接入
对于已有的 OpenAI SDK 代码,只需要修改两处:
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:6060/v1", # 指向 RouteLLM server
api_key="not-needed" # 如果未配置 api-key
)
response = client.chat.completions.create(
model="router-mf-0.7", # router-{类型}-{阈值}
messages=[{"role": "user", "content": "Explain quantum entanglement"}]
)
model 字段的格式是 router-{router_name}-{threshold}。同一个 server 加载了多个 router 时,客户端可以按请求选择不同的 router 和阈值 — 例如对实时聊天用 router-mf-0.5(质量优先),对批量任务用 router-mf-0.8(成本优先)。
Server 支持 streaming(stream=True),通过 SSE(Server-Sent Events)逐 chunk 返回响应。
8 总结与生产考量
Router 选择决策树
选择哪种 router 取决于你的约束条件:
- 有偏好对比数据(如 Chatbot Arena 格式)→ MF Router,泛化能力最强
- 有分类标签数据(strong/tie/weak)→ BERT Router,低延迟、零外部依赖
- 任务类型已知且固定 → Semantic Routing(参见 路由分类器 中的 Semantic Routing 部分)
- 弱模型就是某个小 LM → Causal LM Router,可实现 zero-marginal-cost routing
- 只想快速评估 → SW-Ranking,无需训练
生产注意事项
模型更新需重新校准:当 strong 或 weak 模型更新(如 GPT-4 升级到 GPT-4o)时,router 的偏好数据和训练权重可能不再准确。至少需要重新跑 calibrate_threshold,理想情况下应该用新模型对重新收集偏好数据并重新训练。
Embedding API 的延迟与成本:MF 和 SW-Ranking Router 在推理时依赖 OpenAI Embedding API。每次路由调用增加约 50ms 延迟和微量 API 费用。对于高 QPS 场景,需要评估这部分开销是否可接受 — 或考虑切换到 BERT Router。
Fallback 策略:router 异常时(如 Embedding API 超时、模型加载失败),应默认路由到强模型。Causal LM Router 的源码中就实现了这个模式 — calculate_strong_win_rate 在 output 为 None 时返回 1(即路由到强模型)。
动态阈值调整:阈值不必固定。可以根据业务时段(高峰期提高阈值降成本)、预算消耗速率(快超预算时提高阈值)、或用户等级(付费用户降低阈值保质量)动态调整。
下一篇将介绍另一类路由策略 — 级联与自验证:不是事先判断 query 难度,而是”先让弱模型试试,不行再升级”。