SWE-bench 论文笔记:Can Language Models Resolve Real-World GitHub Issues?
1. 背景与动机
当前大语言模型的 benchmark 已经饱和,需要能覆盖更加前沿与大语言模型边界的benchmark。
一个好的 benchmark 构建起来非常困难,一是所选的任务需要有挑战性,二是模型输出的结果要方便验证。
现实的软件工程任务就非常满足这个要求,其本身就是一个很有挑战性的任务,需要理解庞大的代码库,定位问题并进行修复。修复的结果使用单元测试就能很好的验证。
而 HumanEval 等 benchmark 中,大部分只涉及几行代码就能解决的问题,无法满足当前要求,基于此,SWE-Bench 就被提出。
2. 核心贡献
提出了 SWE-Bench 用来评估真实软件工程任务上的解决能力,每个任务都需要LLM生成一个描述对现有仓库的修改的patch,修改后的代码会使用当前仓库的测试框架进行评估。
发布了一个SWE-bench-train数据集,包含 37 个仓库的 19000 个非测试任务实例。
3. 方法论
3.1 挑选 SWE-Bench 实例的方法

三步走,首先挑选 12 个 Python 代码比例在 90% 以上的流行仓库;
接下来从这些仓库中,挑选出满足:
-
PR必须明确 resolve 至少 1 个 issue
-
已经被 merge 的 PR
-
PR 必须修改/新增仓库件的测试文件
最后从候选的 PR 中,进行以下操作:
-
把 PR 涉及的所有相关测试用例(包括新增和修改的)提取出来的到 test_patch,并将其应用到base_commit 上;
-
在没有应用修复(新增)代码的情况下,运行这些测试:
- 预期:应该失败,证明 Bug 确实存在。
-
在应用修复(新增)代码的情况下,再次运行这些测试:
- 预期:测试应该**通过,**证明代码修复确实有效。
筛选标准:至少有1个测试用例在运行前失败,运行后通过,就保留,即至少需要一个
Fail_to_Pass,但不要求所有失败测试都被修复成功。
根据上述的操作,从 90000 多个 PR 中,最终过滤出了 2924 个符合要求的 PR 组成了SWE-Bench。
这里,选择至少通过 1 个测试用例的任务,是为了避免一些 trivial 的任务(没有实质性修改BUG、只是调整代码格式或者文档等),只要符合,就可以入选 SWE-Bench 中,这个是 SWE-Bench 的筛选标准,而不是 PR 的合并标准,一个合格 PR 被 merge 应该能通过所有的测试用例。
总结来说:

3.2 任务制定方法

