【学习笔记】AIHOT 拆解(五)模型榜:把 22 路公开评测压成一个共识排名——软支持度、加权不完全 Kemeny 与 HiGHS 精确求解

19 min

整理日期:2026-09-29 调研方式:完整克隆 [KKKKhazix/AIHOT],精读 packages/backend/src/leaderboard/method/v15.ts(379 行,方法本体)、kemeny.ts(149 行,HiGHS 整数规划求解器)、run.ts(99 行,发布门槛)、compute.ts(worker 线程卸载)、fetch/refresh.ts(101 行抓取调度)、fetch/identity.ts(73 行模型身份)与 docs/leaderboard.md。行号与常数以 commit 589f79e 为准。 本系列:一·总览 · 二·信源与抓取 · 三·精选与评分 · 四·聚簇与热度 · 五·模型榜(本篇) · 六·技术栈 写作动机:这个模块和新闻站主业完全无关,却是全仓库唯一一段「可以当论文读」的代码。作者在 /leaderboard/rules 页面公开全部方法,且 v15.ts 文件头写着「Everything here is part of the method: changing any constant changes reader-visible rankings and needs a new method version」——方法即代码,代码即方法。

一、太长不看

  1. 问题不是「平均分」而是「共识排序」:22 路公开评测量表不同(Elo 分、正确率、通过数)、覆盖不完整(每家只测部分模型)、误差异质(有的给置信区间有的不给)——加权平均在这三个问题下都会系统性出错。AIHOT 的目标是找一个完整排名,使「被它违反的加权净支持」最小(Kemeny 准则),再从排名反构一个 0–100 指数。
  2. 软支持度把分数差变成确信度:两模型在同一评测上的比较,若该评测发布了误差界,用 2Φ(Δ/SE) − 1(正态 CDF)给出 −1 到 +1 的软支持;没有误差模型的退化为符号。差 1 分但置信区间重叠的两个模型,不再产生虚假的强序关系。
  3. Kemeny 排序用整数规划精确解:y(i,j) 0-1 变量表示 i 排 j 前,目标函数化归后线性,三角(传递性)约束懒惰添加直到解是全序;HiGHS 求解器 mip_rel_gap = 0(必须证明最优)、限时 300 秒、固定随机种子。求解放在 worker 线程。
  4. 指数不是能力距离:SCORE_DEFINITION 原话——「Cumulative Kemeny ordering support relative to fixed anchors; not capability distance or calibrated win probability」。做法是从底部累积相邻名次的「翻转代价」,对 18 个固定锚点模型做 logistic 平均映射到 0–100。锚点只定尺度,不决定谁排哪。
  5. 发布门槛是方法论的一部分:总榜要求每模型 ≥3 来源、≥3 评测族、≥3 运营机构、≥3 类别、≥2 个直接锚点;发布窗口 18 个月;证据图必须单连通分量;证据指纹(方法的 SHA-256)不变就不发布;任何公开榜未解到最优 → 整轮标 failed、线上保留上一轮。
  6. 稳定性是算出来展示的:逐个踢掉运营机构/评测来源(重算资格)、每个来源权重 ±20% 扰动、全部比较退化为序数——每个模型给出「排名区间」,波动 ≥3 位标 sensitive。读者能看到结论对方法扰动的敏感度。
  7. 抓取层同样防御性十足:某个来源解析出的行数少于上次快照的一半按「坏了」处理而不是「新闻」处理,沿用旧快照;模型身份靠别名表归一,新名字按 slug 规则自动建档(「厂商可能不规整」)。

二、问题定义:为什么不是加权平均

三家评测,A 给 Elo 分(±30)、B 给正确率(±2%)、C 给通过数(无误差)。模型甲在 A 第一、B 缺考、C 第五;模型乙处处第二。加权平均没法回答三个问题:量表不可比(Elo 差 30 和正确率差 2% 不是一个东西)、覆盖不完整(甲在 B 缺考要怎么处理)、误差异质(A 上的「第一」可能和第二统计上不可分)。

