数据截至 (上游 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)因此瘦成纯粹的字段搬运:
parse_eval_script(utils.py:213-217)把eval_script全文切成行列表;record_test_exit_code给每行包上退出码记录;FAIL_TO_PASS/PASS_TO_PASS若是 JSON 字符串就json.loads(utils.py:268-269);- 多模态题面(文本补丁带不动的图片基线)走
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)。一道题的容器内时间线:
- 起容器:容器
command="tail -f /dev/null"让它挂着待命。 - 打模型补丁:把
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,命中即停。全失败就判这道题「打补丁失败」。
- 跑 eval 脚本:把
test_spec.eval_script写成/eval.sh拷进容器(:359-364)执行。执行前还有一步大扫除git checkout -- . ; git clean -fd(:306)——把上一轮残留清干净。 - 收输出 + 判分:测试输出写进
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:37 把 eval_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 与否」。