Harness · Coding Agent Book02 / Aider
研究总览
M02 · SOURCE-GROUNDED TUTORIAL

Aider
从源码学会它怎么工作

以 Git、Repo Map 和可替换编辑协议取胜;不是工具调用型自治平台。 我们不把 README 当结论,而是沿主循环、工具、上下文、权限、扩展、协作和状态一路读到实现。

Python · Git-native Pair ProgrammerApache-2.05dc9490bb35f16 个结论 · 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

跟踪一个任务:从输入到交付

把下面九步当成你读源码时的“地图坐标”。每到一个节点,都能回到后面的章节查具体实现。

读图提醒

箭头只表示控制面之间的关系,不代表每个实现都同步、串行或拥有 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

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M04 · TOOLS

工具系统:Agent 的手脚怎样被注册和调度

模型看见的工具说明,和真正执行工具的代码,是不是同一个东西?并发、编辑和失败结果怎么处理?

先用一个生活比喻

工具系统像机场:模型提交登机牌,注册表确认航班,权限闸机检查证件,调度器决定跑道,最后才允许真正起飞。

这套实现先回答了什么?

Aider 把“模型应该怎样描述改动”做成多种可换的方言,并按模型能力选择最合适的一种。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
编辑协议是可替换 Coder 家族aider/coders/base_coder.py:124Aider 把“模型应该怎样描述改动”做成多种可换的方言,并按模型能力选择最合适的一种。
编辑先 dry-run,再授权,再落盘aider/coders/base_coder.py:2269模型给出的补丁先试演,确认目标文件允许修改后才真正写;补丁坏了会把错误退回给模型修。
核心不是 MCP/函数工具循环aider/models.py:1006Aider 专注“在 Git 仓库里改代码”,不是一个可随时挂几十种业务工具的通用 Agent 平台。
04
L2 · fact · aider-edit-001

编辑协议是可替换 Coder 家族

先看源码事实

Coder.create 按 edit_format 选择 Whole、Diff、Unified Diff、Patch、Function、Architect、Ask、Context 等子类;切换格式时会先摘要旧格式历史,避免模型模仿旧输出协议。

翻译成白话

Aider 把“模型应该怎样描述改动”做成多种可换的方言,并按模型能力选择最合适的一种。

为什么这对自研重要

编辑可靠性可以按模型定制,而不必让所有模型都走同一个 JSON tool schema。

固定提交源码摘录
  124      @classmethod
  125      def create(
  126          self,
  127          main_model=None,
  128          edit_format=None,
  129          io=None,
  130          from_coder=None,
  131          summarize_from_coder=True,
  132          **kwargs,
  133      ):
  134          import aider.coders as coders
  135  
  136          if not main_model:
  137              if from_coder:
  138                  main_model = from_coder.main_model
  139              else:
  140                  main_model = models.Model(models.DEFAULT_MODEL_NAME)
  141  
  142          if edit_format == "code":
  143              edit_format = None
  144          if edit_format is None:
  145              if from_coder:
  146                  edit_format = from_coder.edit_format
  147              else:
  148                  edit_format = main_model.edit_format
      … 42 lines omitted; exact range 124–201 …
  191              if hasattr(coder, "edit_format") and coder.edit_format == edit_format:
  192                  res = coder(main_model, io, **kwargs)
  193                  res.original_kwargs = dict(kwargs)
  194                  return res
  195  
  196          valid_formats = [
  197              str(c.edit_format)
  198              for c in coders.__all__
  199              if hasattr(c, "edit_format") and c.edit_format is not None
  200          ]
  201          raise UnknownEditFormat(edit_format, valid_formats)
为什么相信这条结论?查看 3 处证据
05
L1 · fact · aider-edit-002

编辑先 dry-run,再授权,再落盘

先看源码事实

apply_updates 先解析 edits、执行 apply_edits_dry_run、逐文件 allowed_to_edit/prepare_to_edit,最后才 apply_edits;格式错误或异常会转成 reflected_message。

翻译成白话

