为什么选这个案例:它是本系列里少数"真实软件 + 客观判据"的端到端场景——需求(issue 描述)是模糊的自然语言,验收(测试由红转绿)是可机械判定的布尔值,中间全是 Agent 的自主决策。这条链路把 pr03 的 Agent 能力、pr06 的 Critic、pr09 的失败编码、pr14 的统计、pr15 的版本锁一次性串起来。

阅读路径:§1 讲清任务是什么(含流程图),§2 钉死评测协议(必须讲透的一段),§3–§4 是复现策略与 Agent 侧实现,§5 是务实的复现策略——跑通 vs 读懂失败的分工,§6 实验设计与统计,§7 可跑脚手架代码,§8 失败归因表,§9 效度威胁与诚实标注,§10 与其他案例文章的接口。


1. SWE-bench 任务到底是什么

每条样本(一个 instance)本质是一个四元组:真实 repo + 出问题的 commit + 一条自然语言 issue(problem_statement)+ 一份人工标注的"黄金补丁"(gold patch)及其配套测试。任务不是"续写代码",而是"像一位刚接手陌生代码库的工程师一样,读懂 issue、定位缺陷、改对地方、并且不碰坏别的东西"。

判定的关键在"配套测试"被拆成两组:一组是本该因这个 bug 而失败、修好后应当通过的测试(FAIL_TO_PASS,即缺陷的"最小复现");另一组是修之前修之后都必须一直通过的既有测试(PASS_TO_PASS,防止"修一个坏一片")。Agent 看不到黄金补丁,只能看到 issue 与仓库;它产出一个 patch,交给隔离环境去跑这两组测试来裁决。

输入 repo + issue + commit 软件工程 Agent ReAct + 编辑/搜索/shell (承 pr03) 产出 patch 可 git apply 的 diff 隔离测试容器 git apply + pytest FAIL_TO_PASS 判定 PASS_TO_PASS 回归 裁决:resolved = (FAIL_TO_PASS ⊆ green) ∧ (PASS_TO_PASS ⊆ green) resolved = ✓ 计入通过率 未解决 → 落 F 码 归因,见 §8
图 1-1 · SWE-bench 一条 instance 的端到端判定链路;Agent 只见 issue,成败由隔离容器里的两组测试客观裁决

与"代码补全"评测的根本差异:HumanEval 类基准只测"函数体对不对",而 SWE-bench 测的是跨文件定位 + 需求理解 + 不破坏回归——这三件恰恰是 pr03 Agent loop 里最容易翻车的地方。因此它是端到端工程实践最合适的载体:没有单一标准答案路径,却有一个残酷而客观的判据。


2. 评测协议:这一节必须讲清

很多人把 SWE-bench 简化成"跑分",但真正值得教的是协议设计——它决定了这个分数到底承诺了什么。协议由三个契约组成:隔离环境(每次从 base_commit 起一个干净容器,杜绝跨任务串味)、patch 判定(用 git apply 检验补丁可应用性,格式非法直接算失败)、以及两组测试的联合裁决。

2.1 fail-to-pass:相关测试从红转绿

FAIL_TO_PASS 是黄金补丁配套的那些"在缺陷版仓库里失败、在修复版里通过"的测试。它等价于把 issue 的"最小复现"编译成了可执行断言。Agent 的补丁只有让这一组全部转绿,才算真正触碰到了那个缺陷。单看"能跑起来""改动了文件"完全不够——改错地方、改了个 cosmetic,代码照样能跑,但 f2p 依旧红。

2.2 pass-to-pass:不破坏既有测试

PASS_TO_PASS 是一组"修复前后都必须通过"的既有测试,用来捕获回归破坏。真实工程里,"修一个 bug 顺手改坏三个功能"是常态;协议用 p2p 把这种代价显式地算进分数。这也是为什么单看 f2p 会高估能力:一个激进但鲁莽的补丁可能刷绿 f2p,却把 p2p 打崩,最终判负。

2.3 一条 instance 的联合判据

