AI/ML 详解:同名不同物 — 模型版本学与评测可复现性

Day 56 · 2026-08-30
面向:有编程经验的非 AI 方向工程师 · 难度:方法论 / 前沿

指称漂移与模型版本学Referential Drift & Model Versioning

测量方法论版本学
一句话类比

模型名是 Docker 的 :latest 标签,不是 Git 的 commit SHA。它是可变指针,不是内容寻址的不可变引用。你在论文里读到的「o3」、你 SLA 里写的「o3」、半年后 API 真正返回的「o3」,可以是三坨不同的权重。你团队里没人会拿 :latest 做生产回滚基准,但整个行业都在拿模型名做基准。

它解决什么问题 + 工作机制

痛点:科学的最低要求是可指称——「实验对象是什么」必须能被唯一确定。而基准分数、论文结论、成本模型、SLA,全都靠一个字符串来指称对象。一旦名字→实际系统的映射会变,挂在这个名字上的结论就静默失效:没有报错,没有版本冲突,只有某天分数莫名掉了几个点。

机制上,一个「模型名」底下串着一整条会变的栈,每层都改变可观测行为:

「同一个名字」底下的可变栈(任一层变化 → 行为变,名字不变)

底座权重 checkpoint→蒸馏 / 量化版本
↓
隐藏 system prompt→安全过滤 / 拒答层
↓
推理时算力预算→路由到哪个副本→你看到的输出

你能观测的只有最右边一格;名字只锚定了最左边一格,而且锚得很松

这不是假设。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)   # 用它当结果表的主键,而不是用模型名
常见误区 + 实践场景
「我固定了模型名,实验就可复现了」——不对。固定名字只固定了请求,没固定服务。尽量用带日期/版本后缀的快照 id,并把响应返回的实际 model id 记进结果,两者不一致时报警。开源权重之所以在研究上更硬,正因为它是内容寻址的:一个 checkpoint hash 就唯一确定了实验对象。
📌 BigCat 场景:工作流里凡是「上周还好好的、这周输出风格变了」的怪事,先别怀疑自己的 prompt——把 resp.model 记下来对比。当成分布式系统里的依赖版本漂移处理:pin 快照、记指纹、留基线样本集做回归。
Takeaway + 思考题
💡 模型名是可变指针;能复现的实验必须锚在内容指纹上,不是锚在字符串上。
🤔 你系统里有多少判断,实际是挂在一个「会变的名字」上的?如果它今晚静默换了权重,你多久能发现?

Scaffold 混杂变量Scaffolding as a Confounder

实验设计反直觉
一句话类比

像有人拿着一张「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 个点——足以颠倒榜单上任何两个相邻名次。

SWE-bench Verified · Claude 3.7 Sonnet · 唯一变化的是 scaffold

标准配置 62.3%
自定义脚手架 70.3%

分数=模型×scaffold×采样预算 k×评分协议
榜单只标注了左起第一项
代码示例
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 等于没报
常见误区 + 实践场景
「A 榜上比 B 高 3 分,所以 A 更强」——在 agentic 基准上几乎没有信息量:A、B 常跑在不同 scaffold、不同采样预算下。比较模型必须控制变量:同一 harness、同一 k、同一评分协议。否则你比的是两个团队的工程能力,不是两个模型的能力。
📌 BigCat 场景:给自己的 agent 建私有评测集时,把 scaffold 版本号和采样预算写进结果表的主键。否则三个月后你看到一条上扬曲线,却分不清是换了模型、改了 prompt,还是当初把重试从 1 次调成了 3 次。
Takeaway + 思考题
💡 Agent 基准测的是「模型 × 脚手架」的联合系统;不报 scaffold 和 k 的分数,等价于不报硬件的性能测试。
🤔 如果一个分数可以靠加大 k 单调抬升,那「能力」这个词到底该定义在哪个预算档位上?
📎 工程对应 → super-individual D6:Eval 工程

实现即定义:评测的可复现性The Harness Is the Benchmark

可复现性评分协议
一句话类比

