跳到主要内容

第 1 章 · 编辑格式:LLM 怎么「说出」一次改动

本章讲:Aider 不让模型"调用工具写文件",而是让它在自然语言回复里夹一段约定格式的补丁。我们看这段补丁长什么样、有哪几种、以及解析器怎么从一坨自由文本里把编辑切出来。

1.1 核心思想:编辑即语言

大多数 agent 让模型走 function-calling("调用 write_file(path, content)")。Aider 选了另一条路:

模型照常用文字回复,只是在回复里嵌入一段机器能解析的补丁块

为什么?因为 LLM 在海量代码/文档上见过的"diff""冲突标记""代码块"远多于某个具体工具的 JSON schema。用模型最熟悉的文本格式,它出错率最低。 这就是 Aider 把默认格式设计成酷似 git 冲突标记的原因。

1.2 三种主要编辑格式

Aider 有十几个 *Coder,但核心是三族编辑格式:

格式(edit_format长什么样适合实现
diff(SEARCH/REPLACE)「旧代码块 ⟶ 新代码块」,像冲突标记默认、绝大多数强模型EditBlockCoder
udiff标准 unified diff(@@ ... @@ + -/+ 行)GPT-4 Turbo 等爱"偷懒省略"的模型UnifiedDiffCoder
whole直接回完整文件内容弱模型 / 小文件WholeFileCoder

选哪种由模型决定:Modelaider/models.py:configure_model_settings 里按模型名设 self.edit_format(例如多处 self.edit_format = "diff"aider/models.py:439 起),Coder.create 默认就用 main_model.edit_formatbase_coder.py:148)。

1.3 默认格式:SEARCH/REPLACE 块

这是要重点理解的格式。一个块由"文件名 + 围栏 + 旧代码 + 分隔线 + 新代码"组成。来自 Aider 自己喂给模型的示例提示editblock_prompts.py:46-54,示意保留原貌):

mathweb/flask/app.py
```python
<<<<<<< SEARCH
from flask import Flask
=======
import math
from flask import Flask
>>>>>>> REPLACE
```

规则被写死在系统提醒里(editblock_prompts.py:120-159 system_reminder),其中最关键的两条:

  • SEARCH 段必须"逐字符"匹配现有文件("EXACTLY MATCH ... character for character",editblock_prompts.py:134)。
  • 新建文件:用一个 SEARCH 段为空、REPLACE 段是文件内容的块(editblock_prompts.py:152-155)。

这两条决定了第 2 章的全部难度:模型被要求逐字匹配,但它做不到——所以应用端必须容错。

1.4 解析:从自由文本里切出编辑

模型回复是"解释 + 若干块"混在一起的自由文本。EditBlockCoder.get_editseditblock_coder.py:21)调用 find_original_update_blocks 逐行扫描切块。

标记不是写死的字符串,而是正则,容忍 5–9 个 </=/>editblock_coder.py:386-388):

HEAD = r"^<{5,9} SEARCH>?\s*$" # 容忍 <<<<< 到 <<<<<<<<<
DIVIDER = r"^={5,9}\s*$"
UPDATED = r"^>{5,9} REPLACE\s*$"

原理演示(极简版,抓住"状态机扫行"的直觉;示意,非源码):

# 演示:扫描每一行,在 HEAD/DIVIDER/UPDATED 之间切换状态收集文本
def parse_blocks(lines):
i = 0
while i < len(lines):
if HEAD.match(lines[i]): # 进入一个块
i += 1
search = collect_until(lines, i, DIVIDER) # 收 SEARCH 段
i = search.end
replace = collect_until(lines, i, UPDATED) # 收 REPLACE 段
yield (current_filename, search.text, replace.text)
i += 1
# 重点看:文件名不在块里,要从块「上方几行」回溯

真实实现的几个巧妙细节:

  • 文件名回溯。 文件名单独一行在块上方find_filenameeditblock_coder.py:538)回看前 3 行,并做"精确名 → basename → 模糊匹配 → 带扩展名"四级挑选(editblock_coder.py:576-599),专门对付 DeepSeek 这类会多套一层围栏的模型。
  • shell 命令也顺手解析。 扫到 ```bash 这类围栏且后面不是编辑块时,把内容当 shell 命令产出(editblock_coder.py:452-485,以 yield None, ... 标记),后续可让用户确认执行。
  • 解析失败立即变成可回灌的报错。 缺 DIVIDER 等结构错误会 raise ValueError,并把"已处理到的文本 + ^^^ 错误"拼进异常(editblock_coder.py:530-533)——这条报错最终成为反思循环的输入(见第 4 章)。

1.5 udiff 与整文件(各一句)

  • udiffudiff_coder.py:find_diffs)解析标准 @@ hunk,应用走 search_replace.py 的策略阶梯(第 2 章末)。它的提示反复强调"别跳过空行/注释",因为 unified diff 的上下文行必须对齐。
  • wholewholefile_coder.py)最简单:模型回"文件名 + 完整内容",直接整体覆盖;胜在弱模型也能做对,输代价是 token 多、改大文件易超输出上限。

1.6 小结与下一章

你现在知道了模型怎么把一次改动"说出来",以及 Aider 怎么把它从自由文本里切出来。但切出来的 (文件, 旧文本, 新文本) 里那段"旧文本"几乎从不和磁盘逐字一致——怎么把它"容错地"落到真实文件上,是 第 2 章 的主题,也是 Aider 工程含量最高的部分。

代码地图

主题文件符号
diff 解析入口aider/coders/editblock_coder.pyEditBlockCoder.get_editsfind_original_update_blocks
标记正则aider/coders/editblock_coder.pyHEADDIVIDERUPDATED
文件名回溯aider/coders/editblock_coder.pyfind_filenamestrip_filename
提示与规则aider/coders/editblock_prompts.pyEditBlockPrompts.system_reminderexample_messages
udiff 解析aider/coders/udiff_coder.pyfind_diffs
整文件aider/coders/wholefile_coder.pyWholeFileCoder
格式选择aider/models.pyModel.configure_model_settingsedit_format