aider 实战复盘:让/drop不再误清启动时通过--read指定的只读参考文件
【免费下载链接】aideraider is AI pair programming in your terminal项目地址: https://gitcode.com/GitHub_Trending/ai/aider
这是一篇基于 aider 官方"录音会话"文档 dont-drop-original-read-files 的技术复盘文章。该会话记录了 aider 开发者使用 aider 自身去修改 aider 的一个真实迭代过程:让无参执行
/drop(以及/reset)时,保留启动时通过--read传入的只读文件,只清理会话中的可编辑文件与其他临时添加的文件。读完本文,你将理解 aider 中"只读文件 / 可编辑文件"的上下文模型、/drop命令的内部实现与测试保障,并掌握一种"用 AI 结对编程改造 AI 工具"的可复用工作流。
一、背景:这个录音会话在解决什么问题
aider 允许你把文件分成两类带进聊天上下文:
- 可编辑文件:启动时通过位置参数或
--file传入,会话中可用/add添加,模型可以修改它们; - 只读文件:启动时通过
--read传入,会话中可用/read-only添加,模型只能参考、禁止编辑。
问题出在/drop命令上。它用于"从会话中移除文件以释放上下文空间"。改进之前,直接输入无参的/drop(裸/drop)会把可编辑文件、会话中途添加的只读文件以及启动时用--read特意指定的只读文件一并清空。而--read文件通常是每次启动时都要加载的固定参考资料(规范、接口定义、依赖清单等),一执行/drop就被清掉,用户需要重启会话才能恢复,非常打断工作流。
因此本次需求非常明确(对应录音 0:01 的旁白):
"We're going to update the /drop command to keep any read only files that were originally specified at launch."
即:/drop与/reset应保留"启动时指定的只读文件",只清理可编辑文件与之后新增的文件。与此同时,用户显式执行/drop 某个文件时,仍然应当可以把它移除——保留并不等于"锁定"。
这段会话对应的页面属于 Screen recordings 系列,全文以 asciinema 终端录像 + 分段旁白 + 音频片段的形态存在(音频元数据见 assets/audio/dont-drop-original-read-files/metadata.json),主题是 "aider 开发者用 aider 增强 aider"。这类页面最有价值的并不是操作演示本身,而是它浓缩出的完整开发流程,本文会逐段还原并结合当前仓库源码做深度讲解。
二、先建立概念:aider 的只读文件机制
在进入/drop的改动前,先看只读文件在当前仓库中是如何落地的,这直接决定了改动的形态。
1. 启动入口:--read
只读文件的主要入口是命令行参数--read,定义在 aider/args.py:
group.add_argument( "--read", action="append", metavar="FILE", help="specify a read-only file (can be used multiple times)", ).complete = shtab.FILE它采用action="append",意味着可以多次传参,也可以一次传多个文件。
2. 主流程解析与传递
在 aider/main.py 中,--read的值会被解析成绝对路径,并且支持传目录(目录会被递归展开为其中所有文件):
read_only_fnames = [] for fn in args.read or []: path = Path(fn).expanduser().resolve() if path.is_dir(): read_only_fnames.extend(str(f) for f in path.rglob("*") if f.is_file()) else: read_only_fnames.append(str(path))随后这个列表被分两路传递:
- 作为
original_read_only_fnames传入Commands(aider/main.py),Commands是承载/drop、/reset、/add、/read-only等会话内斜杠命令的对象; - 作为
read_only_fnames传入Coder.create(...)(aider/main.py),进入 AI 编码核心。
3. Coder 侧:只读文件进上下文的方式
Coder在初始化时把启动传入的只读文件解析为绝对路径集合abs_read_only_fnames,并在文件不存在时给出告警而非静默失败(aider/coders/base_coder.py):
if read_only_fnames: self.abs_read_only_fnames = set() for fname in read_only_fnames: abs_fname = self.abs_root_path(fname) if os.path.exists(abs_fname): self.abs_read_only_fnames.add(abs_fname) else: self.io.tool_warning(f"Error: Read-only file {fname} does not exist. Skipping.")当这些文件内容被注入 prompt 时,会带上明确不可编辑的系统提示前缀(aider/coders/base_prompts.py):
Here are some READ ONLY files, provided for your reference. Do not edit these files!与此同时,终端 UI 也会用(read only)标注它们(见 aider/io.py 中format_files_for_input的相关实现),/tokens命令统计上下文占用时同样会把read-only文件单列。
由此得到一个重要事实:在 Coder 内部,可编辑文件(abs_fnames)与只读文件(abs_read_only_fnames)是两套独立的集合。所谓"保留启动时的只读文件",在实现上就是对后一个集合做一次过滤——这正是下面核心改动的着眼点。
三、/drop与/reset的核心改动逻辑
1. 状态记录:original_read_only_fnames
Commands对象的构造函数接收original_read_only_fnames并保存为集合(aider/commands.py):
# Store the original read-only filenames provided via args.read self.original_read_only_fnames = set(original_read_only_fnames or [])这是判断"某只读文件是否来自启动时--read"的唯一依据。为了让会话中切换模型等操作重建的Commands不丢失这份记忆,clone()也会把它带过去(aider/commands.py):
def clone(self): return Commands( self.io, None, voice_language=self.voice_language, ... original_read_only_fnames=self.original_read_only_fnames, )2. 核心:_drop_all_files
无参/drop与/reset最终都汇聚到_drop_all_files(aider/commands.py):
def _drop_all_files(self): self.coder.abs_fnames = set() # When dropping all files, keep those that were originally provided via args.read if self.original_read_only_fnames: # Keep only the original read-only files to_keep = set() for abs_fname in self.coder.abs_read_only_fnames: rel_fname = self.coder.get_rel_fname(abs_fname) if ( abs_fname in self.original_read_only_fnames or rel_fname in self.original_read_only_fnames ): to_keep.add(abs_fname) self.coder.abs_read_only_fnames = to_keep else: self.coder.abs_read_only_fnames = set()理解这段逻辑要抓住三点:
- 可编辑文件无条件全部清空:第一行直接把
abs_fnames置空; - 只读文件做"白名单过滤":仅当存在
original_read_only_fnames时,遍历当前所有只读文件,同时用绝对路径和相对路径两种形式与启动名单比对,命中的保留,其余丢弃; - 无启动只读文件时行为不变:
else分支清空全部只读文件,兼容旧版行为。
同时支持绝对路径与相对路径双比对,是因为--read传入的相对路径在解析后可能以多种形式存在,用get_rel_fname归一化后比对更稳妥——这也解释了为什么源码要同时检查abs_fname in ... or rel_fname in ...。
3.cmd_reset复用同一清理逻辑
/reset("Drop all files and clear the chat history")内部同时调用_drop_all_files()与_clear_chat_history()(aider/commands.py),因此启动时传入的只读文件在/reset后同样会被保留。配套代码注释也明确说明清理的是历史消息与"新增"的文件。
4. 显式/drop <文件>依然可以删掉只读文件
保留策略只作用于"全量清理",不阻止用户显式点名移除。cmd_drop在带参数时会先对只读文件做匹配(aider/commands.py):
# Handle read-only files with substring matching and samefile check read_only_matched = [] for f in self.coder.abs_read_only_fnames: if expanded_word in f: read_only_matched.append(f) continue # Try samefile comparison for relative paths try: abs_word = os.path.abspath(expanded_word) if os.path.samefile(abs_word, f): read_only_matched.append(f) except (FileNotFoundError, OSError): continue for matched_file in read_only_matched: self.coder.abs_read_only_fnames.remove(matched_file) self.io.tool_output(f"Removed read-only file {matched_file} from the chat")也就是说,对只读文件的删除走的是子串匹配 +os.path.samefile兜底,对可编辑文件的删除则额外支持 glob 通配符。这样既实现了"启动只读文件默认受保护",又保留了用户手动清除它们的逃生通道。
5. 用户可见的行为差异
无参/drop时的提示信息会根据是否有启动只读文件而变化(aider/commands.py):
Dropping all files from the chat session except originally read-only files. # 有 --read 传入时 Dropping all files from the chat session. # 无 --read 传入时四、测试保障:行为被完整固化在测试套件中
本仓库的测试直接印证了上述所有行为边界,全部集中在 tests/basic/test_commands.py,值得逐条对照:
| 测试方法 | 行号 | 验证的行为 |
|---|---|---|
test_drop_with_original_read_only_files | tests/basic/test_commands.py | 构造 1 个启动只读文件 + 1 个可编辑文件 + 1 个后加只读文件,执行裸/drop后,可编辑文件与后加只读文件被清空,启动只读文件被保留 |
test_drop_specific_original_read_only_file | tests/basic/test_commands.py | 对启动只读文件显式执行/drop orig_read_only.txt,可以被正常移除(保护不是锁定) |
test_drop_with_no_original_read_only_files | tests/basic/test_commands.py | 没有启动只读文件时,裸/drop行为不变,提示 "Dropping all files from the chat session." |
test_reset_with_original_read_only_files | tests/basic/test_commands.py | /reset同样保留启动只读文件,同时清空聊天历史 |
test_reset_with_no_original_read_only_files | tests/basic/test_commands.py | 无启动只读文件时/reset全清 |
test_reset_after_coder_clone_preserves_original_read_only_files | tests/basic/test_commands.py | 经历Coder.clone(如模型切换场景)后original_read_only_fnames状态不丢失 |
test_drop_bare_after_coder_clone_preserves_original_read_only_files | tests/basic/test_commands.py | clone 之后裸/drop仍能正确保留启动只读文件 |
这几条测试恰好覆盖了"保留路径、显式删除路径、无名单路径、reset 路径、clone 状态传播"五类关键场景,是本次改动在回归层面的完整契约。除此之外,test_cmd_drop_*系列(tests/basic/test_commands.py)还覆盖了目录 drop、glob 通配符 drop、按 git 根路径 drop 等既有行为,保证了新逻辑不破坏旧功能。
五、从录音会话提炼的可复用"自举"开发工作流
该录音最独特的价值在于:它完整记录了开发者如何指挥 aider 去修改 aider 自身。把旁白时间线(dont-drop-original-read-files 的 Commentary 部分)映射到最终仓库代码上,可以还原出一条极具参考价值的协作流程:
- 0:01 精确描述需求:开场就给定验收标准——"
/drop保留启动时通过--read指定的只读文件"。不模糊、可测试,是整段 AI 协作的起点。 - 0:10 定位代码范围:明确改动涉及的模块——主 CLI 参数解析与斜杠命令处理,即
main.py、args.py、commands.py这几个文件,避免 AI 漫无目的地发散。 - 0:20–1:20 先讲清变更语义再动手:在让 AI 写代码前先解释清楚行为变化(保留什么、清理什么),对应最终实现的
_drop_all_files白名单过滤思路。 - 1:30 代码评审并拒绝不优雅的实现:旁白明确说 "I'd prefer not to use
hasattr(), let's ask for improvements"——开发者对 AI 的初版方案提出代码风格要求并让它自行改进。这类"要求重构"的指令,正是把 AI 输出从"能跑"推向"可维护"的关键一步。 - 1:45 手动测试:在实际环境中验证行为是否符合预期,即上文第三节展示的提示文案与文件保留效果。
- 2:10 跑既有测试套件做回归:确保改动没有破坏原有的
/drop、/reset、/add、/read-only相关行为。 - 2:19 让 AI 补测试:直接要求 aider 为本次改动增加测试覆盖,对应第四节中那一批
test_drop_with_original_read_only_files等用例。 - 2:50 审查测试并收尾:确认测试断言合理后结束迭代。
这套流程本质上是一种人机循环的最小闭环:精确需求 → 缩小代码搜索面 → 语义澄清 → 编码 → 风格评审与重构 → 手工冒烟 → 回归测试 → 补测试 → 复审。它不依赖任何特殊工具,任何开发者都能在自己项目里复现。你也可以直接参考 Screen recordings 系列的其他会话,观察同一套工作流在不同功能(/architect自动接受、--read文件被丢弃等问题)上的变体运用。
六、实用总结:在自己的会话中用好这套语义
基于以上源码与测试分析,你可以在日常使用中这样利用该行为:
- 启动时把"始终需要的参考资料"放进
--read:例如架构文档、数据库 schema、编码规范,使用aider --read docs/spec.md --read config/schema.json,它们会被标记为 read-only 并注入系统提示上下文。 - 放心使用裸
/drop释放上下文:可编辑文件与会话中途新增的文件会被清空,而--read的参考文件会原样保留,提示会明确告诉你 "except originally read-only files"。 - 想要换一套参考文件时,显式点名删除:
/drop docs/spec.md仍然有效,支持相对路径、绝对路径子串匹配,可编辑文件还可使用 glob(如/drop *.log)。 /reset做"干净重启"时同理:它会保留--read文件但清空历史消息;如果连参考文件也想全部重置,直接重启 aider 进程即可。- 注意适用前提:上述"保留"只针对启动时通过
--read(或--read目录递归展开)传入的文件;会话中用/read-only新增的文件不属于受保护名单,会被裸/drop清掉。
需要留意的是,录音页面里展示的是该功能落地的开发过程快照,当前仓库中的实现(commands.py、main.py、base_coder.py及相关测试)是经过后续演化的最新形态,但"保留启动只读文件"这一核心语义始终如一,你可以用文中所给的文件路径直接查阅验证。
【免费下载链接】aideraider is AI pair programming in your terminal项目地址: https://gitcode.com/GitHub_Trending/ai/aider
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考