Aider

一句话定位

Aider 是一个终端 CLI 里运行的 pair-programming 工具,其核心特征是刻意不用 OpenAI function-calling / MCP 做文件编辑,而是让 LLM 在 markdown 回复里写文本编辑格式(SEARCH/REPLACE、whole-file、udiff 等),由 Aider 自己的 parser 抽取并应用;配合一个上限 3 次的 lint/test 失败自动反射(reflection)循环,以及围绕 models.py 里几十个模型量身定制的 ModelSettings(编辑格式、提示位置等)——是”重模型协同设计、轻通用 agent 框架”的代表。

核心架构总览(目录结构关键路径 + 引用的 commit)

  • repo: https://github.com/Aider-AI/aider,分析基准 commit 5dc9490bb35f9729ef2c95d00a19ccd30c26339c(shallow clone HEAD,提交日期 2026-05-22 07:02:20 -0700),2026-07-07 clone 并抽取关键文件。
  • 包身份:PyPI 包 aider-chat,CLI 入口 aider / python -m aider;官方文档站 aider.chat 由同仓库 aider/website/(Jekyll)构建。
  • 关键路径:
    • aider/main.py(1275 行)——CLI 参数解析、Coder 构造、顶层 while True REPL 循环,SwitchCoder 异常驱动模式切换。
    • aider/coders/base_coder.py(2485 行)——Coder 基类:agent 循环、prompt 组装、发送/接收、reflection 循环、应用编辑、自动提交、lint/test 循环。
    • aider/coders/chat_chunks.py——ChatChunks 消息分桶数据类(system/examples/done/repo/readonly/chat_files/cur/reminder + cache-control 头注入)。
    • aider/coders/editblock_coder.py + editblock_prompts.py——SEARCH/REPLACE 编辑格式的 parser 与专属 system prompt。
    • aider/coders/architect_coder.py + architect_prompts.py——Architect/Editor 双模型编排。
    • aider/coders/base_prompts.py——各编辑格式 prompt 类的公共基类 CoderPrompts
    • aider/coders/shell.py——shell 命令建议的 prompt 片段。
    • aider/coders/__init__.py——所有 coder 类的注册表(__all__)。
    • aider/history.py(143 行)——ChatSummary:token 预算触发的递归历史摘要。
    • aider/repomap.pyget_tags/get_ranked_tags 约 365-465 行读全)——tree-sitter 标签抽取 + networkx PageRank 选取 repo-map 内容。
    • aider/repo.py——git 集成(自动提交、.aiderignore、tracked-files 扫描)。
    • aider/models.py——ModelSettings 数据类;每个模型族硬编码的 edit_format/use_repo_map/reminder 位置等。
    • aider/run_cmd.py(132 行)——shell 执行:pexpect 交互式 spawn 或 subprocess.Popen 回退,无任何沙箱
    • aider/commands.py(约 1712 行)——/-命令分发表。
    • aider/watch.py——文件监听(watchfiles),扫描源码内联 AI!/AI? 注释作为触发通道。
    • aider/analytics.py(259 行)——opt-in PostHog/Mixpanel 遥测。
    • aider/io.py——原始 LLM 请求/响应记录(log_llm_history)与人类可读聊天记录(append_chat_history)。

Agent Loop(主循环 / 何时继续何时停)

  • 顶层:main()aider/main.py:1159-1181)—— while True: coder.run() 无限循环;SwitchCoder 异常(由 /ask/code/architect 命令或 --message 触发)使 Coder.create() 重建新 Coder 实例并携带历史,循环继续。
  • 单轮循环:Coder.run()Coder.run_one()base_coder.py:924-944)实现一个反射循环send_message() 执行后,若 self.reflected_message 被设置(由 lint 错误、test 错误、编辑格式解析失败,或”提到了不在 chat 中的文件”提示触发),则把该反射消息作为下一轮”user”发送,上限 max_reflections = 3(类属性,base_coder.py:101)。
  • send_message()base_coder.py:1419-1623)是单轮主体:构造消息 → 调模型(LiteLLM 异常重试退避)→ 解析并应用编辑 → 自动提交 → 可选自动 lint → 可选自动 test → 视情况设置 reflected_message
  • 判断:这不是 ReAct 式”工具调用→观察→决策下一步”的开放循环,而是异常/状态驱动、范围限定(仅针对解析编辑/提交/lint/test 这几件事)的有界自动续跑,硬上限 3 次。

