- 后端
- API网关
- 模型推理服务
- AI Agent
【免费下载链接】semantic-router
An open, programmable decision layer for models and compute.
本文围绕 vLLM Semantic Router 项目中的Elo 评分模型选择(Elo Rating Selection)展开,介绍一种基于运行时用户反馈、借助 Bradley-Terry 成对比较框架持续更新模型评分的在线学习式路由策略。读者将掌握该算法的数学原理、Go 核心实现、完整配置方式、反馈提交接口与持久化存储机制,并能在实际网关部署中通过elo算法让表现更好的模型在候选集中脱颖而出、被更频繁地选中。
Elo 评分选择的核心思想
传统的模型路由(如 RouteLLM 论文中训练的方式)是基于偏好数据训练出的静态路由器模型,一次训练、长期生效。而 Semantic Router 的 Elo 评分选择采用了完全不同的路线:一套运行时评分系统,根据实时用户反馈动态更新模型排名。
其基本逻辑是:
- 模型收到正向反馈(thumbs up)时,评分增加;
- 模型收到负向反馈(thumbs down)时,评分降低;
- 随着时间推移,表现更好的模型评分攀升,被选中的概率也随之提高。
这套方法基于Bradley-Terry 模型(成对比较框架),通过在线学习持续改进模型选择质量——不需要离线训练,反馈即学习,评分即策略。该实现的核心位于 src/semantic-router/pkg/selection/elo.go,代码注释中明确标注其思想参考自 RouteLLM 论文(arXiv:2406.18665),但在工程形态上改为运行时动态更新。
算法工作流程
流程可以拆解为以下四步(与原文档一致):
- 所有模型以初始评分起步(默认1500);
- 用户在响应后提供反馈(赞 / 踩);
- 评分依据反馈与"期望结果"之间的差距进行调整;
- 高评分模型被更频繁地选中。
其中第 3 步"期望结果"是关键:评分更新不是简单地加分减分,而是同时考虑两个模型的相对实力——这正是 Bradley-Terry 模型发挥作用的地方。
数学基础:Bradley-Terry 期望得分与评分更新
期望得分(Expected Score)
模型 A 击败模型 B 的期望概率为:
E_A = 1 / (1 + 10^((R_B - R_A) / 400))其中:
R_A= 模型 A 的评分R_B= 模型 B 的评分E_A= 期望得分(A 获胜的概率)
评分更新公式
收到反馈后,评分按如下公式更新:
R'_A = R_A + K × (S_A - E_A)其中:
K= K 因子(控制波动幅度,默认 32)S_A= 实际结果(1 = 胜,0.5 = 平,0 = 负)E_A= 期望得分
计算示例
模型 A:评分 1500,模型 B:评分 1400 A 的期望得分:1 / (1 + 10^((1400-1500)/400)) = 0.64 若 A 获胜(S_A = 1): 新评分 = 1500 + 32 × (1 - 0.64) = 1500 + 11.5 = 1511.5 若 A 落败(S_A = 0): 新评分 = 1500 + 32 × (0 - 0.64) = 1500 - 20.5 = 1479.5可以看到:爆冷获胜时加分多,正常获胜时加分少;同理,爆冷落败时扣分也多。这种不对称性正是 Elo 系统能快速识别真实实力差距的原因。
源码级实现:EloSelector 的 Select 与 UpdateFeedback
在 src/semantic-router/pkg/selection/elo.go 中,EloSelector是整套算法的核心类型,文档中的核心 Go 代码即来自该文件的实际实现:
// Select returns the model with highest Elo rating func (s *EloSelector) Select(ctx context.Context, selCtx *SelectionContext) (*SelectionResult, error) { var bestModel string var bestRating float64 = math.Inf(-1) for _, candidate := range selCtx.CandidateModels { rating := s.getRating(candidate.Model, selCtx.DecisionName) if rating > bestRating { bestRating = rating bestModel = candidate.Model } } return &SelectionResult{ SelectedModel: bestModel, Score: bestRating, Method: MethodElo, }, nil } // UpdateFeedback adjusts ratings based on user feedback func (s *EloSelector) UpdateFeedback(winner, loser string, tie bool) { ratingA := s.getRating(winner) ratingB := s.getRating(loser) // Calculate expected scores expectedA := 1.0 / (1.0 + math.Pow(10, (ratingB-ratingA)/400.0)) expectedB := 1.0 - expectedA // Actual scores var actualA, actualB float64 if tie { actualA, actualB = 0.5, 0.5 } else { actualA, actualB = 1.0, 0.0 } // Update ratings s.setRating(winner, ratingA + s.kFactor*(actualA-expectedA)) s.setRating(loser, ratingB + s.kFactor*(actualB-expectedB)) }从当前仓库的完整源码看,实际实现比示例更丰富,包含若干值得注意的增强点:
1. 选择阶段(Select)
真实实现(elo.go第 300-400 行)在选出最高分模型之外,还做了这些工作:
- 按候选模型逐个取分:
getRatingsForCandidates按"分类专属评分优先、全局评分兜底、默认初始评分最后"的顺序取分(elo.go); - 概率化打分:选择得分采用 Bradley-Terry 概率标度
P = 10^(R/400) / Σ 10^(R_i/400)计算,而不是直接使用原始 Elo 分值,便于与其他选择器统一比较(elo.go); - 成本调节:当配置了
cost_scaling_factor且上下文携带CostWeight时,会对得分做归一化成本惩罚——更便宜的模型获得加分,从而实现质量与成本的平衡(elo.go); - 置信度计算:基于
min_comparisons阈值用 sigmoid 函数计算置信度1 / (1 + e^(-k*(comparisons-threshold))),比较次数越多置信度越高(elo.go); - 结果说明:返回的
SelectionResult包含Reasoning(如 "Selected based on Elo rating 1632.0 (win rate: 20/25)"),支持决策可解释性。
2. 反馈更新阶段(UpdateFeedback)
真实实现支持三种反馈模式(elo.go):
| 模式 | 输入 | 更新方式 |
|---|---|---|
| 成对比较 | 同时设置WinnerModel与LoserModel | 标准 Elo 双向更新 |
| 正向单模型反馈 | 仅设置WinnerModel | 评分增加K × 0.1(K 因子的 10% 奖励) |
| 负向单模型反馈 | 仅设置LoserModel | 评分减少K × 0.1(K 因子的 10% 惩罚) |
单模型反馈模式(applyWinnerOnlyFeedbackLocked/applyLoserOnlyFeedbackLocked)专门服务于"只知道单个模型表现好坏、没有对照模型"的自动信号式反馈场景,例如基于信号检测的隐式反馈(elo.go)。反馈对象Feedback结构体定义于 selector.go,支持Query、WinnerModel、LoserModel、Tie、DecisionName、UserID、SessionID、FeedbackType、Confidence等字段,其中UserID可用于按用户个性化学习。
3. 分类加权(Category-Weighted)
EloConfig.CategoryWeighted默认开启(DefaultEloConfig中置为true)。开启后,EloSelector内部维护两套评分表:全局评分globalRatings与按决策分类区分的categoryRatings(decision name → model → rating),反馈会同时更新两套评分(elo.go)。这意味着同一模型在不同类型请求(如"代码生成"与"数学推理")下可以有各自独立的评分,路由决策更具针对性。InitializeFromConfig还可以把配置中已有的静态 ModelScores(0-1 区间)线性映射到 Elo 评分区间1000 + score × 1000(即 1000-2000),作为冷启动先验(elo.go)。
4. 选择器工厂注册
在 src/semantic-router/pkg/selection/factory.go 中,当配置method: elo时,工厂会调用NewEloSelector(f.cfg.Elo)创建选择器并注册到Registry(选择器接口Selector定义于 selector.go,统一暴露Select/Method/UpdateFeedback等接口)。
配置详解
原文档给出的配置结构如下,它对应真实配置中decision.algorithm部分的elo算法块:
decision: algorithm: type: elo elo: k_factor: 32 # How quickly ratings change (16-64 typical) initial_rating: 1500 # Starting rating for new models storage_path: /data/elo-ratings.json # Persist ratings auto_save_interval: 1m # Save frequency models: - name: gpt-4 backend: openai - name: gpt-3.5-turbo backend: openai - name: claude-3-opus backend: anthropic对照 src/semantic-router/pkg/config/selection_config.go 中EloSelectionConfig的定义,完整的elo配置块支持以下全部参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
initial_rating | 1500 | 新模型的起始评分 |
k_factor | 32 | 评分变化速率(16-64 为典型区间) |
category_weighted | true | 是否启用按分类(decision)区分的评分 |
decay_factor | 0.0 | 对历史比较施加的时间衰减(0-1,0 表示不衰减) |
min_comparisons | 5 | 评分被认为是"稳定"所需的最小比较次数,也用于置信度计算 |
cost_scaling_factor | 0.0 | 成本考虑因子(0 = 完全忽略成本) |
storage_path | 空 | 评分持久化文件路径(如/var/lib/vsr/elo_ratings.json),不设置则仅存内存 |
auto_save_interval | 30s | 自动保存频率(仅当设置storage_path时生效) |
这些默认值在 elo.go 的DefaultEloConfig()中定义:InitialRating: 1500、KFactor: 32、CategoryWeighted: true、DecayFactor: 0、MinComparisons: 5、CostScalingFactor: 0。代码中对应的常量DefaultEloRating = 1500.0、EloKFactor = 32.0位于同一文件顶部(elo.go)。注意原文档中的auto_save_interval: 1m与代码默认 30s 都合法,实际以配置为准,resolveAutoSaveInterval会对非正数等非法值做防御性回退。
模型清单部分的name/backend声明用于确定候选模型池;若模型在 配置的 ModelParams 中带有Pricing.PromptPer1M单价信息,InitializeFromConfig还会将其作为成本表注入选择器,供成本调节使用。
提交反馈:驱动评分更新的入口
反馈通过 API 提交,原文档给出的接口示例如下:
# Positive feedback (model performed well) curl -X POST http://localhost:8080/api/v1/feedback \ -H "Content-Type: application/json" \ -d '{ "request_id": "req-123", "model": "gpt-4", "rating": 1 }' # Negative feedback (model performed poorly) curl -X POST http://localhost:8080/api/v1/feedback \ -H "Content-Type: application/json" \ -d '{ "request_id": "req-456", "model": "gpt-3.5-turbo", "rating": -1 }'需要注意的是,文档中的反馈 API 是面向网关部署形态的示例入口;在库的源码层面,驱动评分更新的真正接口是EloSelector.UpdateFeedback(ctx, *Feedback)。对照 selector.go 中Feedback的字段设计,一个完整的成对比较反馈(更贴近底层语义)大致对应:
feedback := &Feedback{ Query: "translate this paragraph", WinnerModel: "gpt-4", LoserModel: "gpt-3.5-turbo", Tie: false, DecisionName: "translation", // 分类加权时指定决策上下文 } selector.UpdateFeedback(ctx, feedback)正向/负向单模型反馈则只需设置WinnerModel或LoserModel其中之一,对应 API 中"rating": 1/"rating": -1的语义。
查看当前评分
curl http://localhost:8080/api/v1/ratings响应示例:
{ "ratings": { "gpt-4": 1632, "gpt-3.5-turbo": 1485, "claude-3-opus": 1558 }, "last_updated": "2024-01-15T10:30:00Z" }在库内部,EloSelector.GetLeaderboard(category)提供了等价的能力:按评分降序返回[]*ModelRating(可指定分类,空分类则返回全局榜单),每个ModelRating还携带Comparisons、Wins、Losses、Ties统计信息(elo.go)。持久化文件的 JSON 结构(StoredRatings)也包含version、last_updated、global_ratings与category_ratings字段(storage.go),与上述响应结构一致。
K 因子调优
K 因子决定单次反馈对评分的扰动幅度,是系统"学习速度"的总开关:
| K-Factor | 行为特征 | 适用场景 |
|---|---|---|
| 16 | 缓慢、稳定的变化 | 反馈一致的成熟系统 |
| 32 | 均衡(默认值) | 大多数生产环境 |
| 64 | 快速、剧烈变化 | 快速实验、新部署期 |
从源码实现看,K 因子还影响单模型反馈的步长(K × 0.1),因此调整k_factor会同时缩放两种反馈模式的更新幅度。建议从默认值 32 开始,观察一段时间评分演化后再决定是否调整。
持久化存储
开启storage_path即可让评分跨重启持久化:
elo: storage_path: /data/elo-ratings.json auto_save_interval: 1m原文档说明该文件具备原子写安全与备份轮转能力,这在 src/semantic-router/pkg/selection/storage.go 中有完整的实现佐证:
- 原子写:
saveToFile先写入elo-ratings.json.tmp临时文件,再通过os.Rename原子重命名替换正式文件,避免写一半导致损坏(storage.go); - 自动保存:
StartAutoSave启动后台 goroutine,通过 ticker 周期性检查dirty标志(MarkDirty置位),仅在评分确实变化时才落盘;关闭时执行最后一次最终保存(storage.go); - 损坏备份:读取时若发现文件损坏,会把原文件备份为
elo-ratings.json.corrupted后再报错,防止现场丢失(storage.go); - 数据清洗:
sanitizeRatings会把异常评分钳制在 100-3000 区间、清除非负统计值,防御脏数据(storage.go); - 存储接口抽象:
EloStorage接口(LoadRatings/SaveRatings/LoadAllRatings/SaveAllRatings/Close)支持文件与内存(测试用MemoryEloStorage)两种后端,便于替换(storage.go)。
启动时,NewEloSelector会通过loadFromStorage把文件中的全局评分与分类评分一并载入内存(elo.go)。
可观测性与测试验证
EloSelector通过 src/semantic-router/pkg/selection/metrics.go 中的 Prometheus 指标暴露演化过程,包括model_elo_rating(Gauge,当前评分)、model_feedback_total(Counter,反馈次数)以及选择相关的耗时/得分/置信度指标,便于监控"评分漂移"与模型支配情况。
仓库测试 src/semantic-router/pkg/selection/selector_elo_test.go 验证了算法的收敛行为:模拟 strong-model 分别击败 medium-model、weak-model,且 medium-model 击败 weak-model,共 10 轮(30 次成对反馈)后断言strong > medium > weak的评分次序成立,strong-model应累计 20 胜、weak-model应累计 20 负,且GetLeaderboard返回的榜单前三名顺序正确——这为"反馈越多,排名越可靠"提供了可执行的验证路径。相关生命周期与持久化行为还可参考 storage_test.go。
最佳实践
- 收集一致的反馈:确保反馈真实反映输出质量,避免噪声数据干扰评分;
- 从默认 K 因子起步:先观察行为再调整,不要一上来就用激进参数;
- 启用持久化:避免重启丢失已积累的评分(配置
storage_path+auto_save_interval); - 监控评分漂移:留意是否有模型被不公平地长期支配(可借助
model_elo_rating等指标); - 用先验引导冷启动:若已知模型的历史质量,可通过
InitializeFromConfig的静态 ModelScores 映射或直接设置initial_rating初始化评分,让新模型从合理起点开始学习; - 善用分类加权:请求类型差异明显的场景建议保持
category_weighted: true,让每个决策维度拥有独立的评分体系; - 配合成本因子:多模型成本差异较大时,可设置
cost_scaling_factor让评分在质量与成本之间取得平衡。
总结
Elo 评分选择为 Semantic Router 提供了一条"反馈即学习、零训练成本"的在线路由路径:以 Bradley-Terry 成对比较为数学内核,以EloSelector为运行时载体,通过UpdateFeedback消费用户反馈并持续刷新全局与分类两套评分,最终在每次请求到来时把流量导向当前评分最高的模型。它区别于 RouteLLM 等静态预训练路由器的地方在于——路由策略本身随线上反馈实时演化。结合原子写持久化、自动保存、损坏备份、Prometheus 指标与收敛性测试,这套实现可以独立、安全地嵌入生产网关,作为多模型调度决策层的一个稳定算法选项。进一步阅读可关注 elo.go、selection_config.go 与 storage.go 三份核心文件。
- 后端
- API网关
- 模型推理服务
- AI Agent
【免费下载链接】semantic-router
An open, programmable decision layer for models and compute.
相关推荐
semantic-router 的 Elo 选择算法:基于反馈评分驱动的模型路由实践指南
semantic router 的 Elo 选择算法:基于反馈评分驱动的模型路由实践指南 导读 本文围绕 semantic router 提供的 elo 模型选
后端API网关模型推理服务AI Agentsemantic-router 中的 Elo 评分模型选择:运行时在线学习选路算法实现指南
semantic router 中的 Elo 评分模型选择:运行时在线学习选路算法实现指南 本文围绕 semantic router 的 Elo Rating
后端API网关模型推理服务AI Agentsemantic-router 的 GMT Router 算法实战:基于多轮用户反馈的个性化模型选择
semantic router 的 GMT Router 算法实战:基于多轮用户反馈的个性化模型选择 gmtrouter 是 semantic router 中
后端API网关模型推理服务AI Agent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考