跳到主要内容

第 3 章 · 判分内核(日志 → status map → resolved)

这是「分数」真正诞生的地方。输入是上一章产出的测试日志 test_output.txt,输出是这道题 resolved: true/false。三步走:切日志 → 解析成状态表 → 和 F2P/P2P 比对。

3.1 第一步:从日志切出测试输出、解析成 status map

get_logs_evalswebench/harness/grading.py:39)做两件事:

  1. 先排雷:若日志里出现 APPLY_PATCH_FAIL/RESET_FAILED/TESTS_ERROR/TESTS_TIMEOUT 任一标记,直接返回「没测出结果」(:60-73)。
  2. 切段 + 解析:用第 2 章的两个标记把测试输出段切出来,再交给按仓库选定的解析器
# 真实源码节选 (grading.py:53, 79-89)
log_parser = MAP_REPO_TO_PARSER[repo]
...
test_content = content.split(START_TEST_OUTPUT)[1].split(END_TEST_OUTPUT)[0]
status_map = log_parser(test_content, test_spec)
if not status_map: # Modal 环境下输出可能跑到标记外
status_map = log_parser(content, test_spec) # 退而解析整段日志

这是它在干嘛:不同测试框架的输出长得完全不同,所以每个仓库映射到一个专门的解析器MAP_REPO_TO_PARSERswebench/harness/log_parsers/__init__.py:10)。解析结果是 {测试名: 状态} 的字典(status map)。

3.1.1 解析器举例:pytest vs Django

  • pytestparse_log_pytestlog_parsers/python.py:7-26):pytest -rA 的每行形如 PASSED test_x.py::test_foo。解析器逐行看开头是否是某个 TestStatus,是则把 行[1](测试名)映射到 行[0](状态)。
  • Djangoparse_log_django:64+):Django 的 runner 输出是 test_foo ... ok 这种,解析器改为匹配 ... ok / ... FAIL / ... ERROR 等后缀。

状态枚举见 TestStatusconstants/__init__.py:52-57):PASSED/FAILED/SKIPPED/ERROR/XFAIL

3.2 第二步:定义「通过」和「失败」

判分前先统一口径(grading.py:27-35):

# 真实源码
def test_passed(case, sm): # 在 map 里且状态是 PASSED 或 XFAIL
return case in sm and sm[case] in [TestStatus.PASSED.value, TestStatus.XFAIL.value]

def test_failed(case, sm): # 不在 map 里,或状态是 FAILED/ERROR
return case not in sm or sm[case] in [TestStatus.FAILED.value, TestStatus.ERROR.value]

注意两个细节:XFAIL(预期失败)算通过;一个测试根本没出现在日志里失败——保守判定,防止「测试压根没跑」被误当成功。

3.3 第三步:和 F2P/P2P 比对

get_eval_tests_reportgrading.py:94)拿 status map 去逐个核对 gold 给的两个列表:

  • 遍历 FAIL_TO_PASS 的每个测试 → 用 test_passed 分进 success / failure(:144-147)。
  • 遍历 PASS_TO_PASS 的每个测试 → 同样分桶(:150-153)。

比对矩阵(注释在 :110-120):

gold 类别评测结果含义
F2P通过成功(问题被解决)
F2P失败失败(没解决)
P2P通过成功(没改坏)
P2P失败失败(改坏了别的)

然后算两个比率(:194-212):compute_fail_to_pass = F2P 通过数 / F2P 总数;compute_pass_to_pass 同理。空集合按惯例记 1(:199-200)。

3.4 最终判定:resolved 三档

get_resolution_statusgrading.py:215-232)给出三档结论:

「怎么读:必须两条线都满分,才算 FULL(真正解决)。」

f2p == 1 且 p2p == 1 ─────────────► RESOLVED_FULL ✓ 算解决
0 < f2p < 1 且 p2p == 1 ────────────► RESOLVED_PARTIAL 部分
其它(含 p2p 没满分 = 改坏了) ────────► RESOLVED_NO ✗

get_eval_report:235)里只有 FULL 才把 resolvedTrue:289-290)。换句话说:只要弄坏了任何一个 P2P 测试,或没修好全部 F2P 测试,这道题就不算解决。这正是 SWE-bench 难刷分的根本原因——它要的是「真修好且没副作用」。

3.5 报告对象:每道题的四个布尔位

get_eval_report 产出的 report_map[instance_id] 带四个关键标志(:256-261):patch_is_None(补丁是空)、patch_existspatch_successfully_applied(日志成功解析出结果)、resolved。逐层短路:补丁为空就直接返回,解析不出结果就停在 patch_successfully_applied=False

3.6 一个边界:只有失败信号的仓库

少数前端仓库(chartjs/Chart.jsprocessing/p5.jsmarkedjs/marked,见 FAIL_ONLY_REPOSconstants/__init__.py:129-133)的测试框架不会逐个报「通过」,只报失败。对它们用 EvalType.FAIL_ONLYgrading.py:282-286):判定逻辑反过来——只要某测试没被明确标 FAILED 就当通过check_fail_only:130-137)。

3.7 汇总成 run report

所有题判完,make_run_reportswebench/harness/reporting.py:17)扫每道题的 report.json,分桶成 resolved / unresolved / error / empty_patch / incomplete,打印汇总并落盘成 <model>.<run_id>.json:127-159)。这份文件里的 resolved_ids 数量除以总数,就是大家常引用的「SWE-bench 解决率」。

3.8 小结

  • 判分三步:切日志段 → 按仓库选解析器得 status map → 和 F2P/P2P 比对。
  • 缺席的测试算失败;XFAIL 算通过;保守。
  • resolved 只认 FULL:F2P 全过 P2P 全过。
  • 个别前端仓库走「只看失败」的反向判定。