AI 修改老项目时,如何控制影响范围
在新项目里,AI 写错一段代码通常比较容易发现。老项目则不一样。
一个看起来简单的需求:
给订单列表增加一个订单状态筛选条件。可能会碰到很多历史约束:
- Controller 使用旧版参数对象;
- Service 同时被后台任务调用;
- Repository 查询包含租户和软删除条件;
- 状态值不是数据库中的真实枚举名称;
- 前端依赖当前的分页和排序格式;
- 某些历史行为没有测试,也没有文档。
如果直接让 AI “顺便整理一下代码”,一个状态筛选可能演变成 DTO 重命名、公共查询重写、依赖升级和几十个文件变化。
修改老项目的关键不是让 AI 写得更多,而是控制它能改变什么,并用证据证明没有破坏既有行为。
1. 为什么 AI 容易在老项目里扩大修改
AI 往往倾向于生成结构清晰、风格统一的代码。面对历史代码时,它可能主动做这些事情:
- 把字段注入改成构造器注入;
- 把旧 DTO 替换成新的
record; - 重命名含义不够理想的方法;
- 合并重复查询;
- 升级依赖和语言版本;
- 统一异常类型;
- 给附近代码补格式化。
这些建议未必错误,但它们和当前需求不是同一个任务。
修改范围越大,Review 越困难。真正的业务变化会被淹没在重命名、格式化和重构中,回滚时也很难只撤掉状态筛选。
所以我会先给任务设置一个原则:
当前需求只购买了一个“状态筛选”,没有购买整个订单模块的现代化改造。
2. 修改前先建立基线
在让 AI 动手之前,先确认工作区状态:
git status --shortgit diffgit log -5 --oneline这里要区分两类修改:
- 当前任务开始前已经存在的用户修改;
- AI 为当前任务新增的修改。
如果工作区本来就不干净,不要让 AI 执行可能覆盖内容的重置命令。至少记录已有修改涉及哪些文件,并明确告诉 AI:
当前工作区已有未提交修改,这些修改不属于本任务。不要撤销、覆盖、格式化或移动它们。如果目标文件与已有修改重叠,先停止并说明冲突位置。接着运行当前项目的基础验证:
mvn testmvn package这一步建立“修改前基线”。如果测试在开始前已经失败,后面就不能把所有失败都归因于 AI,也不能声称本次改动让完整测试保持通过。
正确的记录方式是:
修改前:共 128 个测试,已有 2 个失败。失败用例:InventoryJobTest、LegacyPaymentTest。它们与订单查询无直接关系,本任务不得扩大失败数量。3. 让 AI 先画出影响面
老项目最危险的做法,是只打开一个 Controller 就开始修改。
先让 AI 阅读相关调用链:
需求:给订单列表增加可选的订单状态筛选。
请先只读分析,不要修改文件。
需要回答:1. 订单列表从 Controller 到数据库的完整调用链;2. 查询参数对象被哪些入口使用;3. Repository 查询中已有的租户、用户权限和软删除条件;4. 订单状态在接口、Java 枚举和数据库中的映射;5. 当前分页、排序和空参数行为;6. 相关测试及缺失的保护;7. 实现这个需求所需的最小文件集合。假设 AI 找到了下面的结构:
OrderController.list ↓OrderQueryService.search ↓OrderSpecificationFactory.build ↓OrderRepository.findAll(specification, pageable)同时发现:
OrderQuery还被管理员后台接口使用;OrderSpecificationFactory自动增加tenantId和deleted = false;- 接口状态
PAID对应数据库值PAY_SUCCESS; - 当前不传状态时返回所有有权限查看的订单。
这些信息直接决定实现方式。AI 如果绕过 OrderSpecificationFactory 新写 Repository 方法,很可能漏掉租户和软删除条件。
4. 把既有行为写成保护清单
新增状态筛选不应该改变这些已有行为:
## 必须保持
- 不传 status 时,查询结果与修改前一致。- 分页页码、每页数量和默认排序不变。- tenantId、用户权限和 deleted = false 条件仍然生效。- 管理员接口继续支持原有 OrderQuery。- 非法状态仍按项目现有方式返回 400。- 不修改数据库字段和状态枚举。这类约束叫作“非回归要求”。Spec 不仅要描述新功能,还要写清楚哪些旧行为不能变化。
对于缺少测试的老项目,可以先补特征测试,也就是把当前行为固定下来:
@Testvoid shouldReturnSameOrdersWhenStatusIsAbsent() { OrderQuery query = new OrderQuery(null, null, null);
Page<OrderSummary> result = service.search( currentUser, query, PageRequest.of(0, 20) );
assertThat(result.getContent()) .extracting(OrderSummary::orderNo) .containsExactly("ORD-003", "ORD-002", "ORD-001");}特征测试不代表历史行为一定完美,但它能提醒我们:当前任务正在改变一个原本没有要求改变的行为。
5. 明确允许修改的文件
根据调用链,最小修改范围可能是:
允许修改:- OrderQuery.java- OrderSpecificationFactory.java- OrderQueryServiceTest.java- OrderControllerTest.java
明确不修改:- OrderEntity.java- OrderStatus.java- OrderRepository.java- 数据库迁移文件- Maven 依赖和全局配置- 管理员订单接口然后再给 AI 实现任务:
请只实现可选的订单状态筛选。
要求:- 只修改允许列表中的文件;- status 为空时完全保持原查询行为;- 复用现有状态映射,不新增枚举值;- 在已有 Specification 上追加条件,不重写查询链;- 不进行重命名、格式化或无关重构;- 先补充失败测试,再做最小实现;- 如果发现必须修改范围外文件,先说明原因并等待确认。文件白名单不是绝对安全措施,但它能显著减少 AI 自行扩展任务。
6. 最小实现是什么样
查询对象只增加一个可选字段:
public record OrderQuery( LocalDate startDate, LocalDate endDate, ApiOrderStatus status) {}如果老项目使用普通 Java Bean,就继续遵循现有风格,不要为了这一处改成 record。
在现有 Specification 中追加条件:
public Specification<OrderEntity> build( long tenantId, long currentUserId, OrderQuery query) { Specification<OrderEntity> specification = Specification .where(belongsToTenant(tenantId)) .and(visibleTo(currentUserId)) .and(notDeleted()) .and(createdBetween(query.startDate(), query.endDate()));
if (query.status() != null) { specification = specification.and( hasInternalStatus(statusMapper.toInternal(query.status())) ); }
return specification;}核心变化只有这几行:
if (query.status() != null) { specification = specification.and(...);}租户、权限、软删除、日期和排序逻辑全部沿用原实现。
这比新增一个看起来更简洁的 Repository 查询更安全,因为原来的安全条件不会被重新复制或遗漏。
7. 测试新增行为和不变行为
至少覆盖三组测试。
新行为:按照状态筛选
@Testvoid shouldFilterOrdersByStatus() { OrderQuery query = new OrderQuery(null, null, ApiOrderStatus.PAID);
service.search(currentUser, query, PageRequest.of(0, 20));
verify(specificationFactory).build( currentUser.tenantId(), currentUser.id(), query );}更理想的 Repository 集成测试还应插入多个状态的订单,确认结果只包含目标状态。
不变行为:状态为空
@Testvoid shouldNotAddStatusConditionWhenStatusIsAbsent() { OrderQuery query = new OrderQuery(null, null, null);
Specification<OrderEntity> specification = factory.build( 10L, 42L, query );
List<OrderEntity> result = repository.findAll(specification);
assertThat(result).extracting(OrderEntity::getOrderNo) .containsExactlyInAnyOrder("ORD-001", "ORD-002", "ORD-003");}安全边界:租户和权限仍然有效
@Testvoid statusFilterMustNotBypassTenantBoundary() { OrderQuery query = new OrderQuery(null, null, ApiOrderStatus.PAID);
Specification<OrderEntity> specification = factory.build(10L, 42L, query); List<OrderEntity> result = repository.findAll(specification);
assertThat(result) .allMatch(order -> order.getTenantId() == 10L) .allMatch(order -> order.isVisibleTo(42L));}新增筛选测试通过,只能证明筛选有效。安全边界测试用于证明 AI 没有为了实现筛选而绕过原来的权限链。
8. 用影响矩阵做 Review
修改完成后,可以快速整理一张影响矩阵:
| 入口 | 是否使用 OrderQuery | 预期影响 | 验证方式 |
|---|---|---|---|
| 用户订单列表 | 是 | 新增可选状态筛选 | Controller + 集成测试 |
| 管理员订单列表 | 是 | 无状态时行为不变 | 原有回归测试 |
| 定时对账任务 | 否 | 不应受影响 | 确认调用链 |
| 订单导出 | 使用独立参数 | 不应受影响 | 导出模块测试 |
AI 可以帮助生成矩阵,但开发者要确认它有没有漏掉动态调用、反射、事件监听或外部接口。
接着查看实际修改:
git status --shortgit diff --statgit diff --checkgit diff如果计划修改 4 个文件,实际却出现 17 个文件,先停止继续生成代码,找出扩大的原因。
9. 常见的越界修改
顺手升级依赖
为了使用新的 API,我把 Spring Boot 升级到了最新版本。依赖升级应该单独立项、单独测试、单独回滚,不应该藏在筛选需求里。
统一格式化整个模块
格式化会产生大量噪声,让真正的业务 Diff 难以审查。格式治理应该使用独立提交。
替换公共 DTO
一个查询对象可能被多个入口复用。随意重命名字段或构造器会影响编译之外的 JSON 兼容性。
重写已有查询
AI 可能觉得 Specification 太复杂,改成一条新 JPQL。新查询如果漏掉租户、权限或软删除,就是严重回归。
修改测试来适配新实现
如果原测试保护的行为没有被需求明确改变,就应该修改实现,而不是让测试接受新行为。
10. 让每次修改都容易回滚
控制影响范围也包括回滚设计。
推荐把任务拆成小提交:
test: cover existing order query behaviorfeat: add optional order status filtertest: cover status filter and tenant boundary如果项目约定一个功能只使用一个提交,也至少要保证提交中没有无关格式化和依赖升级。
上线前确认:
- 数据库没有不可逆变化;
- 新参数是可选的,旧客户端不传时仍能工作;
- 回滚应用版本不会遇到新数据格式;
- 前端可以隐藏筛选入口作为临时降级;
- 日志和监控能区分非法状态与系统错误。
本例没有数据库迁移,接口新增可选参数,因此回滚相对简单。这也是最小方案的额外价值。
11. 一套适合老项目的 Prompt
你正在修改一个已有生产系统。
开始前:1. 检查工作区状态,不要覆盖已有修改;2. 阅读需求相关的完整调用链;3. 找出公共类型的所有调用方;4. 列出当前行为、权限条件和测试基线;5. 只输出最小实现方案,不修改文件。
方案确认后:- 只修改明确允许的文件;- 不升级依赖;- 不重命名公共接口;- 不格式化无关代码;- 不修改数据库结构;- 先用测试固定旧行为,再增加新行为;- 每个阶段展示 Diff 和验证结果;- 如果必须扩大范围,先停止并说明原因。这段 Prompt 不能替代 Code Review,但可以减少很多不必要的变化。
12. 我的老项目修改清单
- 修改前工作区状态已经记录;
- 基础测试和构建结果已经记录;
- 完整调用链和公共调用方已经阅读;
- 既有行为与非回归要求已经写清楚;
- 允许和禁止修改的文件已经明确;
- 缺少测试的历史行为已用特征测试保护;
- 实现复用原有权限和查询链;
- 实际 Diff 与计划文件基本一致;
- 没有依赖升级、全局格式化和无关重构;
- 目标测试、完整测试和构建已经运行;
- 回滚方法已经确认。
总结
AI 修改老项目时,真正的风险不是它不会写状态筛选,而是它不知道哪些历史行为不能碰。
控制影响范围需要四层保护:
上下文:先理解完整调用链边界:明确允许和禁止修改什么证据:用测试和 Diff 证明行为恢复:让修改能够独立回滚最好的改动不一定最漂亮,而是能以最小风险满足当前需求,并让下一位开发者容易理解、验证和撤销。
If this article helped you, please share it with others!
Some information may be outdated