Kemeny 准则换了一个目标:把每家评测的每个成对比较变成带权重的「i 应排在 j 前的支持度」,求一个全序使被违反的支持度总和最小。它天然处理不完整覆盖(缺的比较就是没有票)、天然容纳软支持(票有强弱)、输出是排名而非分数(量表差异被绕开了)。代价是:Kemeny 优化是 NP 难(等价于反馈弧集问题),所以要 ILP 精确求解器。

三、数据层:22 路证据怎么进来

  • 抓取调度(fetch/refresh.ts):每天 4 次(02:05/08:05/14:05/20:05),10 个抓取器(Artificial Analysis、LMArena、LiveBench、Epoch、EQ-Bench、Vals、DeepSWE、TapTap、Mercor、terminalBench)各自独立——失败的那个沿用上一份快照,方法把旧证据「carry forward」。并发度 2 且按注册顺序存储:「模型身份解析可能依赖前一个来源的别名,并发抓取不许重排快照写入」。
  • 合理性检查:plausible()——解析出行数 < 上次快照的一半 → 判「解析坏了」而非「世界变了」,沿用旧快照。0 行直接判坏。
  • 汇率与价格:USD/CNY 用欧洲央行参考汇率(经 Frankfurter API),落 fx_rates 表且价格列引用「哪天哪家汇率」;官方 API 价格来自种子文件,刷新时给没价格的模型补上。
  • 模型身份(fetch/identity.ts):lb_aliases 表把「每个来源对同一模型的不同叫法」归一到 lb_models;没见过的名字先按 slug 匹配("Llama 3 8B" → "llama-3-8-b"、"GPT-4o" → "gpt-4-o"),还不存在就自动建档(发布日期取来源给的,厂商缺省「其他」)并记下别名,下次直接解析。
  • key 体系:没有 ARTIFICIAL_ANALYSIS_API_KEY 时该项份额空着、不转给别家——「所以自部署站的榜和 AIHOT 的不一样」是明示的行为,不是 bug。

四、证据预算与来源登记

BUDGETS 十档:综合评测 30%、真人盲选 10%、编程与设计 12%、写作与表达 9%、数学与推理 12%、知识与事实 6%、视觉理解 6%、工具与办公 6%、中文与多语言 6%、行业专业任务 3%(专项合计 60%)。

SCORING_SOURCES 共 22 个计分来源,每个登记:权重、评测族(family)、运营机构(operator)、预算归属、是否有误差模型(intervalSd)、方向。全表(源码 SCORING_SOURCES 原始登记,权重为小数):

来源权重预算机构intervalSd计票
artificial-analysis(Intelligence 综合)0.30综合AA—✅
arena-text(文本盲选)0.05盲选LMArena1.96✅
arena-creative-writing(创意写作)0.05盲选LMArena1.96✅
arena-vision(视觉)0.06视觉LMArena1.96✅
arena-webdev(网页开发)0.024编程LMArena1.96✅
livebench-coding / reasoning / writing0.024 / 0.042 / 0.03编程/推理/写作LiveBench—✅
deepswe-v1-1(真实仓库级 SWE)0.036编程DataCurve1.96✅
taptap-maker(游戏开发)0.036编程TapTap1.96✅
epoch-frontiermath(/tier4)0.036 / 0.012推理Epoch AI1✅
epoch-gpqa / epoch-chess / epoch-mystery0.02 / 0.018 / 0.012知识/推理Epoch AI1✅
epoch-simpleqa0.04知识Epoch AI1✅
eq-creative / eq-longform0.036 / 0.024写作EQ-Bench—✅
mercor-apex-agents0.04工具Mercor—✅
vals-finance-agent0.03专业Vals AI1✅
artificial-analysis-multilingual0.06多语言AA—❌ 占位
tau-banking0.02工具Sierra—❌ 占位

几个值得注意的设计:

  • family 与 operator 双维度:同一家机构的多张榜单(Artificial Analysis 的综合与多语言、LMArena 的四张、Epoch 的五张)族不同但机构相同——资格策略同时在两个维度上要求多样性,防止「一家机构的五张榜」冒充独立证据。
  • scoring: false 的占位设计:artificial-analysis-multilingual(多语言)和 tau-banking(工具)两个来源保留预算份额但不投票——注释:「等待可比证据的预算化来源:它占着份额但不出票」。需要它出票的证据结构出现之前,其余来源按各自权重归一,而不是把缺席者的票转给别人(那样会悄悄改变其他来源的相对话语权)。
  • 误差模型现状:LMArena 系 intervalSd: 1.96(发布的区间即 ±1.96 SE)、Epoch/Vals 系 1、Artificial Analysis 与 LiveBench/EQ-Bench 无(退化为符号支持)。

