- 后端
- API网关
- 模型推理服务
- AI Agent
【免费下载链接】semantic-router
An open, programmable decision layer for models and compute.
elo是 Semantic Router(v0.3 文档体系)中一类反馈驱动的模型选择算法:它用 Elo 式的评分系统对候选模型进行两两比较并持续更新排名。本文以 elo.md 为主体,结合 pkg/selection/elo.go 的源码实现,完整讲解算法原理、选择流程、反馈 API、持久化机制与全部配置参数,读完即可在路由决策中配置并使用algorithm.type: elo实现"越用越准"的在线模型排序。
概述:elo 是什么
elo是一种基于历史成对比较(pairwise comparison)反馈的模型选择算法。它不再依赖一次性启发式规则,而是把用户/评估者每一次"哪个模型表现更好"的反馈沉淀为可复用的评分:候选模型通过 Elo 评分系统被持续排序,评分随新反馈不断演化。
该算法在源码中作为一类独立的选择方法存在,其方法标识定义于 pkg/selection/selector.go:
// MethodElo uses Elo rating system with Bradley-Terry model // Models are scored based on pairwise comparisons using preference feedback MethodElo SelectionMethod = "elo"其理论依据是RouteLLM: Simple and Effective LLM Routing(arXiv:2406.18665)一文,核心是利用Bradley-Terry 模型做成对偏好学习;在 pkg/selection/selector.go 的包注释中同样明确标注了该论文引用。
核心优势
- 复用历史反馈:不只看当前请求的即时启发式信号,而是利用历史积累的成对偏好数据。
- 在线学习:随着更多比较反馈到达,评分持续更新,无需重新训练模型。
- 类别加权:支持按决策类别维护独立评分,不同工作负载互不干扰。
- 可配置时间衰减:可逐步遗忘过时比较,适应模型能力漂移。
- 成本感知可选:在质量与价格之间做可配置的权衡。
算法原理:Bradley-Terry 模型与 Elo 更新
Elo 评分建立在Bradley-Terry 模型之上,该模型估计"模型 A 优于模型 B"的概率:
$$P(A \succ B) = \frac{1}{1 + 10^{(R_B - R_A) / 400}}$$
每次成对比较后,评分按如下规则更新:
$$R_A' = R_A + K \cdot (S_A - E_A)$$
其中:
- $K$ 为学习率(配置项
k_factor,默认 32,值越大评分波动越剧烈); - $S_A$ 为实际结果(胜 1.0、负 0.0、平 0.5);
- $E_A = P(A \succ B)$ 为期望得分。
源码中这条公式被完整实现于 pkg/selection/elo.go 的applyPairwiseFeedbackLocked:
// Calculate expected scores using Bradley-Terry model // E_a = 1 / (1 + 10^((R_b - R_a) / 400)) expectedWinner := 1.0 / (1.0 + math.Pow(10, (loserRating.Rating-winnerRating.Rating)/400.0)) expectedLoser := 1.0 - expectedWinner // Determine actual scores actualWinner, actualLoser := 1.0, 0.0 if feedback.Tie { actualWinner = 0.5 actualLoser = 0.5 } // Update ratings: R' = R + K * (actual - expected) winnerRating.Rating += e.config.KFactor * (actualWinner - expectedWinner) loserRating.Rating += e.config.KFactor * (actualLoser - expectedLoser)注意平局(tie)时,胜负双方实际得分各取 0.5,更新后评分总和守恒。每次比较同时累加Comparisons,并根据是否平局分别统计Wins/Losses/Ties。
当开启category_weighted时,每个决策维护独立的按类别评分(categoryRatings以decision name → model → rating的映射存储,见 elo.go),因此模型在 "math" 类别的表现不会影响其 "coding" 类别评分。
源码实现:EloSelector 的构成与初始化
配置结构与默认值
EloConfig结构体定义于 pkg/selection/elo.go,字段与 YAML 配置一一对应;其默认值来自 DefaultEloConfig 与顶层常量:
| 常量 | 值 | 含义 |
|---|---|---|
DefaultEloRating | 1500.0 | 新模型的初始评分 |
EloKFactor | 32.0 | 默认学习率 |
EloMinRatingFromScore | 1000.0 | 静态分数(0–1)转换 Elo 的基准 |
EloRatingRange | 1000.0 | 分数区间(0–1 映射到 1000–2000) |
默认配置为InitialRating=1500、KFactor=32、CategoryWeighted=true、DecayFactor=0.0、MinComparisons=5、CostScalingFactor=0.0。
评分数据结构
每个模型维护一条ModelRating记录(elo.go),包含评分与胜负平统计,同时作为持久化 JSON 结构:
type ModelRating struct { Model string `json:"model"` Rating float64 `json:"rating"` Comparisons int `json:"comparisons"` Wins int `json:"wins"` Losses int `json:"losses"` Ties int `json:"ties"` }注册与工厂装配
elo通过 pkg/selection/factory.go 的Factory.Create()创建:case MethodElo分支先构造NewEloSelector(f.cfg.Elo),再调用InitializeFromConfig(f.modelConfig, f.categories)用模型配置初始化评分与成本;在CreateAll()中还会注册到全局Registry(factory.go),便于运行时按方法名取出。
InitializeFromConfig(elo.go)承担"冷启动种子"职责:
- 为所有配置中的模型建立全局评分,初始值取
InitialRating; - 从模型配置的
Pricing.PromptPer1M读取成本,供成本感知选择使用; - 若开启
category_weighted,则把每个类别下的静态分数(ModelScores,0–1)线性映射为 Elo 评分:rating = 1000 + score * 1000。
关于"学习到的评分不应被静态种子覆盖"这一点,elo_category_seed_test.go 专门做了回归测试:从存储恢复出的已学习评分(如 1740)在InitializeFromConfig之后保持不变,而缺失的模型才使用静态分数作为种子(见 TestInitializeFromConfigSeedsMissingCategoryRating)。
选择流程(Select)
整体选择流程如下:
Select的源码实现(elo.go)与流程图中"按评分排名"的直观描述略有差异,实际采用概率化加权选择:
- 取评分:
getRatingsForCandidates按决策名优先查类别评分,缺失则回退全局评分,再回退到initial_rating(对应流程图中"Enough comparisons?"的 fallback 分支)。 - 计算选择概率:以标准 Elo 概率尺度
P_i = 10^(R_i/400) / Σ 10^(R_j/400)归一化为分数(这是 Bradley-Terry 模型的等价形式)。 - 成本调整:若
CostScalingFactor > 0且SelectionContext.CostWeight > 0,调用applyCostAdjustment(见下节)。 - 选出最优:取分数最高的模型,并基于评分稳定性计算置信度。
成本感知调整
applyCostAdjustment(elo.go)先把各候选成本归一化到 0–1(0 为最便宜,1 为最贵),再对便宜模型加成分数:
normalizedCost := (cost - minCost) / (maxCost - minCost) costBonus := (1.0 - normalizedCost) * costWeight * e.config.CostScalingFactor scores[model] *= (1.0 + costBonus)当所有候选成本相同(maxCost == minCost)时不做任何调整;costWeight来自请求上下文,cost_scaling_factor决定整体权衡强度。
置信度计算
calculateConfidence(elo.go)用 sigmoid 函数把比较次数映射到 0–1 置信度,min_comparisons即阈值:
k := 0.2 // Steepness threshold := float64(e.config.MinComparisons) confidence := 1.0 / (1.0 + math.Exp(-k*(float64(rating.Comparisons)-threshold)))比较次数达到阈值后,置信度快速趋近于 1;评分尚不稳定时置信度较低。选中的SelectionResult会携带Method: MethodElo、Score、Confidence以及形如Selected based on Elo rating 1568.0 (win rate: 20/30)的Reasoning(类别加权模式下还会加上Category 'xxx':前缀)。
GetLeaderboard(elo.go)则按评分降序返回全部模型的排行榜,可用于调试与监控。
反馈流程(Feedback)
反馈是elo学习的唯一数据来源,整体流程如下:
反馈 API
通过 HTTP 接口提交成对反馈以更新 Elo 评分:
curl -X POST http://localhost:8000/api/v1/feedback \ -H "Content-Type: application/json" \ -d '{ "query": "Solve: 2x + 5 = 15", "winner_model": "gpt-4", "loser_model": "llama-3.2-3b", "decision_name": "math_reasoning" }'反馈字段说明:
| 字段 | 必填 | 说明 |
|---|---|---|
query | 是 | 原始查询文本 |
winner_model | 是 | 更优的模型名 |
loser_model | 否 | 被否定的模型(用于成对比较) |
tie | 否 | 两个模型表现相当 |
decision_name | 否 | 类别上下文(用于类别加权 Elo) |
user_id | 否 | 用户标识(按用户追踪) |
confidence | 否 | 反馈置信度(0.0–1.0) |
源码中的三种反馈模式
Feedback结构定义于 pkg/selection/selector.go,除了文档表格列出的字段,还包含response(响应文本)、session_id、conversation_id、feedback_type(取值如satisfied、need_clarification、wrong_answer、want_different)与timestamp,供隐式反馈与多轮会话场景使用。
UpdateFeedback(elo.go)的处理路径是:先NormalizeFeedback规范化并校验,再更新全局评分,若开启category_weighted且提供了decision_name则同步更新类别评分,最后persistRatings。其内部updateRatingLocked(elo.go)支持三种模式:
- 成对比较(
WinnerModel与LoserModel都设置):标准 Elo 更新,走applyPairwiseFeedbackLocked; - 正向单模型反馈(仅
WinnerModel):applyWinnerOnlyFeedbackLocked给予K * 0.1的小幅奖励(约为 K 因子的 10%),用于信号驱动、只知道"某个模型表现好"的场景; - 负向单模型反馈(仅
LoserModel):applyLoserOnlyFeedbackLocked施加同等幅度的小幅惩罚。
反馈校验契约
feedback.go 定义了严格的反馈契约与对应错误:
- 反馈对象为空 →
ErrFeedbackRequired; winner_model与loser_model均为空 →ErrFeedbackModelRequired;- 两个模型名相同(含去空格后相同)→
ErrFeedbackSelfComparison(禁止自我比较); confidence超出 [0,1]、为 NaN 或无穷 →ErrFeedbackConfidenceRange。
NormalizeFeedback还会对所有状态键(模型名、决策名、用户/会话/对话 ID、反馈类型)做TrimSpace规范化,feedback_test.go 与 TestValidateFeedbackRejectsInvalidContracts 覆盖了上述全部边界情况。
持久化与自动保存
elo的评分是本地学习状态。配置storage_path后,评分通过FileEloStorage(pkg/selection/storage.go)持久化为 JSON 文件,包含version、last_updated、global_ratings与category_ratings四个部分(StoredRatings结构,见 storage.go)。
几个值得注意的实现细节:
- 原子写入:
saveToFile先写临时文件(path + ".tmp")再os.Rename原子替换,避免中途崩溃产生半截文件(storage.go)。 - 防御性加载:文件不存在或为空时返回全新状态;文件损坏时先备份为
path + ".corrupted"再报错(storage.go)。 - 数据清洗:
sanitizeRatings会把评分钳制在100–3000合理区间,并把负数的胜负平统计归零(storage.go)。 - 启动加载:
NewEloSelector配置了存储路径后,会先loadFromStorage()恢复已有评分再启动后台自动保存(elo.go)。 - 自动保存:
StartAutoSave以后台 goroutine 定期把"脏"状态(MarkDirty标记)写入磁盘,关闭时做最后一次保存(storage.go)。auto_save_interval若缺失或非法,会回退到默认的30s(auto_save_interval.go),因此文档示例中的1m仅是合法取值之一。
多副本状态告警
:::warning Multi-replica state
Elo 评分无论是保存在内存还是本地storage_path,都属于本地学习状态。在 Helm 部署下,只要某个活跃决策使用了algorithm.type: elo,多副本 router 渲染默认会被拒绝——因为各副本的本地评分会各自演化、产生不一致的选择结果。请保持单一 router 副本,或将选择器状态外部化(如共享存储/外部服务),再考虑关闭该安全护栏。
:::
配置详解
完整配置片段如下(对应ModelSelectionConfig.Elo,解析于 pkg/selection/factory.go):
algorithm: type: elo elo: initial_rating: 1500 # 新模型的起始评分 k_factor: 32 # 学习率(越高波动越大) category_weighted: true # 启用按类别独立评分 decay_factor: 0.0 # 旧比较的时间衰减(0 = 不衰减) min_comparisons: 5 # 评分稳定所需的最小比较次数 cost_scaling_factor: 0.0 # 成本权重(0 = 忽略成本) storage_path: /var/lib/vsr/elo_ratings.json # 评分持久化文件路径 auto_save_interval: 1m # 自动保存频率参数一览
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
initial_rating | float | 1500 | 新模型的起始 Elo 评分 |
k_factor | float | 32 | 评分波动性(建议范围 1–100) |
category_weighted | bool | true | 启用按类别独立评分 |
decay_factor | float | 0.0 | 旧比较的时间衰减(0–1) |
min_comparisons | int | 5 | 评分稳定所需的最小比较次数 |
cost_scaling_factor | float | 0.0 | 每 1M tokens 成本惩罚(0 = 忽略) |
storage_path | string | — | Elo 评分的持久化文件路径 |
auto_save_interval | string | 1m | 自动保存间隔(如5m、30s;源码默认回退 30s) |
参数背后的实现影响
initial_rating:决定新模型冷启动时的起点,也用于"无记录"时的评分回退(getRatingsForCandidates的最后兜底分支);decay_factor目前作为配置契约保留,默认 0 表示不衰减。k_factor:直接缩放每次更新的步长,updateRatingLocked系列函数与单模型反馈(K * 0.1)都依赖它。category_weighted:控制Select优先查类别评分还是全局评分(elo.go),也决定反馈是否同步更新类别评分。min_comparisons:作为置信度 sigmoid 的阈值,比较次数越过该值后置信度快速上升。cost_scaling_factor:只有大于 0 且请求携带CostWeight时才启用成本调整。storage_path/auto_save_interval:控制评分生命周期的两个关键参数——前者决定是否启用文件持久化,后者决定落盘频率;不配置storage_path时评分仅存内存。
它解决了什么问题?
当模型质量随时间漂移时,静态优先级列表与一次性启发式会迅速过时。elo让路由从成对反馈中持续学习,使候选排名反映的是观测到的实际胜负,而不是冻结的假设。源码中的 selector_elo_test.go 直观演示了这一演化过程:通过 10 轮"强胜中、中胜弱、强胜弱"的模拟反馈,strong-model/medium-model/weak-model的评分严格按预期排序(强 > 中 > 弱),排行榜顺序与胜负统计(强 20 胜、弱 20 负)均被断言验证——这就是"评分随反馈演化"的最小闭环。
何时使用
- 你能够收集路由级别(route-level)的反馈或质量比较;
- 排名需要随着更多比较反馈持续提升;
- 某条路由存在可重复的工作负载,评分体系能沉淀价值;
- 你希望在线学习,而不想为每次模型切换重新训练。
已知限制
- 需要足够的成对比较:评分在稳定之前需要足够样本,由
min_comparisons控制; - 冷启动问题:新模型一律从
initial_rating起步,与真实能力无关(不过可借助类别的静态ModelScores种子加速收敛); - 无语义理解:
elo纯粹由反馈驱动,不分析查询语义。
进一步探索
想深入验证或改造elo行为,可以围绕以下源码文件继续阅读:
- 核心实现:pkg/selection/elo.go(选择、反馈更新、成本调整、置信度、排行榜)
- 方法注册与装配:pkg/selection/factory.go、pkg/selection/selector.go
- 反馈校验契约:pkg/selection/feedback.go
- 持久化实现:pkg/selection/storage.go、pkg/selection/auto_save_interval.go
- 测试用例:selector_elo_test.go、elo_category_seed_test.go、feedback_test.go
- 指标观测:pkg/selection/metrics.go(
ModelEloRating、ModelComparisons、ModelWinRate等指标随选择与反馈自动上报)
其中elo在源码的算法分级测试中被归类为TierSupported(生产可用)(tier_test.go),无需额外外部依赖即可运行,适合作为反馈闭环型路由的落地起点。
- 后端
- API网关
- 模型推理服务
- AI Agent
【免费下载链接】semantic-router
An open, programmable decision layer for models and compute.
相关推荐
Semantic Router 的 Elo 评分模型选择:基于在线反馈的动态路由算法指南
Semantic Router 的 Elo 评分模型选择:基于在线反馈的动态路由算法指南 本文围绕 vLLM Semantic Router 项目中的 Elo
后端API网关模型推理服务AI Agentsemantic-router 的 Elo 选择算法:基于反馈评分驱动的模型路由实践指南
semantic router 的 Elo 选择算法:基于反馈评分驱动的模型路由实践指南 导读 本文围绕 semantic router 提供的 elo 模型选
后端API网关模型推理服务AI Agentsemantic-router 中的 Elo 评分模型选择:运行时在线学习选路算法实现指南
semantic router 中的 Elo 评分模型选择:运行时在线学习选路算法实现指南 本文围绕 semantic router 的 Elo Rating
后端API网关模型推理服务AI Agent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考