第 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_whitespace(editblock_coder.py:134),按"完美 → 容忍缩进"顺序试。
2.3 逐级看
① 完美匹配 perfect_replace
最朴素:把 part 当作连续若干行,在 whole 里滑动窗口找逐行元组相等(editblock_coder.py:146-154)。命中就直接拼接替换。
② 容忍缩进 replace_part_with_missing_leading_whitespace
这是最常救场的一级(editblock_coder.py:243)。直觉:模型通常整体性地搞错缩进——要么全去掉、要么统一少缩进几格。算法:
- 把
part和replace一起"反缩进"到能去掉的最大公共空白(editblock_coder.py:249-256)。 - 滑窗找一个位置:去掉缩进后非空白内容逐行一致,且每行缺的缩进量完全相同(
match_but_for_leading_whitespace,editblock_coder.py:276)。 - 把这"统一缺的缩进"补回到
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 在第 ④ 级之后有一句 return(editblock_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_lines(editblock_coder.py:602,SequenceMatcher 找最像的一段)给出 "Did you mean to match some of these actual lines?" - 若 REPLACE 内容已在文件里,提示"你是不是不需要这个块了?"
- 最后
raise ValueError(res)。
这个 ValueError 被上层 apply_updates 捕获并存进 self.reflected_message(base_coder.py:2305-2316),成为下一轮发给模型的消息。模型看到"你以为的旧代码长这样,实际长那样",往往一次就能改对。这就是 Aider 的自纠环(详见第 4 章)。
2.6 应用层的额外韧性
EditBlockCoder.apply_edits(editblock_coder.py:41)还有两处实用兜底:
- 找错文件也能救。 若某编辑在指定文件里没匹配上、且 SEARCH 非空,会遍历会话里其它文件再试一遍(
editblock_coder.py:58-66,issue #2258)。 - 新建文件。 SEARCH 段为空且文件不存在 →
touch出空文件再把 REPLACE 当内容追加(do_replace,editblock_coder.py:370-379)。
2.7 udiff 的平行机制(一句)
udiff 格式不走上面的阶梯,而走 search_replace.py 的策略列表 udiff_strategies(search_replace.py:558-561):把多个"搜索-替换函数 × 预处理器"组合排队,由 flexible_search_and_replace(search_replace.py)依次尝试,思路同样是"多策略降级",只是换了实现载体。
2.8 小结
本章的精华一句话:Aider 把"模型记错代码"当成常态来设计——用一条保守、可解释的降级匹配阶梯尽力落地,落不下就把"实际代码长这样"原样回灌让模型自纠,而不是用激进的模糊匹配去赌。下一章看模型怎么在没看到全部代码的情况下,仍知道仓库里有什么。
代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 降级阶梯入口 | aider/coders/editblock_coder.py | replace_most_similar_chunk、perfect_or_whitespace |
| 完美匹配 | aider/coders/editblock_coder.py | perfect_replace |
| 容忍缩进 | aider/coders/editblock_coder.py | replace_part_with_missing_leading_whitespace、match_but_for_leading_whitespace |
| 省略号 | aider/coders/editblock_coder.py | try_dotdotdots |
| 死代码模糊匹配 | aider/coders/editblock_coder.py | replace_closest_edit_distance(不可达) |
| 失败报错构造 | aider/coders/editblock_coder.py | EditBlockCoder.apply_edits、find_similar_lines |
| 写盘/新建 | aider/coders/editblock_coder.py | do_replace、strip_quoted_wrapping |
| udiff 策略阶梯 | aider/coders/search_replace.py | udiff_strategies、flexible_search_and_replace |