从排行榜数字到可重放证据:UK AISI 与 EvalEval 的基准复现思路

2026-09-22 32 预计阅读时间: 1 分钟
来源: huggingface.co AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:12 分钟

模型排行榜通常只给出一个醒目的分数,却很少回答更重要的问题:用了哪一版数据、怎样调用模型、评分器是否变化、失败样本能否重新检查。UK AISI 与 EvalEval 对基准结果可复现性的关注,指向了一个关键转变——评测结果不应只是表格中的数字,而应是一组可以保存、审计和重放的实验记录。

由于现有摘要没有披露具体接口和内部实现,下面不会臆测 UK AISI 或 EvalEval 的产品细节,而是围绕这一主题给出一套可以直接落地的复现方法。

“可复现”不是只固定一个随机种子

对 AI 基准而言,可复现至少包含三个层次:

  1. 实验可追踪:知道使用了什么数据集、模型、提示词模板、推理参数和评分器。
  2. 评分可重放:即使模型接口已经变化,仍能利用保存的原始输出重新运行评分逻辑。
  3. 结果可比较:两次运行的差异能够定位到数据、模型、执行环境或评测代码,而不是只看到总分发生变化。

固定 seed 当然有用,但它无法解决所有问题。托管模型可能在相同名称下更新权重或服务栈;并发调度、采样实现和硬件差异也可能带来变化。因此,更稳妥的目标不是承诺“每个 token 永远相同”,而是保存足够完整的证据,让其他人能够解释差异。

一份合格的运行清单通常应包含:

  • 数据集名称、版本和内容哈希;
  • 模型标识,以及供应商提供的快照或版本号;
  • system prompt、用户提示词模板及其哈希;
  • temperaturetop_p、最大输出长度和随机种子;
  • 评测器名称、版本及代码哈希;
  • Python、依赖、操作系统和硬件信息;
  • 每道题的原始输入、原始输出和单项得分;
  • 超时、拒答、解析失败与重试次数。

把运行过程拆成不可混淆的三层

评测系统最好将下面三件事分开保存。

1. 数据快照

不要只记录类似 benchmark-latest 的别名。别名可能指向新内容,应同时记录不可变版本或对规范化数据计算 SHA-256。涉及受限数据时,可以只公开哈希、样本 ID 和访问说明,而不是直接发布内容。

2. 模型原始输出

不要只保留最终分数。解析器可能存在缺陷,裁判模型也可能升级;如果保存了逐样本输出,就可以在不再次调用昂贵模型的情况下重新评分。

原始记录还应区分以下状态:

  • 模型正常回答但答案错误;
  • 请求超时;
  • 服务端返回错误;
  • 输出无法被解析;
  • 安全策略拒绝回答。

把这些情况统统记成零分,会让不同系统之间的比较失真。

3. 独立评分器

评分器应像生产代码一样进行版本管理。对精确匹配、代码执行、模型裁判等不同评分方式,应明确其边界:大小写是否敏感、空白如何处理、代码运行有哪些资源限制、裁判模型使用了哪一个版本。

这样,模型执行和结果评分便可以独立进行:模型调用一次,评分器可以重放多次。

一个可运行的最小复现实验

下面是一个使用 Python 标准库的最小示例。它不是对 UK AISI 或 EvalEval 内部实现的复刻,而是展示如何生成包含数据哈希、运行器哈希、环境信息、逐题输出和汇总分数的评测产物。

将以下内容保存为 benchmark.py,使用 Python 3.9 或更高版本运行。接入真实模型时,只需替换 invoke_mock,并把供应商返回的模型快照、请求 ID 和错误信息写入记录。

import argparse
import hashlib
import json
import platform
import random
import sys
from datetime import datetime, timezone
from pathlib import Path

CASES = [
    {'id': 'geo-1', 'prompt': 'Capital of France?', 'expected': 'paris'},
    {'id': 'math-1', 'prompt': 'What is 2 + 2?', 'expected': '4'},
    {'id': 'color-1', 'prompt': 'Color of a clear daytime sky?', 'expected': 'blue'},
]


def canonical_bytes(value):
    return json.dumps(
        value,
        ensure_ascii=False,
        sort_keys=True,
        separators=(',', ':'),
    ).encode('utf-8')


def sha256_bytes(value):
    return hashlib.sha256(value).hexdigest()


def invoke_mock(prompt):
    text = prompt.lower()
    if 'france' in text:
        return 'Paris'
    if '2 + 2' in text:
        return '4'
    if 'daytime sky' in text:
        return 'Blue'
    return 'unknown'