令 green 为该次运行结束后所有通过的测试集合,则单条样本被解决当且仅当两组测试都落在 green 里:

$$\text{resolved} \;=\; \big(\text{FAIL\_TO\_PASS} \subseteq \text{green}\big)\;\wedge\;\big(\text{PASS\_TO\_PASS} \subseteq \text{green}\big)$$

整体通过率为样本上的平均,工程上常配 pass@1(单次即解决的比例)与 pass@k(k 次采样至少一次解决):

$$\text{pass@1}=\frac{1}{N}\sum_{i=1}^{N}\text{resolved}_i,\qquad \text{pass@}k=\mathbb{E}_i\Big[1-\!\!\prod_{j=1}^{k}\!\big(1-\text{resolved}_{i,j}\big)\Big]$$

思考题:如果一个 Agent 从不调用测试、只凭"我觉得改对了"就交补丁,最可能栽在 f2p 还是 p2p?(答案:两者皆可能,但 p2p 尤其隐蔽,因为它惩罚的是你没意识到的破坏。)这正是 §4 让 Agent 具备 shell/pytest 自检工具的意义。


3. 子集复现策略:讲清方法,不求全量

全量 SWE-bench 有上千条 instance,环境构建动辄数小时——一次性全量跑既不可能也没必要。本篇的目标是方法可复现而非分数可比,因此只抽一小撮 easy 样本(社区标注为 Lite / easy 分层的子集)即可把整条链路演示清楚。

维度实操做法要记录的字段
样本规模抽 5–10 条 easy instance(覆盖 ≥2 个 repo)instance_id 列表入 Git
模型固定单一模型,勿中途换版本model + 权重版本 / 服务 tag
运行时镜像、Python、依赖一次钉死Docker 镜像 digest + lockfile
仓库快照锁定 base_commit,不接受漂移git commit hash(对接 pr15 hash 锁)

版本清单(对接 pr10 / pr13 / pr15):把"模型 / 运行时 / commit"三元组当作每次运行的外键写进 traces,任何一次复现都必须能凭这三项还原现场。少记录一项,这条实验就退化成"不可复现的轶事"。


4. Agent 侧实现:ReAct + 编辑 / 搜索 / shell

Agent 内核直接复用 pr03 交付的 ReAct 循环:Thought → Action → Observation 交替推进。区别在于工具集必须面向"软件工程"配齐三类:

  • search / read:跨文件检索与读源码——解决"定位不到文件"(§8 的 F1)。
  • edit_file:产出结构化 diff,而不是整文件重写——为"可 git apply"负责。
  • shell:允许在沙箱里 pytest / git diff 自检——把 f2p/p2p 的信号前移进决策环。

最后一步是把改动导出成标准 unified diff(git diff),作为提交给评测容器的 patch。这里最容易犯的错是格式非法(F4):手搓字符串拼 diff、行号 / 上下文行数不对、缺 @@ 头,都会让 git apply 直接拒绝。

工程教训(呼应 §2):Agent 不能"以为改对了就交卷"。让它在提交前自跑一次目标测试,把红/绿作为 Observation 回填 ReAct 循环,是把 pass@1 从"碰运气"变成"有反馈地收敛"的关键设计。


5. 务实的复现策略:跑通 vs 读懂失败

本篇最"工程"的一句判断是:从零跑通 SWE-bench 极其耗时,时间几乎全烧在装环境上,方法反而学不到。 因此把目标显式拆开——"从零跑通"与"读懂失败"是两件事:前者做一次端到端演示建立具身感即可,后者交给预跑日志(traces)来做重点剖析。

环节目标做法
端到端演示建立"协议长什么样"的具身感完整跑 1 例:issue→Agent→patch→容器判定绿;重点在解析 f2p/p2p 输出
失败归因把重心放在读懂失败用预跑日志(提前跑好的 traces.csv + report.xml)给失败样本打 F 码
改进优先级产出可执行的下一步汇总通过率、交叉表 F 码 × 出现率,得出"下一步改哪个"的结论(对接 pr09)