五、软支持度与净支持矩阵

pairSupport(a, b) 的分支逻辑:

const soft = !ordinal && reg.interval_sd != null && 双方都有上下界;
if (!soft) return Math.sign(diff);          // 没有误差模型:只给方向
const sa = (a.upper - a.lower) / (2 * reg.interval_sd);   // 从发布区间反推 SE
const sb = (b.upper - b.lower) / (2 * reg.interval_sd);
const se = Math.sqrt(sa*sa + sb*sb);        // 零协方差假设
return se > 0 ? 2 * ndtr(diff / se) - 1 : Math.sign(diff);   // 2Φ(Δ/SE) − 1
  • 2Φ(Δ/SE) − 1 是「两个独立正态的均值差符号置信度」:Δ = 0 时为 0(不表态),Δ ≫ SE 时趋 ±1(强支持)。分数差被误差归一——同一分差在严谨评测上票更重,在噪声评测上更弱;
  • 零协方差是简化(同榜内模型间误差当然相关),但它是保守方向的简化:相关性会放大 SE 差异的效应,忽略它让支持度更接近符号而非极端值;
  • ordinal: true 强制全序数模式(只用符号)——这是稳定性分析的一景:如果软支持只在误差假设下成立,全序数重跑排名应当大体不动。

netMatrix() 把所有来源的所有成对比较加权累加成一个反对称矩阵 M(M[i][j] = −M[j][i],即「i 在 j 前的净加权支持」),同时记权重矩阵 W(双方共同被多少权重覆盖)和 shared(直接比较次数)。后续一切都吃这个矩阵。

六、Kemeny 求解:ILP 的形式与懒惰约束

kemeny.ts 的整数规划形式:

  • 变量:y(i,j) 对每个 i<j,1 表示 i 排 j 前,取值 {0,1},共 n(n−1)/2 个;
  • 目标:最小化被违反的净支持。数学上 Σ_{被违反对} |M_ij| 可线性化为 Σ max(0, M_ij) − Σ y(i,j)·M_ij(常数项进目标偏移)——一个标准的 feedback-arc-set 化归;
  • 约束的巧妙处:理论上全序需要指数级的传递性约束(对每个三元组 i>j, j>k ⇒ i>k),这里是懒惰添加——先解无约束的松弛,检查解里有没有传递性违例(a + b − c > 1 或 c − a − b > 0),有就加上对应三角约束重解,直到没有违例。「直到 incumbent 是全序」;
  • 严格性:mip_rel_gap: 0, mip_abs_gap: 0(必须证明到最优,不许近似收工)、random_seed: 0(可复现)、限时 300 秒;超时未证最优会被上层当「不可发布」处理;
  • 两阶段 tie-breaking:第一阶段解出最优序后,第二阶段保持最优成本不变(加一条成本上界约束,slack 与常数项成比例),最小化与「已发布基序」的成对冲突数——同一个最优解空间里选最接近现状的那个,避免每次重算时并列模型随机换位。注释诚实说明了这个 slack 的标度问题及失败时的回退(第一阶段结果作数);
  • 解完后 reversalCost() 用矩阵精确重算成本(不信求解器的浮点汇报),排序从 y 矩阵的胜场数重构。

整个求解通过 compute.ts 丢进 worker 线程(HiGHS 的 WASM 计算不阻塞事件循环)。

七、指数构建与展示

排名出来了,分数是从排名反构的:

  • gaps:对每对相邻名次,强制翻转它们(force: [[lo, hi]])再解一次 ILP,成本差 = 「让这对换位需要的额外支持」——这就是展示给读者的「名次差距」(差距 0.3 和差距 30 的相邻是两个世界);
  • v:从底部往上累积 gaps(第 i 名的值 = 它到末尾所有相邻差距之和)——本质是「从垫底爬到这里要逆多少支持」;
  • 指数:对 18 个锚点模型(ANCHORS,从 claude-fable-5 到 grok-4-3 的一串固定名单)在当前榜上的 v 值做 logistic(v − anchor) 平均 ×100。锚点的注释是「they never decide who ranks where」——只定尺度,不动排名。SCORE_DEFINITION 显式声明这个分数「不是能力距离、不是校准过的胜率」,防止过度解读。

