跳到主要内容

OpenAI simple-evals — 架构与原理

30 秒导读: 这是 OpenAI 用来给自家模型「跑分」的轻量库——你给它一个模型和一个基准(MMLU、MATH、HealthBench…),它把题目发给模型、收回答案、判对错、算出一个分数。它刻意做得很薄:全库的骨架只有两个抽象——Sampler(怎么问一个模型)和 Eval(怎么判一个基准),每个基准就是这两者的一次组合。

1. 这是什么(零基础也能懂)

一句话定义: simple-evals 是一个语言模型评测库——把标准化的题目喂给模型,自动判分,产出一个可复现的准确率。

解决什么问题 / 给谁用。 当你说「o3 在 MMLU 上拿了 92.9 分」,这个数字是怎么来的?评测对提示词写法极其敏感:同一个模型,用「你是一位专家程序员…」的角色扮演提示、还是用简单的「一步步解这道题」,分数能差很多(README.md:60-64)。OpenAI 开源这个库,就是为了把自己公布跑分时的判分逻辑摊开,让别人能复现。

它的取向很明确:只用零样本 + 思维链(zero-shot chain-of-thought)——不塞少样本示例、不做角色扮演,只给一句朴素指令,因为这更贴近真实使用(README.md:64)。

它能做什么:

  • 内置一批主流基准:MMLU、MATH、GPQA、DROP、MGSM、HumanEval、SimpleQA、BrowseComp、HealthBench。
  • 内置几种模型「适配器」:OpenAI Chat Completions、OpenAI Responses(含 o 系推理模型)、Anthropic Claude。
  • 跑完产出一个 HTML 报告(每道题的对话 + 判分)和一个 JSON 指标文件。

用起来什么样。 列出能评的模型、然后挑一个模型 + 题数开跑:

# 列出所有可评模型
python -m simple-evals.simple_evals --list-models

# 用 gpt-4.1 跑,每个 eval 取 10 道题
python -m simple-evals.simple_evals --model gpt-4.1 --examples 10

命令行解析与模型注册表见 simple_evals.py:29-315;每个模型名映射到一个预配置好的 Sampler 实例

一句话直觉/类比。 把它想成一台自动判卷机:Sampler 是「监考老师」——负责把卷子递给某个考生(模型)并收回答卷;Eval 是「阅卷标准」——负责拿到答卷后按某个基准的规则打分。换台考生只换监考老师,换张卷子只换阅卷标准,机器的传动结构(并发跑、聚合分数、出报告)永远不变。

2. 顶层全景(它大概怎么转)

怎么读下面这张图: 从左到右是一次评测的数据流。中间那条竖线是全库的唯一耦合点——Eval 只通过 sampler(messages) 这一个调用碰到模型,完全不关心对面是 OpenAI 还是 Claude。

simple_evals.py (编排层 / orchestrator)
────────────────────────────────────────────────────────────
--model 名 ──► 模型注册表 dict ──► 一个 Sampler 实例
--eval 名 ──► get_evals() ──► 一个 Eval 实例


eval_obj(sampler) ← 「拿这个考生,按我的标准跑」


┌──────────────────────── Eval.__call__ ────────────────────────┐
│ 对每道题并发跑 fn(row): │
│ │
│ ① 组 prompt ──► ② sampler(messages) ──► ③ 抽答案 ──► ④ 判分 │
│ │ │
│ ▼ │
│ ┌─── Sampler ───┐ (问模型这一步,可换实现) │
│ │ OpenAI / o系 │ │
│ │ Responses │ ← HTTP 调真实 LLM API │
│ │ Claude │ 带指数退避重试 │
│ └───────────────┘ │
│ │
│ 把每题的 SingleEvalResult 收集起来 ──► aggregate_results │
└────────────────────────────────────────────────────────────────┘


EvalResult(score, metrics, htmls, convos) ──► 写 /tmp/*.html + *.json

部件一句话职责:

部件干什么在哪个文件
编排层 main解析命令行、建模型注册表、对「模型 × eval」笛卡尔积逐个跑、写报告simple_evals.py:29
SamplerBase / Sampler定义「怎么问一个模型」:输入消息列表,返回 SamplerResponsetypes.py:18sampler/*.py
Eval定义「怎么判一个基准」:输入一个 sampler,返回 EvalResulttypes.py:59、各 *_eval.py
common.py共享件:并发执行、结果聚合、答案正则、HTML 报告模板、文本归一化common.py
types.py五个数据类/基类:Message/SamplerResponse/Eval/EvalResult/SingleEvalResulttypes.py

主线走一遍(高层,不进代码):

  1. 命令行选定一个模型名和(可选的)eval 名。main 在一个大 dict 里查到对应的 Sampler 实例(simple_evals.py:144-315),在 get_evalsmatch 到对应的 Eval 实例(simple_evals.py:350-435)。
  2. 对每个「模型 × eval」组合,执行一行 result = eval_obj(sampler)(simple_evals.py:476)——这就是整个库的核心调用
  3. Eval.__call__ 内部把数据集每道题丢进一个 fn,用线程池并发跑(common.map_with_progress,common.py:219);fn 里组 prompt → 调 sampler(...) → 抽答案 → 判分 → 返回一个 SingleEvalResult
  4. 所有单题结果交给 aggregate_results(common.py:183)求均值/标准差,打包成 EvalResult
  5. 编排层把 EvalResult 渲染成 HTML 报告 + JSON 落到 /tmp/,最后把所有组合的分数透视成一张表打印出来(simple_evals.py:481-525)。

3. 阅读地图(建议顺序)

这个项目结构清晰、规模中等,拆成四章由浅入深:

  1. 01-sampler-and-eval-protocol.md — 先吃透两个接口。理解了 SamplerEval 的契约,整库就通了。包含一次端到端的代码走读。
  2. 02-grading-strategies.md — 全库真正的工程含量在「判分」。这一章把四种判分套路讲清楚:正则抽字母、LLM 判数学等价、DROP 的 F1 词袋、LLM 当裁判(SimpleQA/BrowseComp)。
  3. 03-healthbench-rubric-grading.md — 旗舰 eval。它把「判一个标准答案」升级成「按几十条加权 rubric 逐条打分」,是 README 明说会长期维护的三个 eval 之一。
  4. 04-clever-bits-and-boundaries.md — 收尾:防泄题的 XOR 加密、GPQA 选项乱序消除位置偏差、bootstrap 误差棒等巧妙处;以及它的边界、横向对比和代码地图。

如果你只想要一句话精华: 评测的难点从来不是「调用模型」,而是「把模型吐出的那段自由文本,可靠地坍缩成一个可比较的判分」——读第 2、3 章。