模型会获得相应的 issue 文本以及完整的代码库,任务是模型基于当前代码库进行修改以修复当前 issue。在实际操作中,以 patch 文件的方式来体现模型为了解决 issue 修改了代码库中的哪些行的代码。
使用 unix 的 patch 命令将解决方案应用到原始代码库中,然后执行与之相关的测试用例,如果补丁应用成功并且所有测试用例都通过,则认为解决了问题。
3.3 执行验证
前面筛选只保证了 PR merged、看起来关联 issue,也修改了测试文件,但这仍然可能出现很多坏样本,如:
-
这个版本根本装不上/依赖对不上(跑不了);
-
PR 加的测试并不能稳定复现问题(测不出来);
-
测试失败是因为缺包/缺符号/名字不对,而不是因为bug(不公平、也不能代表修复能力)
所以,这里构建的时候做了执行验证:对每个候选任务,放到一个确定的执行上下文中,真实安装+打补丁+跑测试,任意一步失败就丢弃。
核心的执行流程是:
-
checkout 到 base commit (PR 合入前的代码 C)
-
在对应的 conda 环境中安装
-
先打上 test patch T(PR 新增/修改的测试)
-
跑测试,得到 logpre (修复前的测试日志)
-
再打上 solution patch (PR 的非测试代码改动,也就是 gold 修复)
-
再跑一次测试,得到 logpost(修复后的测试日志)
在 logpre 里,至少要有一个测试是 fail,在 logpost 里,这个测试变成 pass,如果找不到任何这样的测试,就认为这个任务测不出 PR 解决了什么,或者是 PR 改的是重构/风格/不影响测试的内容,所以剔除。
PR diff 的 .patch 由很多文件 block 组成,每个 block 对应一个文件的改动,在提取的过程中,用路径关键词(如test/testing/test等),把测试文件路径的 blocks 捞出来合并成 test patch(T),其余 blocks 合并成 solution 。
这里跑测试的范围并不是全量测试用例,而是从 test patch 中的 file blocks 里提取出的相关路径。
此外,还会排除一种不公平情况:测试调用了 PR 才新建的函数/类(因为命名可能完全没在 issue 里出现,这样等于让模型猜名字)。
论文中提到他们会在 logpre 里检查是否出现了ImportError/AttributeError ,并把这类任务实例移除掉。
3.4 任务实例的四大组件
最终,我们按照上述流程,得到了任务实例的4大组件:
-
C(Codebase):代码库快照(仓库 + base commit)
-
P(Problem statement):问题描述(相关 issue 文本与评论,这里需要截断到 PR 首次提交前,避免泄露解法)
-
T(test Patch):测试(从 PR 的测试改动中提取,作为验证目标)
-
:gold patch(PR 的飞测试改动部分,作为参考解)
并且测试补丁和 gold patch 都用 .patch格式保存。
3.5 优势
-
所有的任务实例都来自真实软件工程任务,传统的 benchmark 只涉及很短的输入和输出,而 SWE-Bench的任务实例每一个都是一个庞大的代码仓库+与之相关的一个issue。
-
能持续更新,在Python仓库上的更新的操作几乎不需要人工参与,可以挑选更新日期在模型发布之后的实例来保证模型从未见过。
-
issue的描述文本长而详细,平均达到了195个词,并且代码仓库通常包含成千上万个文件,解决这些实例需要在海量代码中找出那些需要被修改的少数几行代码。
-
对于每个实例,至少有一个
fail_to_pass的 test case,其中 40%的实例至少包含 2 个这类case,此外,还会运行 51 个(中位数)测试用例,以确保系统的原有功能不出问题。 -
跨上下文的代码编辑任务,任务实例不仅要求模型生成简短的代码片段,还要求它们在大型代码库的多个位置进行修改,SWE-Bench参考解决方案平均修改了1.7个文件、3.0个函数以及32.8行代码。
-
任务实例的解决方法多样,不局限于一种解决方法。
3.6 讨论
在执行验证阶段,本文将同一批测试用例跑了2次:
-
logpre:base commit + test patch T(只上测试,不上修复)
-
logpost:base commit + T + (再上 gold patch 修复)
对每个被运行到了测试 ,根据修复前/后的pass/fail,就会形成一个 分类:
| logpre | logpost | 类别 | 类别含义 |
|---|---|---|---|
| fail | pass | FAIL_TO_PASS | 有效修复:表明测试用例成功捕获Bug,并且被补丁完美修复。 |
| pass | pass | PASS_TO_PASS | 回归保护:补丁未对现有正确逻辑造成破坏,保证了系统的向后兼容性。 |
| fail | fail | FAIL_TO_FAIL | 无关错误/噪声:补丁未能改变失败状态。通常意味着该报错与当前Bug无关,或由环境及解析早上引起。 |
| pass | fail | PASS_TO_FAIL | 负面影响/不稳定:打补丁后反而使原有功能报错。通常代表测试用例不稳定(噪声)、验证集不一致,或补丁引入了新 Bug |
环境噪声:测试失败并不是因为代码逻辑没修复,而是因为运行环境导致的失败/不稳定,如版本不兼容、依赖问题等。
解析噪声:测试实际上跑了,也有结果,但把日志转成哪些测试pass/fail的过程中可能出错或不完备,这就会把某个测试的状态判错,或者漏掉。通常以下原因可能导致:
-
不同仓库/框架日志格式差异很大
-
测试名可能带参数化/动态生成、输出里可能截断/折行
-
有些结果不是简单的pass/fail,如(error、skipped、xfail/xpass等),本文中明确说了,无论状态是什么,最终都必须被映射成 pass 或 fail。
验证集合出现不一致:可以理解为,再定义的测试集合中,出现了连参考修复 都无法让它保持良性的测试行为。
过滤掉 F2F 和 P2F,本质上是为了提高评测的信噪比。作者宁可接受漏掉模型引发的一些深水区 Bug(假阳性),也不愿意接受因为环境问题错杀模型写的好代码(假阴性)。
所以,论文明确,评测时不把 FAIL_TO_FAIL 和 PASS_TO_FAIL 纳入计分,最终只要求模型让 FAIL_TO_PASS(该修好的必须修好)与 PASS_TO_PASS(别引入回归)全部通过。
4. 实验方法
每个任务的 issue 描述通常都很短(平均 195 个words),但整个代码库非常庞大(平均438K lines),整个输入会导致上下文过于庞大,超出了大部分模型的上下文窗口,为了解决这个问题,文章中提出使用基于上下文检索的方案,且因为自然语言查询代码与文档这种场景,使用向量检索的方式不合适,所以最终采用了 BM25 检索方法来为每个任务提供上下文。
同时,使用了 Oracle 检索 来作为理论对照(因为实际大部分情况我们无法预先知道),即直接提取 Github上解决该 issue 的 patch 中所有被编辑的文件。
最终,我们将任务指令、问题文本、检索到的文件和文档、示例补丁文件以及补丁生成提示词(如要求模型按 diff 格式生成、仅修改指定代码段等)一起构建成模型最终的输入。
输入的结构大致长这样:
1. 开头:指令 + <issue> ... </issue> 包住 issue 文本2. Code区块:<code> ... </code> 包住若干检索到的文件内容,每个文件用[start of ...] /[end of ...] 标记边界3. 示例patch文件, 并解释patch file里会包含:文件名、行号范围、删除行/新增行,且一个 patch可以修改多个文件。4. 最后是patch 提示词: - 要模型解决上面的 issue - 生成一个单独的 patch file - 这个 patch file 要能用 git apply 直接应用到仓库中 - 并且要求输出必须是上面示例的格式 - 然后让模型从 Respond below: 之后开始输出因为要处理长序列,所以论文选取了 ChatGPT-3.5(gpt-3.5-turbo-16k-0613)、GPT-4(gpt-4-32k-0613)、Claude2 和 SWE-Llama,下面是相关的实验结果。