为什么提供预跑日志:它把"环境搭建的运气"从结论里剥离出去。若从零跑,失败往往是"依赖没装齐"而非"Agent 能力不足"——那会污染归因。预跑日志保证比对的是同一批决策轨迹,比较的才不是噪声。

5.2 可复现性检查清单(提交结果前逐条打勾)

  • ☑ 每条 run 记录了 模型 / 运行时 / base_commit 三元组,可还原现场(§3)
  • ☑ 隔离容器镜像带 digest,依赖用 lockfile 钉死,非"我机器上能跑"
  • ☑ patch 以 git diff 原生产出,容器内 git apply 成功记录为通过格式关
  • ☑ f2p / p2p 的测试清单原样来自 instance 元数据,未被手工裁剪
  • ☑ 所有轨迹汇入统一 traces.csv(沿用 pr03 schema,新增 task 级列,见 §7)
  • ☑ 每条失败都打了 F 码且可回溯到原始 trace 片段,双人标注一致性留档
  • ☑ 报告中的任何 pass@1 数字均标注来源、子集与版本,不跨口径比较、不作承诺(§9)

6. 实验设计:通过率曲线与统计严谨性

有了可复现的骨架,才能谈"控制变量做对比"。两个最值得实际跑的对照,都是承前启后的:

  • 通过率 × 模型规模:同一批 instance,比较不同参数量的模型 pass@1,观察"更大是否更稳"以及边际收益。
  • 通过率 × 是否加 Critic:对照组只跑 ReAct,实验组在提交前加一轮 Critic 自检(承 pr06),看它对 p2p 回归破坏(F3)是否有抑制作用。

统计侧严格照 pr14:每条样本多次重复(不同 seed / 温度),报告均值 + 95% 置信区间 + 效应量,而非单点分数。因为 SWE-bench 的 pass@1 对随机性极其敏感——一次绿不代表分布上绿。

模型规模 →(参数量,对数轴示意) pass@1(均值)↑ 仅 ReAct ReAct + Critic 示意曲线,非实测值
图 6-1 · pass@1 随模型规模上升但边际递减;加 Critic 整体抬升,差距主要来自更少 p2p 回归(示意)

预期趋势(供对照参考):曲线先陡后平——规模到一定阈值后,pass@1 的增益被"定位/回归"这类非参数瓶颈卡住;此时 Critic 对照的增益反而更明显,因为它补的是自检而非智力。

6.1 混杂控制与随机化

对照实验的结论只有在混杂被压住时才成立。SWE-bench 场景里的主要污染源:模型服务端的并发/温度、容器冷启动导致的超时、不同 instance 天然难度差异。做法遵循 pr14:运行顺序随机化、同批次固定预热、把"instance 难度"作为配对变量(同一 instance 的两个条件成对比较,而非拿两组不同 instance 求平均);效应量用配对差 report,置信区间按 instance 维度自助法(bootstrap)估计,避免把"哪条样本被抽到"的运气当成模型差异。

可延伸:若把子集从 easy 换到 hard,加 Critic 的增益会放大还是缩小?可以先假设、再用 §7 的脚手架在预跑日志上验证——这就是"假设 → 实验 → 归因"的完整闭环。


7. 最小复现脚手架(可直接改造)

以下四段构成一条完整链路:(a) 取任务+建隔离环境 → (b) 调 Agent 出 patch → (c) 容器内跑 pytest 判定 → (d) 轨迹落统一 CSV 并汇总通过率。所有外部接口(CLI 名、字段、镜像)以官方仓库为准,此处只给工程骨架。代码中 < / > / & 已按 HTML 规则转义,复制回终端时请还原。

7.1 (a) 拉取 task 的 repo / test 配置并建隔离环境

Bash
# 装官方工具并导出任务规格(含 repo / base_commit / 两组测试清单)
pip install swebench

