aider 实战复盘:一次横跨多文件的复杂代码改造——用 prompt_toolkit 替换 /dev/null 的协作式调试全流程 📅 发布时间:2026/9/9 13:57:07 👁 浏览次数: aider 实战复盘一次横跨多文件的复杂代码改造——用 prompt_toolkit 替换 /dev/null 的协作式调试全流程【免费下载链接】aideraider is AI pair programming in your terminal项目地址: https://gitcode.com/GitHub_Trending/ai/aider导读本文基于 aider 官方提供的一则真实会话实录complex-change.md展开讲解用户要求把测试代码中用/dev/null重定向 stdin 以模拟无输入的旧写法替换为prompt_toolkit提供的输入 Mock 函数。这是一个典型的横跨多个源码文件的复杂改动涉及测试文件、CLI 入口与交互层同步修改并在过程中经历了多轮报错 → 粘贴 API 签名 → 修正的协作式调试。读完本文你将掌握 aider 多文件改造的完整节奏、edit block 代码格式的读法、^C中断与外部编辑文件等协作技巧以及输入/输出句柄贯穿注入这一可测试性设计模式在 aider 自身代码中的最终落地形态。这则实录属于 示例会话合集Example chat transcripts 中的一员同目录下还收录了生成新代码、修改开源仓库、黑盒测试、数据分析和文档更新等不同类型任务的完整对话。一、场景设定要解决什么问题会话开头的操作和第一句话概括了任务全貌 $ aider tests/test_main.py aider/getinput.py Added tests/test_main.py to the chat Added aider/getinput.py to the chat #### instead of using /dev/null to send no input in the main tests, can we use the prompt_toolkit input mocking functions?要点有三个启动命令aider file1 file2把tests/test_main.py与aider/getinput.py直接加入会话。启动时指定的文件会立即进入 LLM 的可见且可编辑集合。任务本质测试里原先用open(os.devnull, r)把 stdin 指向空设备模拟用户不输入任何内容现在想改用prompt_toolkit官方提供的 Mock 输入函数让测试更贴近真实交互对象。为什么是复杂改动main()的调用方在测试侧而输入行为的实际承载者在getinput.py的交互类中两者之间还隔着aider/main.py的调用链。改测试必然牵动生产代码的签名设计——这正是多文件协调改动的最佳案例。说明实录发生在项目早期当时的交互层模块名为aider/getinput.py。在 当前仓库 中同类职责的InputOutput类已经演进并迁移到 aider/io.py下文与当前仓库实现的对应一节会展开对照。二、阅读实录前必须先懂的会话格式打开 examples/README.md 可以看到这段实录所用的会话排版约定以开头的是aider 工具自身的输出例如Added xxx to the chat、Applied edit to xxx、Commit hash message。以####开头的是用户在对话中敲入的请求或反馈。其余正文以及嵌入其中的 edit block是LLM 的回复。LLM 提出修改时会输出可机器解析的edit block示例格式如下hello.py ORIGINAL print(hello) print(goodbye) UPDATED即第一行是目标文件路径 ORIGINAL到之间是被替换的旧代码到 UPDATED之间是写入的新代码。aider 收到这种格式后会自动应用到磁盘文件并打印Applied edit to path。正如 README 所总结的会话运作机制每次 LLM 给出代码修改aider 都会自动把它应用到源文件对应实现可以在 base_coder.py 中看到self.io.tool_output(fApplied edit to {path})见 base_coder.py。应用完修改后aider 会自动提交到 git并附上一段描述性的提交信息。LLM 只能看到与会话中已添加的文件用户既可以在命令行启动时加文件也可以随时用对话内命令/add追加。实录中就多次出现Added xxx to the chat通知。三、第一轮改动测试全面切换到 prompt_toolkit 输入 MockLLM 给出的第一套方案包含四处 edit block分别对应 import 调整和三个测试方法的调用改写。① import 部分引入create_pipe_input与DummyOutputtests/test_main.py ORIGINAL import os import sys import tempfile from unittest import TestCase from aider.main import main import subprocess import os import tempfile from unittest import TestCase from aider.main import main import subprocess from prompt_toolkit.input import create_pipe_input from prompt_toolkit.output import DummyOutput UPDATED注意这里顺手删掉了import sys——因为新方案不再需要手工保存、恢复sys.stdin吗并不尽然后面会看到sys.stdin的保存恢复依然保留。这处 import 删除其实是 LLM 的一个疏漏随后在 git 层面被捕捉见第四节。② 第一个测试方法main([])tests/test_main.py ORIGINAL with open(os.devnull, r) as dev_null: save_stdin sys.stdin sys.stdin dev_null main([]) sys.stdin save_stdin pipe_input create_pipe_input() save_stdin sys.stdin sys.stdin pipe_input main([], inputpipe_input, outputDummyOutput()) sys.stdin save_stdin pipe_input.close() UPDATED③ 第二个测试方法main([foo.txt])tests/test_main.py ORIGINAL with open(os.devnull, r) as dev_null: save_stdin sys.stdin sys.stdin dev_null main([foo.txt]) sys.stdin save_stdin self.assertTrue(os.path.exists(foo.txt)) pipe_input create_pipe_input() save_stdin sys.stdin sys.stdin pipe_input main([foo.txt], inputpipe_input, outputDummyOutput()) sys.stdin save_stdin pipe_input.close() self.assertTrue(os.path.exists(foo.txt)) UPDATED④ 第三个测试方法main([--yes, foo.txt])tests/test_main.py ORIGINAL with open(os.devnull, r) as dev_null: save_stdin sys.stdin sys.stdin dev_null main([--yes, foo.txt]) sys.stdin save_stdin self.assertTrue(os.path.exists(foo.txt)) pipe_input create_pipe_input() save_stdin sys.stdin sys.stdin pipe_input main([--yes, foo.txt], inputpipe_input, outputDummyOutput()) sys.stdin save_stdin pipe_input.close() self.assertTrue(os.path.exists(foo.txt)) UPDATED这段改造透露了两个设计意图用create_pipe_input()创建可编程写入的管道输入模拟无输入场景用DummyOutput()吞掉渲染输出避免测试时终端被 prompt 界面刷屏同时大胆地给main()传了input、output关键字参数——而彼时main()根本还不接受这两个参数。aider 依次应用了四处编辑并自动提交 Applied edit to tests/test_main.py Applied edit to tests/test_main.py Applied edit to tests/test_main.py Applied edit to tests/test_main.py Commit c177e29 aider: Replaced usage of /dev/null with prompt_toolkits input mocking functions in tests.多行Applied edit证明一次对话回合内aider 可以连续应用同一文件的多处编辑并将它们打包成一次带描述的 git 提交。这是 edit block 机制与 git 自动化结合的直观体现。四、对话外的改动被探测到git 的带外变更提示第一轮修改落地后会话中出现了一段非常特殊的交互 Git repo has uncommitted changes. Suggested commit message: Added import statement for sys module in test_main.py. Commit before the chat proceeds [y/n/commit message]? y Commit 6724fcd Added import statement for sys module in test_main.py.这说明用户在自己的编辑器里、在对话之外out-of-band手动修改了tests/test_main.py——补回了第一节中被误删的import sys。aider 虽然不直接感知编辑器内容但它检测到了 git 工作区存在未提交的改动于是在继续对话之前暂停并询问用户输入y采用 aider 建议的提交信息并提交输入n不提交直接继续输入其他文字以该文字作为自定义提交信息提交。这是 aider 与 git 深度集成的关键设计一方面保证每个代码改动都有清晰的提交边界另一方面避免对话外改动污染下一次自动提交的可追溯性。你在本地开发时如果也想体验同样的保护只要让仓库处于 git 管理之下并正常启动 aider 即可。五、调试循环 #1TypeError 暴露main()签名缺口/add把源码引入会话第一轮乐观的调用改写立即在测试运行中暴露问题#### I am getting this error: FAILED tests/test_main.py::TestMain::test_main_with_empty_git_dir_new_file - TypeError: main() got an unexpected keyword argument input这正是多文件改动的典型连锁反应测试侧已经按新契约调用但生产侧签名尚未跟上。用户处理方式很规范先通过/add aider/main.py把 CLI 入口源码加入会话出现Added aider/main.py to the chat再原样粘贴 pytest 的报错文本作为提示。LLM 据此把main()扩展为接受可选参数aider/main.py ORIGINAL def main(argsNone): if args is None: args sys.argv[1:] def main(argsNone, inputNone, outputNone): if args is None: args sys.argv[1:] if input is None: input sys.stdin if output is None: output sys.stdout UPDATED这段修复的价值在于兼容性设计默认值回落为sys.stdin/sys.stdout使得无人传参时行为与原来完全一致而测试可以显式注入 Mock。修改被应用后自动提交 Applied edit to aider/main.py Commit 79dfba9 aider: Fixed main() function to accept input and output keyword arguments.给读者的实践启示当 aider 报错只涉及签名缺失时最快的做法就是把完整 traceback 原样贴进对话并确保相关源文件在会话内用/add或启动参数添加LLM 通常能一步到位。六、需求澄清把句柄贯穿到InputOutput与 prompt_toolkit第一轮只修好了main()的外壳但用户随即提出了更准确的诉求——参数不能停在main()门口必须真正流到读取输入的地方#### we need to pass those into InputOutput and to prompt_toolkit right?这处澄清非常关键它把问题从让函数签名不报错升级为让 Mock 输入真正生效。LLM 的回应是两段式修改①InputOutput.__init__接受并保存input/outputaider/getinput.py ORIGINAL class InputOutput: def __init__(self, pretty, yes, input_history_file, chat_history_file): class InputOutput: def __init__(self, pretty, yes, input_history_file, chat_history_file, inputNone, outputNone): self.input input self.output output UPDATED②get_input()内调用prompt()时把句柄透传进去aider/getinput.py ORIGINAL line prompt( show, completercompleter_instance, historyFileHistory(self.input_history_file), stylestyle, reserve_space_for_menu4, complete_styleCompleteStyle.MULTI_COLUMN, ) line prompt( show, completercompleter_instance, historyFileHistory(self.input_history_file), stylestyle, reserve_space_for_menu4, complete_styleCompleteStyle.MULTI_COLUMN, inputself.input, outputself.output, ) UPDATED③main.py构造InputOutput时继续下传aider/main.py ORIGINAL io InputOutput(args.pretty, args.yes, args.input_history_file, args.chat_history_file) io InputOutput(args.pretty, args.yes, args.input_history_file, args.chat_history_file, inputinput, outputoutput) UPDATED至此整条链路的雏形形成tests → main(argv, input, output) → InputOutput(input, output) → prompt(...)。aider 应用修改并提交 Applied edit to aider/getinput.py Applied edit to aider/getinput.py Applied edit to aider/main.py Commit cea9d2a aider: Updated InputOutput class to accept input and output arguments and passed them to prompt_toolkit.prompt() function.实践启示当用户发现 LLM 的修改签名对但语义不够深时一句精确的需求澄清we need to pass those into X and to Y right?就能把方向掰正。与其重新描述整个问题不如指出缺失的那一环。七、调试循环 #2 / #3API 参数名两次猜错粘贴签名加速收敛链路打通后prompt_toolkit又开始拒绝参数#### FAILED tests/test_main.py::TestMain::test_main_with_empty_git_dir_new_file - TypeError: prompt() got an unexpected keyword argument inputLLM 第一次猜测是参数名应为stdin/stdout于是把inputself.input, outputself.output改成stdinself.input, stdoutself.output并提交Commit 55d3540。但这依然错误——prompt_toolkit.shortcuts.prompt()实际上不接受这些参数名。此时用户采取了最高效的调试投喂方式直接把prompt_toolkit文档中prompt()的完整签名抄进对话那一长串带默认值的类型签名在实录中清晰可见。LLM 意识到prompt()不支持直接注入句柄转而提议引入Application类回复进行到一半时—— ^C KeyboardInterrupt用户按下了^C中断了这次回复。因为该回答已经开始跑偏Application方案远比实际需要的复杂。随后用户补上真正有用的信息prompt_toolkit.shortcuts.PromptSession类的完整构造函数签名可以看到它接受input: Input | None None, output: Output | None None。LLM 顺势切换到正确的PromptSession方案① import 变更aider/getinput.py ORIGINAL from prompt_toolkit import prompt from prompt_toolkit.shortcuts import PromptSession UPDATED② 用PromptSession承载句柄再调session.prompt()aider/getinput.py ORIGINAL line prompt( show, completercompleter_instance, historyFileHistory(self.input_history_file), stylestyle, reserve_space_for_menu4, complete_styleCompleteStyle.MULTI_COLUMN, inputself.input, outputself.output, ) session PromptSession( messageshow, completercompleter_instance, historyFileHistory(self.input_history_file), stylestyle, reserve_space_for_menu4, complete_styleCompleteStyle.MULTI_COLUMN, inputself.input, outputself.output, ) line session.prompt() UPDATED随后提交 Applied edit to aider/getinput.py Applied edit to aider/getinput.py Commit ac92cca aider: Replaced prompt() with PromptSession() to correctly pass input and output arguments.这段实录浓缩了两条重要协作经验把权威 API 签名粘贴进对话远比让 LLM 凭记忆猜参数名高效连猜两次参数名的代价都不如一段签名省时。^C是合法的协作手段当模型回复跑偏时立即中断再用一句澄清或补充文档片段重新提问能得到质量更高的下一条回复。中断不会破坏会话上下文前文的目标与已应用修改都仍然有效。八、测试端最终收敛create_input(StringIO())服务端改用PromptSession后用户又提出一次测试端微调#### try using create_input(StringIO()) in the tests #### the ones that currently use create_pipe_input()即把三处create_pipe_input()统一替换为create_input(StringIO())tests/test_main.py ORIGINAL from prompt_toolkit.input import create_pipe_input from prompt_toolkit.input import create_input from io import StringIO UPDATED以及每一处调用点tests/test_main.py ORIGINAL pipe_input create_pipe_input() pipe_input create_input(StringIO()) UPDATED同一文件中共有四处相同替换其中一处 import、三处调用点。create_input(StringIO())的含义是用内存字符串流StringIO()直接构造一个prompt_toolkit的输入对象——内容为空即无输入与当年/dev/null的语义一致但完全在进程内完成无需触碰真实文件描述符。全部应用并提交后 Applied edit to tests/test_main.py Applied edit to tests/test_main.py Applied edit to tests/test_main.py Applied edit to tests/test_main.py Commit 4bb043f aider: Replaced the usage of create_pipe_input() with create_input(StringIO()) in the test cases.用户最终确认#### nice, that worked!整个改造闭环完成从/dev/null重定向 →create_pipe_input→create_input(StringIO())配套生产代码从main()签名到交互层完成了一次完整的依赖注入改造。九、对照当前仓库这次改造的最终形态长什么样作为开源项目aider 本体一直在演进。虽然complex-change.md记录的是历史状态但对照 当前仓库 的源码恰好可以验证这次改造的思路正是如今架构的基石也方便读者把历史对话翻译成现在的代码长什么样。1.main()的现代签名当前 aider/main.py 的定义为def main(argvNone, inputNone, outputNone, force_git_rootNone, return_coderFalse):与实录中修复后的版本同源只是后续又增加了force_git_root指定 git 根目录与return_coder脚本化场景返回 Coder 对象而非执行循环两个可选项支撑了 scripting.md 描述的编程式调用。2.InputOutput已迁移并深度绑定PromptSession实录中的aider/getinput.py在现仓库中已不存在交互核心类InputOutput定义在 aider/io.py。它的构造函数同样接受inputNone, outputNone见 aider/io.py并在初始化时把句柄交给PromptSessionif fancy_input: # Initialize PromptSession only if we have a capable terminal session_kwargs { input: self.input, output: self.output, lexer: PygmentsLexer(MarkdownLexer), editing_mode: self.editingmode, } ... self.prompt_session PromptSession(**session_kwargs)见 aider/io.py。这正是实录第八节引入PromptSession的延续如今不再用函数式prompt()而是持有会话对象供 get_input 等流程按需调用。此外还有一个容易被忽略的设计细节当外部注入了output即非真实终端时InputOutput会自动关闭彩色渲染见 aider/io.py保证测试输出干净、不污染断言。3. 测试端演化为DummyInput/DummyOutput实录最终采用create_input(StringIO())而现仓库 tests/basic/test_main.py 的测试则更直接统一使用prompt_toolkit的现成 Dummy 对象from prompt_toolkit.input import DummyInput from prompt_toolkit.output import DummyOutput见 tests/basic/test_main.py。典型调用形如main([--no-git, --exit, --yes], inputDummyInput(), outputDummyOutput())见 tests/basic/test_main.py。DummyInput不阻塞等待任何终端输入天然适合无输入/直接退出的 CLI 用例。从历史方案到当前形态可以清晰看到同一条演进主线把 I/O 从全局隐式状态sys.stdin/ 文件重定向变成显式注入的参数让测试彻底脱离真实终端。4. 可测试性红利面向脚本与自动化调用input/output注入带来的不仅是一两个测试用例的便利。return_coderTrue、force_git_root等新参数与句柄注入叠加后aider 的main()已经可以作为一个可编程函数被其他 Python 代码直接驱动这也是 scripting.md 介绍的用法。一次针对测试的改造最终反哺出了项目级的脚本化接口——这是依赖注入式重构最常见的长期回报。十、从这则实录提炼的通用工作方法多文件改动先铺测试侧契约再让报错牵引生产侧补齐。实录中测试先行调用main(..., input..., output...)随后以 pytest 的 TypeError 为路标修改签名是一种低成本、可验证的推进顺序。粘贴报错与 API 签名是最有效的两种调试输入。整段会话的收敛速度几乎与用户提供的证据质量成正比第一次 TypeError 一步修复而 API 参数名则经历了两次猜测——直到用户贴出PromptSession构造签名才终结。跑偏时果断^C。中断不是失败而是把预算从无用的长回复中省下来用于一条澄清 一条更准的回复。善用 git 保护与对话外编辑。aider 自动应用编辑、自动提交并能在检测到工作区带外变更时主动暂停征询提交意见Commit before the chat proceeds [y/n/commit message]?让每一次变更都留痕、可回滚。把 I/O 句柄设计为显式参数。从/dev/null→create_pipe_input→create_input(StringIO())→DummyInput/DummyOutput的演进本质都是让程序输入从哪来、输出到哪去可被替换从而获得脱离真实终端环境做自动化测试乃至脚本化复用的能力。延伸阅读了解 edit block 与会话排版约定examples/README.md同一合集中的其他实战实录semantic-search-replace.md、add-test.md、update-docs.mdInputOutput类定义与PromptSession组装aider/io.pyCLI 入口main()的现代签名aider/main.py无终端场景下的测试写法tests/basic/test_main.pyaider 的编程式脚本化调用方式docs/scripting.md【免费下载链接】aideraider is AI pair programming in your terminal项目地址: https://gitcode.com/GitHub_Trending/ai/aider创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考