两个团队都说「我实现了 JSON 解析」,但一个接受尾逗号一个不接受——规范之外的边界情况,由实现来定义。基准也一样:「MMLU」不是一个数据集,它是数据集 + prompt 模板 + 打分方式 + 抽取规则的四元组。换掉任一项,同一个模型能报出差好几个点的成绩,而两边都没撒谎。

它解决什么问题 + 工作机制

痛点:你想复现别人的数字,跑出来差 4 个点,于是花一周怀疑自己的环境。真因通常在评分协议里,而论文往往只写了「我们在 MMLU 上评测」。EleutherAI 团队的 Lessons from the Trenches on Reproducible Evaluation of Language Models(arXiv 2405.14782)把这件事讲透了:模型分数对评测设置高度敏感,跨方法比较因此极难成立;结论是真正可共享的不是分数,而是完整的评测代码与配置——这正是 lm-eval 这类 harness 存在的理由。

从数据集到一个数字之间,有五道会改结果的闸门:

数据集→prompt 模板
措辞 / chat 模板
→few-shot
条数 / 顺序
→打分方式
生成 vs 比 logprob
→答案抽取
正则 / 归一化
→分数

论文里通常只公开第一格和最后一格

最容易被忽视的是打分方式。多选题有两条主流路径:(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 偏爱长选项 —— 排序可能整个反过来
# 同一模型、同一题、两种都「正确」的评分协议 → 两个不同的基准分
常见误区 + 实践场景
「基准是个数据集,谁跑都一样」——错。基准是数据集 + 协议,协议藏在代码里。「我们用 lm-eval v0.4.x 的 mmlu 任务、5-shot、默认模板」是可复现的表述;「我们在 MMLU 上得了 X 分」不是。复现别人的数字前,先问「你用的哪个 harness、哪个版本」。
📌 BigCat 场景:把评测 harness 当依赖管——锁版本、进 lockfile、随结果归档配置。这和处理「同一段 SQL 在不同 optimizer 版本下执行计划变了」是同一套纪律:结果不可比的根因往往不在数据,在执行引擎。
Takeaway + 思考题
💡 分数不可移植,配置才可移植:能共享的最小可复现单元是「harness 版本 + 完整配置」,不是那个数字。
🤔 如果一个基准的分数只在给定实现下有意义,那跨论文、跨年份的「进步曲线」,我们到底在比较什么?

构念效度与误差棒Construct Validity & Error Bars

测量理论统计
一句话类比

拿「单元测试通过率」当「代码质量」的指标,会遇到两个独立的问题:一是它测的是代理不是本体(测试可以写得很弱,也可以被针对性地喂答案);二是它是有限抽样,本身有方差(跑 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 题上通常根本不显著。而且这还是乐观估计——同仓库、同题型的题目彼此相关(成簇),真实方差比独立假设更大。

n=500 时两个「相差 1.4 个点」的模型,95% CI 几乎完全重叠

模型 A 66 ├┤ 74  点估计 70.3%
模型 B 65 ├┤ 73  点估计 68.9%

→ 排名靠前 ≠ 更强;有效对策:配对比较(同题比同题)把共同难度差消掉
代码示例
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 的默认做法
常见误区 + 实践场景
「榜单上差 1 分是真差距」——在几百道题的基准上,1 分通常淹没在噪声里。反过来也要小心:分数没掉不等于没退化,聚合分数会把你在意的那个子集的退化平均掉。总分之外,永远要看你关心的切片。
📌 BigCat 场景:建私有评测集时,宁可 150 道贴合真实任务的题 + 配对比较,也不要 1500 道泛化题目。前者构念效度高(真的在测你的用途),配对设计又把方差压下来;后者分数漂亮,却对你的决策没有信息量。
Takeaway + 思考题
💡 一个分数要能被相信,得同时过三关:测的是对的东西(效度)、没被污染(干净)、差距超过噪声(显著)。
🤔 你手上正在依赖的那个「模型 A 更好」的判断,如果补上误差棒,还剩下多少?
📎 工程对应 → super-individual D6:Eval 工程

深入资源Further Reading

深入思考Deep Questions

1. 卡1 说名字会漂移,卡3 说实现即定义。把两者叠加,「模型 A 在基准 B 上得 X 分」这句话还剩多少信息量?
这句话表面是二元函数 f(模型, 基准),实际是 f(权重快照, 推理配置, scaffold, 采样预算, harness 版本, 评分协议, 抽取规则)——七个自变量只锁了两个,而这两个本身还是可变指针。所以它约等于「某个未指明系统在某个未指明协议下的一次观测」。不是零信息:数量级仍可靠(70% 与 20% 的差别抹不平),但不足以支撑细粒度排序——而排序恰恰是榜单的主要用法。解法和处理不可靠外部依赖同构:把隐含自变量显式化(记指纹、锁版本、报 k),把细粒度比较内部化(只信自己固定 harness 上跑出来的对比)。
2. 卡2 说分数可以靠加大采样预算 k 单调抬升。那「模型能力」能不能脱离预算被定义?
严格说不能,只能相对于一条能力-成本曲线定义:横轴推理预算(k 或美元),纵轴成功率;单点分数只是曲线上一个采样。不同模型的曲线形状不同:有的 pass@1 低但随 k 陡升(生成多样,能被筛选器救回),有的 pass@1 高但迅速饱和(错得自信,多试无用)。二者在某个 k 上可能打平,适用场景却相反。这接回 Day 54 的生成-验证鸿沟:k 的价值完全取决于你有没有比生成更强的验证器。没有验证器时,加大 k 只是多了几个分不清好坏的候选——pass@k 那条曲线在现实里兑现不了。报单点分数不仅丢了预算维度,还默认了「验证器免费且完美」。
3. 开源权重是内容寻址的(一个 hash 定一个模型),闭源 API 不是。这是否意味着只有开源上的研究才算科学?
结论太强,方向对。可复现性有层级:开源权重给「同一对象 + 同一环境」的完全可复现;闭源 API 至多给「同一快照 id + 同一参数」的条件可复现——条件是厂商没有静默换掉底层服务,而这一点你无法验证,只能信任。所以闭源上的实验更接近观测性科学(像天文学:对象不受你控制,你能做的是记录观测条件、重复观测、报告漂移)而非受控实验。这要求不同的纪律:标注观测日期、保留原始输出、维护一个跑得起来的金标准回归集去探测漂移。真正危险的是用受控实验的语气写观测性结果——把「GPT-X 具有性质 P」写成永久事实,而它其实是某个时间窗口内某个服务端点的行为。
4. 卡4 的两个问题(效度、方差)能不能靠「把基准做大做难」一起解决?
只能解决一半,还会引入新问题。加大 n 压方差(SE 按 √n 降),对效度毫无帮助——测错东西测一万遍只是错得更精确。二者还有张力:题目要多就得放宽收集标准或自动生成,这恰恰增加污染风险,也更容易混进弱判定(GSM1k 与 SWE-Bench+ 的发现都指向这里)。「做难」同样不是万灵药:更难常意味着更小众、分布更偏,离你真实用途更远,效度反降。提升效度靠的是另一类工作:重建同分布的新鲜题、加固判定、明确写出这个基准宣称测什么、不测什么。正确姿势是拆开做——统计设计管方差,测量理论管效度。混为一谈的结果,是把噪声压到 0.1 个点,却仍在测一件没人想测的事。
5. 把四张卡合起来:什么样的模型评测结论,才可以写进决策文档?
一张四项检查表,缺一项就降级为「参考信息」。(a) 指称:实验对象可唯一确定——记带版本的快照 id 与响应返回的实际 model id,标注观测日期,开源模型记 checkpoint hash(卡1)。(b) 联合系统:分数必须连同 scaffold 版本、采样预算 k、是否做了筛选/重排一起报,比较时三者一致(卡2)。(c) 协议:harness 版本、prompt 模板、few-shot 配置、抽取规则随结果归档,配置文件本身进版本库(卡3)。(d) 统计与效度:点估计带 95% 置信区间,比较优先用配对差分,并说明这个集宣称测什么、做没做污染检查(卡4)。再加一条元规矩:外部榜单数字进决策文档前一律降一档权重——它至多说明数量级,细粒度排序必须在你自己的评测栈上复现。本质和分布式系统的纪律同构:不可信的外部依赖不是不能用,而是必须显式声明不确定性边界,并在自己这侧建立可观测基线。