模型给出的补丁先试演,确认目标文件允许修改后才真正写;补丁坏了会把错误退回给模型修。

为什么这对自研重要

编辑协议的验证和文件授权是两个独立关卡。

固定提交源码摘录
 2269      def prepare_to_edit(self, edits):
 2270          res = []
 2271          seen = dict()
 2272  
 2273          self.need_commit_before_edits = set()
 2274  
 2275          for edit in edits:
 2276              path = edit[0]
 2277              if path is None:
 2278                  res.append(edit)
 2279                  continue
 2280              if path == "python":
 2281                  dump(edits)
 2282              if path in seen:
 2283                  allowed = seen[path]
 2284              else:
 2285                  allowed = self.allowed_to_edit(path)
 2286                  seen[path] = allowed
 2287  
 2288              if allowed:
 2289                  res.append(edit)
 2290  
 2291          self.dirty_commit()
 2292          self.need_commit_before_edits = set()
 2293  
      … 32 lines omitted; exact range 2269–2336 …
 2326  
 2327              self.reflected_message = str(err)
 2328              return edited
 2329  
 2330          for path in edited:
 2331              if self.dry_run:
 2332                  self.io.tool_output(f"Did not apply edit to {path} (--dry-run)")
 2333              else:
 2334                  self.io.tool_output(f"Applied edit to {path}")
 2335  
 2336          return edited
为什么相信这条结论?查看 1 处证据
06
L1 · limitation · aider-tools-001

核心不是 MCP/函数工具循环

先看源码事实

核心 Coder 的外部能力来自固定 CLI 命令、Git、文件编辑格式、lint/test、网页抓取和可选单一 function edit schema;仓库核心未实现通用 MCP 工具注册与动态插件调度。

翻译成白话

Aider 专注“在 Git 仓库里改代码”,不是一个可随时挂几十种业务工具的通用 Agent 平台。

为什么这对自研重要

专注带来可预测性和编辑深度,连接器生态与跨系统工作流则明显受限。

边界与风险
  • 结论针对该快照的核心仓库;第三方封装可在 Aider 外层增加 MCP。
固定提交源码摘录
 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"]}}
为什么相信这条结论?查看 2 处证据
小练习 4

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M05 · CONTEXT

上下文:有限窗口怎样装下长任务

当对话、工具输出、计划和记忆越来越多,系统怎样决定留下什么、折叠什么、放到哪里?

先用一个生活比喻

上下文不是聊天记录,而是一张会整理的工作台:常用零件放桌面,旧材料装进档案盒,必要时只留下索引卡。

这套实现先回答了什么?

Aider 不把所有材料乱塞成一团,而是把“规则、示例、历史、仓库地图、文件正文、当前问题”分舱装箱。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
上下文被拆成稳定的 ChatChunksaider/coders/base_coder.py:1226Aider 不把所有材料乱塞成一团,而是把“规则、示例、历史、仓库地图、文件正文、当前问题”分舱装箱。
Repo Map 是基于符号引用图的 PageRankaider/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

本课读过的实现文件

文件索引帮助你在课程外继续追踪调用链;每一条路径来自固定提交的证据账本。

  1. 01aider/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
  2. 02aider/models.pyL985–1037, L339–358, L625–645, L1006–1009
  3. 03aider/repomap.pyL300–363, L365–545, L629–706
  4. 04aider/history.pyL27–96, L98–123
  5. 05aider/coders/patch_coder.pyL210–217
  6. 06aider/coders/udiff_coder.pyL46–49
  7. 07aider/run_cmd.pyL42–84, L89–128
  8. 08aider/commands.pyL558–646, L312–332
  9. 09aider/coders/architect_coder.pyL6–48
  10. 10aider/io.pyL754–765, L1117–1136
  11. 11aider/analytics.pyL119–204, L213–254
下一步

从课程回到报告,做一次反向核验

教程负责让你读懂,报告负责让你查证。打开报告页,任选一个章节,尝试只靠源码摘录复述它的边界。

查看 Aider 报告 ↗