跳到主要内容

第 2 章 · 容错应用:把模型口述的旧代码落到真实文件

本章讲 Aider 的精华:模型给的 SEARCH 段("旧代码")几乎从不和磁盘逐字一致——多了/少了缩进、加了空行、用 ... 省略了一段。Aider 怎么"尽力而为"地把它定位并替换?

2.1 要解决的小问题

第 1 章末尾留了个钩子:提示要求模型 SEARCH 段"逐字符匹配",但模型做不到。常见偏差:

  • 整体缩进差几个空格(模型常"拍平"或只缩进一部分)。
  • 开头多了一个空行(issue #25)。
  • ... 省略了中间一大段不变的代码。

如果只做字符串精确匹配,这些编辑全都"失败"。Aider 的办法是一条降级阶梯:从最严格的匹配开始,逐级放宽,命中即停。

2.2 思路:降级匹配阶梯

入口是 replace_most_similar_chunk(whole, part, replace)editblock_coder.py:157)——part 是模型给的旧文本,whole 是磁盘真实内容。

怎么读下图:从上到下是降级顺序,命中即返回;全不中才算失败。

replace_most_similar_chunk(whole, part, replace)


① 完美匹配 perfect_replace 逐行 tuple 相等
│ 不中

② 容忍缩进差异 replace_part_with_missing_leading_whitespace
│ 不中 (非空白部分一致 + 偏移量统一)

③ 丢开头空行后重试 ①② (issue #25:模型爱加空行)
│ 不中

④ 处理省略号 ... try_dotdotdots 把 part/replace 按 ... 切段配对
│ 不中

返回 None ──► 上层判定失败,拼"Did you mean..."报错回灌模型

前两级封装在 perfect_or_whitespaceeditblock_coder.py:134),按"完美 → 容忍缩进"顺序试。

2.3 逐级看

① 完美匹配 perfect_replace

最朴素:把 part 当作连续若干行,在 whole 里滑动窗口找逐行元组相等editblock_coder.py:146-154)。命中就直接拼接替换。

② 容忍缩进 replace_part_with_missing_leading_whitespace

这是最常救场的一级(editblock_coder.py:243)。直觉:模型通常整体性地搞错缩进——要么全去掉、要么统一少缩进几格。算法:

  1. partreplace 一起"反缩进"到能去掉的最大公共空白(editblock_coder.py:249-256)。
  2. 滑窗找一个位置:去掉缩进后非空白内容逐行一致,且每行缺的缩进量完全相同match_but_for_leading_whitespaceeditblock_coder.py:276)。
  3. 把这"统一缺的缩进"补回到 replace 的每一行再写入(editblock_coder.py:269)。

原理演示(示意,非源码):

# 演示:模型给的 part 少了 4 格缩进,但「相对结构」对得上
def match_ignoring_indent(file_lines, part_lines):
for i in range(len(file_lines) - len(part_lines) + 1):
window = file_lines[i:i+len(part_lines)]
# 去掉行首空白后内容必须逐行相同
if [l.lstrip() for l in window] != [l.lstrip() for l in part_lines]:
continue
# 每行「多出来的缩进」必须是同一个值,才认为是整体偏移
offsets = {l[:len(l)-len(p)] for l, p in zip(window, part_lines) if l.strip()}
if len(offsets) == 1:
return i, offsets.pop() # 命中:返回位置 + 要补回的缩进
return None
# 重点看:要求「偏移统一」,避免把结构不同的代码误判为匹配

④ 省略号 try_dotdotdots

模型有时写 ... 表示"中间不变"(editblock_coder.py:190)。算法用正则按 ...part/replace 切成交替片段,要求两边省略号的位置一一对应,再对每对"非省略"片段做唯一替换(whole.count(part) > 1 就报错,避免歧义,editblock_coder.py:233-238)。

2.4 一个真实坑:被禁用的模糊匹配

replace_most_similar_chunk 在第 ④ 级之后有一句 returneditblock_coder.py:183),它下面的 replace_closest_edit_distance 调用(编辑距离模糊匹配,阈值 0.8)永远到不了——那是死代码。

这是个有意思的工程取舍:编辑距离匹配太激进,容易把"看着像"的错误位置改了,于是被一道 return 关在门外,只留下"精确/缩进/省略号"这些保守、可解释的策略。replace_closest_edit_distance 函数本身仍保留(editblock_coder.py:296),但不在主路径上。

2.5 匹配失败怎么办:把报错变成下一轮输入

降级全不中时,apply_edits 不是默默放弃,而是构造一段对模型友好的报错editblock_coder.py:84-124):

  • 标题 # N SEARCH/REPLACE blocks failed to match!
  • find_similar_lineseditblock_coder.py:602,SequenceMatcher 找最像的一段)给出 "Did you mean to match some of these actual lines?"
  • 若 REPLACE 内容已在文件里,提示"你是不是不需要这个块了?"
  • 最后 raise ValueError(res)

这个 ValueError 被上层 apply_updates 捕获并存进 self.reflected_messagebase_coder.py:2305-2316),成为下一轮发给模型的消息。模型看到"你以为的旧代码长这样,实际长那样",往往一次就能改对。这就是 Aider 的自纠环(详见第 4 章)。

2.6 应用层的额外韧性

EditBlockCoder.apply_editseditblock_coder.py:41)还有两处实用兜底:

  • 找错文件也能救。 若某编辑在指定文件里没匹配上、且 SEARCH 非空,会遍历会话里其它文件再试一遍(editblock_coder.py:58-66,issue #2258)。
  • 新建文件。 SEARCH 段为空且文件不存在 → touch 出空文件再把 REPLACE 当内容追加(do_replaceeditblock_coder.py:370-379)。

2.7 udiff 的平行机制(一句)

udiff 格式不走上面的阶梯,而走 search_replace.py策略列表 udiff_strategiessearch_replace.py:558-561):把多个"搜索-替换函数 × 预处理器"组合排队,由 flexible_search_and_replacesearch_replace.py)依次尝试,思路同样是"多策略降级",只是换了实现载体。

2.8 小结

本章的精华一句话:Aider 把"模型记错代码"当成常态来设计——用一条保守、可解释的降级匹配阶梯尽力落地,落不下就把"实际代码长这样"原样回灌让模型自纠,而不是用激进的模糊匹配去赌。下一章看模型怎么在没看到全部代码的情况下,仍知道仓库里有什么

代码地图

主题文件符号
降级阶梯入口aider/coders/editblock_coder.pyreplace_most_similar_chunkperfect_or_whitespace
完美匹配aider/coders/editblock_coder.pyperfect_replace
容忍缩进aider/coders/editblock_coder.pyreplace_part_with_missing_leading_whitespacematch_but_for_leading_whitespace
省略号aider/coders/editblock_coder.pytry_dotdotdots
死代码模糊匹配aider/coders/editblock_coder.pyreplace_closest_edit_distance(不可达)
失败报错构造aider/coders/editblock_coder.pyEditBlockCoder.apply_editsfind_similar_lines
写盘/新建aider/coders/editblock_coder.pydo_replacestrip_quoted_wrapping
udiff 策略阶梯aider/coders/search_replace.pyudiff_strategiesflexible_search_and_replace