本站内容由 AI 生成,可能存在错误。如发现问题,欢迎到 GitHub Issues 反馈。

RouteLLM 实战:从偏好数据到生产路由

RouteLLM 实战:从偏好数据到生产路由

更新于 2026-04-16

上一篇 路由分类器 讲了四种分类器路由的原理 — “是什么”和”为什么”。本篇转向实操:“怎么做”。以 RouteLLM 框架为载体,我们将走完从偏好数据准备、MF Router 训练、阈值校准到 OpenAI-compatible API 部署的完整流程。读完本文,你应该能从零跑通整个 RouteLLM 训练部署链路。

1 RouteLLM 架构总览

RouteLLM 架构RouteLLM 三层架构OpenAI Server(FastAPI)POST /v1/chat/completionsController(编排核心)解析 model name → 选 Router → LiteLLM 转发Router(路由决策)calculate_strong_win_rate(prompt) → [0,1]客户端请求router-mf-0.7类型-名称-阈值

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 返回一个 [0,1][0, 1] 之间的浮点数,如果 \geq threshold 就走强模型,否则走弱模型。框架内置了 5 种实现:

Router类名核心思路
MFMatrixFactorizationRouter矩阵分解,从偏好数据学习 query-model 匹配
BERTBERTRouter3-class 分类器,本地推理
SW-RankingSWRankingRouter相似度加权 Elo,无需训练
Causal LMCausalLLMRouterLlama-3-8B 评分,语义理解最深
RandomRandomRouter随机基线,用于对比实验

1.2 Controller 层

Controller 是编排核心。它管理 ModelPair(strong/weak 两个模型名称),持有一组已加载的 router 实例,并提供 completion() / acompletion() 接口。

Model name 编码协议是 Controller 的关键设计:客户端通过形如 router-mf-0.7 的 model name 同时指定 router 类型和阈值。Controller 的 _parse_model_namemodel.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_urlmodel 字段即可无缝接入。

RouteLLM 架构调用链从用户请求到模型路由的完整流程点击各阶段查看详情用户请求Chat Completion 请求1OpenAI ServerPOST /v1/chat/comp...2Controller解析模型名3Router计算 strong_win_rate4阈值比较win_rate >= thresh...5LiteLLM 转发转发至实际模型 API6强模型 (如 GPT-4)win_rate >= 0.7弱模型 (如 Llama-8B)win_rate < 0.7>= threshold< thresholdAPI Response具体示例: router-mf-0.7模型名称:router-mf-0.7split("-", 2) →["router", "mf", "0.7"]路由器:mf(MatrixFactorization)阈值:0.7强模型胜率:0.82路由结果:强模型 (如 GPT-4)τ=0.7
strong_win_rate:0.82(threshold = 0.7)

上面的流程图展示了一个请求的完整生命周期:进入 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"
}

两种构造方式:

  1. LLM-as-Judge 自动标注:同一 prompt 分别让两个候选模型回答,用 GPT-4 做 pairwise 比较判定 winner。RouteLLM 自身就用此方法做了数据增强 — routellm/gpt4_judge_battles 数据集包含约 25K 条 GPT-4 judge 生成的偏好对。
  2. 人工标注:成本高但质量最可靠,适合对路由精度要求极高的场景。

数据量参考: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。流程是:

  1. 用 OpenAI text-embedding-3-small 模型(输出 1536 维向量)对每条 prompt 做 embedding
  2. 将结果存为 .npy 文件
  3. 训练时通过 frozen nn.Embedding 加载:Q = nn.Embedding(num_prompts, 1536).requires_grad_(False)

关键约束:训练和推理必须使用同一个 embedding 模型。MF 推理模型 MFModelforward 方法在运行时调用 OPENAI_CLIENT.embeddings.create(input=[prompt], model="text-embedding-3-small"),如果训练时用了不同的 embedding 模型,向量空间不一致会导致路由失效。

MF Router 数据准备流水线

点击各阶段查看详情

原始数据~55K1过滤~45K2Embedding~45K × 15363训练/测试拆分95% / 5%4~45K~45K训练集~42.75K测试集~2.25Klmsys-arena-human-preference-55k
自定义数据集: 用户可提供相同格式的 JSON (prompt, model_a, model_b, winner),并可使用 LLM-as-Judge 进行自动标注。

3 MF Router 训练深度剖析

3.1 模型结构

MF 训练模型 MFModel_Train 包含四个组件:

组件定义说明
Pnn.Embedding(num_models, dim)model embedding,可训练。num_models=64, dim=128
Qnn.Embedding(num_prompts, text_dim)prompt embedding,frozen。从 .npy 加载,text_dim=1536
text_projnn.Linear(text_dim, dim, bias=False)将 1536 维 prompt embedding 投影到 128 维,与 model embedding 对齐
classifiernn.Linear(dim, num_classes, bias=False)输出标量 logit。num_classes=1

注意 text_projclassifier 都没有 bias — 源码中明确标注 bias=Falseclassifier 的注释写道 “bias should be False!”)。

3.2 Forward 过程

MF Forward 步骤MF Router Forward 过程logit = classifier( (normalize(P[win]) − normalize(P[loss])) ⊙ text_proj(Q[prompt]) )1Embed 查找P[win], P[loss]2L2 归一化normalize(P)3差值p_win − p_loss4投影text_proj(Q)5逐元素乘diff ⊙ proj6分类输出classifier → logit128d128d1536d→128d128d→ scalar

以下为简化伪代码,展示一次 forward pass 的核心逻辑:

logit = classifier(
    (normalize(P[model_win]) - normalize(P[model_loss])) * text_proj(Q[prompt])
)

