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,分析基准 commit5dc9490bb35f9729ef2c95d00a19ccd30c26339c(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 TrueREPL 循环,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.py(get_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.py的ChatSummary类——当too_big(done_messages)超过max_chat_history_tokens(默认取自main_model.max_chat_history_tokens)时触发;递归二分消息为 head/tail,用model.simple_send_with_retries配合prompts.summarizesystem 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.py、udiff_coder.py、patch_coder.py)解析不同文本格式表达同一个”编辑文件”工具概念——格式的选择是模型专属配置(models.py里ModelSettings.edit_format),不是 LLM 运行时的工具选择决策。 - 确实存在 OpenAI function-calling 变体(
editblock_func_coder.py、wholefile_func_coder.py、single_wholefile_func_coder.py,用functions = [...]JSON-schema 类属性,经Coder.send(messages, functions=self.functions)→ LiteLLMfunctions=参数下发),但这些是遗留/实验性代码——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 是独立的类(
EditBlockPrompts、WholeFilePrompts、UnifiedDiffPrompts、ArchitectPrompts、AskPrompts、HelpPrompts、ContextPrompts、PatchPrompts),均继承CoderPrompts(base_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_context,commands.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_message(base_coder.py:1599-1623),上限max_reflections=3。 - 没有跨会话学习、没有持久化的”经验教训”、没有自我修改的 prompt、没有由 Aider 自身触发的微调循环。
benchmark/目录(SWE-bench、Exercism/polyglot 基准,benchmark/benchmark.py、problem_stats.py、over_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_key,us.i.posthog.com)与遗留 Mixpanel 路径(默认禁用/注释掉),经百分比抽样弹窗确认(Analytics.need_to_ask,PERCENT=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-file→io.log_llm_history(role, content)(io.py:754),记录每条 prompt/response。 - 人类可读会话记录:
--chat-history-file(默认.aider.chat.history.md)经io.append_chat_history(io.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:1604、1620)。 - 聊天中提到的 URL → 抓取前确认(
check_for_urls,base_coder.py:964-984)。 --yes-always/-y绕过以上所有确认(例如return_coder=True编程式/API 嵌入场景自动启用,main.py:546-547)。
- LLM 提到不在 chat 中的文件 → 添加前确认(
- API key 管理:仅通过环境变量(
ANTHROPIC_API_KEY、OPENAI_API_KEY、通用--api-key provider=key、--set-env),经.env文件搜索路径加载(load_dotenv_files,main.py:361-387),另有~/.aider/oauth-keys.env优先加载(OpenRouter OAuth 流程获取的 key,aider/onboarding.py未完整读取但main.py:792offer_openrouter_oauth有引用)。无密钥保险库、无轮换、无作用域受限的短时凭证——纯环境变量/dotenv 模型。 scrub_sensitive_info(format_settings.py,main.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/Dockerfile、benchmark/docker.sh、benchmark/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(布尔)、reminder(system_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_tokensCLI 参数受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.pyaider/coders/base_coder.pyaider/coders/chat_chunks.pyaider/coders/editblock_coder.pyaider/coders/editblock_prompts.pyaider/coders/architect_coder.pyaider/coders/architect_prompts.pyaider/coders/base_prompts.pyaider/coders/shell.pyaider/coders/__init__.pyaider/history.pyaider/repomap.pyaider/repo.pyaider/models.pyaider/run_cmd.pyaider/commands.pyaider/watch.pyaider/analytics.pyaider/io.pyaider/website/docs/repomap.mdaider/website/_posts/2023-10-22-repomap.mdaider/website/_posts/2024-09-26-architect.mdaider/website/docs/more/edit-formats.mdbenchmark/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.pyarchitect_coder.pyarchitect_prompts.pybase_coder.pybase_prompts.pybenchmark-README.mdblog-architect.mdblog-repomap.mddocs-edit-formats.mddocs-repomap.mdeditblock_coder.pyeditblock_prompts.pyhistory.pymodels.pyrepomap.pyrun_cmd.pyshell.pywatch.py