造 bug 的四种策略(bug_gen)
本章讲整条流水线的「源头」:候选 bug 是怎么被造出来的。先讲共同骨架,再逐个讲四种策略,最后讲怎么把小 bug 合成大 bug。
1. 它要解决的小问题
要造一道编程练习题,本质是**「拿一段正确代码,改出一个会弄挂测试的版本」**。难点有三:
- 改完必须语法仍然合法(否则连 import 都过不了,谈不上「弄挂某个测试」)。
- 改动要像真 bug(运算符写反、漏个循环、少个赋值),而不是乱码。
- 要能规模化——一个仓库里成百上千个函数,每个都能批量试改。
SWE-smith 的答案:先把仓库里每个函数/类抽成一个 CodeEntity,再对它施加「变异器」。四种策略就是四类变异器。
2. 四种策略一览
| 策略 | 怎么造 bug | 确定性? | 要不要 LLM | 入口 |
|---|---|---|---|---|
| Procedural | 在 AST 上做规则变异(翻转运算符、删循环……) | 是(种子可复现) | 否 | bug_gen/procedural/generate.py |
| LLM-modify | 把函数源码丢给 LLM,让它「给我引入一个 bug」 | 否 | 是 | bug_gen/llm/modify.py |
| LLM-rewrite | 把函数体挖空,让 LLM 从签名+上下文重新实现(自然带 bug) | 否 | 是 | bug_gen/llm/rewrite.py |
| PR-mirror | 拿一个真 SWE-bench PR 的修复 diff,反向复刻成 bug | 否 | 是(辅助) | bug_gen/mirror/generate.py |
四者产物完全统一:每个 bug = 一个 bug__<策略>__<hash>.diff 补 丁 + 一个 metadata__*.json,落在 logs/bug_gen/<repo>/... 下,由 collect_patches.py 汇总。这套统一产物是后续验证能「一视同仁」的前提。
3. 共同骨架:CodeEntity + BugRewrite + 落补丁
所有策略都走同一个三步循环,先建立直觉:
仓库 ──extract_entities()──▶ [CodeEntity, CodeEntity, ...] 每个=一个函数/类
│
每个 entity ──▶ 某策略产出 BugRewrite(改写后的代码)
│
BugRewrite ──generate_patch_fast / get_patch──▶ 一个 .diff 补丁
CodeEntity(swesmith/constants.py:88):带file_path / line_start / line_end / node / src_code,以及一组属性标签_tags(见下)。BugRewrite(swesmith/constants.py:138):一次改写的结果,核心字段rewrite(改写后的整段代码)、explanation(人话解释)、strategy(哪种策略)、cost(LLM 花了多少钱)。它的get_hash()用改写文本算 8 位哈希,用来给 bug 取唯一名(swesmith/constants.py:159、generate_hash在:174)。- 落补丁有两条路:procedural 用纯内存的
generate_patch_fast(bug_gen/utils.py:58,不碰 git,快),LLM 路线用apply_code_change+get_patch(bug_gen/utils.py:16、:195,真改文件再git diff)。
关键细节:落代码时要补回缩进。 CodeEntity 抽取时把源码 dedent 过(见 03 章),所以写回文件时要按 indent_level * indent_size 把每行重新缩进——apply_code_change 和 generate_patch_fast 里都有同一段逻辑(bug_gen/utils.py:26-31 与 :91-96)。
4. 策略一:Procedural(确定性 AST 变异)——项目最硬核的部分
4.1 思路
不调用任何模型,纯靠规则改 AST:把 + 换成 -、把 < 换成 >、删掉一个循环、删掉一个赋值……每种规则就是一个 ProceduralModifier。这条路确定、免费、可复现(给定随机种子结果一样),是 5.2 万数据集的主力来源。
4.2 两层匹配:先看「这个函数配不配被这样改」
每个 entity 在抽取时就被打上属性标签(CodeProperty 枚举,swesmith/constants.py:47):HAS_LOOP、HAS_IF_ELSE、HAS_BINARY_OP、IS_CLASS……(Python 版怎么打标签见 bug_gen/adapters/python.py:10 的 _analyze_properties)。
每个变异器声明自己需要哪些标签(conditions)。can_change 做门禁:标签全满足、且复杂度落在区间内,才允许改(bug_gen/procedural/base.py:34):
# 示意,非源码 —— 变异器的准入检查
def can_change(self, entity):
return (all(c in entity._tags for c in self.conditions) # 需要的属性都有
and self.min_complexity <= entity.complexity <= self.max_complexity)
比如「删循环」只作用于「是函数 + 含循环」的 entity——CommonPMs.REMOVE_LOOP 的 conditions=[IS_FUNCTION, HAS_LOOP](bug_gen/procedural/base.py:126)。这一层避免了「想删循环却找不到循环」的无效尝试。
4.3 概率性变异:flip() + 多次尝试
命中条件后,变异器遍历 AST 节点,对每个候选点按概率 flip() 决定改不改。Python 用 LibCST 的 CSTTransformer(bug_gen/procedural/python/base.py:8):
# 示意,非源码 —— 翻转二元运算符
class Transformer(...):
def leave_BinaryOperation(self, original, updated):
if self.flip(): # 按 likelihood 掷骰子
new_op = FLIPPED_OPERATORS[type(updated.operator)]() # < → >, + → - ...
return updated.with_changes(operator=new_op)
return updated
真实实现:OperationFlipOperatorModifier(bug_gen/procedural/python/operations.py:89),翻转映射表 FLIPPED_OPERATORS 在同文件 :7(Add→Subtract、LessThan→GreaterThan、Is→IsNot……)。删除类变异更直接,命中就 RemoveFromParent:RemoveLoopModifier(bug_gen/procedural/python/remove.py:8)对 For/While 节点掷骰子删除。
flip() 本身就是「随机数 < likelihood」(bug_gen/procedural/base.py:31)。modify() 会最多试 max_attempts(默认 5)次,直到代码真的变了才算成功,否则返回 None(bug_gen/procedural/python/base.py:22-49)——因为掷骰子可能一次都没翻到。
4.4 共有的「变异目录」与各语言权重
所有语言共享一份变异类型目录 CommonPMs(bug_gen/procedural/base.py:58),它把每种 bug 的 name(如 func_pm_flip_operators)、人话 explanation、所需 conditions 集中定义。各语言再实现对应的 Transformer。Python 启用哪些、各自概率多少,写在 bug_gen/procedural/python/__init__.py:25 的 MODIFIERS_PYTHON 列表里(例:运算类 likelihood=0.4,删除类 0.25)。
扩展名 → 变异器列表的总映射在 bug_gen/procedural/__init__.py:20 的 MAP_EXT_TO_MODIFIERS。generate.py 的主循环就是:对每个扩展名、每个变异器,筛出能改的 entity,逐个 _process_candidate(bug_gen/procedural/generate.py:35)。
4.5 巧妙处:interleave 模式
顺序模式会「先把一种变异在所有函数上做完,再做下一种」,导致输出里同类 bug 扎堆。--interleave(bug_gen/procedural/generate.py:114)先把所有 (candidate, modifier) 对收集起来、random.shuffle 打散再处理,让不同类型的 bug 均匀混合——对训练数据多样性更友好。
5. 策略二 & 三:两种 LLM 造 bug
这两种都要调模型(litellm),思路相反但产物一样。
LLM-modify(直接引入 bug)——把函数源码塞进 prompt,让模型「给我引入一个微妙的 bug」,n_bugs 个 completion 就是 n 个候选 bug。每个 completion 解析出代码块和 Explanation: 段,包成 BugRewrite(bug_gen/llm/modify.py:96-111)。温度设为 1 以求多样。
LLM-rewrite(挖空重写)——更聪明的一招。 不是「让模型搞破坏」,而是:
- 把函数体挖空成 stub(只留签名+docstring+
TODO: Implement this function),用CodeEntity.stub(Python 版在bug_gen/adapters/python.py:115,靠 ASTFunctionBodyStripper删 body)。 - 把「整个文件 + 这个挖空的函数签名」给模型,让它老老实实重新实现(prompt 见
configs/bug_gen/lm_rewrite.yml:明确「不要改签名、不要动别的代码」)。 - 模型重写出的版本自然会偏离原意——这就是 bug,而且往往比「故意搞破坏」更像真实开发者写错。
流程在 bug_gen/llm/rewrite.py:84:先 apply_code_change 挖空 → 调模型 → git reset --hard 还原 → 把模型的重写 apply_code_change 上去 → get_patch 取 diff。
6. 策略四:PR-mirror(把真 PR 反向复刻成 bug)
思路:SWE-bench 里有真实「修 bug 的 PR」,其修复 diff 的反面就是一个真 bug。但 PR 是针对老版本仓库的,直接反向 apply 常对不上。所以 mirror/generate.py 分两档:
- 先试直接 apply(
process_single_instance里apply_patches,bug_gen/mirror/generate.py:288)——能贴上就最省事。 - 贴不上就 LLM「恢复」:
recover_sweb_inst(:104)逐文件,把「当前文件内容 + PR 的 diff」给模型,让它在当前代码上复刻等价改动(RECOVERY_PROMPT)。
还有准入门禁 should_attempt_recovery(:70):改动文件数 / 行数 / 单文件长度超阈值就跳过——太大的 PR 复刻不可靠。
7. 合并:把小 bug 拼成大 bug
单点 bug 偏简单。bug_gen/combine/same_file.py 把同一文件里多个已验证的小补丁组合 apply,只要合并后还能干净 apply,就生成一个 combine_file__<hash>.diff(bug_gen/combine/same_file.py:84-107),难度更高。它刻意排除掉某些类型避免套娃(EXCLUDED_BUG_TYPES,:33),并贪心地用掉一组补丁后清掉含重叠文件的其余组合(:112-115)。同目录还有 same_module.py 做跨文件版本。
8. 本章代码地图
| 主题 | 文件 | 关键符号 |
|---|---|---|
| 变异基类 + 准入 | swesmith/bug_gen/procedural/base.py | ProceduralModifier、can_change、flip、CommonPMs |
| Python 变异引擎 | swesmith/bug_gen/procedural/python/base.py | PythonProceduralModifier、Transformer、modify |
| 运算符翻转 | swesmith/bug_gen/procedural/python/operations.py | OperationFlipOperatorModifier、FLIPPED_OPERATORS |
| 删除类变异 | swesmith/bug_gen/procedural/python/remove.py | RemoveLoopModifier、RemoveConditionalModifier |
| procedural 主循环 | swesmith/bug_gen/procedural/generate.py | main、_process_candidate、process_with_timeout |
| 扩展名→变异器 | swesmith/bug_gen/procedural/__init__.py | MAP_EXT_TO_MODIFIERS |
| LLM 直接改 | swesmith/bug_gen/llm/modify.py | gen_bug_from_code_lm |
| LLM 挖空重写 | swesmith/bug_gen/llm/rewrite.py | main(挖空→重写→diff) |
| PR 反向复刻 | swesmith/bug_gen/mirror/generate.py | recover_sweb_inst、should_attempt_recovery |
| 补丁合并 | swesmith/bug_gen/combine/same_file.py | main、COMBINE_FILE |
| 落补丁工具 | swesmith/bug_gen/utils.py | generate_patch_fast、apply_code_change、get_patch |
| 汇总补丁 | swesmith/bug_gen/collect_patches.py | main |