记忆与上下文管理(压缩、长期记忆、会话持久化)

  • 短/中期:cur_messages(未提交的当前轮次)+ done_messages(编辑成功后折叠进历史,经 move_back_cur_messages)。
  • 压缩:aider/history.pyChatSummary 类——当 too_big(done_messages) 超过 max_chat_history_tokens(默认取自 main_model.max_chat_history_tokens)时触发;递归二分消息为 head/tail,用 model.simple_send_with_retries 配合 prompts.summarize system prompt 摘要 head 部分,若仍过大则继续递归(深度守卫至 3 层),否则退化为 summarize_all 整体压缩成一段。
  • 长期/跨调用持久化记忆:不存在向量/embedding 式长期记忆。替代方案:
    • .aider.chat.history.md(默认)——完整人类可读会话记录,append-only(io.append_chat_history, io.py:1117)。
    • --llm-history-file——每次 LLM 调用的原始请求/响应日志(io.log_llm_history, io.py:754)。
    • /save / /load 持久化/重放一组 /-命令(更像宏/脚本,不是原始记忆)——commands.py: cmd_save/cmd_load
    • restore_chat_history 参数可在新会话启动时把旧 .aider.chat.history.md 内容回放进来。
  • Repo-level 上下文(非”记忆”但功能类似外部知识):repo-map(见工具体系一节)每轮从当前 git 树现算,不持久化/不学习。