# 从 all_tasks 里抽一条 easy 样本,单独落 task.json 便于版本管理
jq 'select(.instance_id=="sympy__sympy-17630")' all_tasks.jsonl > task.json

# 用官方 Dockerfile 建隔离镜像,digest 记进版本清单(§3 / pr15)
docker build -t swebench-run -f docker/Ubuntu.gp .
docker run --rm -v "$PWD:/io" swebench-run \
  python /io/run_setup.py --instance_id sympy__sympy-17630
# 容器内会把 repo checkout 到 base_commit 并装好测试依赖,现场可还原

7.2 (b) 调 Agent 生成可 git apply 的 patch

Python
import os, json
from agent import ReActAgent          # 承 pr03:ReAct + edit/search/shell 工具

task  = json.load(open("task.json"))
MODEL = os.getenv("MODEL", "llama-3.1-70b")   # 记录模型/运行时/commit

agent = ReActAgent(
    model=MODEL,
    tools=["search", "edit_file", "shell"],   # shell 让 Agent 能自跑 pytest(§4)
    repo=task["repo"], base_commit=task["base_commit"],
)

# 接口以官方为准:prompt 组织、终止条件、最大步数按你的框架实现
patch = agent.solve(issue=task["problem_statement"])

path = f"patches/{task['instance_id']}.patch"
open(path, "w", encoding="utf-8").write(patch)   # 必须是 git diff 原生输出

7.3 (c) 容器内跑 pytest 判定 fail-to-pass / pass-to-pass

Bash
cd /testbed
# 先过格式关:apply 失败即判 F4,不再往下跑
git apply /io/patches/sympy__sympy-17630.patch || { echo "F4:patch_illegal"; exit 1; }

# 相关测试:应从红转绿(FAIL_TO_PASS)
python -m pytest $(jq -r '.FAIL_TO_PASS[]' /io/task.json | tr '\n' ' ') \
  --junitxml=/io/report_f2p.xml

# 既有测试:应一直为绿,破了就是回归(PASS_TO_PASS)
python -m pytest $(jq -r '.PASS_TO_PASS[]' /io/task.json | tr '\n' ' ') \
  --junitxml=/io/report_p2p.xml

7.4 (d) 统一 traces.csv(沿用 pr03 schema + task 级列)并汇总

Python
import os, glob, csv
import pandas as pd
from judge import parse_junit          # 解析 report_f2p.xml / report_p2p.xml

# 在 pr03 的 run_id/step/phase/... 基础上,新增 task 级判定列
FIELDS = ["run_id", "task_id", "instance", "model", "base_commit",
          "elapsed_s", "resolved", "fail_code"]

rows = []
for pf in glob.glob("patches/*.patch"):
    inst = os.path.basename(pf)[:-6]
    f2p  = parse_junit("report_f2p.xml")
    p2p  = parse_junit("report_p2p.xml")
    resolved = all(t == "green" for t in f2p) and all(t == "green" for t in p2p)
    code = "" if resolved else classify(inst, f2p, p2p)   # §8 F 码归因
    rows.append(dict(run_id=RUN, task_id=inst, instance=inst, model=MODEL,
                     base_commit=COMMIT, resolved=int(resolved), fail_code=code))

with open("traces.csv", "a", newline="", encoding="utf-8") as f:
    w = csv.DictWriter(f, fieldnames=FIELDS)
    if f.tell == 0:
        w.writeheader
    w.writerows(rows)

df = pd.read_csv("traces.csv")
print("pass@1 =", round(df.resolved.mean, 3), "n =", len(df))

诚实提示:上面的 swebench CLI、镜像文件、字段名会随官方版本演进;请以其仓库为准微调。骨架的价值在于把"环境/Agent/判定/轨迹"四件事解耦——换模型只改 §7.2,换判据只改 §7.3,逻辑不动。


8. 失败归因表:F 码 × 出现率 × 改进优先级

预跑日志真正的价值在"看懂失败"。把每条未解决样本按轨迹特征归到一个 F 码(沿用 pr09 的失败编码精神,但针对软件工程场景定制),再做"出现率 × 可修复性"的交叉,产出改进优先级——而不是笼统地说"模型不行"。

