🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度
1. 这次要拆的仓库:一个 1800 行的订单单体
Aider 拆 Python 单体仓库,最怕的不是模型写不出函数,而是拆到一半通道抖了、Key 换了、diff 对不上账。所以动手之前我先把供应商钉死:TaoToken 作为默认通道,Base URL 固定写 https://taotoken.net/api,Key 放进环境变量,后面每次 Aider 会话都复用它。Aider 会把每一次编辑做成 git commit,而调用记录在同一个账号下,这样「哪个 commit 花在哪一步」事后能对上,不用翻聊天记录猜。
这次拆的是一个订单服务单体,目录长这样:
order-monolith/
├── app.py # 1786 行,Flask 路由 + SQLAlchemy 模型 + 订单状态机
├── invoices.py # 210 行,发票渲染
├── reconcile.py # 320 行,定时对账任务
├── settings.py
└── tests/
├── conftest.py
├── test_api.py
├── test_state_machine.py
└── test_invoice.py
app.py 的问题是典型的历史积累:Order、OrderItem 这些模型写在文件上半部分,状态流转的 can_transition()、apply_transition() 混在中间,Flask 的 @app.route 铺在下面,末尾还塞了两个 if __name__ 的调试入口。任何一次改动都会牵动 1786 行文件的读入上下文,Aider 的 repo map 再聪明也救不了这种「所有东西都在一个文件里」的结构。
拆分的目标目录:
order_monolith/
├── core/
│ ├── models.py # SQLAlchemy 模型
│ └── state.py # 状态机与合法流转
├── services/
│ ├── order_service.py
│ └── invoice_service.py
├── api/
│ ├── routes.py
│ └── schemas.py
├── jobs/
│ └── reconcile.py
├── infra/
│ └── db.py
└── settings.py
目标不是「看起来整洁」,而是四条硬边界。第一条:只做文件移动与 import 调整,函数体一行不改。状态机里有一处 40 行的 apply_transition,如果模型顺手给它加个事务包装,pytest 可能仍然绿,但那次 diff 就不可信了。第二条:对账任务只允许生成纯函数或 SQL 字符串,任何 session.commit()、任何连数据库的调试代码都必须在本地由我执行,代码本身不碰生产库。第三条:每个模块单独一次会话,改完立刻跑 pytest -q,绿了才进下一个模块。第四条:tests/ 里的导入路径最终要收敛到新包,不允许用 __getattr__ 或 sys.path 补丁兜底,那种兜底会让半年后的静态检查彻底失效。
验收清单写在这里,后面每一步都对照它:pytest -q 全部通过且用例数不变;git log --oneline 里每一步都能读出「这一步干了什么」;目录树 diff 只有移动和新增 __init__.py;grep -r "from app import" 的结果为空。这四条比「结构好看」重要得多,因为拆分这种任务一旦破坏测试,回滚成本远高于重构收益。
2. Aider 接上统一通道:Key、Base URL 与 .aider.conf.yml
Key 从 TaoToken 模型广场 所在的站点创建,创建完先别急着写代码,把两个环境变量设好,再验证一次连通性,避免把「配错 base」和「配错模型名」两个错误混在一次调试里。
export OPENAI_API_KEY=YOUR_API_KEY
export OPENAI_API_BASE=https://taotoken.net/api
注意这里的 Base URL 末尾不带 /v1,直接就是 https://taotoken.net/api。Aider 在识别到 OpenAI 兼容端点后,会自己拼上具体的请求路径,你多写一段 /v1 反而会拼成不存在的地址。Key 用占位符 YOUR_API_KEY 表示,前面加 openai/ 前缀的模型名留给下一步。
最小可运行的启动命令:
aider --model openai/YOUR_MODEL_ID \
--openai-api-base https://taotoken.net/api \
--openai-api-key "$OPENAI_API_KEY"
命令行参数每次敲太长,实际用的时候我会写成 .aider.conf.yml 放在仓库根目录:
openai-api-base: https://taotoken.net/api
model: openai/YOUR_MODEL_ID
auto-commits: true
auto-test: true
test-cmd: pytest -q
map-tokens: 1024
这里有三个取舍值得说清楚。Key 不写进 yml,因为配置文件哪怕加了 .gitignore,也有可能在 git add -A 的时候被打包送进提交记录,环境变量是更稳的做法。auto-commits: true 打开之后,Aider 每完成一次编辑就自己提交一次,提交信息以 aider: 开头,这正是本篇要产出的「命令历史」的来源,如果关掉它,就得自己手动整理 diff。map-tokens: 1024 是给 repo map 设上限,在 30 个文件左右的小仓库里完全够用,省下来的上下文留给真正要改的文件。
模型名这一项必须强调:YOUR_MODEL_ID 以模型广场展示的 ID 为准,不要凭印象写 gpt-5、claude-4 这类名字,也不要拿别处的公告号当配置值。广场上写什么就填什么,openai/ 前缀是 Aider 用来判断「走 OpenAI 兼容协议」的标记,和模型本身叫什么名字无关。配错模型名的报错通常出现在第一次请求返回里,而不是启动阶段,所以先跑一次最小请求再开始拆,比拆到一半发现模型名不对省事得多。
进仓库之前还有一步:把 baseline 提交干净。
git init
git add -A
git commit -m "baseline: monolith before split"
Aider 依赖 git 来判断工作区状态,仓库里如果已经有未提交的改动,它会先问你一堆问题,甚至拒绝开始。基线提交完成之后,每一次 aider: 提交都对应一次明确的拆分动作,回滚只需要 git revert 那一条 commit,不用手动挑文件。
2.1 上下文怎么喂:/add 与 /read-only 的分工
Aider 的上下文模型很简单:只有被显式加进会话的文件才会被改,repo map 只提供索引,不提供全文。所以这一步的关键是分清「要改的」和「只需读的」。/read-only(老版本里叫 /read)把文件加进上下文但不允许编辑,适合放 tests/、settings.py 和暂时不动的基础设施;/add 加进来的文件才是可写目标。拆包第一阶段我就是用这个分工先把全景看清楚,再逐个模块动手。
3. 用 /add 圈住上下文:三步下达拆分计划
拆分这件事不适合一句话交给 Agent 自由发挥。单体拆包的本质是一组有序的移动操作,中间任何一步跨模块跳跃都会让 diff 变得不可审查。所以我把计划压成三步,每一步只允许改一类东西。
3.1 第一步:只要地图,不许改文件
第一条指令只读:
/read-only app.py invoices.py reconcile.py settings.py tests/
读取这个仓库,只输出一份模块依赖清单,不要修改任何文件。要求:
1. app.py 里每个顶层定义分别属于哪个业务域(订单、发票、对账、基础设施);
2. 列出所有跨文件的 import,标出可能的循环依赖;
3. 指出 tests/ 里哪些导入路径依赖 app.py 的内部符号。
这一步的价值不在于让模型多聪明,而在于把「依赖事实」从我的记忆里搬到会话里。同一个 Prompt 我也在同一把 Key 上跑过两次,得到的两份清单在分组上略有出入,但循环依赖的位置一致,说明结论是稳的。清单里如果有明显错误——比如把 can_transition 归到 API 层——我会在下一轮纠正,而不是留着让它带着错误假设去改文件。
3.2 第二步:一次只迁一个包
地图确认后,第二步开始真正动文件,但一次只迁一个域。订单域是最大的一块,先拿它开刀:
/add app.py core/
把订单相关的 SQLAlchemy 模型从 app.py 移到 core/models.py,
把状态机与合法流转判定移到 core/state.py。
约束:
- 只做移动和 import 调整,函数体一行不改;
- 不改任何 Flask 路由的 URL 和返回结构;
- 移动完成后,把 tests/test_state_machine.py 的导入改到新路径;
- 每个文件顶部保留原有的模块级 docstring。
改完运行 pytest -q,把完整输出贴出来。
Prompt 里「函数体一行不改」这条不是客套。Aider 有自动 lint 和格式化倾向,如果不明确写死,它可能在移动时顺手把 except Exception: 改成 except Exception as e:,这种改动会让 diff 从「纯移动」变成「移动 + 行为微调」,审查成本立刻上升。同样的理由,我要求它保留 docstring——docstring 位置变动会让 git diff --color-moved 认不出这是移动。
会话里还要用到两个命令控制范围。/drop 把已经改完、不再需要编辑的文件移出上下文,避免下一轮对话被无关文件干扰;/undo 回滚 Aider 的最后一次编辑,虽然它同时会撤销对应 commit,但在「刚改完就发现方向错了」的时候比手动 revert 快得多。订单域迁完后,app.py 从 1786 行降到大约 900 行,repo map 的负担明显轻了。
3.3 第三步:import 收敛与公开导出
最后一个包迁完,剩下的问题是导入路径。tests/conftest.py 里往往藏着最顽固的引用:
/drop
/add app.py core/ services/ api/ jobs/ infra/ tests/
现在收敛导入路径。要求:
1. 所有 tests/ 里的导入改成从新包导入,禁止保留 from app import;
2. 每个包的 __init__.py 只 re-export 稳定对外名字,不写逻辑;
3. app.py 保留为应用入口,只做 create_app() 和路由注册;
4. conftest.py 里的 fixture 路径同步更新。
改完运行 pytest -q 和 grep -rn "from app import" . 两条命令,贴出结果。
grep 那条命令是整个拆分任务的验收开关。pytest 绿只证明运行时没坏,grep 为空才证明没有视觉上看不出来的兼容层。如果 grep 还有结果,说明某一处导入被漏掉了,这时候单独发一轮「只修这一处」的指令,比让它批量改所有文件安全。
4. 命令历史与目录树 diff:这次拆分的可复现产出
4.1 Aider 会话命令历史
下表是我这次会话里实际敲过的命令序列。会话启动后,所有编辑都走这几条指令,没有在编辑器里手改过任何一行。
| 序号 | 输入 | 期望副作用 |
|---|---|---|
| 1 | aider --model openai/YOUR_MODEL_ID --openai-api-base https://taotoken.net/api | 读取 .aider.conf.yml,加载 repo map,无文件改动 |
| 2 | /read-only app.py invoices.py reconcile.py settings.py tests/ | 只读上下文,输出依赖清单 |
| 3 | /add app.py core/ + 订单域迁移 Prompt | 生成 core/models.py、core/state.py,一次 aider: 提交 |
| 4 | /run pytest -q | 状态机相关用例通过 |
| 5 | /drop 后 /add app.py invoices.py services/ + 发票域 Prompt | 生成 services/invoice_service.py |
| 6 | /add reconcile.py jobs/ infra/ + 对账域 Prompt | 生成 jobs/reconcile.py,纯函数,无数据库连接 |
| 7 | /add app.py api/ + 路由域 Prompt | 生成 api/routes.py、api/schemas.py |
| 8 | /add app.py core/ services/ api/ jobs/ infra/ tests/ + 收敛 Prompt | 更新全部导入路径 |
| 9 | /run pytest -q 与 /run grep -rn "from app import" . | 全绿,grep 无输出 |
第 5、6、7 步中间都夹了一次 /run pytest -q,我把它省略在表里,因为它不是独立决策点,只是每步的验收动作。真正需要看的是第 8 步之后的状态:如果收敛完之后测试挂了,问题一定在 import 而不是逻辑,因为逻辑从第 3 步之后就没动过。
4.2 拆分后的目录树 diff
order-monolith/
-├── app.py # 1786 行
-├── invoices.py # 210 行
-├── reconcile.py # 320 行
-├── settings.py
-└── tests/
- ├── conftest.py
- ├── test_api.py
- ├── test_state_machine.py
- └── test_invoice.py
+├── app.py # 96 行,只剩 create_app 与路由注册
+├── order_monolith/
+│ ├── __init__.py
+│ ├── core/
+│ │ ├── __init__.py
+│ │ ├── models.py
+│ │ └── state.py
+│ ├── services/
+│ │ ├── __init__.py
+│ │ ├── order_service.py
+│ │ └── invoice_service.py
+│ ├── api/
+│ │ ├── __init__.py
+│ │ ├── routes.py
+│ │ └── schemas.py
+│ ├── jobs/
+│ │ ├── __init__.py
+│ │ └── reconcile.py
+│ ├── infra/
+│ │ ├── __init__.py
+│ │ └── db.py
+│ └── settings.py
+└── tests/
+ ├── conftest.py
+ ├── test_api.py
+ ├── test_state_machine.py
+ └── test_invoice.py
invoices.py 和 reconcile.py 两个根文件消失,内容分别落到 services/invoice_service.py 与 jobs/reconcile.py;settings.py 移进包内并被 app.py 以 order_monolith.settings 引用。测试文件名一个没动,只改了导入行,这让 diff 的审查者能快速确认「测试覆盖面没有变化」。
4.3 模块迁移对照表与运行声明
| 原位置 | 新位置 | 迁移方式 | 是否改逻辑 |
|---|---|---|---|
app.py 模型定义段 | core/models.py | 整段剪切 | 否 |
app.py 状态机段 | core/state.py | 整段剪切 | 否 |
app.py 订单服务函数 | services/order_service.py | 函数搬移 | 否 |
invoices.py | services/invoice_service.py | 整文件移动 | 否 |
app.py 路由段 | api/routes.py | 拆分为 Blueprint | 仅注册方式,URL 不变 |
reconcile.py | jobs/reconcile.py | 整文件移动 | 否 |
settings.py | order_monolith/settings.py | 整文件移动 | 否 |
| 数据库 session 构造 | infra/db.py | 抽离 | 否 |
这张表里唯一需要解释的是路由那一行。Flask 的 @app.route 改成 Blueprint 之后,注册代码从「装饰器直接挂 app」变成「app.register_blueprint」,这是结构性改动,不是逻辑改动。URL 规则、方法、返回体全部保持原样,tests/test_api.py 的断言因此不需要调整。
关于数字,这里要把话说明白:本文不含排行分数,也不写 token 消耗的具体数字。原因很简单,本次没有资料包,也没有任何公榜快照被引用,凭记忆写一个「消耗了多少万 token」既不严谨也不可复现。真实用量请以 Aider 会话里的 /tokens 输出和统一网关控制台的用量页为准,两者能对上账。如果后续要做横向对照表,正确做法是用同一把 Key、同一段 Prompt、同一份 baseline 仓库,在同一个时间窗口内跑完,并明确标注「一次运行,不代表公榜」,而不是把不同天的结果拼成一张实力表。
5. Aider 拆包时踩过的坑:401、模型名、脏工作区、import
这四个错基本覆盖了第一次接通道拆仓库会撞到的全部问题,按出现频率排序。
5.1 401 与 Key 的传递方式
最容易被忽略的是 Key 被配置文件覆盖。如果你在 .aider.conf.yml 里写过 openai-api-key,同时又设了 OPENAI_API_KEY 环境变量,Aider 的优先级判断会让其中一个生效,而你未必记得哪个在前。排查方法很直接:把 yml 里的 Key 行删掉,只留环境变量,重启会话。另外注意 shell 里的引号,YOUR_API_KEY 如果从某个带换行的文件里 cat 出来,会带一个不可见字符,请求头看起来正常但服务端判定无效,表现同样是 401。
5.2 模型名不被识别
报错通常长这样:请求发出去了,返回里说模型不存在或参数不合法。这时候不要去改 Base URL,先打开模型广场核对 ID 的拼写。openai/ 前缀后面跟的必须和广场展示完全一致,大小写、连字符、版本后缀都算。Base URL 始终是 https://taotoken.net/api,末尾不加 /v1,这一条在排障时不要动摇,否则你会同时在两个变量上做实验,最后分不清是哪个改动生效了。
5.3 git 脏工作区导致会话卡住
Aider 在仓库有未提交改动时会反复询问,拆包过程中这种打断很致命。规矩很简单:开始拆之前 git status 必须是干净的;Aider 每次自己提交之后,不要手动再改文件;确实需要手改一处时,先 /exit,改完提交,再重新进会话。这么做还有一个好处,你的手动改动和 Aider 的改动在 git log 里分得清清楚楚,出了问题是模型改错了还是人改错了,一眼能看出来。
5.4 pytest 通过但 import 路径还留着旧引用
这是最隐蔽的一种:测试全绿,因为 conftest.py 里恰好有一行 sys.path 或旧的 from app import,而这条路径在新结构下刚好也能工作。它会一直潜伏到某天你删掉根目录的 app.py 才爆。防御手段就是前面那条 grep -rn "from app import" .,这条命令的输出必须是空的,pytest 的绿色不能替代它。同样值得加进验收的还有一句 python -c "import order_monolith",从仓库根目录执行,确认包能被正常导入而不是靠当前工作目录的巧合。
5.5 对账模块的安全边界
jobs/reconcile.py 里原本有一段直接 session.commit() 的收尾逻辑。迁移时我的 Prompt 明确要求把它改成返回待执行 SQL 或纯计算函数,真正的提交动作留给本地脚本。Agent 不应该、也不需要在你的生产库或生产机上执行任何业务写入,它能做的是生成命令和 SQL,由你在本地跑完再把结果贴回会话。这条边界在拆包任务里尤其重要,因为对账逻辑最容易顺手带上一个真实连接。
6. 验证通过之后:把这次拆分的命令固化下来
拆包的收尾不是「结构好看」,而是四件事同时成立:pytest -q 全绿且用例数与基线一致;grep -rn "from app import" . 无输出;git log --oneline 里每个 aider: 提交都能对应到上表的某一行;目录树 diff 只有移动和新增 __init__.py。把 .aider.conf.yml、baseline commit hash、以及第 4.1 节那张命令表一起放进仓库的 docs/split.md,下次再拆另一个单体时,换掉 Prompt 里的模块名就能直接复用这套流程。
跑完验证之后,先打开 模型对话 确认这次用的模型 ID 与广场展示一致,顺手用一句「解释 core/state.py 的流转约束」验证通道可用。长期要在多个仓库反复跑 Aider,可以看 Coding Plan;Key 的创建与轮换在 控制台 完成,同时把这次几次会话的调用量对上账。团队里其他人想用同一个 Base URL 接 Claude Code,三件套的写法对照 接入文档。
🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度




被折叠的 条评论
为什么被折叠?



