ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

semantic-router 中的 Elo 评分模型选择:运行时在线学习选路算法实现指南

semantic-router 中的 Elo 评分模型选择:运行时在线学习选路算法实现指南
  • 后端
  • API网关
  • 模型推理服务
  • AI Agent

【免费下载链接】semantic-router

An open, programmable decision layer for models and compute.

项目地址:https://gitcode.com/gh_mirrors/sem/semantic-router
点击查看免费下载

本文围绕 semantic-router 的 Elo Rating 模型选择机制展开:它如何用 Bradley-Terry 模型驱动的运行时评分系统,根据真实用户反馈持续调整各候选模型的权重并选出最优模型。读完本文后,你将理解该算法的数学原理与选路/反馈闭环、能够正确配置elo参数(含各字段默认值与源码依据),并掌握反馈提交、评分查看与持久化存储的完整操作与实现细节。

一、运行时评分体系:机制总览与流程

Elo Rating 选择机制使用一个**运行时评分系统(runtime rating system)**对模型进行排序:获得正向反馈的模型加分,获得负向反馈的模型扣分。随着时间推移,表现更好的模型评分升高、被选中得更频繁。该方法基于 Bradley-Terry 模型(成对比较框架),通过在线学习持续改进模型选择(详见 elo 教程)。

关于 RouteLLM 的说明:RouteLLM 论文(Ong et al.)训练的是静态路由模型——在偏好数据上离线训练后用于路由决策。semantic-router 的实现采取了不同路径:一个运行时 Elo 评分系统,依据线上实时反馈动态更新评分,而非依赖预训练的静态路由策略。

算法整体流程如下(继承自原文档):

工作流程可以概括为四步:

  1. 所有模型从初始评分开始(默认:1500);
  2. 用户在获得响应后提供反馈(点赞/点踩);
  3. 评分根据反馈与“预期结果”的偏差进行调整;
  4. 评分更高的模型被选中得更频繁。

在源码中,这一机制由 EloSelector 实现,它实现了统一的 Selector 接口(Select/Method/UpdateFeedback等方法),并由 工厂 Create() 在method: elo时实例化、在CreateAll()中随其余选择器一起注册到全局 Registry。

二、数学基础

2.1 期望得分(Bradley-Terry 模型)

模型 A 战胜模型 B 的期望概率:

E_A = 1 / (1 + 10^((R_B - R_A) / 400))

其中:

  • R_A= 模型 A 的评分
  • R_B= 模型 B 的评分
  • E_A= 期望得分(A 获胜的概率)

2.2 评分更新

反馈之后,评分按下式更新:

R'_A = R_A + K × (S_A - E_A)

其中:

  • K= K-factor(控制波动幅度,默认:32)
  • S_A= 实际结果(1 = 胜,0.5 = 平,0 = 负)
  • E_A= 期望得分

2.3 示例计算

Model A: Rating 1500, Model B: Rating 1400 Expected score for A: 1 / (1 + 10^((1400-1500)/400)) = 0.64 If A wins (S_A = 1): New rating = 1500 + 32 × (1 - 0.64) = 1500 + 11.5 = 1511.5 If A loses (S_A = 0): New rating = 1500 + 32 × (0 - 0.64) = 1500 - 20.5 = 1479.5

可以看到:高评分模型赢下“被预期会赢”的对局时,只获得小幅加分(+11.5);而爆冷输掉时扣分更多(-20.5)——这正是 Bradley-Terry 框架“按意外程度修正”的体现。

三、核心算法与源码实现剖析

3.1 选择流程:Select

原文档给出的核心算法示意(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 }

当前源码中的 Select 实现在此基础上更完整,实际执行逻辑为:

  1. 取候选评分:getRatingsForCandidates优先取该决策(DecisionName)类别下的评分,取不到再回退到全局评分,都缺失则用initial_rating构造默认评分(源码 L622-L651);
  2. Bradley-Terry 概率化得分:每个候选模型按标准 Elo 概率尺度10^(rating/400)归一化为得分占比,总分异常为 0 时退化为均匀分布;
  3. 成本调整(可选):当cost_scaling_factor > 0且上下文带有CostWeight时,applyCostAdjustment会把成本归一化到 0–1,让更便宜的模型获得乘数加成(源码 L687-L723);
  4. 置信度:calculateConfidence用 Sigmoid 函数(陡度 k=0.2、阈值为min_comparisons)把“比较次数”映射为 0.5–1 之间的置信度——比较样本越少,结果越不可信(源码 L725-L738);
  5. 返回SelectionResult,其中Reasoning形如Selected based on Elo rating 1511.5 (win rate: 12/20),并对全部候选记录RecordEloRating指标。

3.2 反馈更新:UpdateFeedback 的三种模式

原文档给出的反馈更新示意(Go):

// 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)) }

源码中的 UpdateFeedback 支持三种反馈模式,入口在 updateRatingLocked:

