端到端评估运行器:五节课的管道,一次粘合 本节摘要:五节课的管道,一节课把它们粘起来。运行器读 70 节的任务规格,经适配器调模型,用 71 与 72 节打分,附上 73 节的校准报告,发出 74 节的排行榜。配套一个 接口(mock、本地、API 任模型都能用小方法面满足),用线程池并行跑任务,单遍同时组合指标层与校准层,发出每模型 喂进排行榜聚合器,输出 JSON 报告加 markdown 表;demo 自终止,干净跑退出码 0,校验或运行失败非零。读完本节,你交付的不是一个脚本,而是一个指标无关、提供商无关、可接真实模型的评估系统。 对应原课程:Phase 19 · Lesson 75 · (原英文 )。本节属第 20 章「毕业项目」的评估赛道,是本赛道的收官。
本节摘要:五节课的管道,一节课把它们粘起来。运行器读 70 节的任务规格,经适配器调模型,用 71 与 72 节打分,附上 73 节的校准报告,发出 74 节的排行榜。配套一个
ModelAdapter接口(mock、本地、API 任模型都能用小方法面满足),用线程池并行跑任务,单遍同时组合指标层与校准层,发出每模型EvalRun喂进排行榜聚合器,输出 JSON 报告加 markdown 表;demo 自终止,干净跑退出码 0,校验或运行失败非零。读完本节,你交付的不是一个脚本,而是一个指标无关、提供商无关、可接真实模型的评估系统。
对应原课程:Phase 19 · Lesson 75 ·
end-to-end-eval-runner(原英文phases/19-capstone-projects/75-end-to-end-eval-runner/docs/en.md)。本节属第 20 章「毕业项目」的评估赛道,是本赛道的收官。
阅读完本节,你应当能够:
ModelAdapter 接口,让任模型(mock、本地、API)用小方法面满足。EvalRun 记录,直喂排行榜聚合器。70~74 节每节拥有一个模块,运行器是集成点,它导入而非复制这些模块的逻辑。运行器是评估赛道的「端到端系统」对应物:前面五节课的部件单独成立什么也不证明,只有粘成一条流水线、跑同一固定集、出统一报告,才算交付。
适配器是运行器与任模型之间的缝,接口故意很小:
class ModelAdapter: model_id: str def generate(self, prompt: str, task: TaskSpec) -> Generation: ...
Generation 是 dataclass,含:
text:模型的自由格式输出。confidence:[0,1] 的 float,表示模型对该答案的自报概率。token_nll:可选,生成 token 上的负对数似然之和。token_count:可选,生成 token 数。运行器里的 mock 适配器提供三种风味:RuleBasedAdapter(确定、近乎完美)、NoisyAdapter(过自信、常错)、BiasedAdapter(某类好、另类差)。demo 在 70 节固定集上跑全部三种。
运行器用 concurrent.futures.ThreadPoolExecutor 按模型并行跑任务,工作数默认取 8 与任务数的较小者。线程够用,因为真实模型调用的瓶颈是网络 I/O;code_exec 路径在任务内起自己的子进程,执行器只调度等待。确定性测试下,运行器暴露 run_eval(adapters, tasks, parallel=False) 让测试钉死执行顺序。
每任务:
EvalRun 记录。(置信度, 正确) 对追加进校准缓冲。正确 信号对 exact_match 风格指标(exact_match、accuracy、code_exec)是 score >= 1.0,对分级指标是 score >= 0.5。阈值在 _correct_from_score,运行器不暴露公开覆盖。
def _score_one(adapter, task): prompt = render(task) gen = adapter.generate(prompt, task) norm = post_process(gen.text, task["post_process"]) s = score(task["metric_name"], norm, task["targets"]) return EvalRun(model_id=adapter.model_id, task_id=task["task_id"], metric_name=task["metric_name"], score=s, category=task["category"]), (gen.confidence, _correct(s))
每任务有结果后,运行器调 74 节的 aggregate 与 pairwise_diffs、73 节的 CalibrationReport.from_predictions,输出单 JSON 信封:
{ "leaderboard": [...], "pairwise": [...], "calibration": { "model_id_a": {"ece": 0.04, "brier": 0.10, "populated_bins": 8}, "...": {} }, "summary": {"tasks": 10, "models": 3, "wall_seconds": 1.2} }
运行器还把 markdown 表写到 stdout,让用户能把结果贴进 PR 评审。
demo 在 70 节的十条固定任务上跑三个 mock 适配器,墙钟应低于 10 秒,干净跑退出码 0。干净标准:每任务在 70 节下校验通过;每任务在 71、72 节下打分;73 节校准报告无错聚合;排行榜把规则适配器严格排在随机适配器之上。任一破裂,运行器非零退出并在 JSON 信封里带结构化错误。
| 维度 | 本节运行器 | lm-eval-harness | HELM | OpenCompass |
|---|---|---|---|---|
| 任务格式 | 70 节 JSONL | YAML/Py 类 | 严格 JSON | Py 类 |
| 适配器 | ModelAdapter.generate |
LM 抽象类 |
Service |
APITokenizer |
| 并行 | 线程池 | 线程/进程 | 进程 | 线程/分布式 |
| 指标分派 | 单字段 metric_name |
注册表 | 中央矩阵 | 注册表 |
| 校准 | 内建(73 节) | 部分 | 内建 | 部分 |
业界共识与本节一致:适配器是缝,运行器对指标与提供商都无关。HELM 的中央指标矩阵对应本节的封闭 metric_name 词表;lm-eval 的 LM 抽象对应本节 ModelAdapter。差别在本节单遍同时算指标与校准,而多数框架分两趟。
run_eval(adapters, tasks):本节是评估赛道的集成点;70~74 节每节拥有一个模块,运行器导入而非复制。ModelAdapter + Generation:任何模型只需写约三十行胶水(挑一个有免费额度的提供商),排行榜就亮起来;加第二个提供商,框架做剩下的活。main.py 是集成,通过一个小的 _load_sibling 助手按相对路径解析导入其他五课模块;Generation、EvalReport、ModelAdapter 在本地定义,mock 适配器在文件底部。从顶到底读:略读导入,看 run_eval,再看 _score_one,再看适配器,末尾 demo 是入口。code/tests/test_runner.py 钉死适配器接口、单遍循环、并行等于顺序、校准缓冲、JSON 信封形状。
本节不调真实模型、不实现 API key 流或限流处理、不做流式或部分生成(适配器每次返回一个生成)、不做重试或缓存——那些关切在适配器层;运行器对指标与提供商都无关。
💡 这个运行器是地基。生产评估系统再加:按
(task_id, model_id, model_version)键的结果缓存、跟踪每运行美元与 token 的成本账本、限流退避层、pass-at-k 任务的采样策略、长套件的流式输出格式。每一个都是单一关切,包裹运行器而不改指标或聚合层——这种分离正是契约的意义。
ModelAdapter 胶水,在固定集上看排行榜亮起来。(task_id, model_id, model_version) 键,重跑时命中缓存,报告墙钟节省。summary。code_exec 任务每模型采 n=10,接 72 节 pass_at_k,在排行榜里报 pass-at-1/5/10。ModelAdapter.generate(prompt, task) -> Generation(text, confidence, token_nll, token_count),mock/本地/API 任模型用小方法面满足。(置信度, 正确) 对进缓冲,正确阈值 exact_match 类 >=1.0、分级类 >=0.5。下一节,我们将切换到分布式训练赛道(Track H),从「集合通信从零」开始——用纯 Python 模拟 all-reduce、all-gather、broadcast 等集体通信原语,为后续 DDP、ZeRO、流水线并行打地基。