模型名是 Docker 的 :latest 标签,不是 Git 的 commit SHA。它是可变指针,不是内容寻址的不可变引用。你在论文里读到的「o3」、你 SLA 里写的「o3」、半年后 API 真正返回的「o3」,可以是三坨不同的权重。你团队里没人会拿 :latest 做生产回滚基准,但整个行业都在拿模型名做基准。
痛点:科学的最低要求是可指称——「实验对象是什么」必须能被唯一确定。而基准分数、论文结论、成本模型、SLA,全都靠一个字符串来指称对象。一旦名字→实际系统的映射会变,挂在这个名字上的结论就静默失效:没有报错,没有版本冲突,只有某天分数莫名掉了几个点。
机制上,一个「模型名」底下串着一整条会变的栈,每层都改变可观测行为:
这不是假设。ARC Prize 在 2024 年 12 月验证了一个未发布的 o3 预览版(high 配置、极高推理算力),在 ARC-AGI-1 上拿到 87.5%;2025 年真正发售的 o3,在同一基准上 low 档约 41%、medium 档约 53%。OpenAI 说明生产版 o3 与当初的预览版并非同一个模型(生产版整合了多模态能力,预览版当时用的推理算力预算在产品里并不可得)。名字相同,数字差三十多个点——指称漂移最干净的标本。
另一个方向是代号漂移:内部代号「Orion」曾被广泛预期是 GPT-5,最终于 2025 年 2 月以 GPT-4.5 发布。传闻里的代号性能和产品名的实测性能之间,连稳定对应关系都没有。
import anthropic, hashlib, json client = anthropic.Anthropic() # 需要 ANTHROPIC_API_KEY REQUESTED = "claude-sonnet-4-5" # 你请求的名字:一个可变指针 resp = client.messages.create( model=REQUESTED, max_tokens=64, temperature=0, messages=[{"role": "user", "content": "回复 OK"}], ) # 关键:响应体带回服务端实际使用的 model id —— 记它,别记你请求的字符串 print("requested:", REQUESTED, "| served:", resp.model) # 给每次评测运行打「内容指纹」:任一维度变了,历史分数就不再可比 run = {"served_model": resp.model, "temperature": 0, "max_tokens": 64, "prompt_tmpl": "eval-v3", "harness": "lm-eval@0.4"} fp = hashlib.sha256(json.dumps(run, sort_keys=True).encode()).hexdigest()[:12] print("run_fingerprint:", fp) # 用它当结果表的主键,而不是用模型名
resp.model 记下来对比。当成分布式系统里的依赖版本漂移处理:pin 快照、记指纹、留基线样本集做回归。像有人拿着一张「MySQL:18000 QPS」的图找你,却不说索引、连接池、缓存命中率、机器规格。你会立刻说:这不是数据库的属性,这是整个系统的属性。Agent 基准分数完全一样——分数属于 (模型 × scaffold × 采样预算) 这个联合系统,但排行榜只给你一列「模型名」。
痛点:排行榜的数据结构本身在撒谎。它呈现为「模型名 → 分数」的映射,暗示分数是模型的内在属性。但在 agentic 基准(SWE-bench、终端类任务)里,模型只是被反复调用的一个函数,外面那层脚手架(scaffold)——检索哪些文件、跑不跑测试、失败重试几次、候选补丁怎么排序——贡献了大量分数。
最关键的混杂是把多次尝试折叠成一个数。设单次尝试成功率为 p,若 scaffold 允许 k 次独立尝试、并能用回归测试筛出对的那个,成功率就变成:
pass@k = 1 − (1 − p)k
直觉解释:(1−p) 是一次失败的概率,k 次全失败就是它的 k 次方,用 1 减掉即「至少成功一次」。含义是只要 p>0,分数就随预算单调上升——p=0.3 时 pass@1≈0.30,pass@10≈0.97。所以只报分数不报 k 的榜单,藏起了一个可以随便调的自变量。
量级有多大?Anthropic 公布的数字:Claude 3.7 Sonnet 在 SWE-bench Verified 上是 62.3%,加自定义 scaffold 后 70.3%。同模型、同基准、同一天,差 8 个点——足以颠倒榜单上任何两个相邻名次。
import numpy as np def pass_at_k(n, c, k): """pass@k 的无偏估计(HumanEval 论文的标准做法): n 次采样中 c 次正确 → 1 - C(n-c,k)/C(n,k),用连乘避免大组合数溢出。""" if n - c < k: return 1.0 # 错的样本不够凑满 k 次全错,必然成功 return 1.0 - np.prod(1.0 - k / np.arange(n - c + 1, n + 1)) # 同一个模型、同一道题,单次成功率固定为 30%(100 次采样对 30 次) # 唯一变化的是 scaffold 允许试几次 for k in (1, 2, 5, 10, 40): print(f"pass@{k:<2} = {pass_at_k(n=100, c=30, k=k):.3f}") # pass@1 =0.300 / pass@5 =0.839 / pass@40=1.000 # 模型能力一个比特都没变,涨的全是采样预算 —— 报分不报 k 等于没报
两个团队都说「我实现了 JSON 解析」,但一个接受尾逗号一个不接受——规范之外的边界情况,由实现来定义。基准也一样:「MMLU」不是一个数据集,它是数据集 + prompt 模板 + 打分方式 + 抽取规则的四元组。换掉任一项,同一个模型能报出差好几个点的成绩,而两边都没撒谎。
痛点:你想复现别人的数字,跑出来差 4 个点,于是花一周怀疑自己的环境。真因通常在评分协议里,而论文往往只写了「我们在 MMLU 上评测」。EleutherAI 团队的 Lessons from the Trenches on Reproducible Evaluation of Language Models(arXiv 2405.14782)把这件事讲透了:模型分数对评测设置高度敏感,跨方法比较因此极难成立;结论是真正可共享的不是分数,而是完整的评测代码与配置——这正是 lm-eval 这类 harness 存在的理由。
从数据集到一个数字之间,有五道会改结果的闸门:
最容易被忽视的是打分方式。多选题有两条主流路径:(a) 让模型生成,再用正则抠出「A/B/C/D」;(b) 不生成,直接比较四个选项各自的 log P(选项 | 题干),取最大。路径 (b) 里还有一个岔路——要不要按 token 数归一:
scoreraw = Σ log P(ti) vs scorenorm = (1/T) Σ log P(ti)
直觉解释:每个 token 的 log 概率都是负数,所以选项越长,累加和越负。不归一化就系统性偏爱短选项;除以 token 数 T,变成「平均每 token 有多自然」,就转而偏爱长而流畅的选项。两种都合理,两种会选出不同答案——「同一个 MMLU」于是有了两个分数,差距常大于榜单上相邻名次之差。
import torch from transformers import AutoTokenizer, AutoModelForCausalLM tok = AutoTokenizer.from_pretrained("gpt2") m = AutoModelForCausalLM.from_pretrained("gpt2").eval() def score(ctx, cont): """返回候选续写的 (原始 log 似然和, 长度归一均值)""" ids = tok(ctx + cont, return_tensors="pt").input_ids n_ctx = len(tok(ctx).input_ids) with torch.no_grad(): lp = torch.log_softmax(m(ids).logits[0, :-1], dim=-1) tgt = ids[0, 1:] # 每个位置的真实下一 token s = lp[range(len(tgt)), tgt][n_ctx - 1:] # 只取候选答案那几个 token return s.sum().item(), s.mean().item() q = "The capital of France is" for c in [" Paris", " the beautiful old city of Paris"]: raw, norm = score(q, c) print(f"{c!r:34} raw={raw:8.2f} len-norm={norm:6.2f}") # raw 偏爱短选项,len-norm 偏爱长选项 —— 排序可能整个反过来 # 同一模型、同一题、两种都「正确」的评分协议 → 两个不同的基准分
mmlu 任务、5-shot、默认模板」是可复现的表述;「我们在 MMLU 上得了 X 分」不是。复现别人的数字前,先问「你用的哪个 harness、哪个版本」。拿「单元测试通过率」当「代码质量」的指标,会遇到两个独立的问题:一是它测的是代理不是本体(测试可以写得很弱,也可以被针对性地喂答案);二是它是有限抽样,本身有方差(跑 20 个测试和跑 2000 个,可信度完全不同)。基准分数一模一样地吃这两记。
问题一:构念效度(construct validity)——测的东西是不是你以为的那个东西。Raji 等人 2021 的 AI and the Everything in the Whole Wide World Benchmark(arXiv 2111.15366)系统地指出:被当作「通用能力」里程碑的那些基准,其任务选择本身是任意的,把它们的分数读成「通用智能进展」存在根本的效度问题。
两个硬实证。污染方向:Scale AI 团队重建了一套与 GSM8k 同分布、同难度的新题 GSM1k(arXiv 2405.00332),发现部分模型族掉最多 13 个百分点,呈系统性过拟合迹象(多个前沿模型族则基本没掉)——原分数里混了「记住了」而非「会做」的成分。弱判定方向:SWE-Bench+(arXiv 2410.06992)审查 SWE-bench 上被判「解决」的补丁,发现约 32.7% 的通过样本答案就写在 issue 或评论里(solution leakage),另约 31.1% 因测试太弱而无法判定补丁真的正确。那个数字里有相当一部分,不是在测「修 bug 的能力」。
问题二:统计不确定性。Miller 2024 的 Adding Error Bars to Evals(arXiv 2411.00640)说了句朴素却被普遍忽略的话:评测就是实验——题目是从一个看不见的超总体里抽的样,分数因此是估计量,必须带误差棒。n 道题、正确率 p 时,均值的标准误是:
SE ≈ √( p(1−p) / n )
直觉解释:分子 p(1−p) 是单题对错这个伯努利变量的方差(p=0.5 时最大,越靠近 0 或 1 越小);除以 n 再开方,就是「多测几道题,噪声按 √n 变小」。代进 n=500、p=0.70:SE≈2.0%,95% 置信区间约 ±4 个百分点。所以榜单上 70.3% 与 68.9% 的差距,在 500 题上通常根本不显著。而且这还是乐观估计——同仓库、同题型的题目彼此相关(成簇),真实方差比独立假设更大。
import numpy as np rng = np.random.default_rng(0) n = 500 # 基准题目数 a = rng.random(n) < 0.703 # 模型 A 每题对错(真实率 70.3%) b = rng.random(n) < 0.689 # 模型 B(真实率 68.9%) def se(x): # 均值的标准误 = 样本标准差 / √n return x.std(ddof=1) / np.sqrt(len(x)) print(f"A = {a.mean():.3f} ± {1.96*se(a):.3f}") # 95% CI 约 ±0.04 print(f"B = {b.mean():.3f} ± {1.96*se(b):.3f}") # 配对差分:同一道题上比 A 和 B,消掉「题目本身难易」这个共同噪声源 d = a.astype(float) - b.astype(float) print(f"A-B = {d.mean():+.3f} ± {1.96*se(d):.3f}") # 区间跨 0 → 差距不显著。配对比独立比较更省题,是私有 eval 的默认做法