模式输入条件行为
成对比较WinnerModel与LoserModel都非空标准 Elo 更新(applyPairwiseFeedbackLocked),双方期望得分由 Bradley-Terry 公式计算,Tie时双方按 0.5 计
正向自反馈仅WinnerModel非空该模型获得K × 0.1的小幅加分,计入 Wins(源码 L554-L565)
负向自反馈仅LoserModel非空该模型被扣K × 0.1,计入 Losses(源码 L567-L578)

成对模式的核心更新逻辑(applyPairwiseFeedbackLocked)与上文数学公式完全一致:

expectedWinner := 1.0 / (1.0 + math.Pow(10, (loserRating.Rating-winnerRating.Rating)/400.0)) expectedLoser := 1.0 - expectedWinner winnerRating.Rating += e.config.KFactor * (actualWinner - expectedWinner) loserRating.Rating += e.config.KFactor * (actualLoser - expectedLoser)

同时每次更新都会累加Comparisons/Wins/Losses/Ties统计,并通过RecordFeedbackMetrics记录新旧评分的监控指标。更新完成后调用persistRatings()将存储标记为 dirty(见第七节)。

3.3 全局评分 + 分类别评分双层结构

一个容易忽视但很重要的设计:EloSelector内部维护两套评分(源码 L100-L106):

  • globalRatings:与类别无关的全局评分;
  • categoryRatings:按决策名(decision name / 类别)维度的评分。

当category_weighted开启(默认开启)且反馈/选择携带DecisionName时,反馈会同时更新全局与该类别两份评分(updateCategoryRatings);选择时则优先使用类别评分、缺失再回退全局。这意味着同一模型在“代码”类别和“客服”类别下可以拥有不同的 Elo 表现,避免单一全能分掩盖领域差异。

冷启动播种:InitializeFromConfig会把配置中的类别级ModelScores(0–1 静态分)线性映射为 1000–2000 区间的初始 Elo(rating = 1000 + score × 1000,常量见 源码 L37-L41),对应 源码 L254-L297。这为“用已知先验质量做 bootstrap”提供了实现支撑(见第九节最佳实践第 5 条)。

3.4 反馈契约与校验

反馈输入的统一结构是 Feedback,关键字段包括:Query、WinnerModel、LoserModel、Tie、DecisionName、UserID、SessionID、ConversationID、FeedbackType(取值如satisfied、need_clarification、wrong_answer、want_different)、Confidence(0.0–1.0)。

进入选择器前,NormalizeFeedback / ValidateFeedback 会做契约校验:

  • winner_model与loser_model至少一个非空;
  • 两者都非空时不得是同一模型(禁止自我比较);
  • confidence必须在 0.0–1.0 之间且不能为 NaN/Inf。

四、配置

教程给出的基础配置示例(v0.2):

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

结合源码,elo参数块的完整字段与默认值如下。参数定义见 EloConfig(选择器侧)与 EloSelectionConfig(配置解析侧),默认值来自 DefaultEloConfig:

参数YAML 键默认值说明
初始评分initial_rating1500新模型的起始 Elo
K 因子k_factor32控制每次比较的评分变化幅度,越大越敏感、越易波动
分类别评分category_weightedtrue按决策类别维护独立评分,未命中时回退全局评分
时间衰减decay_factor0.0对陈旧比较施加时间衰减(0–1,0 表示不衰减)
稳定比较数min_comparisons5达到该比较次数后评分才被认为“稳定”,同时是置信度 Sigmoid 的阈值
成本缩放cost_scaling_factor0.00 表示忽略成本;大于 0 时对更便宜模型加成
存储路径storage_path空(不持久化)设置后启动时加载、反馈后落盘,例如/var/lib/vsr/elo_ratings.json
自动保存间隔auto_save_interval30s仅在设置storage_path时生效;配置非法值会回退 30 秒并告警

补充两点源码级事实:

  • auto_save_interval的解析由 resolveAutoSaveInterval 完成:任何缺失或非正值都会回退到 30 秒的defaultAutoSaveInterval并输出告警,确保坏配置既不会让time.NewTickerpanic,也不会静默禁用周期落盘;
  • method取值elo时,工厂会创建EloSelector并调用InitializeFromConfig注入模型配置与类别配置(factory.go L170-L175)。

五、提交反馈

按教程文档,使用反馈 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 }'

从源码结构看,无论 HTTP 层如何映射,最终都会落到选择器的UpdateFeedback入口,其输入契约即第三节 3.4 所述的Feedback结构。因此对接反馈时需要注意:

  • 正向/负向单模型反馈对应“自反馈”模式,仅调整该模型±K×0.1(K=32 时为 ±3.2 分),波动远小于成对比较,适合点赞/点踩这类只有单一模型上下文的场景;
  • 若同时给出winner_model与loser_model,则走完整的成对 Elo 更新,并可附带tie: true表示平局;
  • 携带decision_name时,会在category_weighted开启下同步更新该类别的评分;
  • confidence字段用于隐式反馈(检测可能不确定)场景,取值必须落在 0.0–1.0。

六、查看当前评分

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" }

