第二十六篇:旧项目维护利器:如何让AI理解历史债务、添加新功能
📌标签:
#遗留系统#技术债#重构#维护#实战
接手一个三年没动过的代码库,或者需要在满是“技术债”的老项目中添加新功能——这是每个开发者都躲不开的噩梦。文档缺失、命名混乱、架构过时、测试覆盖率为零……Claude Code 的出现,让这个曾经痛苦的过程变得可控甚至可预测。它不是银弹,但它能成为你最得力的“考古助手”和“外科医生”。
1. 旧项目维护的三大痛点(AI 能解决的部分)
| 痛点 | 传统做法 | Claude Code 的解法 |
|---|---|---|
| 理解代码 | 手动翻阅几十个文件,画调用关系图,费时费力 | 一次性读取整个模块,AI 自动生成架构摘要和调用链 |
| 定位修改点 | 全局搜索关键词,逐一检查是否遗漏 | Agent 模式 + Grep 工具,自动分析影响范围 |
| 安全添加功能 | 害怕破坏现有逻辑,写一堆兼容代码 | Plan 模式预审 + 自动运行测试验证,最小侵入 |
| 重构债务 | 风险高,往往不了了之 | 分批重构,每次只改一小块,测试确保不破坏 |
| 补充测试 | 为遗留代码写测试极其枯燥 | AI 自动生成测试用例,覆盖边界条件 |
旧项目的复杂性不在于单文件有多难,而在于文件之间的隐式依赖和历史决策的不可见性。Claude Code 的 200K 上下文和 Agent 闭环,正好能应对这种“全局理解”的需求。
2. 第一步:让 AI 成为代码考古学家
当你第一次面对一个陌生老项目时,不要急着修改。先让 Claude Code 帮你“挖掘”出关键信息。
2.1 生成项目架构摘要
我刚接手这个 Node.js 项目,请帮我分析: 1. 项目的整体架构(MVC?三层?) 2. 核心入口文件和路由组织方式 3. 数据库访问层使用的 ORM 或驱动 4. 有哪些明显的技术债迹象(如代码重复、过长函数、深层嵌套) 5. 缺失的文档或测试覆盖薄弱的地方Claude Code 会通过读取package.json、目录结构、关键文件,输出一份类似以下的分析报告:
### 项目考古报告 **架构**:接近 MVC,但控制器直接操作数据库(没有 service 层),导致逻辑分散。 **入口**:`app.js` → 注册路由(`routes/index.js` 中手动 require)→ 控制器散落在 `controllers/` 下。 **数据层**:使用原生 `mysql2` 连接池,SQL 语句拼接在控制器中,没有使用 ORM。 **技术债**: - `utils/helper.js` 长达 800 行,包含日期、字符串、加密等杂项函数,需拆分。 - 存在大量 `console.log` 而非日志库。 - 3 个文件中有超过 15 层的回调嵌套(callback hell)。 - 没有单元测试,仅有少量手工测试脚本。 **建议优先处理**: 1. 将数据库操作抽离到 `repositories/` 层。 2. 把 helper.js 按职责拆分。 3. 引入日志库(如 winston)替换 console。有了这份报告,你就知道从哪里下手,也可以用来向团队或上级说明重构的必要性。
2.2 追踪一个功能的完整调用链
当需要修改某个具体功能时(比如“修改用户积分计算逻辑”),让 AI 帮你找到所有相关代码:
我要修改用户积分计算逻辑。请找出: - 积分计算的入口函数在哪里 - 哪些地方调用了这个函数 - 积分数据最终写入哪个数据库表Claude Code 会通过 Grep 和 Read 工具,输出从 HTTP 请求到数据库写入的完整路径,并标注每个环节的文件和行号。
3. 第二步:安全添加新功能——隔离与兼容
在旧项目中加新功能,最大的风险是“破坏现有行为”。Claude Code 的 Plan 模式和测试自动化为你提供了安全网。
3.1 使用 Plan 模式审核修改计划
提示词:
/mode plan 我要在 `order.js` 中添加一个新的折扣计算逻辑:如果订单总金额超过 1000 元,打 9 折。请分析现有代码,生成一个最小侵入的修改计划。AI 会输出:
【计划】添加满 1000 元 9 折逻辑 1. 影响分析 - `order.js` 中的 `calculateTotal` 函数目前直接返回 `sum(items.price)` - 该函数在 3 个地方被调用(checkout, invoice, admin report) - 没有现有测试覆盖此函数 2. 修改方案(最小侵入) a. 在 `calculateTotal` 函数内部,计算完原始总额后,添加: if (originalTotal >= 1000) return originalTotal * 0.9; b. 不修改函数签名,不影响调用方 c. 添加注释说明折扣规则 3. 验证方法 - 手动调用该函数传入 >1000 的订单,断言返回值为原价 *0.9 - 运行现有的 checkout 流程,确保 UI 显示正确 4. 风险 - 如果未来需要更复杂的折扣规则(如叠加),此硬编码方式不易扩展。 - 建议后续重构为策略模式。 是否执行?(需要切换到 Default 模式)审阅后你发现需要更灵活,可以要求修改计划:“改成可配置的折扣率,从环境变量中读取”。AI 会重新生成计划。
3.2 让 AI 自动编写兼容性代码
在旧项目中,新功能可能需要兼容旧的数据格式或 API 版本。你可以让 AI 生成适配层:
在新功能 `sendNotification` 中,需要同时支持旧版(`notify` 函数,参数为 userId, message)和新版(`notifyV2`,参数为 userObject, messageTemplateId)。请生成一个适配器函数,根据传入参数类型自动路由。AI 会输出带有类型检测的适配器代码,确保不会因为调用方未升级而崩溃。
3.3 利用测试作为安全网
在修改遗留代码前,先让 AI 生成针对现有行为的测试用例(Characterization Tests)。这些测试会捕获当前输出,作为回归检测的基线。
在修改 `formatPrice` 函数之前,请先生成一组测试用例,覆盖它当前的所有可能输入(正常数字、负数、零、浮点数、非数字),并记录输出。用 Jest 实现。AI 会生成测试,运行后得到当前行为的快照。然后你再让 AI 修改函数,只要测试仍然通过,就说明行为没有被破坏。
4. 第三步:偿还技术债——渐进式重构
技术债往往不是一天形成的,也不能一天还清。Claude Code 可以帮你以低风险的方式逐步重构。
4.1 提取函数(Extract Method)
`utils/helper.js` 中的 `processData` 函数有 200 行,请将其中的日期格式化逻辑提取为一个独立函数 `formatDate`,放在同一个文件中,并更新原函数的调用。AI 会安全地完成提取,并保持外部行为不变。
4.2 拆分大文件
`userController.js` 有 1200 行,包含用户注册、登录、资料管理、密码重置等多个功能。请帮我拆分成多个文件:`authController.js`(登录/注册/重置)、`profileController.js`(资料管理),并更新路由引用。AI 会:
- 创建新文件,移动对应函数
- 修改原文件,删除已移动的代码
- 更新
routes/index.js中的 require 路径 - 运行测试确认无破坏
4.3 替换过时依赖
旧项目可能使用已经被废弃的库(如request、moment)。Claude Code 可以帮你批量替换:
将项目中所有使用 `request` 库发送 HTTP 请求的地方,替换为 `axios`,并保持 API 兼容(如 .then 和 callback 风格同时支持)。先生成计划,不要执行。AI 会搜索所有request(调用,分析每个调用的参数和回调模式,生成详细的替换方案。你批准后,它会逐个文件修改,并运行测试验证。
5. 处理没有测试的遗留代码
旧项目往往没有自动化测试,这使得任何修改都像是在走钢丝。Claude Code 可以帮助你快速建立“安全网”。
5.1 为关键路径生成集成测试
没有现有测试。请为订单创建流程(从 `POST /orders` 到数据库写入)生成一个端到端的集成测试,使用 supertest 和真实的测试数据库。AI 会:
- 读取路由、控制器、数据库操作代码
- 生成一个发送请求 → 断言响应 → 查询数据库验证的完整测试
- 配置测试数据库(如使用事务回滚或独立数据库)
5.2 生成快照测试(Snapshot Testing)用于 UI
对于 React/Vue 旧项目,可以生成快照测试来捕获当前组件的渲染输出,之后任何修改导致 UI 变化都会暴露。
为 `UserProfile` 组件生成快照测试,覆盖不同 props 组合(无数据、加载中、正常数据、错误状态)。5.3 让 AI 评估测试覆盖率
运行 `npm run test:coverage`,分析报告,列出哪些文件覆盖率低于 50%。然后为其中一个文件 `src/legacy/payment.js` 生成补充测试。6. 处理隐式依赖和全局状态
旧项目中最棘手的问题是隐式依赖——一个函数偷偷修改了全局变量,或者依赖某个模块的初始化顺序。Claude Code 可以通过静态分析帮助你发现这些陷阱。
6.1 查找全局变量
找出项目中所有在函数外部定义的变量(全局作用域),并标记哪些被多个文件引用。AI 会通过 Grep 和 AST 分析(虽然不是完美,但能提供候选列表),输出高风险全局变量。
6.2 检测模块间的循环依赖
分析 `src/` 下模块间的依赖关系,输出是否存在循环依赖(如 A → B → C → A)。AI 会读取所有import/require语句,构建依赖图,找出环。对于发现的循环依赖,它可以建议重构方向。
6.3 为隐式依赖添加显式注入
`payment.js` 中直接使用了 `global.config.stripeKey`。请修改为从参数传入(依赖注入),并更新所有调用方。AI 会修改函数签名,并逐个修改调用位置,传入config对象。
7. 实战案例:修复一个古老的时间戳 bug
背景:一个五年的项目,用户反馈“订单创建时间在某些时区显示错误”。代码中时间处理使用原生Date,但多处直接存储本地时间字符串而非 UTC。
用户输入:
订单创建时间在非 UTC 时区显示错误。请分析所有涉及订单时间的代码,统一改用 UTC 存储,并在显示时转换为用户时区。Claude Code 执行过程:
- 搜索时间相关代码:Grep 查找
new Date()、toLocaleString、moment(如果有)以及数据库中的created_at字段。 - 定位关键点:
- 订单创建时,在
orderController.js中直接new Date()存入了order.time。 - 前端展示时,直接
new Date(order.time).toLocaleString()没有指定时区。
- 订单创建时,在
- 生成计划(Plan 模式):
- 修改后端:存储
new Date().toISOString()(UTC 字符串)。 - 修改数据库字段类型(如果使用 SQLite 或 MySQL,可能需要迁移)。
- 修改前端:引入
dayjs配合utc插件,将 UTC 字符串转为用户时区。 - 为已有数据编写迁移脚本:读取旧的非 UTC 时间,转换为 UTC 存储。
- 修改后端:存储
- 执行:按计划逐个文件修改,并运行现有的手工测试(AI 无法自动测试时提醒你验证)。
- 验证:修改后,AI 可以生成一个测试脚本,模拟不同时区下的显示,确保正确。
结果:2 小时内完成,原本预估需要一整天。
8. 常见陷阱与应对
| 陷阱 | 表现 | 应对 |
|---|---|---|
| AI 过于乐观 | 认为可以安全重构,忽略了某个隐蔽的副作用 | 使用 Plan 模式审阅,要求 AI 列出所有副作用和风险点 |
| 测试不足导致回归 | AI 生成的新代码通过了现有测试,但现有测试本身就不充分 | 先让 AI 生成表征测试(characterization tests),再修改 |
| 环境差异 | AI 在本地测试通过,但部署到生产(或旧版本 Node)失败 | 在 CLAUDE.md 中写明生产环境版本和限制,AI 会遵循 |
| 数据库迁移风险 | AI 生成的迁移脚本可能丢失数据 | 要求 AI 生成可逆的迁移(up + down),并在测试数据库先行验证 |
| 第三方 API 变更 | AI 假定某个外部 API 的行为与代码中一致,但实际已变化 | 让 AI 先调用 MCP 获取最新 API 文档,或人工确认 |
9. 建立旧项目维护的 SOP(标准操作流程)
结合上述经验,你可以为团队制定一个用 Claude Code 维护旧项目的流程:
- 考古阶段:让 AI 生成架构摘要和技术债清单。
- 安全网阶段:为关键路径生成表征测试和集成测试。
- 计划阶段:使用 Plan 模式生成修改计划,评估影响范围。
- 执行阶段:在 Default 模式下逐步实施,每完成一小块就运行测试。
- 验证阶段:AI 自动运行测试,并提示你进行手工验证的关键点。
- 记录阶段:将本次修改的决策和注意事项记录到
CLAUDE.md或项目文档中,供后续参考。
10. 下篇预告
掌握了旧项目维护,接下来我们学习如何让 Claude Code 参与到代码审查流程中——使用/review-pr命令结合 GitHub Actions,自动化检查代码规范、潜在 bug 和安全问题。
👉下一篇:用/review-pr+ GitHub Actions 自动跑查代码规范
思考题(自测理解)
- 在添加新功能到旧项目时,为什么“先写表征测试”比“直接写新代码”更安全?请从回归风险角度说明。
- 假设你有一个关键函数
calculateTax,没有任何测试,逻辑复杂且包含多个分支。你会如何利用 Claude Code 为它生成测试?步骤是什么? - Claude Code 在分析旧项目时,可能会因为某些文件过大(超过 200K token)而无法一次性读取。你会如何处理这种文件?(提示:结合
@file行号、grep分段读取)
旧代码不是债务,而是宝藏——它记录了无数决策和教训。Claude Code 可以帮你把它变成可维护的资产。下一章,我们把它用到代码审查流水线上。