表 2 是使用 BM25检索,不同上下文长度下的模型解决率,可以看到,长上下文上,SWE-Llama的解决率为0,说明这个模型在长上下文上存在明显短板。
在做 BM25检索的时候,作者把每个文件的内容在进入检索系统之前,在文件内容前面拼上文件路径,这样 issue 中如果直接提到了文件名/路径, BM25 更容易把它检索出来。
表 3 是不同最大上下文长度下,相对于基准文件(Oracle 文件,解决问题所需的关键文件)的 BM25召回率,可以观察到,上下文越长,召回率越高,说明更长的上下文能容纳更多检索到的相关文件,为模型提供更加充分的问题背景。

表 4 对比了不同模型最大 Token 数(上下文窗口大小),以及它们能覆盖的 SWE-Bench 任务实例比例,可以观察到,上下文窗口越大,覆盖的实例越多,ChatGPT-3.5 仅能覆盖 58.1%的任务实例,而Claude2 和 SWE-Llama 能覆盖 95% 以上,这直接影响了模型在 SWE-bench 上的可评估范围。表4 的 Caption 中也提到了:Llama-tokenized sequences are 42% longer on average than the equivalent sequence tokenized for GPT-4。
也就是说,最大 Token 数是模型自身 Token 体系下的数值,不同模型的分词器不同,导致同一段原始代码/文本,切出来的 Token 数不一样,比如SWE-Llama 的 10k 最大 Token 数和 GPT4 的 10k 最大 Token 数,虽然表明数值相同,但实际上能容纳的原始代码量不同,Llama 的分词器切的更细,10k Llama token,只相当于约 7k 的GPT4 token。

表 5 这里对比了不同模型在 SWE-bench 和 SWE-bench Lite 上的性能,其中:
-
% Resolved:模型成功解决问题的比例,即成功应用的补丁中,能够完全解决目标 issue 的比例。 -
% Apply:成功应用补丁的比例,模型生成的补丁文件能够成功应用到原始代码库的比例,仅关注补丁格式是否合法、能否被代码库接收,不涉及功能是否正确。
如果 % Apply低,说明模型对补丁格式(如diff语法、文件路径规范)、代码库结构(如文件层级、行号对应关系)不熟悉,生成的补丁存在语法错误或路径不匹配,这个属于格式层面的失败。
如果 % Apply 高但 % Resolved 低,说明模型能生成格式合规的补丁,但缺乏核心的软件工程推理能力,如找不到真正的Bug 位置、修复逻辑不完整、破坏了原有功能(PASS TO PASS 测试失败),属于逻辑层面的失败。

