跳到主要内容

数据截至 (上游 commit 7a21e0577295)

第 2 章 · 评测主流程(TestSpec 与镜像)

本章追一条主线:从 predictions.json 到「测试输出日志」。2026 年中的重构改变了本章的地基: 镜像与 eval 脚本不再由 harness 现场生成,而是数据集每道题自带(image + eval_script 字段);TestSpec 从「编译器产物」退化成「薄数据类」。以前的「配方表 + 三段 bash + 三层镜像」全部退役。

2.1 入口:哪些题要跑

顶层入口是 main(swebench/harness/run_evaluation.py:699)。它先把预测加载成 instance_id → prediction 的字典,再用 get_dataset_from_preds(run_evaluation.py:542)筛出真正要跑的题:

  • 没有对应预测的 → 跳过(:609 一带);
  • 已经跑过(report.json 已存在)的 → 跳过(断点续跑,:624);
  • 空补丁的 → 跳过(:627 起)。

--predictions_path gold 是个特例:直接把数据集里的 gold patch 当作预测返回(get_predictions_from_file,swebench/harness/utils.py:37),用来自检环境。

2.2 TestSpec:从「编译器产物」退成「薄数据类」

老架构(已退役):harness 拿 SWEbenchInstance,按 MAP_REPO_VERSION_TO_SPECS[repo][version] 查配方表,由 make_repo_script_list/make_env_script_list/make_eval_script_list 现场拼出三段 bash(建仓/装环境/跑测试)。这套「评测时编译脚本」的体系——连同 versioning/ 子系统、constants/ 下的配方表——已全部移除

新架构: 数据集(HuggingFace)每道题直接带两个字段——image(预建好的镜像名)和 eval_script(评测脚本全文)。TestSpec 的 docstring 直说了前提:「Assumes images are already built and available」(swebench/types.py:25-28)。

make_test_spec(swebench/harness/utils.py:251-273)因此瘦成纯粹的字段搬运:

  1. parse_eval_script(utils.py:213-217)把 eval_script 全文切成行列表;
  2. record_test_exit_code 给每行包上退出码记录;
  3. FAIL_TO_PASS/PASS_TO_PASS 若是 JSON 字符串就 json.loads(utils.py:268-269);
  4. 多模态题面(文本补丁带不动的图片基线)走 image_assets 字段(types.py:40-42)。

TestSpec.eval_script 属性再把行列表拼回带 shebang 的 bash(types.py:44-49)。「脚本怎么装环境、怎么跑测试」这个问题,从评测时的代码逻辑,变成了数据集构建时的既成事实——这是本次重构的核心取舍:评测侧变简单、变确定,造题侧一次性把环境钉死进镜像。

2.3 镜像模型:三层 → 每题一个扁平预建镜像

老架构(已退役):base → env → instance 三层 FROM 链,env 镜像靠「环境脚本内容的 sha256」当缓存键复用。新架构下 sweb.base.*/sweb.env.* 命名已不存在,镜像是一题一个的扁平 sweb.eval.*:

  • 命名:sweb.eval.<arch>.<instance_id>:<tag>;发布到 DockerHub 时把 DockerHub 不允许的双下划线替换成魔法 token _1776_(ImageSpec.name,swebench/image_builder/image_spec.py:37-44);
  • 远程优先:is_remote_image(image_spec.py:51-53)——namespace 非空就从 DockerHub 拉官方预建镜像,省去本地构建;ARM/Mac 上 --namespace '' 改为本地构建;
  • 本地构建:build_instance_images(image_builder/docker_build.py:115)→ build_instance_image(:145),由 image_builder/prepare_images.py 编排。

为什么敢扁平化:重活(装环境)已经以「预建镜像发布到 DockerHub」的方式一次性做掉了——缓存复用从「同一台机器跨次跑」升级成「全网共享一份预建镜像」,三层缓存键的复杂度自然不再需要。

2.4 跑一道题:run_instance

核心在 run_instance(swebench/harness/run_evaluation.py:229)。一道题的容器内时间线:

  1. 起容器:容器 command="tail -f /dev/null" 让它挂着待命。
  2. 打模型补丁:把 model_patch 写成 patch.diff 拷进容器,逐个尝试三种命令直到成功(:301 起):
# 真实源码(run_evaluation.py:54-58):多级 fallback
GIT_APPLY_CMDS = [
"git apply --verbose",
"git apply --verbose --reject",
"patch --batch --fuzz=5 -p1 -i", # 最宽松:容忍行号/上下文偏移
]

这是它在干嘛:模型给的补丁经常和真实文件有轻微偏差,于是从最严格的 git apply 一路降级到最能容错的 patch --fuzz=5,命中即停。全失败就判这道题「打补丁失败」。

  1. 跑 eval 脚本:把 test_spec.eval_script 写成 /eval.sh 拷进容器(:359-364)执行。执行前还有一步大扫除 git checkout -- . ; git clean -fd(:306)——把上一轮残留清干净。
  2. 收输出 + 判分:测试输出写进 test_output.txt(:259-265),交给 get_eval_report 判分,结果写 report.json

2.5 eval.sh 内部:契约没变,产地变了

eval.sh 的内容契约和旧版一致:先把测试文件还原到 base_commit、再打官方 test_patch、然后在 >>>>> Start Test Output / >>>>> End Test Output 两个标记之间跑测试。差别只在产地——旧版由 harness 用 make_eval_script_list_py 现场生成,新版在数据集构建时就已经写好、随 eval_script 字段带来(task/repo.py:37eval_script 落盘为镜像里的 eval.sh)。

这两个标记(START_TEST_OUTPUT/END_TEST_OUTPUT,harness/constants/__init__.py:51-52)依然是判分器精确切出测试输出段的契约——见第 3 章。

2.6 小结

  • 数据集自带 image + eval_script;TestSpec 是薄数据类,make_test_spec 只做字段搬运。
  • 镜像扁平化:每题一个 sweb.eval.* 镜像,默认从 DockerHub 拉预建镜像,本地构建走 image_builder/
  • run_instance 起容器 → 多级 fallback 打补丁 → 跑 eval.sh → 收日志。
  • eval.sh 的内容契约(还原测试、打测试补丁、标记切分)不变,只是产地从「评测时生成」挪到「造题时生成」。

下一章:日志怎么变成「resolved 与否」。