def exact_match(response, expected):
    return int(response.strip().casefold() == expected.strip().casefold())


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--model', default='mock-model-v1')
    parser.add_argument('--seed', type=int, default=7)
    parser.add_argument('--output', default='run.json')
    args = parser.parse_args()

    random.seed(args.seed)
    runner_path = Path(__file__)

    config = {
        'model': args.model,
        'seed': args.seed,
        'temperature': 0,
        'evaluator': 'casefold-exact-match-v1',
    }

    records = []
    for case in CASES:
        response = invoke_mock(case['prompt'])
        records.append({
            'case_id': case['id'],
            'prompt': case['prompt'],
            'prompt_sha256': sha256_bytes(case['prompt'].encode('utf-8')),
            'expected': case['expected'],
            'response': response,
            'response_sha256': sha256_bytes(response.encode('utf-8')),
            'score': exact_match(response, case['expected']),
            'status': 'ok',
        })

    artifact = {
        'schema_version': '1.0',
        'created_at': datetime.now(timezone.utc).isoformat(),
        'dataset_sha256': sha256_bytes(canonical_bytes(CASES)),
        'runner_sha256': sha256_bytes(runner_path.read_bytes()),
        'config': config,
        'environment': {
            'python': sys.version,
            'platform': platform.platform(),
        },
        'summary': {
            'correct': sum(item['score'] for item in records),
            'total': len(records),
            'accuracy': sum(item['score'] for item in records) / len(records),
        },
        'records': records,
    }

    Path(args.output).write_text(
        json.dumps(artifact, ensure_ascii=False, indent=2),
        encoding='utf-8',
    )
    print(json.dumps(artifact['summary'], indent=2))
    print(f'Wrote reproducibility artifact to {args.output}')


if __name__ == '__main__':
    main()

运行两次并检查关键字段:

python benchmark.py --model mock-model-v1 --seed 7 --output run-a.json
python benchmark.py --model mock-model-v1 --seed 7 --output run-b.json

python - <<'PY'
import json

a = json.load(open('run-a.json', encoding='utf-8'))
b = json.load(open('run-b.json', encoding='utf-8'))

for key in ('dataset_sha256', 'runner_sha256', 'config', 'records', 'summary'):
    print(f'{key}:', 'MATCH' if a[key] == b[key] else 'DIFFERENT')
PY

不要直接期待两个 JSON 文件的整体哈希相同,因为其中包含运行时间和环境信息。真正需要比较的是数据、代码、配置、逐样本响应和评分结果。若要实现字节级一致,可以把时间等易变元数据放入单独的清单文件。

从本地脚本扩展到团队评测流水线

进入 CI 或共享评测平台后,可以将产物结构固定下来:

schema_version: '1.0'
run:
  benchmark: qa-mini
  dataset_digest: sha256:REPLACE_ME
  model: provider/model-snapshot
  seed: 7
  decoding:
    temperature: 0
    top_p: 1
    max_output_tokens: 128
scoring:
  evaluator: exact-match
  evaluator_version: '1.0.0'
artifacts:
  raw_responses: artifacts/responses.jsonl
  scores: artifacts/scores.jsonl
  summary: artifacts/summary.json

实际使用时,应将 REPLACE_ME 替换为数据文件的真实哈希,并尽量使用不可变的模型快照。流水线还应保存依赖锁文件,例如 requirements.txtuv.lock 或容器镜像摘要,而不是只记录一个容易漂移的镜像标签。

对于模型裁判,还需要额外记录裁判提示词、裁判模型版本和原始判定理由。模型裁判本身也可能不稳定,因此适合报告多次判定的一致率,或抽取一部分样本进行人工复核。

采用前检查:复现性不等于有效性

可复现的评测仍可能测错东西。一个错误评分器完全可以稳定地给出错误结论;公开测试集也可能已经进入训练语料。因此,团队在发布结果前应同时检查:

  • 能否从运行清单恢复数据、配置和评测器;
  • 能否只用原始输出重新计算总分;
  • 数据哈希、代码哈希和模型版本是否不可变;
  • 超时、拒答和解析失败是否被单独统计;
  • 是否报告重复运行的方差,而不只发布最好的一次;
  • 是否检查数据污染、测试集泄漏和指标局限;
  • 涉及敏感数据时,复现材料是否遵守隐私和授权边界。

UK AISI 与 EvalEval 所强调的方向之所以重要,是因为它把评测从“相信这个分数”推进到了“检查这份证据”。最务实的起点并不复杂:固定输入、保存原始输出、版本化评分器,再为每次运行生成机器可读的清单。做到这四点,排行榜才开始具备工程意义。


相关推荐