图4 这里是 SWE-bench 在 Oracle检索下,三个模型在 12 个仓库中的解决率对比。
5. 结论
在表 5 中可以看到,使用BM25检索的模型解决SWE-bench 任务实例的性能,总体来看,这些模Ω在解决问题时都遇到了很大的困难,即使表现最好的 Claude 3 Opus 也只有 3.79% 的解决率。

附录中的 表 18 使用了 Orcale检索的情况下,Claude2 的解决率从 1.97% → 4.80%。

总体结论:
-
作者测试的这些模型在 SWE-bench 上几乎不会修真实的 issue。
-
检索器非常关键,给到对的文件,成绩会明显上升,这也说明瓶颈之一是,模型常常没有拿到关键上下文/没定位到要改的文件。但同时作者也提到了,就算BM25 在更大窗口下能包含提高 Oracle文件的概率,但模型也未必能用好这些上下文。
-
不同仓库的难度差异明显,但每个模型解决的具体实例集合不一定重叠,在 Oracle设置下,Claude2 解了 110个、SWE-Llama 解了 91 个,但 Cluade2 只覆盖了 SWE-Llama 解集合的 42%。
-
上下文越长反而越糟,模型会被噪声代码干扰,定位能力不足。以 Claude2 为例子,在BM25 下 13k 上下文最好,27k/50k 反而下降。为了进一步验证噪声问题,它们做了一个
oracle-collapsed:只保留 PR 真正改动行附近(±15行)的上下文,结果显著提升。 -
新旧问题不是关键,作者把 Oralce 设计下的任务按 PR 年份(是否在 2023 之前)切分,发现多数模型前后差异不大,只有 GPT-4 在这个切分下表现出明显差异(表 7)。
-
微调模型对上下文分布移位敏感,训练时看 Oracle,测试时用 BM25 结果会意外的差,作者猜测,它们训练用的 Oracle 风格上下文(基本都是要修改的文件)微调的,而 BM25 会塞进很多无需修改的文件,模型却被训练成了看到的文件都要修改,所以导致结果变差。
-
直接生成 Patch 比重写整个文件更可行,但模型依然常输出不规范的 Patch。
-
模型给出的补丁倾向少改、改小。补丁远短于人类的 Gold Patch,且经常只改动 1 个文件,作者进行了对比,发现 Gold Patch 平均 74.5 行,而模型成功 apply 的 Patch 平均只有 30.1 行,且很少改超过 1 个文件。
-
典型的失败模式是:定位对了函数,但漏掉关键条件/约束,以及不按项目风格写代码,作者也总结了一些共性文件:
-
模型经常写更原始/直接的 Python,不太利用代码库已有工具或第三方库;
-
解决问题更“贪心化”(之图把眼前的问题解决),不太顾及代码风格或隐含约束;
-
相比之下,Gold Patch 往往会做更结构化、更大范围的改动,甚至提前规避未来问题。
-
-
此外,本文还用 Radon 之类的工具来计算 Cyclomatic complexity 和 Halstead complexity,对比模型 patch 和 gold patch 改动后代码的复杂度变化,图 10 展示了一个典型的现象:模型可能用更少行数修复了bug,但把条件逻辑塞进了核心类中,导致圈复杂度上升;而 gold patch 虽然更长,但改在更合适的位置、结构也更加清晰,还引入了更语义化的错误类型等,这也说明了,测试过了 ≠ 工程质量好。

6. 论文的不足
-
论文中所有的仓库均选自 Python 仓库,且只有 12 个仓库,项目、编程语言和任务范围覆盖面不够全。
-
论文中全部选择热门项目,这会系统性的低估长尾仓库/维护较差的仓库/测试稀缺项目等的真实难度与分布。
-
构造任务实例要求 PR Merged + resolves issue + 修改测试文件,并在执行过滤阶段要求能安装/运行,这会把大量没补充测试的真实修复、未合并但高质量的修复以及依赖复杂导致难复现的修复等case排除在外。
-
测试的范围并不是全仓库回归测试,而是部分相关的测试用例。
-
可复现环境构建仍然需要人工操作。
-
多模态信息没被系统性处理,部分 issue 含图片或者链接,目前处理的都是纯文本任务。
-
训练集的质量控制更加宽松,不需要 PR 提供测试改动,会混入更多的难以通过测试验证的修复/重构/不完全修复等,影响训练质量的一致性。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!