mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2290 words
6 minutes
AI 修改老项目时,如何控制影响范围
2026-06-02

AI 修改老项目时,如何控制影响范围#

在新项目里,AI 写错一段代码通常比较容易发现。老项目则不一样。

一个看起来简单的需求:

给订单列表增加一个订单状态筛选条件。

可能会碰到很多历史约束:

  • Controller 使用旧版参数对象;
  • Service 同时被后台任务调用;
  • Repository 查询包含租户和软删除条件;
  • 状态值不是数据库中的真实枚举名称;
  • 前端依赖当前的分页和排序格式;
  • 某些历史行为没有测试,也没有文档。

如果直接让 AI “顺便整理一下代码”,一个状态筛选可能演变成 DTO 重命名、公共查询重写、依赖升级和几十个文件变化。

修改老项目的关键不是让 AI 写得更多,而是控制它能改变什么,并用证据证明没有破坏既有行为。

1. 为什么 AI 容易在老项目里扩大修改#

AI 往往倾向于生成结构清晰、风格统一的代码。面对历史代码时,它可能主动做这些事情:

  • 把字段注入改成构造器注入;
  • 把旧 DTO 替换成新的 record
  • 重命名含义不够理想的方法;
  • 合并重复查询;
  • 升级依赖和语言版本;
  • 统一异常类型;
  • 给附近代码补格式化。

这些建议未必错误,但它们和当前需求不是同一个任务。

修改范围越大,Review 越困难。真正的业务变化会被淹没在重命名、格式化和重构中,回滚时也很难只撤掉状态筛选。

所以我会先给任务设置一个原则:

当前需求只购买了一个“状态筛选”,没有购买整个订单模块的现代化改造。

2. 修改前先建立基线#

在让 AI 动手之前,先确认工作区状态:

git status --short
git diff
git log -5 --oneline

这里要区分两类修改:

  • 当前任务开始前已经存在的用户修改;
  • AI 为当前任务新增的修改。

如果工作区本来就不干净,不要让 AI 执行可能覆盖内容的重置命令。至少记录已有修改涉及哪些文件,并明确告诉 AI:

当前工作区已有未提交修改,这些修改不属于本任务。
不要撤销、覆盖、格式化或移动它们。
如果目标文件与已有修改重叠,先停止并说明冲突位置。

接着运行当前项目的基础验证:

mvn test
mvn 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 自动增加 tenantIddeleted = false
  • 接口状态 PAID 对应数据库值 PAY_SUCCESS
  • 当前不传状态时返回所有有权限查看的订单。

这些信息直接决定实现方式。AI 如果绕过 OrderSpecificationFactory 新写 Repository 方法,很可能漏掉租户和软删除条件。

4. 把既有行为写成保护清单#

新增状态筛选不应该改变这些已有行为:

## 必须保持
- 不传 status 时,查询结果与修改前一致。
- 分页页码、每页数量和默认排序不变。
- tenantId、用户权限和 deleted = false 条件仍然生效。
- 管理员接口继续支持原有 OrderQuery。
- 非法状态仍按项目现有方式返回 400。
- 不修改数据库字段和状态枚举。

这类约束叫作“非回归要求”。Spec 不仅要描述新功能,还要写清楚哪些旧行为不能变化。

对于缺少测试的老项目,可以先补特征测试,也就是把当前行为固定下来:

@Test
void 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. 测试新增行为和不变行为#

至少覆盖三组测试。

新行为:按照状态筛选#

@Test
void 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 集成测试还应插入多个状态的订单,确认结果只包含目标状态。

不变行为:状态为空#

@Test
void 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");
}

安全边界:租户和权限仍然有效#

@Test
void 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 --short
git diff --stat
git diff --check
git diff

如果计划修改 4 个文件,实际却出现 17 个文件,先停止继续生成代码,找出扩大的原因。

9. 常见的越界修改#

顺手升级依赖#

为了使用新的 API,我把 Spring Boot 升级到了最新版本。

依赖升级应该单独立项、单独测试、单独回滚,不应该藏在筛选需求里。

统一格式化整个模块#

格式化会产生大量噪声,让真正的业务 Diff 难以审查。格式治理应该使用独立提交。

替换公共 DTO#

一个查询对象可能被多个入口复用。随意重命名字段或构造器会影响编译之外的 JSON 兼容性。

重写已有查询#

AI 可能觉得 Specification 太复杂,改成一条新 JPQL。新查询如果漏掉租户、权限或软删除,就是严重回归。

修改测试来适配新实现#

如果原测试保护的行为没有被需求明确改变,就应该修改实现,而不是让测试接受新行为。

10. 让每次修改都容易回滚#

控制影响范围也包括回滚设计。

推荐把任务拆成小提交:

test: cover existing order query behavior
feat: add optional order status filter
test: cover status filter and tenant boundary

如果项目约定一个功能只使用一个提交,也至少要保证提交中没有无关格式化和依赖升级。

上线前确认:

  • 数据库没有不可逆变化;
  • 新参数是可选的,旧客户端不传时仍能工作;
  • 回滚应用版本不会遇到新数据格式;
  • 前端可以隐藏筛选入口作为临时降级;
  • 日志和监控能区分非法状态与系统错误。

本例没有数据库迁移,接口新增可选参数,因此回滚相对简单。这也是最小方案的额外价值。

11. 一套适合老项目的 Prompt#

你正在修改一个已有生产系统。
开始前:
1. 检查工作区状态,不要覆盖已有修改;
2. 阅读需求相关的完整调用链;
3. 找出公共类型的所有调用方;
4. 列出当前行为、权限条件和测试基线;
5. 只输出最小实现方案,不修改文件。
方案确认后:
- 只修改明确允许的文件;
- 不升级依赖;
- 不重命名公共接口;
- 不格式化无关代码;
- 不修改数据库结构;
- 先用测试固定旧行为,再增加新行为;
- 每个阶段展示 Diff 和验证结果;
- 如果必须扩大范围,先停止并说明原因。

这段 Prompt 不能替代 Code Review,但可以减少很多不必要的变化。

12. 我的老项目修改清单#

  • 修改前工作区状态已经记录;
  • 基础测试和构建结果已经记录;
  • 完整调用链和公共调用方已经阅读;
  • 既有行为与非回归要求已经写清楚;
  • 允许和禁止修改的文件已经明确;
  • 缺少测试的历史行为已用特征测试保护;
  • 实现复用原有权限和查询链;
  • 实际 Diff 与计划文件基本一致;
  • 没有依赖升级、全局格式化和无关重构;
  • 目标测试、完整测试和构建已经运行;
  • 回滚方法已经确认。

总结#

AI 修改老项目时,真正的风险不是它不会写状态筛选,而是它不知道哪些历史行为不能碰。

控制影响范围需要四层保护:

上下文:先理解完整调用链
边界:明确允许和禁止修改什么
证据:用测试和 Diff 证明行为
恢复:让修改能够独立回滚

最好的改动不一定最漂亮,而是能以最小风险满足当前需求,并让下一位开发者容易理解、验证和撤销。

Share

If this article helped you, please share it with others!

AI 修改老项目时,如何控制影响范围
https://mizuki.mysqil.com/posts/ai-legacy-project-change-control/
Author
梦幻晨风
Published at
2026-06-02
License
CC BY-NC-SA 4.0

Some information may be outdated

Table of Contents