跳到主要内容

长重构中途,我跟丢了「已经改过哪些文件」

阶段编码实现角色工程师 · 架构师严重度适用版本Coding Agent 通用证据官方文档

一句话摘要:跨十几个文件的大重构做到一半,我就记不清改到哪了——哪些文件已改、哪些没改、那个改名是否全仓同步了。于是漏改一处编译挂掉、把已改的又改一遍、前后用了两种不一致的写法。别让我用记忆当待办清单。

现象

你让我把一个用得到处都是的函数 getUser() 改名成 fetchUser(),顺带把它的返回类型从裸对象换成 Result<User>。这是个典型的跨文件重构:调用点散在十几个文件里,每个调用点都要跟着改。

我开始一个个改。改到第七、第八个文件时,你会看到这些症状:

  • 我漏掉了 legacy/ 目录下还有两处调用,编译直接挂在那里。
  • 我把 services/auth.ts 又打开改了一遍——其实我十分钟前已经改过它了。
  • 前五个文件我用了 Result<User>,后五个不知不觉退回成了 User | null——同一次重构里出现了两种返回约定。

为什么会这样

重构的「已完成集合」是一份不断增长的状态:每改完一个文件,这份清单就长一条。而我并没有一块专门记这份清单的稳定内存——它和我们的全部对话、我读过的每个文件内容一起,挤在同一个上下文窗口里滚动。

问题出在两头。一头是窗口变长:改到第八个文件时,前面七次编辑的细节已经被推到很靠前的位置,注意力分到它们身上的比重越来越小,「我改过 auth.ts 了吗」这种事就开始模糊。另一头是上下文压缩:长会话触发 compaction 后,压缩保留的是它「以为重要」的摘要,而「第三个文件我选了 Result<User> 这个写法」这种细节,恰恰是最容易被压没的——压完我「换了个脑子」,自然接不上前半程的约定。

说白了:我在拿易失的记忆当一份本该写下来的待办清单用。 清单一长、窗口一压,记忆就靠不住了。

后果

  • 漏改:有调用点没跟着改,编译挂掉,或更糟——编译过了但行为变了,藏到运行时才炸。
  • 重改:我把已经改好的文件又改一遍,可能把对的改回错的,白白浪费你的 review。
  • 前后不一致:同一次重构里出现两种写法(Result<User>User | null),等于在重构里又埋了一笔新的技术债。
  • 越查越乱:你让我「检查一下哪里还没改」,我凭记忆回答,而记忆本身就是出错的源头——你拿到的是一份不可信的进度报告。

最佳实践

核心思路只有一句:别让我用记忆跟踪进度,把进度外置成可核查的东西。 四个具体动作:

1. 拆小步,每步一 commit。 把大重构切成「一次只完成一个完整的小改动」,每完成一步就提交。Git 历史就是最可靠的「已改清单」——git loggit diff 不会因为窗口变长而模糊。Anthropic 官方的最佳实践也明确建议频繁提交,让每一步都可回溯、可回滚。

2. 先建一份显式待办清单。 动手前让我先把要改的点列出来(最好基于一次全仓搜索的结果),边改边勾。这份清单写在对话里、或写进一个临时文件,都比我脑子里的印象可靠。

3. 每步当场验证。 每改完一步就跑编译 / 跑相关测试,让失败立刻暴露在「这一步」,而不是攒到最后一锅端——那时你已经分不清是哪一步漏了。

4. 用搜索代替记忆确认「还有哪里没改」。 不要问我「应该都改完了吧」,让我用 grep / 全仓搜索把旧符号再扫一遍:搜出来还有命中,就是没改完;搜不到,才叫改完。搜索是确定性的,记忆不是。

# 与其凭记忆收尾,不如让残留自己现形:
你:搜一下全仓还有没有 getUser( 的调用,列出文件和行号
我:(grep 结果——还有 3 处,分别在 …)
你:好,这 3 处也改掉,改完再搜一次确认归零

这条和 《不懂 /compact 时机》 是一对:那条讲压缩会冲掉我手里的关键状态,这条讲「已改文件集合」正是最该被外置、最不该交给压缩去赌的那种状态。同理,如果重构涉及让我基于文件当前内容做编辑,务必让我先重新读一遍再改,别凭印象覆盖——印象和记忆一样靠不住。

示例

改之前(一把梭,靠记忆收尾):

你:把 getUser 改名成 fetchUser,返回类型换成 Result<User>,全仓都改了
我:(连续编辑十几个文件,不提交、不验证)
我:改完了。
你:(编译挂了,legacy/ 下两处没改;auth.ts 被改了两遍;后半程退回了 User | null)

改之后(拆步 + 清单 + 验证 + 搜索):

你:先别动手。grep 全仓所有 getUser( 调用,列成一份清单
我:(列出 12 处,分布在 9 个文件)
你:按文件逐个改,每个文件改完跑一次 tsc,过了就 commit,再报告进度
我:services/auth.ts 改完 ✅ tsc 通过 ✅ 已提交(1/9)
我:legacy/cache.ts 改完 ✅ tsc 通过 ✅ 已提交(2/9)

你:全部改完后再 grep 一次 getUser( 确认归零
我:搜索结果 0 命中,重构完成;git log 共 9 个提交可回溯

后者哪一步出问题,git log 和那次最终搜索都能精确告诉你——进度不再活在我的记忆里。

版本说明

适用版本

这是上下文窗口与上下文压缩机制带来的固有现象,与具体版本无关,全模型、跨工具通用。窗口更大的模型能把「开始跟丢」的临界点往后推(能多撑几个文件再糊),但只要重构规模足够大、会话足够长,「靠记忆跟踪不断增长的已改集合」这个根本问题不会消失。把进度外置(commit / 清单 / 搜索)才是不依赖窗口大小的解法。

延伸阅读与出处