跳到主要内容

第 3 章:补丁生成与「模糊应用」

ACR 的第二大精华:让 LLM 改代码真的改对。这章讲它为什么让模型直接写 diff,以及那套「贴原文 + 模糊匹配 + 缩进双猜 + pylint 守门」的落地术。

3.1 它要解决的小问题

你可能以为「让 LLM 改代码」就是让它输出一个 git diff 然后 git apply。问题是 LLM 数不准行号、对不齐空白:unified diff 里 @@ -120,7 +120,8 @@ 这种行号、每行前导空格、上下文行必须一字不差,模型几乎做不到,git apply 一不匹配就整个失败。

ACR 的答案:换一种更容错的补丁格式 + 一套确定性的后处理,把「对齐」的脏活从模型手里接过来。

3.2 补丁格式:贴原文,贴新文

写补丁的 prompt(agent_write_patch.USER_PROMPT_INIT,agent_write_patch.py:42)要求模型输出这种块:

# modification 1
```
<file>path/to/file.py</file>
<original>...原始代码片段...</original>
<patched>...修好后的代码...</patched>
```

prompt 里反复强调两件事(agent_write_patch.py:38agent_write_patch.py:76-79):

  • 不要在每行开头带行号(搜索 API 返回的代码是带行号的,模型容易照抄)。
  • <original> 里的片段必须能在原程序里找到一段连续匹配——因为系统要用它来定位。

这就把问题从「写对一个 diff」降级成「贴一段你看过的原文 + 你想要的新文」,后者模型在行。

3.3 落地三步:解析 → 匹配 → 缩进双猜

确定性后处理在 patch_utils.py,核心是 parse_edits + apply_edit

第 1 步:解析(parse_edits,patch_utils.py:31)

用正则从模型响应里抽出每个 <file>/<original>/<patched> 三元组,包成 Edit 对象。细节:对 <file>strip(),但对 original/patched 只去首尾换行不去缩进(patch_utils.py:82-83)——因为去掉行首空白会毁掉 Python 的缩进。还会过滤掉像 # Rest of the code... 这种模型偷懒占位行(patch_utils.py:50-51)。

第 2 步:忽略空白的逐行匹配(apply_edit,patch_utils.py:107)

关键技巧:匹配时把 <original> 每行和原程序每行都 strip() 后再比(patch_utils.py:125-126),即忽略缩进差异找到 <original> 在文件里的起止行:

# app/agents/patch_utils.py:125-138(节选)
cleaned_before_lines = [line.strip() for line in before_lines]
cleaned_orig_lines = [line.strip() for line in orig_prog_lines]
for i in range(len(cleaned_orig_lines) - len(cleaned_before_lines) + 1):
if cleaned_orig_lines[i : i + len(cleaned_before_lines)] == cleaned_before_lines:
match_start = i
match_end = i + len(cleaned_before_lines)
break

注意注释里的强调:这些 strip 后的行只用于匹配,绝不用来拼回程序(patch_utils.py:124)——拼回去时用的是原文件的真实行,避免把空白弄丢。

第 3 步:缩进双猜 + pylint 选择(patch_utils.py:148-180)

匹配到位置后,<patched> 的缩进也未必对。ACR 不赌一种,而是生成两个候选程序:

  • 猜法一(new_prog_1): 假设 patched 各行相对第一行的缩进是对的。算出原文件那段第一行的绝对缩进,给 patched 每行统一加上这个缩进。
  • 猜法二(new_prog_2): 假设 patched 第二行起的绝对缩进就是对的,只修第一行。

然后用 pylint 当裁判:哪个候选通过语法检查(不报 syntax-error)就用哪个;两个都不过就放弃这个 edit(返回 None)。

# app/agents/patch_utils.py:175-180
if lint_python_content(new_prog_1):
new_prog = new_prog_1
elif lint_python_content(new_prog_2):
new_prog = new_prog_2
else:
return None