配套输出:每个模型对前 30 名的逐对净支持(comparisons,模型页展开「它和某某的直接证据是什么」)、覆盖率(覆盖它的来源权重和)、以及加权一致性(observed_weighted_agreement:发布序与每个单源比较的加权一致度)。

八、稳定性分析与发布门槛

computeBoard() 里稳定性那段的计算量常被低估——它把整个求解重跑了 2×机构数 + 2×来源数 + 2 次以上:

  • 踢机构/踢来源:逐个移除每个运营机构和每个评测来源,重算资格(被踢后不再合格的模型退出)重解;另有一档「保持候选集不变」只移除票源。两档分别回答「少了这家的证据,世界排名变多少」和「只是不给它投票呢」;
  • 权重扰动:每个来源权重 ×0.8 / ×1.2 各解一遍;
  • 全序数:ordinal: true 再解一遍(不用任何误差信息)。

每个模型汇总出排名区间 [from, to](含固定候选集的 fixedFrom/fixedTo),to − from ≥ 3 或有情景未解到最优/不可用 → sensitive: true。读者直接看到每个名次在方法扰动下的活动范围——这在公开评测聚合里是罕见的诚实。

发布门槛(run.ts):inputFingerprint() = sha256(stableJson({method: METHOD_VERSION, boards: inputs}))——注释强调「一切能改变排名的都进来,一切不能的都不进(抓取时间、校验戳除外)」。指纹与上一发布轮相同 → unchanged,不发布(这就是「榜单不是每天抖动」的机制保证)。计算后,五个公开榜(overall/coding/reasoning/knowledge/professional)任何一个未解到最优或证据图不单连通 → 整轮 failed,线上保留上一轮,失败原因写进 run 记录。资格策略:总榜 {sources: 3, families: 3, operators: 3, categories: 3, directAnchors: 2};分类榜降为 2 来源 2 机构 1 锚;类别少于 5 个模型不开榜;发布窗口 18 个月(「窗口外的老模型不再排名,日期未知的保留」)。

九、批判与借鉴

  1. 预算权重是主观的,但它是被公开的主观:30/10/60 的分配、每来源的权重,都是作者的选择。方法的诚实不在于权重「客观」,而在于全部可查、改任何常数都要升方法版本并同步 rules 页(「页面上写的必须和实际算法一致」)。
  2. 零协方差假设是全方法最弱的一环:同榜模型误差明显正相关(共用题库、共用裁判),忽略它会让软支持偏强。作者的选择是不做无法验证的建模,把这一假设的检验交给「全序数重跑不动」这个粗 sensitivity。
  3. 计算成本可观:每轮 5 个榜 × (1 基础 + n−1 个相邻翻转 + 稳定性十几次) 次 ILP。每天 4 轮、每轮几分钟 worker 线程——个人项目里这是奢侈的,但也正是「精确解 + 证明最优」的代价。
  4. 锚点定尺度的副作用:新模型大量涌现时,锚点年龄结构变化会平移整个分数分布——跨时间比较分数没有意义(方法声明里其实已经隐含了这一点)。
  5. 可抄的三件东西:「指纹不变不发布」(防无意义抖动的通用模式,适用于任何定期重算的公开数据产品);「行数骤减按坏了处理」(抓取合理性的最小充分检查);「懒惰三角约束解 Kemeny」(任何需要「与成对证据最一致的排名」的场景——issue 去重、候选排序、评审聚合——都可以直接搬这 149 行)。

附:与总览篇的差异说明

本篇是总览篇第九节的展开:总览给了模型榜的功能画像(10 个抓取器、Kemeny、稳定性概念),本篇补齐了软支持度公式、ILP 形式与两阶段求解、指数构建、资格/发布门槛的实现与 22 来源登记表细节。两篇常数一致(method v15、18 个月窗口、连通性门槛、四班每日抓取)。