AI 重构生成了重复文件:3 个原因 + 修复路径

Agent 在原文件旁边建了 `UserList2.tsx` 这种重复文件——安全清理步骤。

让 Claude Code 或 Cursor 重构一个组件,它没改原文件,反而新建了 UserList2.tsx / UserListNew.tsx / user-list-v2.tsx,把改动写进新文件。更糟的是有时它还顺手 import 新文件的同时保留旧文件,两份代码同时存活,bug 修了一半在新文件、另一半还在旧文件里。

本文给你一条”先 diff 出两份文件的真实差异、把好的那份合并完整、再删另一份并让 import 自动跟上”的清理路径,外加在 CLAUDE.md 里写规则防止再犯。

常见原因

按命中率从高到低。

1. Agent 没读到原文件,凭印象重写

最常见。你说”重构 UserList”,agent 用 grep 或 file search 找不到精确匹配(路径有 alias、文件名有破折号、组件不在 components/ 而在 features/),它就假设文件不存在,直接新建。

> Tool: write_file
  path: src/components/UserList2.tsx
  reason: "Creating a new improved version of the UserList component."

如何判断:agent log / chat transcript 里出现 “creating new file” 而你的本意是 edit;或者 git status 里出现 UserList2.tsx 这种带数字 / New / v2 后缀的新文件。

2. 多步 agent 忘了自己已经建过

Aider 或 Codex 在长会话里跨多步执行任务,前面建了 lib/utils/format.ts,几步之后又被要求”为格式化加一个工具”,它在 src/helpers/format.ts 再建一份。两份函数签名相同,逻辑略有差异。

如何判断rg "export function formatDate" -t ts 两次以上命中且文件名不同。

3. 会话中途命名规则漂移

你前半段叫 userService,agent 后半段写成 UserService / user-service,import 路径解析失败时它就建新文件。在 case-insensitive 文件系统(macOS 默认)上特别隐蔽——本地没问题,Linux CI 上挂。

如何判断git ls-files | sort -f | uniq -di 找出大小写重复的文件名。

4. Agent 怕改坏原文件,做”安全副本”

某些 agent(尤其是被反复教育过”小心已 commit 的代码”)的默认行为是:要重大改动时先复制一份再改。比如要把 class component 重写成 hooks,它建 UserListHooks.tsx 而不是替换 UserList.tsx

如何判断:新文件内容是旧文件的”风格升级版”,public API 几乎一致。

5. 多 agent / 多分支并行写同一功能

你早上让 Cursor 写了 analytics/track.ts,下午让 Claude Code 在另一分支写了 lib/analytics.ts,合并时两份共存。

如何判断git log --diff-filter=A --since="1 week" -- "*analytics*" 同一周内多个 commit 新建了类似文件。

6. AI 不知道项目的目录约定

prompt 没给 src/components/ 是 UI、src/features/<name>/ 是业务这种约定,agent 把 UI 组件建在了 features 目录里,旧的 UI 版本还在 components。

如何判断:新旧文件位于不同顶层目录但功能相同。

最短修复路径

Step 1:列出所有可疑重复文件

# 找带数字 / New / v2 后缀的文件
git status --short | grep -E '(2|3|New|new|v2|copy|Copy)\.(ts|tsx|js|jsx|py)$'

# 找最近一周新建的、和已有文件 basename 相似的
git log --diff-filter=A --since="1 week" --name-only --pretty=format: \
  | sort -u | awk -F/ '{print $NF}' | sort | uniq -d

输出每一组重复的两个路径,写到清单上。

Step 2:diff 两份文件,决定保留哪一份

diff -u src/components/UserList.tsx src/components/UserList2.tsx
# 或并排
git diff --no-index src/components/UserList.tsx src/components/UserList2.tsx

逐组按下面表格判断:

情况保留哪份
新文件是完整重写,旧文件没人 import保留新文件,删旧文件
旧文件还在被 import,新文件包含真正的修复把新文件的修复 cherry-pick 到旧文件,删新文件
两份各修了一部分 bug手动合并到旧文件路径,删新文件
完全相同删任意一份

永远保留 import 多的那个路径——少改 import 就少出 bug。

Step 3:把好的内容合到唯一保留的文件

用 agent 帮你 cherry-pick,比让它整体重写更安全:

Prompt:对比 src/components/UserList.tsx 和 src/components/UserList2.tsx,
列出 UserList2 里有但 UserList 里没有的逻辑改动。
然后只把这些改动应用到 UserList.tsx,不要做任何风格 / 排序 / 重命名。

让它先输出 diff 计划,你审核后再执行。

Step 4:删重复文件并修 import

git rm src/components/UserList2.tsx

# 找还有谁在引用被删的文件
rg "UserList2|user-list-v2" --type ts --type tsx

逐个把残留 import 改回正确路径。如果项目用 TypeScript,tsc --noEmit 会立刻把所有失效 import 报出来:

npx tsc --noEmit

Step 5:跑测试 + build 验证

npm test
npm run build

特别注意:case-insensitive 文件系统上本地能跑、Linux CI 挂的情况——git ls-files | sort -f | uniq -di 再扫一次。

预防建议

  • Prompt 里说”编辑 src/components/UserList.tsx”,不要说”创建一个改进版本”;用 @ 提到具体文件路径
  • CLAUDE.md / .cursorrules / AGENTS.md 加一条:“禁止建带数字 / New / v2 / copy 后缀的文件;要替换原文件就直接编辑原路径”
  • pre-commit hook 拦截命名:发现 *2.tsx / *New.tsx / *Copy.tsx 直接拒绝 commit
  • git diff --stat review agent 的每一轮输出,只要看到不熟悉的新文件路径就停下来确认
  • 长会话定期 /clear 或新开 session,避免 agent “忘记自己已经建过”
  • TypeScript 项目把 forceConsistentCasingInFileNames 设为 true,配合 tsc --noEmit 在 pre-commit 阶段就拦下大小写重复

相关阅读

标签: #AI 编程 #排查 #排查