F 码失败模式典型 trace 片段出现率改进优先级
F1定位不到缺陷文件反复 search 命中无关模块,从不 read 真正改点高P0
F2改错处(补丁未命中缺陷)patch 只动了文档/常量,f2p 目标测试仍红高P0
F3破坏 pass-to-pass(回归)f2p 全绿,但 p2p 里若干既有测试转红中P1(上 Critic 自检)
F4补丁格式非法 / 无法 applygit apply 报 corrupt patch,缺 @@ 头低P1(约束 diff 生成)
F5环境 / 依赖差异测试根本跑不起来(import 错、版本冲突)低*P2(属复现脆,非能力)

读表方式:F1+F2 指向"需求→代码定位"能力(应加检索/结构感知工具),F3 指向"缺乏回归自检"(应加 Critic,见 §6 对照),F4 是工具输出的格式约束问题,F5 属于效度噪声而非 Agent 缺陷。注意 *F5 若靠预跑日志已被隔离掉——这正说明为什么"从零跑通"和"读懂失败"要分开(§5)。


9. 效度威胁与诚实标注

把 SWE-bench 当工程实践参照,最大的风险是"把它的成绩当成绝对能力"。必须坦白下面这些效度威胁,并在报告里逐条标注:

  • 数据污染 / 过拟合:热门 repo 的 issue 与修复很可能进了训练语料,高分未必是"现场推理",可能是"背过"。这是社区对 SWE-bench 的核心争议之一。
  • 口径不一:各家报告的 pass@1 用的子集(Full / Lite / Verified)、评测脚本版本、是否允许 oracle 各不相同,跨来源直接比大小几乎无意义。
  • 复现脆:环境依赖多、镜像易漂移,同一套代码换个时间跑结果可能不同(这正是 F5 的来源)。

因此本篇所有数字都遵循一条纪律:标注来源、子集与版本,只陈述"在此配置下观察到什么",绝不承诺"该模型就是 X 分"。 把不确定性讲清楚,本身就是工程实践应传达的诚实(呼应 §5 清单最后一条与 pr14 的置信区间精神)。


10. 与其他案例文章的接口

本篇是整个案例链的收口之一,几乎把其他案例的能力点当输入来组装。下表标明每条依赖来自哪一篇、在这里承担什么角色。

案例能力 / 交付物在 SWE-bench 复现里做什么
pr03Agent 能力(ReAct + 工具 + traces schema)§4 Agent 内核、§7.2 与 §7.4 直接复用其 schema
pr06Critic / 自检§6 "是否加 Critic"对照组,抑制 F3 回归
pr09失败编码精神§8 定制软件工程 F 码,产出改进优先级
pr11 / pr12所用模型的压缩 / 提速背景§6 "通过率 × 模型规模"曲线的选型依据
pr14实证与统计§6 多次重复 + 效应量 + 95% 置信区间
pr15版本与回归管理§3 hash 锁、§5 清单的 commit/镜像 digest 记录
pr18(选做)多模态输入扩展若 issue 含截图/日志图,扩展到多模态定位
◑
一句话记住本篇

SWE-bench 的价值不在刷榜,而在让人亲手设计一次"可复现"、并诚实读懂"它为什么失败"。协议 > 分数,归因 > 跑通。


一页纸总结:任务 = repo issue + gold patch,判据 = resolved = (FAIL_TO_PASS⊆green) ∧ (PASS_TO_PASS⊆green);复现 = 子集抽样 + 模型/运行时/commit 三元组锁版本;实现 = 承 pr03 的 ReAct + 编辑/搜索/shell 产出可 apply 的 diff;分工 = 端到端演示建立具身感 + 读预跑日志做 F 码归因;对比 = 通过率 × 模型规模 / × Critic,配多次重复 + 效应量 + 95% CI;态度 = 数据污染/口径不一/复现脆一律标注来源与版本、不作承诺。