M02 · SOURCE-GROUNDED TUTORIAL
Aider从源码学会它怎么工作 以 Git、Repo Map 和可替换编辑协议取胜;不是工具调用型自治平台。 我们不把 README 当结论,而是沿主循环、工具、上下文、权限、扩展、协作和状态一路读到实现。
Python · Git-native Pair Programmer Apache-2.0 5dc9490bb35f 16 个结论 · 32 处引用
这门课怎么读
先建立直觉,再沿一条任务链钻进代码 参考教程的做法不是把 API 名称罗列出来,而是从一个小白能理解的问题开始,先解释“为什么需要这个机制”,再用概念对比、执行链路和固定提交的源码回答“它究竟怎么做”。本页把 Aider 的 16 个源码结论重新编排成十节课;每个结论都保留证据等级、文件路径、行号和可点击源码。
你会得到 一张可复述的架构地图 一次完整任务的链路追踪 能迁移到自研 Harness 的设计判断
你不会得到 把 README 功能当成已验证事实 把 prompt 约束说成 OS 沙箱 把一次双模型调用夸成多 Agent 平台
M00 · MAP
先看全景:这个 Agent 的控制面在哪里 下面的图不是产品宣传图,而是把固定提交里最关键的入口、循环、模型、工具、安全、状态和协作节点放在一张地图上。
核心机制 交互外环 + 有界 reflection;固定 lint/test/commit 交付链
上下文 ChatChunks + 符号图 PageRank Repo Map + 二分 token 预算
适用建设 强调可审查 diff、Git 工作流、低复杂度结对编程
M00.5 · TRACE
跟踪一个任务:从输入到交付 把下面九步当成你读源码时的“地图坐标”。每到一个节点,都能回到后面的章节查具体实现。
01 提交需求与聊天文件 任务进入
↓
02 构造 ChatChunks + RepoMap 把结果交给下一层
↓
03 经 LiteLLM 采样 把结果交给下一层
↓
04 返回指定编辑协议 把结果交给下一层
↓
05 dry-run 解析补丁 把结果交给下一层
↓
06 确认写入范围 把结果交给下一层
↓
07 落盘、lint/test、Git commit 把结果交给下一层
↓
08 失败可 reflection 把结果交给下一层
↓
09 交付 diff 与结果 交付/续跑
读图提醒 箭头只表示控制面之间的关系,不代表每个实现都同步、串行或拥有 OS 隔离;真正的边界要以对应章节的源码摘录和 caveat 为准。
M01 · ORIENTATION
先把 Agent 看成一台会交付的机器 如果只看 README,你知道它能做什么;钻进源码后,我们要知道它为什么能做、什么时候会停、失败后谁负责收拾。
先用一个生活比喻 把 Agent 想成一间带传送带的工作室:入口收任务,主循环决定下一步,模型负责提出动作,工具负责动手,状态账本负责让下一班人接着干。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
读源码时先问 本课的判断方式 这一层有没有独立证据? 没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。 谁拥有最终控制权? 区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念 固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。
小练习 1 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M02 · LOOP
主循环:模型为什么会继续动 一次模型调用为什么会变成十几步?循环靠什么继续,靠什么停止?
先用一个生活比喻 像一个会看回执的快递员:模型先写行动单,工具返回回执,主循环把回执放回桌面,再让模型决定下一张行动单。
这套实现先回答了什么? Aider 的循环很直接:你说一次,它生成修改;修改格式错了或检查失败,就把错误原样喂回模型再试,但不会无限自修。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 交互外环 + 有界 reflection 内环 aider/coders/base_coder.py:88Aider 的循环很直接:你说一次,它生成修改;修改格式错了或检查失败,就把错误原样喂回模型再试,但不会无限自修。 一次回复后的固定交付链 aider/coders/base_coder.py:1530Aider 更像一条固定流水线,不是让模型临场决定下一步用什么工具。
01 L1 · fact · aider-loop-001
交互外环 + 有界 reflection 内环
先看源码事实 Coder.run 持续读取用户输入;每个输入由 run_one 调用 send_message,若编辑解析、lint 或 test 产生 reflected_message,则最多再反思 3 次。
翻译成白话 Aider 的循环很直接:你说一次,它生成修改;修改格式错了或检查失败,就把错误原样喂回模型再试,但不会无限自修。
为什么这对自研重要 这是“以编辑反馈驱动的局部循环”,而非任意工具调用状态机。
固定提交源码摘录 复制
88 class Coder:
89 abs_fnames = None
90 abs_read_only_fnames = None
91 repo = None
92 last_aider_commit_hash = None
93 aider_edited_files = None
94 last_asked_for_commit_time = 0
95 repo_map = None
96 functions = None
97 num_exhausted_context_windows = 0
98 num_malformed_responses = 0
99 last_keyboard_interrupt = None
100 num_reflections = 0
101 max_reflections = 3
102 edit_format = None
103 yield_stream = False
104 temperature = None
105 auto_lint = True
106 auto_test = False
为什么相信这条结论?查看 2 处证据
02 L1 · fact · aider-loop-002
一次回复后的固定交付链
先看源码事实 模型回复后依次检查文件提及、解析并应用编辑、自动提交、自动 lint、执行建议 shell、可选 test;错误可转成下一轮 reflected_message。
翻译成白话 Aider 更像一条固定流水线,不是让模型临场决定下一步用什么工具。
为什么这对自研重要 确定性强、容易理解,但扩展到浏览器、工单、云资源等非代码工具时不如通用 tool-loop 灵活。
固定提交源码摘录 复制
1530 self.io.tool_output()
1531
1532 self.show_usage_report()
1533
1534 self.add_assistant_reply_to_cur_messages()
1535
1536 if exhausted:
1537 if self.cur_messages and self.cur_messages[-1]["role"] == "user":
1538 self.cur_messages += [
1539 dict(
1540 role="assistant",
1541 content="FinishReasonLength exception: you sent too many tokens",
1542 ),
1543 ]
1544
1545 self.show_exhausted_error()
1546 self.num_exhausted_context_windows += 1
1547 return
1548
1549 if self.partial_response_function_call:
1550 args = self.parse_partial_args()
1551 if args:
1552 content = args.get("explanation") or ""
1553 else:
1554 content = ""
… 58 lines omitted; exact range 1530–1623 …
1613 dict(role="assistant", content="Ok"),
1614 ]
1615
1616 if edited and self.auto_test:
1617 test_errors = self.commands.cmd_test(self.test_cmd)
1618 self.test_outcome = not test_errors
1619 if test_errors:
1620 ok = self.io.confirm_ask("Attempt to fix test errors?")
1621 if ok:
1622 self.reflected_message = test_errors
1623 return
为什么相信这条结论?查看 1 处证据
小练习 2 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M03 · MODEL
模型调用:流式输出如何变成可执行步骤 模型输出的文字、思考、工具调用和错误,经过哪些转换才进入 Agent 状态?
先用一个生活比喻 模型像电话另一端的同事:你听到的不是一整段录音,而是一串实时片段;Harness 要边听边拼装,还要能在电话断线时留下可恢复的记录。
这套实现先回答了什么? Aider 把各家模型 API 的差异交给 LiteLLM,自己的核心只面对一套近似 OpenAI 的消息格式。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 LiteLLM 是统一 Provider 适配层 aider/models.py:985Aider 把各家模型 API 的差异交给 LiteLLM,自己的核心只面对一套近似 OpenAI 的消息格式。
03 L1 · fact · aider-provider-001
LiteLLM 是统一 Provider 适配层
先看源码事实 Model.send_completion 把模型名、messages、stream、temperature、可选单个 function tool 和 extra params 交给 litellm.completion;流式和非流式结果由 Coder 分别消费。
翻译成白话 Aider 把各家模型 API 的差异交给 LiteLLM,自己的核心只面对一套近似 OpenAI 的消息格式。
为什么这对自研重要 接模型很快,但 Provider 行为、重试和能力元数据也部分受 LiteLLM 语义约束。
固定提交源码摘录 复制
985 def send_completion(self, messages, functions, stream, temperature=None):
986 if os.environ.get("AIDER_SANITY_CHECK_TURNS"):
987 sanity_check_messages(messages)
988
989 if self.is_deepseek_r1():
990 messages = ensure_alternating_roles(messages)
991
992 kwargs = dict(
993 model=self.name,
994 stream=stream,
995 )
996
997 if self.use_temperature is not False:
998 if temperature is None:
999 if isinstance(self.use_temperature, bool):
1000 temperature = 0
1001 else:
1002 temperature = float(self.use_temperature)
1003
1004 kwargs["temperature"] = temperature
1005
1006 if functions is not None:
1007 function = functions[0]
1008 kwargs["tools"] = [dict(type="function", function=function)]
1009 kwargs["tool_choice"] = {"type": "function", "function": {"name": function["name"]}}
… 17 lines omitted; exact range 985–1037 …
1027 if "GITHUB_COPILOT_TOKEN" in os.environ:
1028 if "extra_headers" not in kwargs:
1029 kwargs["extra_headers"] = {
1030 "Editor-Version": f"aider/{__version__}",
1031 "Copilot-Integration-Id": "vscode-chat",
1032 }
1033
1034 self.github_copilot_token_to_open_ai_key(kwargs["extra_headers"])
1035
1036 res = litellm.completion(**kwargs)
1037 return hash_object, res
为什么相信这条结论?查看 2 处证据
小练习 3 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M05 · CONTEXT
上下文:有限窗口怎样装下长任务 当对话、工具输出、计划和记忆越来越多,系统怎样决定留下什么、折叠什么、放到哪里?
先用一个生活比喻 上下文不是聊天记录,而是一张会整理的工作台:常用零件放桌面,旧材料装进档案盒,必要时只留下索引卡。
这套实现先回答了什么? Aider 不把所有材料乱塞成一团,而是把“规则、示例、历史、仓库地图、文件正文、当前问题”分舱装箱。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 上下文被拆成稳定的 ChatChunks aider/coders/base_coder.py:1226Aider 不把所有材料乱塞成一团,而是把“规则、示例、历史、仓库地图、文件正文、当前问题”分舱装箱。 Repo Map 是基于符号引用图的 PageRank aider/repomap.py:300它不是简单列目录,而是判断“哪些文件定义了被很多地方引用的名字、哪些又和当前问题相关”,再把最重要的代码骨架给模型。 Repo Map 用二分搜索贴合 token 预算 aider/repomap.py:629先排好“谁最重要”,再用二分法找出能塞进模型窗口的最大一组,而不是拍脑袋截前 100 个。 历史摘要保留近期尾部并递归收缩头部 aider/models.py:339老对话压成摘要,最近几轮尽量原样保留;压完还太大就再压一次。
07 L1 · fact · aider-context-001
上下文被拆成稳定的 ChatChunks
先看源码事实 每次请求按 system、examples、已摘要历史、repo map、只读文件、可编辑文件、当前消息和 reminder 的顺序组装。
翻译成白话 Aider 不把所有材料乱塞成一团,而是把“规则、示例、历史、仓库地图、文件正文、当前问题”分舱装箱。
为什么这对自研重要 分舱让缓存、token 统计和不同模型的 system-message 兼容更可控。
固定提交源码摘录 复制
1226 def format_chat_chunks(self):
1227 self.choose_fence()
1228 main_sys = self.fmt_system_prompt(self.gpt_prompts.main_system)
1229 if self.main_model.system_prompt_prefix:
1230 main_sys = self.main_model.system_prompt_prefix + "\n" + main_sys
1231
1232 example_messages = []
1233 if self.main_model.examples_as_sys_msg:
1234 if self.gpt_prompts.example_messages:
1235 main_sys += "\n# Example conversations:\n\n"
1236 for msg in self.gpt_prompts.example_messages:
1237 role = msg["role"]
1238 content = self.fmt_system_prompt(msg["content"])
1239 main_sys += f"## {role.upper()}: {content}\n\n"
1240 main_sys = main_sys.strip()
1241 else:
1242 for msg in self.gpt_prompts.example_messages:
1243 example_messages.append(
1244 dict(
1245 role=msg["role"],
1246 content=self.fmt_system_prompt(msg["content"]),
1247 )
1248 )
1249 if self.gpt_prompts.example_messages:
1250 example_messages += [
… 77 lines omitted; exact range 1226–1338 …
1328 )
1329 chunks.cur[-1] = dict(role=final["role"], content=new_content)
1330
1331 return chunks
1332
1333 def format_messages(self):
1334 chunks = self.format_chat_chunks()
1335 if self.add_cache_headers:
1336 chunks.add_cache_control_headers()
1337
1338 return chunks
为什么相信这条结论?查看 1 处证据
08 L1 · fact · aider-repomap-001
Repo Map 是基于符号引用图的 PageRank
先看源码事实 RepoMap 用 tree-sitter 查询提取定义/引用,缺少引用时用 Pygments 补齐;构建文件间 MultiDiGraph,并以当前聊天文件、用户提及和符号特征调权后运行 PageRank。
翻译成白话 它不是简单列目录,而是判断“哪些文件定义了被很多地方引用的名字、哪些又和当前问题相关”,再把最重要的代码骨架给模型。
为什么这对自研重要 这是 Aider 最有辨识度的 Harness 能力:用静态代码图在有限 token 内提供全仓导航。
固定提交源码摘录 复制
300
301 # Run the tags queries
302 captures = self._run_captures(Query(language, query_scm), tree.root_node)
303
304 captures_by_tag = defaultdict(list)
305 matches = []
306 for tag, nodes in captures.items():
307 for node in nodes:
308 captures_by_tag[tag].append(node)
309 captures_by_tag[tag].append(node)
310 matches.append((node, tag))
311
312 if USING_TSL_PACK:
313 all_nodes = [(node, tag) for tag, nodes in captures_by_tag.items() for node in nodes]
314 else:
315 all_nodes = matches
316
317 saw = set()
318 for node, tag in all_nodes:
319 if tag.startswith("name.definition."):
320 kind = "def"
321 elif tag.startswith("name.reference."):
322 kind = "ref"
323 else:
324 continue
… 28 lines omitted; exact range 300–363 …
353 tokens = list(lexer.get_tokens(code))
354 tokens = [token[1] for token in tokens if token[0] in Token.Name]
355
356 for token in tokens:
357 yield Tag(
358 rel_fname=rel_fname,
359 fname=fname,
360 name=token,
361 kind="ref",
362 line=-1,
363 )
为什么相信这条结论?查看 2 处证据
09 L1 · fact · aider-repomap-002
Repo Map 用二分搜索贴合 token 预算
先看源码事实 排序后的 tags 被逐步渲染成代码树,算法用二分搜索选择条目数量,以不超预算且尽可能接近预算为目标,15% 误差内可提前停止。
翻译成白话 先排好“谁最重要”,再用二分法找出能塞进模型窗口的最大一组,而不是拍脑袋截前 100 个。
为什么这对自研重要 检索质量和预算控制被明确分成两个阶段,便于调优。
固定提交源码摘录 复制
629 def get_ranked_tags_map_uncached(
630 self,
631 chat_fnames,
632 other_fnames=None,
633 max_map_tokens=None,
634 mentioned_fnames=None,
635 mentioned_idents=None,
636 ):
637 if not other_fnames:
638 other_fnames = list()
639 if not max_map_tokens:
640 max_map_tokens = self.max_map_tokens
641 if not mentioned_fnames:
642 mentioned_fnames = set()
643 if not mentioned_idents:
644 mentioned_idents = set()
645
646 spin = Spinner(UPDATING_REPO_MAP_MESSAGE)
647
648 ranked_tags = self.get_ranked_tags(
649 chat_fnames,
650 other_fnames,
651 mentioned_fnames,
652 mentioned_idents,
653 progress=spin.step,
… 42 lines omitted; exact range 629–706 …
696 break
697
698 if num_tokens < max_map_tokens:
699 lower_bound = middle + 1
700 else:
701 upper_bound = middle - 1
702
703 middle = int((lower_bound + upper_bound) // 2)
704
705 spin.end()
706 return best_tree
为什么相信这条结论?查看 1 处证据
10 L1 · fact · aider-summary-001
历史摘要保留近期尾部并递归收缩头部
先看源码事实 ChatSummary 以模型上下文的 1/16(最少 1K、最多 8K)作为默认历史预算;过大时尽量保留约半数预算的最近消息,摘要较旧头部,必要时最多递归 3 层。
翻译成白话 老对话压成摘要,最近几轮尽量原样保留;压完还太大就再压一次。
为什么这对自研重要 比整段一次摘要更重视近期细节,但摘要是自由文本而非结构化任务状态。
固定提交源码摘录 复制
339 self.max_chat_history_tokens = 1024
340 self.weak_model = None
341 self.editor_model = None
342
343 # Find the extra settings
344 self.extra_model_settings = next(
345 (ms for ms in MODEL_SETTINGS if ms.name == "aider/extra_params"), None
346 )
347
348 self.info = self.get_model_info(model)
349
350 # Are all needed keys/params available?
351 res = self.validate_environment()
352 self.missing_keys = res.get("missing_keys")
353 self.keys_in_environment = res.get("keys_in_environment")
354
355 max_input_tokens = self.info.get("max_input_tokens") or 0
356 # Calculate max_chat_history_tokens as 1/16th of max_input_tokens,
357 # with minimum 1k and maximum 8k
358 self.max_chat_history_tokens = min(max(max_input_tokens / 16, 1024), 8192)
为什么相信这条结论?查看 3 处证据
11 L1 · limitation · aider-context-002
预测超窗时由用户决定是否硬发
先看源码事实 check_tokens 若估算输入超过模型上限,会给出 drop/clear/拆文件建议并询问是否仍继续;Provider 真报 ContextWindowExceeded 时结束本轮,不自动做恢复性压缩再发。
翻译成白话 Aider 会提前报警,但不会偷偷重写上下文;你可以执意发送,失败后自己缩小范围。
为什么这对自研重要 行为透明且可控,但长任务自治恢复弱于带自动 compaction 的通用 Agent。
固定提交源码摘录 复制
1396 def check_tokens(self, messages):
1397 """Check if the messages will fit within the model's token limits."""
1398 input_tokens = self.main_model.token_count(messages)
1399 max_input_tokens = self.main_model.info.get("max_input_tokens") or 0
1400
1401 if max_input_tokens and input_tokens >= max_input_tokens:
1402 self.io.tool_error(
1403 f"Your estimated chat context of {input_tokens:,} tokens exceeds the"
1404 f" {max_input_tokens:,} token limit for {self.main_model.name}!"
1405 )
1406 self.io.tool_output("To reduce the chat context:")
1407 self.io.tool_output("- Use /drop to remove unneeded files from the chat")
1408 self.io.tool_output("- Use /clear to clear the chat history")
1409 self.io.tool_output("- Break your code into smaller files")
1410 self.io.tool_output(
1411 "It's probably safe to try and send the request, most providers won't charge if"
1412 " the context limit is exceeded."
1413 )
1414
1415 if not self.io.confirm_ask("Try to proceed anyway?"):
1416 return False
1417 return True
为什么相信这条结论?查看 2 处证据
12 L1 · fact · aider-git-001
Git 提交是编辑事务和恢复机制
先看源码事实 成功编辑后默认自动提交,修改前的 dirty 文件也可先提交;/undo 只允许撤销当前会话由 Aider 创建的最近提交。
翻译成白话 Aider 用 Git 当保险箱:改前存一份,改后再存一份,出问题可以退回,但不会随便回滚用户自己的提交。
为什么这对自研重要 在代码仓库场景,VCS 原生事务比自造文件快照更透明。
固定提交源码摘录 复制
2375 def auto_commit(self, edited, context=None):
2376 if not self.repo or not self.auto_commits or self.dry_run:
2377 return
2378
2379 if not context:
2380 context = self.get_context_from_history(self.cur_messages)
2381
2382 try:
2383 res = self.repo.commit(fnames=edited, context=context, aider_edits=True, coder=self)
2384 if res:
2385 self.show_auto_commit_outcome(res)
2386 commit_hash, commit_message = res
2387 return self.gpt_prompts.files_content_gpt_edits.format(
2388 hash=commit_hash,
2389 message=commit_message,
2390 )
2391
2392 return self.gpt_prompts.files_content_gpt_no_edits
2393 except ANY_GIT_ERROR as err:
2394 self.io.tool_error(f"Unable to commit: {str(err)}")
2395 return
2396
2397 def show_auto_commit_outcome(self, res):
2398 commit_hash, commit_message = res
2399 self.last_aider_commit_hash = commit_hash
… 13 lines omitted; exact range 2375–2423 …
2413 return
2414 if not self.dirty_commits:
2415 return
2416 if not self.repo:
2417 return
2418
2419 self.repo.commit(fnames=self.need_commit_before_edits, coder=self)
2420
2421 # files changed, move cur messages back behind the files messages
2422 # self.move_back_cur_messages(self.gpt_prompts.files_content_local_edits)
2423 return True
为什么相信这条结论?查看 2 处证据
小练习 5 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M06 · SECURITY
权限与沙箱:能做什么,在哪里做 审批按钮、规则引擎、容器和操作系统沙箱分别解决什么问题?为什么“问过用户”不等于“隔离了风险”?
先用一个生活比喻 审批像门卫问你有没有预约,沙箱像把访客关在指定房间;前者决定是否放行,后者限制放行后能摸到什么。
这套实现先回答了什么? 文件有没有放进聊天,不只是上下文选择,也决定模型能不能直接改它。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 “聊天文件集”就是主要写权限边界 aider/coders/base_coder.py:2215文件有没有放进聊天,不只是上下文选择,也决定模型能不能直接改它。 Shell 在宿主机执行,没有 OS 沙箱 aider/coders/base_coder.py:2434命令执行前会问你,但点了同意以后就是在真实终端里跑;Aider 本身没有容器、seccomp 或工作区文件系统隔离。
13 L1 · fact · aider-permission-001
“聊天文件集”就是主要写权限边界
先看源码事实 模型尝试编辑未加入聊天的文件时,allowed_to_edit 会询问用户;通过后才加入可编辑集合并处理 dirty commit。
翻译成白话 文件有没有放进聊天,不只是上下文选择,也决定模型能不能直接改它。
为什么这对自研重要 Aider 把最重要的权限问题压缩成一个易懂交互,但它不是细粒度路径策略或 RBAC。
固定提交源码摘录 复制
2215
2216 # Seems unlikely that we needed to create the file, but it was
2217 # actually already part of the repo.
2218 # But let's only add if we need to, just to be safe.
2219 if need_to_add and self.auto_commits:
2220 self.repo.repo.git.add(full_path)
2221
2222 self.abs_fnames.add(full_path)
2223 self.check_added_files()
2224 return True
2225
2226 if not self.io.confirm_ask(
2227 "Allow edits to file that has not been added to the chat?",
2228 subject=path,
2229 ):
2230 self.io.tool_output(f"Skipping edits to {path}")
2231 return
2232
2233 if need_to_add and self.auto_commits:
2234 self.repo.repo.git.add(full_path)
2235
2236 self.abs_fnames.add(full_path)
2237 self.check_added_files()
2238 self.check_for_dirty_commit(path)
2239
2240 return True
为什么相信这条结论?查看 1 处证据
14 L1 · limitation · aider-shell-001
Shell 在宿主机执行,没有 OS 沙箱
先看源码事实 模型建议的 shell 命令需显式 yes 确认,然后由 run_cmd 在项目 cwd 通过 shell=True 或交互式 pexpect 启动用户 shell。
翻译成白话 命令执行前会问你,但点了同意以后就是在真实终端里跑;Aider 本身没有容器、seccomp 或工作区文件系统隔离。
为什么这对自研重要 安全依赖人类确认和外部运行环境;自动化模式需要额外容器或最小权限账户。
固定提交源码摘录 复制
2434 def run_shell_commands(self):
2435 if not self.suggest_shell_commands:
2436 return ""
2437
2438 done = set()
2439 group = ConfirmGroup(set(self.shell_commands))
2440 accumulated_output = ""
2441 for command in self.shell_commands:
2442 if command in done:
2443 continue
2444 done.add(command)
2445 output = self.handle_shell_commands(command, group)
2446 if output:
2447 accumulated_output += output + "\n\n"
2448 return accumulated_output
2449
2450 def handle_shell_commands(self, commands_str, group):
2451 commands = commands_str.strip().splitlines()
2452 command_count = sum(
2453 1 for cmd in commands if cmd.strip() and not cmd.strip().startswith("#")
2454 )
2455 prompt = "Run shell command?" if command_count == 1 else "Run shell commands?"
2456 if not self.io.confirm_ask(
2457 prompt,
2458 subject="\n".join(commands),
… 16 lines omitted; exact range 2434–2485 …
2475 exit_status, output = run_cmd(command, error_print=self.io.tool_error, cwd=self.root)
2476 if output:
2477 accumulated_output += f"Output from {command}\n{output}\n"
2478
2479 if accumulated_output.strip() and self.io.confirm_ask(
2480 "Add command output to the chat?", allow_never=True
2481 ):
2482 num_lines = len(accumulated_output.strip().splitlines())
2483 line_plural = "line" if num_lines == 1 else "lines"
2484 self.io.tool_output(f"Added {num_lines} {line_plural} of output to the chat.")
2485 return accumulated_output
为什么相信这条结论?查看 3 处证据
小练习 6 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M07 · ECOSYSTEM
指令、MCP、Skills 与插件:能力如何接进来 一条系统指令、一个 Skill、一个 MCP server 和一个插件,分别在什么时候进入上下文和执行路径?
先用一个生活比喻 这像给工作室接设备:说明书不是设备,设备也不等于电源;成熟 Harness 会分别治理发现、信任、加载、调用和卸载。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
读源码时先问 本课的判断方式 这一层有没有独立证据? 没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。 谁拥有最终控制权? 区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念 固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。
小练习 7 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M08 · COLLABORATION
子 Agent:把一个大任务拆成可治理的协作 什么时候是普通工具调用,什么时候才算子 Agent?子 Agent 的上下文、预算、取消和结果怎样回到父 Agent?
先用一个生活比喻 不是把同事叫来聊天就叫协作;真正的协作要有工单、权限、截止时间、交付物和回收机制。
这套实现先回答了什么? 一个模型负责想清楚“怎么改”,另一个模型只拿方案动手;它们不是并行,也不能继续派生更多 Agent。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 Architect/Editor 是顺序双模型链 aider/coders/architect_coder.py:6一个模型负责想清楚“怎么改”,另一个模型只拿方案动手;它们不是并行,也不能继续派生更多 Agent。
15 L1 · fact · aider-collab-001
Architect/Editor 是顺序双模型链
先看源码事实 ArchitectCoder 先让架构模型产生方案,用户可确认是否编辑;随后创建 editor_coder,清空其历史,以方案作为唯一任务执行修改,再把成本和提交记录回传。
翻译成白话 一个模型负责想清楚“怎么改”,另一个模型只拿方案动手;它们不是并行,也不能继续派生更多 Agent。
为什么这对自研重要 职责分离能让强推理模型配合擅长补丁的模型,但缺少多任务 fan-out、共享黑板和子任务调度。
固定提交源码摘录 复制
6 class ArchitectCoder(AskCoder):
7 edit_format = "architect"
8 gpt_prompts = ArchitectPrompts()
9 auto_accept_architect = False
10
11 def reply_completed(self):
12 content = self.partial_response_content
13
14 if not content or not content.strip():
15 return
16
17 if not self.auto_accept_architect and not self.io.confirm_ask("Edit the files?"):
18 return
19
20 kwargs = dict()
21
22 # Use the editor_model from the main_model if it exists, otherwise use the main_model itself
23 editor_model = self.main_model.editor_model or self.main_model
24
25 kwargs["main_model"] = editor_model
26 kwargs["edit_format"] = self.main_model.editor_edit_format
27 kwargs["suggest_shell_commands"] = False
28 kwargs["map_tokens"] = 0
29 kwargs["total_cost"] = self.total_cost
30 kwargs["cache_prompts"] = False
… 7 lines omitted; exact range 6–48 …
38 editor_coder.cur_messages = []
39 editor_coder.done_messages = []
40
41 if self.verbose:
42 editor_coder.show_announcements()
43
44 editor_coder.run(with_message=content, preproc=False)
45
46 self.move_back_cur_messages("I made those changes to the files.")
47 self.total_cost = editor_coder.total_cost
48 self.aider_commit_hashes = editor_coder.aider_commit_hashes
为什么相信这条结论?查看 2 处证据
小练习 8 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M09 · STATE
会话、持久化与观测:让一次运行变成可追溯事实 如果进程崩了、用户刷新了、任务跑了一夜,系统凭什么恢复并解释“刚才究竟发生了什么”?
先用一个生活比喻 内存像白板,数据库像目录,append-only journal 像监控录像;可靠 Harness 不只保存最后答案,还保存每次转弯。
这套实现先回答了什么? 给用户看的聊天记录、调试模型的原始记录、花费统计和匿名产品埋点是四套不同通道。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 本地历史、原始 LLM 日志、成本和产品分析分层记录 aider/io.py:754给用户看的聊天记录、调试模型的原始记录、花费统计和匿名产品埋点是四套不同通道。
16 L1 · fact · aider-observe-001
本地历史、原始 LLM 日志、成本和产品分析分层记录
先看源码事实 IO 可分别追加输入历史、Markdown 聊天历史和完整 LLM 请求/响应日志;Coder 统计 token/成本;Analytics 经抽样同意后向 PostHog 或本地 JSONL 发事件,并对未知模型名做脱敏。
翻译成白话 给用户看的聊天记录、调试模型的原始记录、花费统计和匿名产品埋点是四套不同通道。
为什么这对自研重要 分层很好,但它不是逐工具 span/trace 的可回放观测系统。
固定提交源码摘录 复制
754 def log_llm_history(self, role, content):
755 if not self.llm_history_file:
756 return
757 timestamp = datetime.now().isoformat(timespec="seconds")
758 try:
759 Path(self.llm_history_file).parent.mkdir(parents=True, exist_ok=True)
760 with open(self.llm_history_file, "a", encoding="utf-8") as log_file:
761 log_file.write(f"{role.upper()} {timestamp}\n")
762 log_file.write(content + "\n")
763 except (PermissionError, OSError) as err:
764 self.tool_warning(f"Unable to write to llm history file {self.llm_history_file}: {err}")
765 self.llm_history_file = None
为什么相信这条结论?查看 4 处证据
小练习 9 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M10 · ENGINEERING
测试、恢复与工程取舍:把漂亮机制变成可靠产品 哪些行为有测试证明?哪些只是配置或 prompt 约定?当安全、性能、可恢复性冲突时,源码选择了什么?
先用一个生活比喻 这像验收一座桥:图纸说明结构,测试证明承重,故障演练证明断电后还能不能让人安全回来。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
读源码时先问 本课的判断方式 这一层有没有独立证据? 没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。 谁拥有最终控制权? 区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念 固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。
小练习 10 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M11 · PRACTICE
把读懂变成会判断 下面的练习不要求你先写一个完整 Agent,而是训练你检查设计边界:事实是什么、推断是什么、如果换成自研产品要补哪一层。
Q1 交互外环 + 有界 reflection 内环 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 这是“以编辑反馈驱动的局部循环”,而非任意工具调用状态机。
证据:aider/coders/base_coder.py:88 Q2 一次回复后的固定交付链 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 确定性强、容易理解,但扩展到浏览器、工单、云资源等非代码工具时不如通用 tool-loop 灵活。
证据:aider/coders/base_coder.py:1530 Q3 LiteLLM 是统一 Provider 适配层 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 接模型很快,但 Provider 行为、重试和能力元数据也部分受 LiteLLM 语义约束。
证据:aider/models.py:985 Q4 上下文被拆成稳定的 ChatChunks 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 分舱让缓存、token 统计和不同模型的 system-message 兼容更可控。
证据:aider/coders/base_coder.py:1226 Q5 Repo Map 是基于符号引用图的 PageRank 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 这是 Aider 最有辨识度的 Harness 能力:用静态代码图在有限 token 内提供全仓导航。
证据:aider/repomap.py:300
APPENDIX · SOURCE INDEX
本课读过的实现文件 文件索引帮助你在课程外继续追踪调用链;每一条路径来自固定提交的证据账本。
01 aider/coders/base_coder.pyL88–106, L876–944, L1530–1623, L1783–1826, L1226–1338, L1396–1417, L1457–1467, L124–201, L2269–2336, L2215–2240, L2434–2485, L2375–2423 02 aider/models.pyL985–1037, L339–358, L625–645, L1006–1009 03 aider/repomap.pyL300–363, L365–545, L629–706 04 aider/history.pyL27–96, L98–123 05 aider/coders/patch_coder.pyL210–217 06 aider/coders/udiff_coder.pyL46–49 07 aider/run_cmd.pyL42–84, L89–128 08 aider/commands.pyL558–646, L312–332 09 aider/coders/architect_coder.pyL6–48 10 aider/io.pyL754–765, L1117–1136 11 aider/analytics.pyL119–204, L213–254
下一步 从课程回到报告,做一次反向核验 教程负责让你读懂,报告负责让你查证。打开报告页,任选一个章节,尝试只靠源码摘录复述它的边界。
查看 Aider 报告 ↗