跳到主要内容

造 bug 的四种策略(bug_gen)

本章讲整条流水线的「源头」:候选 bug 是怎么被造出来的。先讲共同骨架,再逐个讲四种策略,最后讲怎么把小 bug 合成大 bug。

1. 它要解决的小问题

要造一道编程练习题,本质是**「拿一段正确代码,改出一个会弄挂测试的版本」**。难点有三:

  1. 改完必须语法仍然合法(否则连 import 都过不了,谈不上「弄挂某个测试」)。
  2. 改动要像真 bug(运算符写反、漏个循环、少个赋值),而不是乱码。
  3. 要能规模化——一个仓库里成百上千个函数,每个都能批量试改。

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:159generate_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_changegenerate_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_LOOPHAS_IF_ELSEHAS_BINARY_OPIS_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_LOOPconditions=[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→SubtractLessThan→GreaterThanIs→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:25MODIFIERS_PYTHON 列表里(例:运算类 likelihood=0.4,删除类 0.25)。

扩展名 → 变异器列表的总映射在 bug_gen/procedural/__init__.py:20MAP_EXT_TO_MODIFIERSgenerate.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(挖空重写)——更聪明的一招。 不是「让模型搞破坏」,而是:

  1. 把函数体挖空成 stub(只留签名+docstring+TODO: Implement this function),用 CodeEntity.stub(Python 版在 bug_gen/adapters/python.py:115,靠 AST FunctionBodyStripper 删 body)。
  2. 把「整个文件 + 这个挖空的函数签名」给模型,让它老老实实重新实现(prompt 见 configs/bug_gen/lm_rewrite.yml:明确「不要改签名、不要动别的代码」)。
  3. 模型重写出的版本自然会偏离原意——这就是 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 分两档:

  1. 先试直接 apply(process_single_instanceapply_patches,bug_gen/mirror/generate.py:288)——能贴上就最省事。
  2. 贴不上就 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.pyProceduralModifiercan_changeflipCommonPMs
Python 变异引擎swesmith/bug_gen/procedural/python/base.pyPythonProceduralModifierTransformermodify
运算符翻转swesmith/bug_gen/procedural/python/operations.pyOperationFlipOperatorModifierFLIPPED_OPERATORS
删除类变异swesmith/bug_gen/procedural/python/remove.pyRemoveLoopModifierRemoveConditionalModifier
procedural 主循环swesmith/bug_gen/procedural/generate.pymain_process_candidateprocess_with_timeout
扩展名→变异器swesmith/bug_gen/procedural/__init__.pyMAP_EXT_TO_MODIFIERS
LLM 直接改swesmith/bug_gen/llm/modify.pygen_bug_from_code_lm
LLM 挖空重写swesmith/bug_gen/llm/rewrite.pymain(挖空→重写→diff)
PR 反向复刻swesmith/bug_gen/mirror/generate.pyrecover_sweb_instshould_attempt_recovery
补丁合并swesmith/bug_gen/combine/same_file.pymainCOMBINE_FILE
落补丁工具swesmith/bug_gen/utils.pygenerate_patch_fastapply_code_changeget_patch
汇总补丁swesmith/bug_gen/collect_patches.pymain