第 4 章 · 主循环、反思自纠、上下文与 git 兜底
本章把前三章串成端到端:一句话进来,怎么组装上下文、发模型、落编辑、提交、验证,以及出错怎么自动再来一轮。
4.1 反思循环:Aider 的"agent"
Aider 没有复杂的多智能体编排。它的"自主性"就来自一个朴素循环:run_one(base_coder.py:924)。
run_one(message):
while message:
reflected_message = None
send_message(message) # 发模型 → 落编辑 → 验证
if not reflected_message: break # 没有要反思的事,结束
if num_reflections >= 3: 停止 # 上限 max_reflections=3
num_reflections += 1
message = reflected_message # 把"反思消息"当作下一轮输入
什么会写 reflected_message(触发再来一轮)? 这是理解 Aider 的关键——所有"失败"都收敛到同一个字段:
| 触发源 | 写入处 |
|---|---|
| 模型提到了不在会话里的文件,需补充 | base_coder.py:1561-1566(check_for_file_mentions) |
| 编辑格式错误 / SEARCH 没匹配上 | base_coder.py:2315、2327(apply_updates 捕获 ValueError) |
| lint 报错且用户同意修 | base_coder.py:1606 |
| 测试失败且用户同意修 | base_coder.py:1622 |
一句话:Aider 把所有"出岔子"统一成"再给模型一条说明,让它自己改"。 这就是它的自纠本质——简单、可解释、有硬上限防止死循环。
4.2 一次 send_message 都干了啥
send_message(base_coder.py:1419)是单轮的全部动作,顺序很重要:
1. 把用户消息加进 cur_messages
2. format_messages → 组装上下文 chunks(见 4.3)
3. check_tokens:超限就提示并征求是否硬发
4. warm_cache:后台线程定期 ping,保活 prompt 缓存
5. send:流式调 LLM(litellm),带重试/超限/中断处理
6. check_for_file_mentions:模型要更多文件?→ 反思
7. apply_updates:解析+落编辑(第 1、2 章)
8. 有编辑 → auto_commit(git 提交)
9. auto_lint → 失败可反思
10. run_shell_commands:执行模型建议的 shell(需确认)
11. auto_test → 失败可反思
注意第 5 步的健壮性(base_coder.py:1456-1512):指数退避重试可重试异常、ContextWindowExceededError 标记 exhausted、FinishReasonLength(输出被截断)会用 assistant prefill 续写(前提是模型支持,base_coder.py:1492-1505)。
4.3 上下文组装:为缓存而生的固定顺序
format_chat_chunks(base_coder.py:1226)把上下文拆成命名段落,装进 ChatChunks(chat_chunks.py)。all_messages 的拼接顺序是写死的(chat_chunks.py:16-26):
system → examples → readonly_files → repo → done → chat_files → cur → reminder
(系统提示)(示例对话) (只读文件) (repo map)(历史) (会话文件) (本轮)(提醒)
└──────────── 越靠前越稳定、越该被缓存 ───────────┘ └─ 每轮在变 ─┘
为什么这个顺序? 为了 prompt 缓存。稳定不变的内容(系统提示、示例、repo map、只读文件)放前面,变化的(本轮消息、提醒)放后面。add_cache_control_headers(chat_chunks.py:28-41)在几个稳定段的末尾打 cache_control: ephemeral 标记,让 Anthropic 等支持前缀缓存的提供商命中缓存、省钱省延迟。
还有个后台"保活"细节:warm_cache(base_coder.py:1340)起一个守护线程,每隔约 5 分钟发一个 max_tokens=1 的请求 ping 一下,避免缓存过期(base_coder.py:1357-1390)。
提醒位置因模型而异。 system_reminder(SEARCH/REPLACE 规则)放系统消息还是塞进最后一条用户消息,取决于 main_model.reminder(base_coder.py:1320-1329)——有些模型对系统消息里的指令更听话,有些则相反。
4.4 选择围栏:避开和代码冲突
回复里的代码用什么"围栏"包?默认 ```,但如果文件内容本身就含三反引号(比如 Markdown 文件),就会冲突。choose_fence(base_coder.py:609)扫一遍所有会话文件,从候选列表 all_fences(base_coder.py:77-85:三反引号、四反引号、<source>、<code> 等)里挑一个文件内容里没出现过的(base_coder.py:620-624)。这是个小而务实的健壮性设计。
4.5 git 兜底:自动提交与归属
有编辑落地后,auto_commit(base_coder.py:2375)调 GitRepo.commit(repo.py:131)。两个要点:
提交信息由 LLM 生成。 get_commit_message(repo.py:326)把 diff 喂给(通常更弱更便宜的)模型,让它写一句 commit message,支持指定语言(repo.py:336-339)。
作者/提交者归属 有一套规则。 commit 里区分"AI 改的(aider_edits=True)"与"用户改的",再叠加多个 --attribute-* 开关(repo.py:218-267),决定:
| 维度 | 默认行为(AI 编辑) |
|---|---|
| Author | 改成 You (aider) |
| Committer | 改成 You (aider) |
| Co-authored-by 尾注 | 不加(除非显式开 --attribute-co-authored-by) |
| commit message 前缀 | 不加 aider:(除非开对应开关) |
实现上用 contextlib.ExitStack 临时设 GIT_AUTHOR_NAME/GIT_COMMITTER_NAME 环境变量再提交(repo.py:296-311),提交完自动还原。还支持 --no-verify 跳过 pre-commit 钩子(repo.py:278-279)。
这套 git 集成换来一个大好处:每一步 AI 编辑都是一个可回退的提交,用户随时 /undo,等于给"让 AI 改我代码"上了保险。
4.6 编辑格式工厂与"架构师/编辑"分工
工厂: Coder.create(base_coder.py:125)按 edit_format 在 coders.__all__ 里找匹配的子类实例化(base_coder.py:190-194)。切换格式时若历史消息会"教坏"新模型,还会先把历史摘要掉(base_coder.py:161-168),避免 新模型模仿旧格式。
架构师/编辑分工(architect/editor split): ArchitectCoder(architect_coder.py)让一个强模型先"出方案"(不直接产编辑),用户确认后,再 Coder.create 出一个 editor coder(可用另一个更擅长输出 diff 的模型)把方案落成实际编辑(architect_coder.py:20-44)。这是一种"规划与执行"分离的轻量编排——仍然没有跳出"Coder + 反思循环"的框架,只是把一轮拆成两个角色。
4.7 横向对比(同 shelf)
| 维度 | Aider 的取舍 |
|---|---|
| 编辑落地 | 文本补丁 + 容错匹配,不靠 function-calling 写文件——赌"模型最熟文本格式" |
| 自主性 | 朴素反思环(≤3 轮),不做长程 ReAct/计划树——可解释、可控、不易跑飞 |
| 上下文 | PageRank 仓库地图 + 缓存友好的固定段落顺序——为大仓库和省钱优化 |
| 安全兜底 | git 每步提交 + 用户确认 shell——对"AI 改真实代码"的现实让步 |
相比走 function-calling + 长程规划的编码 agent,Aider 更像"把 LLM 当成一个会写 diff 的同事,自己负责把 diff 安全落地"。工程重心在落地的可靠性而非编排的复杂度。
4.8 边界与局限
- 反思上限 3 轮:复杂错误可能修不完就停(
base_coder.py:101、939-941)。 - 强依赖 SEARCH 段质量:模型若大段记错代码,容错阶梯也救不回,只能反复回灌。
- shell 命令需人工确认:不会自动执行(
base_coder.py:2456-2462),安全但不全自动。 - repo map 在超大仓库可能被自动禁用(见第 3 章)。
代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 反思循环 | aider/coders/base_coder.py | run_one、reflected_message、max_reflections |
| 单轮编排 | aider/coders/base_coder.py | send_message、apply_updates、run_shell_commands |
| 上下文段落 | aider/coders/base_coder.py、aider/coders/chat_chunks.py | format_chat_chunks、ChatChunks.all_messages |
| prompt 缓存 | aider/coders/chat_chunks.py、aider/coders/base_coder.py | add_cache_control_headers、warm_cache |
| 围栏选择 | aider/coders/base_coder.py | choose_fence、all_fences |
| git 提交 | aider/repo.py | GitRepo.commit、get_commit_message |
| 归属规则 | aider/repo.py | commit(attribute_* 段) |
| 工厂 | aider/coders/base_coder.py | Coder.create |
| 架构师/编辑 | aider/coders/architect_coder.py | ArchitectCoder.reply_completed |