在源码侧,评分的完整快照可以通过 GetLeaderboard 按评分降序获取(支持传类别名过滤),每个条目是 ModelRating:model / rating / comparisons / wins / losses / ties。落盘的 JSON 结构则定义在 StoredRatings:

type StoredRatings struct { Version int // 版本,便于未来格式迁移 LastUpdated time.Time // 最近更新时间 GlobalRatings map[string]*ModelRating // 全局评分 CategoryRatings map[string]map[string]*ModelRating // 分类别评分 }

七、K-Factor 调优

K-FactorBehaviorUse Case
16Slow, stable changes反馈稳定的成熟系统
32Balanced (default)大多数生产场景
64Fast, volatile changes快速实验、新部署

从源码看,k_factor唯一作用于两处:成对更新中的R' = R + K × (actual - expected),以及自反馈中的±K × 0.1。因此调大 K 会同时放大两类反馈的步长;调优时建议结合min_comparisons控制的置信度一起观察——样本不足时即使评分领先,Select返回的Confidence也会偏低(Sigmoid 尚未越过阈值)。

八、持久化存储

启用storage_path即可在重启之间保留评分:

elo: storage_path: /data/elo-ratings.json auto_save_interval: 1m

教程说明该文件是原子写安全的并带有备份轮转。结合 storage.go 的实现,可以进一步确认这些保障的具体机制:

  1. 原子写:saveToFile 先把 JSON 写入*.tmp临时文件,再os.Rename覆盖目标文件,避免写到一半崩溃导致文件损坏;
  2. 损坏文件备份:若读取到的文件无法解析,loadFromFile 会先把原内容备份为*.corrupted再报错,保留现场便于排查;
  3. 数值消毒:sanitizeRatings 会把越界的评分钳制到 100–3000 区间、非负的胜负平计数归零;
  4. 脏标记 + 周期落盘:反馈更新只调用 MarkDirty 置脏,后台 goroutine(StartAutoSave)按auto_save_interval周期检查脏标记并整体落盘;关闭时(Close)会再做一次最终保存,减少进程退出时的数据丢失窗口;
  5. 启动加载:NewEloSelector在配置了storage_path时先加载既有评分(_global键映射到全局、其余键映射到类别评分),失败仅告警不阻断启动(源码 L128-L156)。

此外,实现还预留了 EloStorage 接口(Load/Save/Close),当前提供文件与内存两种后端,测试可用MemoryEloStorage注入。

九、测试与行为验证

仓库为 Elo 选择器提供了可直接运行的行为测试 selector_elo_test.go:它模拟 10 轮三模型(strong / medium / weak)循环成对反馈,然后断言:

  • 排名演化正确:strong > medium > weak,且 strong 恰好 20 胜、weak 恰好 20 负;
  • GetLeaderboard("")返回的全局榜顺序为strong-model → medium-model → weak-model。
func assertEloRankingEvolution(...) { if strongRating.Rating <= mediumRating.Rating { t.Errorf("strong (%f) should beat medium (%f)", ...) } // ... 验证榜单顺序 if leaderboard[0].Model != "strong-model" { ... } }

这套测试同时覆盖了第 3.1 节所述的评分驱动选路与第 6 节所述榜单排序逻辑,是本地验证 Elo 行为是否符合预期的最轻量手段。另有 storage_test.go、feedback_test.go、auto_save_interval_test.go 等对持久化、反馈校验与自动保存间隔解析做了契约级测试。

十、最佳实践

  1. 收集一致的反馈:确保反馈真实反映响应质量,否则评分会被系统性带偏;
  2. 从默认 K-factor 开始:先使用k_factor: 32观察一段时间,再依据实际行为调整;
  3. 启用持久化:配置storage_path,避免重启丢失已积累的评分;
  4. 监控评分漂移:关注某个模型是否不公平地占据榜首(可结合RecordEloRating指标与GetLeaderboard快照);
  5. 用先验做冷启动:在有已知质量信息时,通过类别ModelScores提供静态分(0–1 映射到 1000–2000 初始 Elo),让新系统不必从“全员 1500”的盲区起步。

参考文件索引

内容路径
Elo 教程(本文主体文档)website/versioned_docs/version-v0.2/tutorials/intelligent-route/model-selection/elo.md
Elo 选择器核心实现src/semantic-router/pkg/selection/elo.go
反馈结构与 Selector 接口src/semantic-router/pkg/selection/selector.go
反馈契约校验src/semantic-router/pkg/selection/feedback.go
文件/内存存储后端src/semantic-router/pkg/selection/storage.go
自动保存间隔解析src/semantic-router/pkg/selection/auto_save_interval.go
选择器工厂与注册src/semantic-router/pkg/selection/factory.go
配置参数解析定义src/semantic-router/pkg/config/selection_config.go
Elo 行为测试src/semantic-router/pkg/selection/selector_elo_test.go
  • 后端
  • API网关
  • 模型推理服务
  • AI Agent

【免费下载链接】semantic-router

An open, programmable decision layer for models and compute.

项目地址:https://gitcode.com/gh_mirrors/sem/semantic-router
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进