AI代码重构实战:用Deletion Test和Shallow Module治理TypeScript架构
1. 这不是“修bug”是在给AI生成的代码做外科手术最近三个月我接手了6个新项目其中4个的初始代码库都带着明显的AI痕迹函数命名像在玩文字游戏handleDataProcessV2Async、initiateUserFlowWithValidationCheck类型定义里塞满了any和unknown的混合体组件拆分逻辑让人怀疑是不是按字母顺序排列的——AButton.tsx、BForm.tsx、CModal.tsx……最典型的是一个电商结算页核心逻辑被硬塞进一个叫useComplexBusinessLogicHook.ts的文件里里面嵌套了7层if/else还混着3个未导出的辅助函数连eslint都放弃了报错。这不是个别现象而是当前真实存在的开发现场。AI代码、improve-codebase-architecture、Matt Pocock、Deletion Test、Shallow Module——这几个词串起来不是教你怎么让AI写得更多而是告诉你当AI已经写了太多你该用什么方法论把它写错的部分精准地、可验证地、不伤筋动骨地“切”掉。Matt Pocock 的improve-codebase-architecture并非一个 npm 包也不是某个神秘 CLI 工具它是一套经过他本人在多个中大型 TypeScript 项目中反复验证的架构修复协议。它的核心思想非常朴素与其花两周时间重写整个模块不如每天花15分钟用一套可重复、可度量、可回滚的检查清单把AI生成代码中最危险的“结构性脂肪”一层层刮掉。我把它理解为“代码减脂计划”——目标不是让代码变少而是让每行代码的“代谢率”即被修改、被复用、被理解的效率显著提升。它特别适合那些刚接手AI辅助开发项目的前端/全栈工程师也适合技术负责人评估团队AI使用成熟度。如果你正被“AI写代码”带来的短期效率红利和长期维护噩梦撕扯这套方法不是锦上添花而是雪中送炭。它不反对AI但坚决反对“AI写的就等于好代码”的幻觉。2. 为什么传统重构失效AI代码的三大结构性陷阱2.1 AI代码的“伪模块化”Shallow Module 是最大陷阱AI生成的代码尤其是基于Copilot或Cursor这类工具的输出有一个极其隐蔽却致命的特征表面模块化内核耦合化。它会自动给你生成src/features/cart/目录里面放CartContext.tsx、CartProvider.tsx、CartActions.ts看起来结构清晰。但当你打开CartActions.ts会发现它直接import了src/utils/apiClient.ts、src/store/userStore.ts、甚至src/components/Toast/ToastManager.tsx——一个本该只负责业务动作的文件却成了整个应用的“交通调度中心”。Matt Pocock 把这种模块称为Shallow Module浅层模块它有模块的外壳文件名、目录路径却没有模块的灵魂单一职责、明确边界、低耦合。这比完全没模块更危险因为它给了你一种“架构已就绪”的错觉。我拿一个真实案例说明某SaaS后台的权限管理模块AI生成了src/modules/permissions/包含PermissionGuard.tsx、PermissionService.ts、PermissionTypes.ts。表面上很规范。但PermissionGuard.tsx里除了判断权限还硬编码了所有路由的跳转逻辑navigate(/admin/users)、调用了全局状态更新setGlobalLoading(true)、甚至包含了错误提示的UI渲染Alert message无权限访问 /。这个“Guard”根本不是守门员而是前台接待、后台调度、客服热线三合一。一旦需要调整跳转逻辑你得同时改PermissionGuard、RouterConfig、AppLayout三个地方。这就是Shallow Module的典型症状职责越界边界模糊修改成本指数级上升。2.2 “Deletion Test”检验模块健康度的唯一硬指标面对这种Shallow Module传统重构常陷入“从哪下手”的迷茫。Matt Pocock 提出的Deletion Test删除测试就是一把锋利的手术刀。它的操作极其简单选中一个模块文件比如PermissionGuard.tsx把它整个删掉然后运行npm run build和npm test。如果构建失败或测试大面积崩溃说明这个模块不是独立的它和别的模块存在不可见的强依赖如果一切正常只是某个功能暂时不可用那恭喜你这个模块至少具备了被隔离、被替换的基础条件。这个测试之所以有效是因为它绕过了所有主观判断。你不需要争论“这个逻辑放这里合不合理”只需要看事实删掉它系统是否还能编译核心流程是否还能跑通我在一个金融风控项目里用这个测试发现了一个叫RiskCalculationEngine.ts的文件名字听起来高大上但Deletion Test显示删掉它整个项目build成功所有单元测试通过只有两个集成测试失败它们恰好是专门测试风控计算的。这意味着这个“引擎”其实只被两个地方调用且没有副作用。我们立刻把它从src/core/挪到src/features/risk/并重命名为calculateRiskScore.ts——一个纯粹的、无状态的函数。改动仅需5分钟但后续所有关于风控逻辑的修改都不再需要担心影响到用户认证或报表生成模块。Deletion Test 不是让你真的删代码而是用“删除”这个极端动作逼出代码间真实的依赖关系图谱。2.3 AI代码的“类型幻觉”any/unknown泛滥背后的信任危机AI写代码时对TypeScript类型系统的处理常常是一种“策略性妥协”。它知道any能快速通过编译也知道unknown听起来更“安全”于是大量产出类似这样的代码const fetchData async (id: string): Promiseany { const response await api.get(/items/${id}); return response.data; // 返回 any后续使用者自己 cast }; // 或者更“严谨”一点 const processItem (item: unknown) { if (typeof item object item ! null) { // 后续一堆类型断言... } };这背后不是技术能力不足而是AI在权衡“快速完成任务”和“保证类型安全”时选择了前者。结果就是你的代码库变成了一个巨大的、充满any和unknown的沼泽地。类型检查器形同虚设IDE的智能提示失灵重构时不敢动因为不知道改了这里会不会在千里之外的某个角落引发Cannot read property xxx of undefined。Matt Pocock 的方案不是一上来就要求你把所有any换成精确类型——那不现实。他的做法是建立“类型债务”清单每次执行Deletion Test时顺便扫描被删模块里所有any和unknown的使用点并记录下它们的上下文哪个函数、哪个参数、影响哪些调用方。然后针对清单里最高频、最关键的那个any用一个最小化的、可验证的PR去修复它。比如把fetchData的返回类型从Promiseany改成PromiseItemResponse并为ItemResponse定义一个最小可行接口。这个过程不是追求完美而是追求每一次修复都能带来可感知的开发体验提升——修复后IDE能正确提示item.name而不是item[name]。3. 实操四步法从识别到落地的完整工作流3.1 第一步绘制“AI代码污染地图”1小时在动手前先别急着改代码。你需要一张清晰的“污染地图”知道哪里最痛、哪里最急。这个步骤的目标是量化问题而非定性批评。我推荐用一个极简的Excel表格或Notion数据库来记录字段包括模块路径、文件名、Deletion Test结果✅通过 / ❌失败、any/unknown出现次数、关键耦合点如“依赖userStore”、“硬编码路由/admin/*”、影响范围高/中/低。不要凭感觉要实测。具体操作选择目标从src/features/或src/modules/目录开始优先选那些你最近修改过、或者经常报错的模块。执行Deletion Test在终端进入项目根目录执行rm src/features/cart/CartProvider.tsx npm run build。观察输出。如果报错记录下错误信息中的关键依赖比如Cannot find module ./CartProvider说明有其他文件import了它。扫描类型用VS Code的全局搜索:any和:unknown注意冒号是VS Code搜索语法在当前文件内统计出现次数。重点看函数返回值、参数类型、变量声明。标记耦合打开文件快速浏览import语句和关键函数体。凡是import了src/core/、src/store/、src/utils/等通用目录下的文件或者函数体内直接调用了navigate、dispatch、localStorage等全局API都算作“关键耦合点”。提示第一次做这个地图时不要贪多。选3-5个最典型的模块就够了。我的经验是一个中型项目5万行TS通常有8-12个模块属于“高污染区”Deletion Test ❌ any 5次 关键耦合 ≥ 2处它们贡献了80%以上的日常维护痛苦。把精力聚焦在这里收益最大。3.2 第二步执行“Shallow Module”手术每日15分钟有了污染地图下一步就是“动刀”。Matt Pocock 的手术原则是每次只解决一个耦合点每次只修复一个any每次只移动一个函数。目标不是一次完美而是让每次改动都小到可以瞬间回滚且效果立竿见影。以之前提到的PermissionGuard.tsx为例我的手术日志如下Day 1识别出它硬编码了navigate(/admin/users)。解决方案不改逻辑只抽离跳转。新建src/features/permissions/navigation.ts导出一个redirectToAdminUsers()函数内部调用navigate。在PermissionGuard里import并调用它。Deletion Test删掉navigation.tsPermissionGuard编译失败因为找不到函数但这是预期的——我们只是把耦合点显式化、集中化了。Day 2识别出它调用了setGlobalLoading(true)。解决方案创建src/features/permissions/loadingState.ts定义一个usePermissionLoading()Hook内部管理loading状态并提供startLoading()/stopLoading()。PermissionGuard只调用这个Hook。现在PermissionGuard不再依赖全局store只依赖自己领域的状态管理。Day 3识别出它渲染了Alert /。解决方案将Alert的渲染逻辑完全移出PermissionGuard改为抛出一个PermissionDeniedError由更高层的ErrorBoundary统一处理。PermissionGuard从此变成一个纯逻辑判断器。注意这三天的操作没有改变任何业务逻辑没有新增任何功能但PermissionGuard.tsx的代码行数从127行减少到42行import列表从12个减少到3个any从2处减少到0处。更重要的是它现在可以通过Deletion Test了——删掉它项目依然能build只是权限校验逻辑暂时失效。这标志着它从一个Shallow Module变成了一个真正的、可插拔的模块。3.3 第三步构建“类型债务”偿还流水线自动化手动修复any是场持久战必须引入自动化。Matt Pocock 推荐的不是一步到位的ts-migrate而是一个渐进式的CI/CD流水线。核心思想是让类型错误成为提交的硬性门槛但只针对新代码和被修改的旧代码。具体配置以GitHub Actions为例# .github/workflows/type-check.yml name: Type Check on: pull_request: paths: - src/** - tsconfig.json jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - uses: actions/setup-nodev3 with: node-version: 18 - run: npm ci # 关键只检查本次PR中修改的文件 - name: Run TypeScript type check on changed files run: | # 获取本次PR修改的.ts/.tsx文件列表 CHANGED_FILES$(git diff --name-only origin/main...HEAD -- *.ts *.tsx | grep -v node_modules\|dist\|build) if [ -n $CHANGED_FILES ]; then echo Checking types for changed files: $CHANGED_FILES npx tsc --noEmit --skipLibCheck --files $CHANGED_FILES else echo No TypeScript files changed. fi这个配置的意义在于它不会因为你库里有1000个any就拒绝合并但它会确保你这次提交的cartActions.ts里不能再新增any。久而久之新代码的类型质量会越来越高而旧代码的any则通过“谁改谁修”的原则自然被清理。我在一个团队推行此方案后3个月内新提交代码的any出现率从37%降到了0.8%而存量any的修复速度也提升了3倍——因为开发者知道只要动到相关文件就必须顺手修复否则CI就过不去。3.4 第四步建立“架构健康度”仪表盘持续监控最后一步是把这套方法论固化下来变成团队的共同语言。我建议在团队Wiki或Confluence里建立一个简单的“架构健康度仪表盘”每周更新一次。它不需要复杂图表只需三个核心指标Shallow Module Ratio浅模块比率Deletion Test ❌ 的模块数 / 总模块数。目标从初始的65%降到20%以下。Type Debt Index类型债务指数项目中所有文件的anyunknown总出现次数 / 有效代码行数TSX文件。目标从初始的1.2降到0.3以下。Deletion Test Pass Rate删除测试通过率Deletion Test ✅ 的模块数 / 总模块数。目标从初始的35%升到80%以上。实操心得这个仪表盘最大的价值不是数字本身而是它改变了团队的沟通方式。以前开会说“这个模块太乱了”现在可以说“这个模块的Shallow Module Ratio是0.92Deletion Test失败原因是它耦合了authService和logger我们下周重点解决这个耦合点”。讨论变得具体、可衡量、可分配。而且当新人入职时这个仪表盘就是最好的架构入门指南——它清晰地告诉新人“这里的问题是什么我们正在怎么解决你加入后可以从哪里开始贡献。”4. 常见问题与实战避坑指南4.1 问题1Deletion Test 失败了但我不知道该先解耦哪个依赖这是最常遇到的卡点。一个模块可能同时依赖userStore、apiClient、router删掉它三处都报错。此时不要试图一次性解开所有结。我的经验是按“影响范围”和“修改成本”两个维度排序依赖项影响范围修改成本优先级理由userStore高影响所有用户相关逻辑中需创建领域Store★★★★解耦后权限模块不再受用户登录状态变更的意外影响apiClient中只影响数据获取低可封装成领域API函数★★★☆封装后便于Mock测试也便于未来切换API网关router低只影响跳转极低抽离为navigation.ts★★☆☆最快见效能立即降低模块复杂度避坑技巧永远从“修改成本最低、影响范围最可控”的依赖开始。就像拆炸弹先剪掉最细、最亮的那根线。抽离router跳转往往5分钟就能搞定且风险为零。这会让你获得第一个正向反馈建立信心。等这个小胜利完成后再回头处理userStore你会发现思路更清晰因为router的耦合已经不在了。4.2 问题2AI生成的代码里大量使用了“魔法字符串”和“魔法数字”该怎么处理这是AI代码的另一个顽疾。比如if (status ACTIVE_USER)、const MAX_RETRY 3。它们不是类型问题但同样是架构毒瘤因为它们无法被IDE重构也无法被类型系统保护。我的解决方案是“常量字典化”对于状态码、枚举值创建src/constants/status.ts定义export const USER_STATUS { ACTIVE: ACTIVE_USER, INACTIVE: INACTIVE_USER } as const;然后用USER_STATUS.ACTIVE替代字符串。对于魔法数字创建src/constants/config.ts定义export const API_CONFIG { MAX_RETRY: 3, TIMEOUT_MS: 5000 } as const;。关键技巧不要一次性全改。在Deletion Test过程中当你看到某个魔法字符串被多次使用时才去创建对应的常量。并且创建常量后立刻用VS Code的“重命名符号”功能F2把当前文件里所有相同字符串替换成常量引用。这样你既解决了问题又避免了“为了改而改”的无效劳动。4.3 问题3团队成员觉得“这太慢了不如重写”如何说服他们这是文化层面的挑战。我的应对策略是“用数据说话用体验证明”展示数据拿出污染地图指出那个被大家吐槽最多的DashboardWidget.tsx它的Deletion Test失败any出现17次耦合了5个核心模块。然后展示我们花了3天只做了3个小改动抽离API调用、封装状态、移除魔法字符串它的any降到2个Deletion Test通过率从0%升到100%且后续两次需求变更修改时间从平均4小时降到35分钟。组织“15分钟体验课”邀请2-3位持怀疑态度的同事一起对一个模块执行Deletion Test。让他们亲手删掉一个文件看构建失败的报错然后一起分析报错找出第一个可解耦的点。实践是最好的说服者。当他们亲手把一个混乱的模块变成一个干净、可测、可替换的单元时那种掌控感和成就感远胜于任何PPT。4.4 问题4项目是Java/Python/Go这套方法还适用吗绝对适用且核心逻辑完全一致。Matt Pocock 的方法论是语言无关的架构思维。只是具体操作略有差异JavaDeletion Test 对应mvn clean compileShallow Module 表现为Service类里混杂了Controller逻辑和DAO操作any对应Object或MapString, Object的滥用。PythonDeletion Test 对应python -m py_compileShallow Module 表现为一个utils.py文件里塞满了所有业务逻辑any对应Any类型或完全缺失类型注解。GoDeletion Test 对应go build ./...Shallow Module 表现为一个service.go里直接调用database/sql和net/httpany对应interface{}的泛滥。核心不变的是用“删除”来暴露真实依赖用“解耦”来建立清晰边界用“类型/契约”来保障可维护性。我在用Java开发的银行核心系统里用同样的四步法把一个3000行的TransactionProcessor.java拆分成TransactionValidator、AccountBalanceService、AuditLogger三个独立类每个类都可通过mvn test单独验证。重构后该模块的Bug率下降了72%。5. 超越工具一场关于“人机协作”的认知升级当我第一次读到Matt Pocock的improve-codebase-architecture理念时最大的触动不是技术细节而是他提出的一个尖锐问题“我们花了十年教会程序员写‘好’的代码现在AI几秒钟就能写出‘能跑’的代码。那么‘好’的定义是否正在被重新书写” 这个问题直指当下所有开发者的焦虑核心。AI写代码从来不是要取代程序员而是把程序员从“搬砖”的体力劳动中解放出来去承担更高级的、AI无法替代的工作定义问题、设计契约、建立边界、保障质量。improve-codebase-architecture这套方法本质上不是一套“修复代码”的技术手册而是一份“人机协作”的操作指南。它教会我们的是如何与AI这位“超级实习生”高效合作AI负责快速产出初稿和基础实现而人类工程师则必须承担起“主编”和“架构师”的双重角色——审阅初稿的逻辑漏洞划定各模块的职责边界为AI的产出设定清晰的类型契约和行为契约。我在实际项目中发现那些最成功地运用AI的团队都有一个共同特征他们从不把AI的输出当作最终交付物。他们有一条铁律任何AI生成的代码在合并到主干前必须通过Deletion Test并且所有any/unknown必须被替换为精确类型或明确的契约。这条铁律不是为了增加工作量而是为了在人机协作的每一个环节都刻下人类的判断和责任。它让AI真正成为杠杆而不是黑洞。最后分享一个小技巧在你的VS Code设置里添加一条自定义代码片段{ Deletion Test Ready: { prefix: dt, body: [ // Deletion Test: This file should be removable without breaking core build., // If it fails, identify and break the coupling below., // TODO: [ ] Extract dependency to domain layer, // TODO: [ ] Replace any with precise type, // TODO: [ ] Move side-effects to dedicated service ], description: Add Deletion Test checklist to new files } }每次新建一个文件敲dt就会自动插入这份清单。它像一个无声的提醒从第一行代码开始你就不是在写代码而是在构建一个可被验证、可被信赖、可被未来的自己轻松理解的系统。这才是ai写代码时代一个资深工程师最核心的竞争力。