工具体系(定义/调用协议/注册/权限)

  • Aider 的主力 coder 不是 function-calling 原生的。文件编辑的”工具”是从 assistant 的 markdown 回复中解析出的文本编辑格式协议
    • EditBlockCoder.get_edits()editblock_coder.py:21-36)调用 find_original_update_blocks(),用正则/状态机解析每个文件的 <<<<<<< SEARCH / ======= / >>>>>>> REPLACE 块(fenced code block + 上一行文件名)。
    • apply_edits()editblock_coder.py:41-)对当前文件内容做 SEARCH 块的字面字符串匹配(do_replace),匹配失败时有模糊匹配提示回退(find_similar_lines),失败会作为”工具调用错误”反馈给 LLM(走反射消息)。
    • 其他 coder(wholefile_coder.pyudiff_coder.pypatch_coder.py)解析不同文本格式表达同一个”编辑文件”工具概念——格式的选择是模型专属配置(models.pyModelSettings.edit_format),不是 LLM 运行时的工具选择决策。
    • 确实存在 OpenAI function-calling 变体(editblock_func_coder.pywholefile_func_coder.pysingle_wholefile_func_coder.py,用 functions = [...] JSON-schema 类属性,经 Coder.send(messages, functions=self.functions) → LiteLLM functions= 参数下发),但这些是遗留/实验性代码——single_wholefile_func_coder 已从 coders/__init__.py__all__ 里注释掉(未注册),意味着 function-calling 路径在当前默认流程中已是死代码。
    • Shell 命令执行是一种准工具:LLM 被提示(coders/shell.py)在回复里建议 ```bash fenced 命令;base_coder.py 通过 coder 的 get_edits() 返回的 (None, command) 元组抽取(editblock_coder.py:33: self.shell_commands += [edit[1] for edit in edits if edit[0] is None]),再由 run_shell_commands()base_coder.py:2434+)经 run_cmd() 执行,必须先经 io.confirm_ask() 用户确认/test场景的非零退出除外)。
  • 未发现通用插件/工具注册中心,也未发现 MCP 支持(已在本 commit 中全文检索,确认不存在)。

Prompt 设计(系统提示结构、动态组装)

  • Coder.format_chat_chunks()base_coder.py:1226-1331)构造系统提示:gpt_prompts.main_system(编辑格式专属类,如 editblock_prompts.py 中的 EditBlockPrompts)+ 可选 model.system_prompt_prefix + 可选示例对话(按 examples_as_sys_msg 设置,要么折叠进系统消息,要么作为独立的 few-shot user/assistant 消息)+ gpt_prompts.system_reminder(追加到系统消息末尾还是作为末尾单独的 system/user 消息,取决于 main_model.reminder"sys" 还是 "user"——这本身是 models.py 里逐模型调优的设置)。
  • Prompt 文本是格式化模板:用 {fence[0]}/{fence[1]}(反引号 vs 备用围栏,经 choose_fence() 自动选择以避免与被讨论代码的围栏冲突)以及 {final_reminders}{shell_cmd_prompt} 等做 .format() 风格插值(fmt_system_prompt)。
  • 消息分桶顺序(ChatChunks.all_messages()chat_chunks.py):system → examples → done(已摘要历史)→ repo(repo-map 消息)→ readonly-files → chat-files(加入 chat 的文件全文)→ cur(当前轮)→ reminder。
  • 各编辑格式的 system prompt 是独立的类(EditBlockPromptsWholeFilePromptsUnifiedDiffPromptsArchitectPromptsAskPromptsHelpPromptsContextPromptsPatchPrompts),均继承 CoderPromptsbase_prompts.py)——这是 Aider 里最接近”prompt 模板库”的东西。

Router / 编排(任务分解、多 agent、子 agent)

  • Aider 唯一的多模型编排是 Architect/Editor 模式architect_coder.py,设计动机见 website/_posts/2024-09-26-architect.md):
    • ArchitectCoder.reply_completed()architect_coder.py:11-48 全文):Architect 模型给出纯文本方案后(除非 auto_accept_architect,否则经 io.confirm_ask("Edit the files?") 确认),构造第二个 Coder 实例(Coder.create(),使用 main_model.editor_model/editor_edit_format),以 architect 的纯文本方案作为 with_message 种子,同步运行(editor_coder.run(with_message=content, preproc=False))。
    • 这是严格的两步流水线(architect 提案 → editor 落地),不是通用多 agent 框架——没有子 agent 派生,没有任务树/planner,没有并行 agent。
    • /ask/code/context 命令(cmd_ask/cmd_code/cmd_contextcommands.py:1182-1198)通过同样的 SwitchCoder 异常机制切换 coder”模式”(不同系统提示/行为)——这是单 agent 模式切换,不是多 agent 编排。
    • 未发现”router LLM 把任务拆解并派发给专用 agent”的机制。

Skill / 插件体系

  • 未发现外部 skill/插件加载机制(无插件清单格式、无 marketplace、无用户扩展代码的动态加载)。
  • 最接近的类比是 commands.py 里固定的 /-命令表(get_commands()、所有 cmd_* 方法)——是硬编码固定表,非用户可扩展。另有 watch.py 里的 “ai-comment” 触发(正则匹配源码注释中的 ai\b/ai!/ai?)作为嵌入代码文件的另一种”调用 Aider”触发通道,但这是固定功能,不是插件系统。
  • 结论:未实现通用 skill/插件架构。

自进化能力(自我改进 / 学习型记忆 / eval 驱动纠错)

  • 唯一的”eval 驱动纠错”是上文的 lint/test 反射循环:auto_lint/auto_test 设置在编辑后运行 linter/测试,失败时询问是否将错误反馈给 LLM 作为 reflected_messagebase_coder.py:1599-1623),上限 max_reflections=3
  • 没有跨会话学习、没有持久化的”经验教训”、没有自我修改的 prompt、没有由 Aider 自身触发的微调循环。
  • benchmark/ 目录(SWE-bench、Exercism/polyglot 基准,benchmark/benchmark.pyproblem_stats.pyover_time.py)是面向开发者的离线评测工具,用于比较模型/编辑格式组合并发布 leaderboard(aider/website/docs/leaderboards/)——它不反哺正在运行的 Aider 会话,也不会自动重训/调优任何东西;是 Aider 维护者据此人工挑选默认 edit_format/模型设置,硬编码进后续版本的 models.py。所以间接看:“benchmark 结果 → 人工筛选 → 新版本里更新的 ModelSettings 默认值”是一个人类主导的项目流程,不是自动化自进化机制。

可观测性(日志 / trace 格式)

  • Opt-in 遥测:aider/analytics.py——PostHog(硬编码 posthog_project_api_keyus.i.posthog.com)与遗留 Mixpanel 路径(默认禁用/注释掉),经百分比抽样弹窗确认(Analytics.need_to_askPERCENT=10 采样)与 --analytics/--analytics-disable 开关控制;analytics.event(name, **kwargs)main.py 数十处调用点触发("launched""cli session""exit"reason="repo""auto_commits" 等)。
  • 本地结构化日志选项:--analytics-log <file> 把同样的事件写成 JSONL(analytics.py:242-254)。
  • 原始 LLM I/O 追踪:--llm-history-fileio.log_llm_history(role, content)io.py:754),记录每条 prompt/response。
  • 人类可读会话记录:--chat-history-file(默认 .aider.chat.history.md)经 io.append_chat_historyio.py:1117),markdown 引用块格式。
  • 未发现 OpenTelemetry/结构化 span-trace 格式,无分布式追踪。是扁平事件/遥测日志,不是 trace/span 模型。

安全与权限(审批门、密钥管理)

  • 审批门全部是交互式 io.confirm_ask(...) 调用,不是正式的权限策略引擎:
    • LLM 提到不在 chat 中的文件 → 添加前确认(check_for_file_mentions 流程)。
    • 建议的 shell 命令 → 执行前确认(cmd_run,自动 /test 非零退出场景除外)。
    • 反射修复 lint/test 错误 → 反馈前确认(base_coder.py:16041620)。
    • 聊天中提到的 URL → 抓取前确认(check_for_urlsbase_coder.py:964-984)。
    • --yes-always/-y 绕过以上所有确认(例如 return_coder=True 编程式/API 嵌入场景自动启用,main.py:546-547)。
  • API key 管理:仅通过环境变量(ANTHROPIC_API_KEYOPENAI_API_KEY、通用 --api-key provider=key--set-env),经 .env 文件搜索路径加载(load_dotenv_filesmain.py:361-387),另有 ~/.aider/oauth-keys.env 优先加载(OpenRouter OAuth 流程获取的 key,aider/onboarding.py 未完整读取但 main.py:792 offer_openrouter_oauth 有引用)。无密钥保险库、无轮换、无作用域受限的短时凭证——纯环境变量/dotenv 模型。
  • scrub_sensitive_infoformat_settings.pymain.py:29 引用)在写入聊天记录前对命令行日志做 API key 脱敏(cmd_line = scrub_sensitive_info(args, cmd_line)main.py:750)——值得记录的隐私细节,但不是完整密钥管理器。

沙箱与执行隔离

  • 正常使用中无沙箱。 run_cmd.py 通过 subprocess.Popen(..., shell=True, cwd=self.coder.root)pexpect.spawn(shell, args=["-i","-c",command])(交互式命令)直接执行 shell 命令——对用户真实 shell/文件系统/网络的完全访问,仅由前述”执行前确认”提示把关。
  • 编辑写文件是直接对真实文件系统的 io.write_text(full_path, new_content) 调用(如 editblock_coder.py:71)——无容器、无 chroot、无”沙箱内预览再应用”步骤(--dry-run 标志跳过写入但仍展示将要发生的操作,不是沙箱)。
  • 唯一讨论 Docker/容器隔离的地方是 benchmark/README.md:该基准测试工具明确警告”将执行 LLM 生成的代码而未经任何人工审查”,建议/要求在”docker 容器内”运行(benchmark/Dockerfilebenchmark/docker.shbenchmark/docker_build.sh 均存在)——这仅限于 Aider 自身内部 SWE-bench/Exercism 评测流水线,不涉及终端用户会话。
  • 结论:沙箱化仅是 benchmark/eval 场景的关切;生产使用是”信任用户自己的机器 + 确认提示”作为唯一防护。

与模型的协同设计

  • aider/models.py:大量硬编码的 ModelSettings 表(grep 到 80+ 个模型专属配置块),每个模型族设置:edit_format(diff/whole/udiff/diff-fenced)、use_repo_map(布尔)、remindersystem_reminder 块放在 "sys" 还是 "user")、examples_as_sys_msg(few-shot 示例是否折叠进系统提示 vs 作为独立轮次——据称部分模型处理多轮 few-shot 较差)、weak_model_name(用于生成 commit message 和历史摘要的廉价模型)、editor_model_name/editor_edit_format(Architect 模式的搭档模型+格式)。
  • website/docs/more/edit-formats.md 明确文档化了每种格式因何种模型而存在——例如 udiff 专为对抗 GPT-4-Turbo 的”偷懒编码”(用 # ... original code ... 省略代码)而设计;diff-fenced 存在是因为 Gemini 模型”经常无法遵守”默认 diff 格式的围栏约定。
  • website/_posts/2024-09-26-architect.md:Architect/Editor 拆分被实证证明能提升基准分数(o1-preview architect + DeepSeek/o1-mini editor → Aider polyglot benchmark 上 85% SOTA)——直接证据表明harness 结构是根据模型行为迭代出来的,而非反过来。
  • 推理模型处理:args.reasoning_effort/args.thinking_tokens CLI 参数受 main_model.accepts_settings 能力列表把关(main.py:836-870)——应用推理强度设置前先做逐模型能力协商。

轨迹利用(session/trajectory 是否反哺训练/评测)

  • 未发现任何会话轨迹被反馈进训练流水线的证据——Aider 是一个客户端 CLI 工具,自身没有训练基础设施。
  • benchmark/ 工具运行的是固定的、已提交进仓库的题目集(Exercism 练习题、经 swe-bench-lite.txt/swe_bench.py 的 SWE-bench-lite)——这些是一次性人工整理的静态评测语料,不是从真实用户会话/轨迹派生的。
  • Opt-in 分析(见”可观测性”一节)只采集粗粒度事件遥测(运行了哪些命令、模型名、花费、退出原因),明确采集代码、聊天消息或 prompt(main.py:645:“Aider respects your privacy and never collects your code, chat messages, keys or personal info”)——所以即便是匿名遥测通道,也不是轨迹采集机制。
  • 聊天记录文件(.aider.chat.history.md--llm-history-file)是用户自用的本地调试/参考产物;仓库中没有任何跨用户上传或聚合这些数据的机制。
  • 结论:未实现——轨迹留在本地,没有反馈进模型训练或来自真实使用的集中评测的通路。

与同类 harness 的关键差异(1-3 条,可以先留一句概述,后续 synthesis 阶段会做跨 harness 对比)

  • 与走 function-calling/MCP 路线的 harness(如 smolagents 的 ToolCallingAgent)相比,Aider 的”编辑”从头到尾是纯文本编辑格式协议(SEARCH/REPLACE 等),function-calling 变体存在但已被从注册表移除、事实上是死代码——这是一个刻意的、有官方博客文章论证的设计选择(针对特定模型的”偷懒编码”/围栏遵从问题),而非能力缺失。
  • Aider 没有通用多 agent/router 框架,唯一的编排是写死的两步 Architect→Editor 流水线;相比之下更强调”针对具体模型族的 prompt/格式精细调优”(models.py 的规模)而不是”更通用的 agent 抽象”。
  • 反射循环(lint/test 失败自动重试)是硬编码上限 3 次的特定场景自动化,不是开放式 ReAct 工具循环——这是”有界、状态触发式自纠正”与”开放式 agentic loop”之间路线差异的一个具体样本,后续跨 harness 对比可用它做参照系。

原始源码定位

  • repo: https://github.com/Aider-AI/aider
  • commit/version analyzed: 5dc9490bb35f9729ef2c95d00a19ccd30c26339c(shallow clone HEAD,提交日期 2026-05-22 07:02:20 -0700)
  • 关键文件列表(相对仓库根目录):
    • aider/main.py
    • aider/coders/base_coder.py
    • aider/coders/chat_chunks.py
    • aider/coders/editblock_coder.py
    • aider/coders/editblock_prompts.py
    • aider/coders/architect_coder.py
    • aider/coders/architect_prompts.py
    • aider/coders/base_prompts.py
    • aider/coders/shell.py
    • aider/coders/__init__.py
    • aider/history.py
    • aider/repomap.py
    • aider/repo.py
    • aider/models.py
    • aider/run_cmd.py
    • aider/commands.py
    • aider/watch.py
    • aider/analytics.py
    • aider/io.py
    • aider/website/docs/repomap.md
    • aider/website/_posts/2023-10-22-repomap.md
    • aider/website/_posts/2024-09-26-architect.md
    • aider/website/docs/more/edit-formats.md
    • benchmark/README.md

一手源存档(sources/)

/Users/zhao/projects/self-wiki/ai-research/sources/harness/aider/ 下:

  • NOTES.md —— 本次调研的完整逐维度笔记(本 dossier 的直接来源)
  • key-files/ —— 上述关键文件的逐字保存副本(提交 5dc9490b 时点),包括:
    • main.py(此处未单独存档,见 NOTES 说明;key-files/ 实际包含以下文件)
    • analytics.py
    • architect_coder.py
    • architect_prompts.py
    • base_coder.py
    • base_prompts.py
    • benchmark-README.md
    • blog-architect.md
    • blog-repomap.md
    • docs-edit-formats.md
    • docs-repomap.md
    • editblock_coder.py
    • editblock_prompts.py
    • history.py
    • models.py
    • repomap.py
    • run_cmd.py
    • shell.py
    • watch.py