你在 Composer 里发起一个跨文件重构——重命名一个类型、传染到 15 个文件、把测试同步更新。前 6 个文件都很漂亮。到第 9 个就开始过不了编译:要么改错了文件,要么把前面改好的又回滚了,要么 import 路径完全是编出来的。模型没变笨,是上下文耗尽了。Composer 的有效窗口是有上限的,超过之后老的编辑会被驱逐,包括这次重构所依赖的类型定义本身。
修这个问题不是”换更大的模型”——而是把重构切块,每个块边界重新建立基线。
常见原因
1. 重构超出了有效上下文
哪怕模型支持 200K token,Composer 还要给工具和 system prompt 留位置,实际能用的代码空间只有 80-120K。20 个 TypeScript 文件的全文读取很容易就把这个空间吃完。
如何判断:统计 Composer 碰过的文件的 token 数。加起来超过 60K 就进入危险区间了。
2. 工具输出把老的编辑挤出去了
每次 tool call 的输出(文件读取、terminal 日志、grep 结果)都在上下文里。中途几次 npm run build 的报错栈就能挤掉前面的 diff。
如何判断:重构中途 Composer 跑过几次 build 或测试?这些输出现在在和真正的代码抢空间。
3. 一开始没给”基准定义”
第一条消息只说”把仓库里 UserId 重命名为 AccountId”,但没贴出标准定义。做到一半模型就忘了这个类型到底长什么样。
如何判断:往上翻——有没有一条消息里写着 interface AccountId 的完整定义?
4. 文件之间的隐式依赖没暴露出来
UserId 被 15 个文件 import,其中 3 个还在 re-export。模型只改了直接 import,漏了 re-export。
如何判断:rg "export.*UserId|export \{.*UserId" --type ts 找 re-export。
5. Composer 工作时你又手动改了文件
你在 Composer 处理第 7 个文件的时候手动改了第 5 个文件的一个错字。模型认为第 5 个文件还是带错字的,下一轮编辑直接把你的修复回滚。
如何判断:git status 有没有显示你做的、但 Composer 没读过的改动?
6. agent 长循环没打 checkpoint
agent 模式连跑 25 个 tool call,你一次 commit 都没。到第 20 个 call 时,模型已经在一个被截断到只有最后 10 个 call 的上下文里工作了。
如何判断:Composer 一条消息里挂了 20+ 个 tool call。
动手前先确认
- 如果 Composer 还在跑就先停下来——让它做完当前文件再 reset。
- 把已经过 lint 和类型检查的文件 commit 掉,留一个干净的回滚点。
- 记下当前用的模型;Opus 4.7、Sonnet 4.6 在长上下文上明显比旧版本好。
需要收集的信息
git status和git diff --stat看重构走到哪一步了。- 当前状态下编译/类型检查的报错。
- 触发这次重构的原始 Composer 消息。
- 一份”已完成 / 已坏掉 / 还没动过”的文件清单。
- 用的是 agent 模式还是普通 Composer。
一步步修复
Step 1:把能跑的部分 commit 掉
跑一遍类型检查 / build。能过的文件全部 stage + commit:
npm run typecheck
git add <files-that-pass>
git commit -m "refactor: rename UserId to AccountId (part 1, files 1-6)"
那些改了一半你不信的文件就丢掉或 stash:
git checkout -- src/api/users.ts src/services/auth.ts
现在你有了一个干净的基线。重构做到一半,但仓库能编译。
Step 2:用明确的基准把重构重新锚定
开一个新的 Composer chat。第一条消息必须钉死标准定义:
We are renaming `UserId` to `AccountId` across the repo.
Canonical definition (do not change this shape):
```ts
// src/types/account.ts
export interface AccountId \{
value: string;
scope: "internal" | "external";
\}
Files already migrated (do not touch):
- src/types/account.ts
- src/api/users.ts
- src/services/auth.ts
Files still to migrate:
- src/components/UserCard.tsx
- src/components/UserList.tsx
- src/hooks/useUser.ts
For each file: read it, plan the change, apply. Stop after each file. Do not move to the next without my OK.
钉死定义 + 已完成清单 + 待办清单,这种结构能把模型重置回一个干净的工作集。
### Step 3:每块控制在 3-5 个文件以内
哪怕开了新 chat,也别一次性贴 20 个文件。每个 Composer chat 上限 3-5 个文件。chat 之间 commit。块越小,模型在每块里都很宽裕,而且一个块爆掉也不会污染其它块。
### Step 4:与其靠记忆,不如重新读文件
要回头编辑 10 轮以前 Composer 碰过的文件时:
Before editing src/api/users.ts again, re-read the current contents. Do not rely on what you remember about this file.
这会强制重新读取,把模型对齐到磁盘上的真实状态。
### Step 5:类型重命名要做 re-export 扫描
```bash
# 找直接重命名遗漏的 re-export
rg "export \{[^}]*UserId[^}]*\}" --type ts
rg "export \* from.*types/user" --type ts
把结果丢给 Composer 让它专门改 re-export。这些点在扫描直接 import 时很容易漏掉。
Step 6:每块结束跑一次干净的类型检查
npm run typecheck 2>&1 | head -50
每块结束后报错数都是 0 就说明你站得稳。报错数往上爬就停手检查——别让 Composer 在污染了的上下文上继续修新的错误。
验证
npm run typecheck和npm run build全跑完整序列后都过。rg "UserId"在仓库里 0 命中(或只剩故意留的兼容别名)。- 最终 diff 和手工重构应该一致,没夹带其它无关改动。
- 跑完整测试套件确认运行时没回归,不只是类型层面通过。
长期预防
- 跨 5 个文件以上的重构,永远不要在一个 Composer chat 里做完——按目录或模块切。
- 多文件重构的开头消息必须钉死标准的 type / interface 定义。
- 块之间一定要 commit,每块都有干净的回滚点,模型也不必去记那些已经写进磁盘的东西。
- agent 模式的重构里硬性限制每次回复最多 10 个 tool call,之后强制重新打 checkpoint。
- 旁边开一个
npm run typecheck --watch,问题一冒头就发现,而不是漂移到第 5 个文件后才暴露。
常见坑
- 编辑失败后用”接着干”这类 prompt——只会往一个已经溢出的 chat 里再塞内容。直接开新 chat。
- 重构中途放任 Composer 反复跑命令;每次
tsc或npm run build输出都吃 1-5K token。 - Composer 还在工作时你手动改文件——这些改动对模型不可见,下一轮就被回滚。
- 信 Composer 那句”我已经更新了所有 15 个文件”——一定要
rg验证。 - 重命名同时存在 type 和 value 形态的符号(TypeScript class、enum)时,没告诉模型哪种形态在哪里。
FAQ
Q:换”长上下文”模型能解决吗? A:天花板会提高,但失败模式不变。够宽的重构终究会撞到上限;切块才是长久之计。
Q:大重构用 agent 模式还是普通 Composer 更好? A:要做大重构,普通 Composer 加每个文件人工确认更稳。agent 模式擅长孤立任务;多文件重构需要人工 checkpoint。
Q:能让 Composer 在 chat 之间”记住”东西吗?
A:它会说能。别信。耐用事实写进 .cursorrules,或者在块边界再贴一次。
Q:要是重构本身就有顺序依赖(后一个文件依赖前一个)怎么办? A:每个文件 commit 后开新 chat,把上一个文件最终状态的关键片段贴进去。麻烦,但稳。