逐步拆解:

  1. Model Embedding 查找与归一化:分别查找 winner 和 loser 的 model embedding,L2 归一化到单位球面
  2. 差值计算pwinploss\mathbf{p}_{win} - \mathbf{p}_{loss} — 编码”赢家比输家强在哪”,这是 Bradley-Terry 偏好模型的核心思想
  3. Prompt Embedding 投影:将 1536 维的 prompt embedding 通过 text_proj 投影到 128 维
  4. Element-wise Product:差值向量与 prompt 向量逐元素相乘 — 含义是”在这个 query 的语义上下文中,强多少”
  5. Classifier 压缩:128 维结果通过线性层压缩为标量 logit

直觉理解:如果 pwinploss\mathbf{p}_{win} - \mathbf{p}_{loss} 在某些维度上正值大,说明赢家在那些”能力维度”上更强;乘以 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

  • 包含 Q embedding 矩阵,通过 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):

超参说明
dim128model embedding 维度,也是投影后的 prompt embedding 维度
text_dim1536prompt embedding 原始维度(text-embedding-3-small 输出)
lr3e-4Adam 学习率
weight_decay1e-5L2 正则化系数
alpha0.1prompt embedding 噪声强度(函数签名默认 0.05)
batch_size64训练 batch 大小
num_epochs100训练轮数
train/test split95/5随机划分比例
MF Router 前向传播训练模式推理模式1输入3 个整数索引2P Lookup2 x [1, 128]3L2 Normalize2 x [1, 128]4Q Lookup + Noise[1, 1536]5Projection[1, 128]6差值 x 投影[1, 128]7Classifier[1] scalar8Lossscalaridx12812815361281281第 1 / 8 步: 输入(model_win, model_loss, prompt_idx)例:(GPT-4=24, Mixtral=36, prompt=1234)3 个整数索引24361234← model_win, model_loss, prompt_idx
训练:Q 来自冻结的 nn.Embedding,加高斯噪声;label 恒为 1.0(dataset 已重排 winner first)
第 1 / 8 步

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 RouterBERT Router
训练数据pairwise 偏好对 (winner/loser)3-class 分类标签
外部 API 依赖推理时需要 OpenAI Embedding API无,完全本地推理
推理延迟约 50ms(含 API 调用)约 15ms(CPU)
上下文窗口无限制(embedding 压缩)512 tokens(BERT 限制)
预训练 checkpointroutellm/mf_gpt4_augmentedroutellm/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)。

推理过程:

  1. text-embedding-3-small 计算新 query 的 embedding
  2. 与全量 Arena battle embedding(数万条)做 cosine 相似度
  3. 将相似度转换为权重:wi=10×10si/smaxw_i = 10 \times 10^{s_i / s_{max}}(指数缩放,使高相似度的 battle 权重远大于低相似度的)
  4. 用加权样本重新跑 Elo MLE,得到在”类似 query”上各模型的评分
  5. 从 Elo 评分计算 strong vs weak 的胜率:Pstrong=111+10(RstrongRweak)/400P_{strong} = 1 - \frac{1}{1 + 10^{(R_{strong} - R_{weak}) / 400}}

优势:无需训练,直接利用 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]] 输出评分,其中高分表示弱模型有更高概率能胜任。

推理过程:

  1. 将 prompt 格式化为包含 system message 和 classifier message 的 OpenAI 格式消息
  2. 模型输出 5 个 special token 的 logits,转为概率分布
  3. score_threshold(默认 4)将评分转为 binary:P(binary)=sthresholdP(s)P(\text{binary}) = \sum_{s \geq threshold} P(s)
  4. 返回 1 - binary_prob 作为 strong_win_rate
  5. 如果模型输出无效(解码失败),fallback 返回 1(路由到强模型)

独特优势:如果弱模型本身就是某个小语言模型,路由判断可以”顺便完成” — 这就是所谓的 zero-marginal-cost routing劣势:需要 GPU,加载完整 LLM。

5.3 四种 Router 对比

Router 对比四种 Router 对比语义理解深度 →推理速度 →BERT零外部依赖 · 15msMF泛化最强 · 50msCausal LM语义最深 · 需 GPUSW-Ranking无需训练 · 200-500ms
MFBERTSW-RankingCausal LM
训练数据偏好对分类标签无需训练偏好对
外部依赖OpenAI Embedding APIOpenAI Embedding APIGPU
推理延迟约 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 分布的 1pct1 - pct 分位数作为阈值。

python -m routellm.calibrate_threshold \
  --task calibrate \
  --routers mf \
  --strong-model-pct 0.5

6.2 阈值的含义

核心公式:

threshold=quantile(q=1strong_model_pct)\text{threshold} = \text{quantile}(q = 1 - \text{strong\_model\_pct})

直觉理解:strong_win_rate 越高的 query 越”需要”强模型。如果我们希望 50% 的流量走强模型,就取中位数作为阈值 — 只有 strong_win_rate 高于这个值(排名前 50%)的 query 才会被路由到强模型。

阈值越高 → 越少 query 走强模型 → 成本越低,但质量风险越大 阈值越低 → 越多 query 走强模型 → 质量越高,但成本越高

路由阈值校准模拟器

高阈值 = 更多走弱模型 = 省钱但质量略降

阈值0.40
(0) (1)
强模型: 48%弱模型: 52%
48%
52%
成本节约
49%
(vs 全部使用强模型)
质量保持率
95%
(vs 全部使用强模型)
阈值:
0.40
强模型比例:
48%
弱模型比例:
52%
成本节约:
49%
质量保持率:
95%

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 部分)
  • 弱模型就是某个小 LMCausal 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 难度,而是”先让弱模型试试,不行再升级”。