lint_python_content(patch_utils.py:202)把内容写进临时文件,跑 pylint --errors-only,只要输出里没有 (syntax-error) 就算过。用 pylint 在两种缩进里选一个语法正确的——这是把「LLM 对不齐缩进」这个高频失败点用确定性手段消化掉的妙招。

3.4 从「落地的改动」到「diff」

上面 apply_edit 是真的去改了工作区的文件。怎么变成最终 .diff?在 convert_response_to_diff(post_process.py:219):

  1. parse_edits 抽出所有 edit,过滤掉改测试文件的(is_test_file,post_process.py:250-254)——ACR 只补源码,不许改测试。
  2. 在仓库目录里(apputils.cd(repo_path)),清干净工作区,逐个 apply_edit 把改动落到文件上(post_process.py:271-284)。
  3. git diff 抓出统一 diff(post_process.py:304-306),再 repo_clean_changes 把工作区还原。
  4. 根据结果打一个 ExtractStatus 标签(post_process.py:65):全匹配且有真实改动 → APPLICABLE_PATCH;一个都没匹配上 → RAW_PATCH_BUT_UNMATCHED;匹配了但 diff 为空 → MATCHED_BUT_EMPTY_DIFF;<original> 是空白 → MATCHED_BUT_EMPTY_ORIGIN……

这个 ExtractStatus 是个有序枚举(_worst_to_best_order,post_process.py:93),从 NO_PATCHAPPLICABLE_PATCH 排好,后面选补丁时用来挑「最好的提取结果」。

3.5 写补丁 agent:重试与反馈

PatchAgent(agent_write_patch.py:86)管「让模型写出一个可应用的补丁」:_write_applicable_patch(agent_write_patch.py:130)循环最多 retries 次,每次写、转 diff、看 ExtractStatus 是不是 APPLICABLE_PATCH;不可应用就把失败原因(extract_msg)当反馈喂回去再试(generatoragent_write_patch.py:310-317)。写不出就抛 InvalidLLMResponse

构造初始对话有两条路(_construct_init_thread,agent_write_patch.py:206):

  • 有 bug location: 直接把 issue + 每个 BugLocation(代码 + 期望行为,BugLocation.multiple_locs_to_str_for_model)摆给模型,不带检索历史——干净、聚焦。
  • 没 bug location(检索阶段没收敛): 退而求其次,复用整段检索对话历史当上下文,只把 system prompt 换成「写补丁」的(agent_common.replace_system_prompt)。

这两条路对应第 2 章末尾的两种结局,保证「即使没精确定位也能尝试写补丁」。

3.6 关键细节 / 坑

  • 过滤测试文件发生在两处。 检索时索引不含测试(第 1 章);补丁时 convert_response_to_diff 再次丢掉改测试的 edit(post_process.py:250)。双保险:ACR 永远只改产品代码。
  • 匹配只取第一处。 apply_edit 找到第一个匹配就 break(patch_utils.py:138),不处理「<original> 在文件里出现多次」的歧义——靠模型给足够独特的片段来避免。
  • <original> 为空白会被专门拦。 MATCHED_BUT_EMPTY_ORIGIN(post_process.py:320-326)会回一句「请在 original 里放非空白原文」让模型重写——防止模型用空 original 来「凭空插入」。
  • 灵感来源。 patch_utils.py 顶部注释标了它借鉴自 gpt-engineer 的 chat_to_files(patch_utils.py:4-6)。

→ 下一章:有测试时,如何用执行信号逼补丁收敛

3.7 代码地图

主题文件符号
补丁 prompt / 格式app/agents/agent_write_patch.pyUSER_PROMPT_INITSYSTEM_PROMPT
写补丁 + 重试app/agents/agent_write_patch.pyPatchAgent._write_applicable_patch_construct_init_thread
解析 editapp/agents/patch_utils.pyparse_editsEdit
模糊应用 + 缩进双猜app/agents/patch_utils.pyapply_editlint_python_content
响应→diff + 状态app/post_process.pyconvert_response_to_diffExtractStatus
提取状态记录app/post_process.pyrecord_extract_statusread_extract_status