我让AI重写了这个老系统

先说结论:AI 写代码比我快 3 倍左右,读懂陌生老代码快 4 到 8 倍。但它只覆盖了 70% 的工作量,剩下 30% 如果也交给它,项目会在第三个月崩掉。

那 30% 不是难代码,是系统里那些谁都没写下来、但所有人都在遵守的隐含规则。


一、这套系统的情况

不是 demo,是一个在跑的业务系统。脱敏后的情况:

项 值
代码量 约 4 万行 Java
历史 7 年,换过 3 拨人
数据库 几十张表,没有外键约束
定时任务 二十几个,其中一半没有任何注释
文档 一份几年前的 Word,十来页,已经和代码对不上
约束 白天不能停机
人手 两个人

我接手的第一个星期什么都没写,只是把 git log 按人排了一遍,发现有三块代码是"幽灵区":近 3 年没人敢动,一改就出线上问题。

这三块就是后面所有麻烦的来源。


二、AI 最快的地方:把老代码变成看得懂的东西

一个 600 行的 Service,二十分钟就能出一张调用地图。

你给它代码,让它回答三个问题:

复制代码
这个类对外暴露了哪些副作用(写库、发消息、调外部接口)?
哪些方法的执行依赖调用顺序?
如果我删掉这个方法,哪些地方会受影响?

它的回答准确率大概八成。这八成不是省时间,是省"我不敢动"的心理成本------以前读这种代码要半天,现在二十分钟有个地图,剩下两成你自己校准。

我给自己定了个规则,全程遵守:

AI 的分析只用来决定"我该去看哪里",不用来决定"改不改"。

这条规则救了我至少两次。有一次它很确定地说某个 @Transactional 方法是独立的,实际上它被一个定时任务用反射调用了,绕过了 Spring 代理------事务根本不生效。这是老系统里最常见的那种"没人说得清为什么这么写"。

所以从第三节开始,做法变了。


三、真正的坑:4 类隐含规则

我最后整理出 4 类。它们的共同点是:没写在任何地方,但破了就出事。

第 1 类:顺序依赖

代码长这样:

java 复制代码
// 看起来完全可以调换顺序
orderService.updateStatus(order);
logService.write(order);
notifyService.push(order);

实际上 notifyService.push 内部读的是 logService 刚写入的那条记录去拼消息体,而且只读最近 5 秒的。调换顺序,消息就是空的,不报错。

AI 看不出这个,因为跨了三个类,而且 notifyService 里用的是 create_time > now() - 5s 这种条件------它会当成查询条件,不会当成依赖。

怎么处理:先别指望自动化。这类规则 AI 发现不了,因为它不知道规则存在。

我做的是两件事:

java 复制代码
// 1. 运行时兜底:拼不出消息就抛,别发空消息
if (logs == null || logs.isEmpty()) {
    throw new IllegalStateException(
        "notify 依赖 log 的 5 秒窗口:读不到记录,多半是调用顺序被改了");
}
java 复制代码
// 2. 集成测试:跑真实下单链路,断言消息体非空
@Test
void notifyMessageShouldNotBeEmpty() {
    Order o = createOrder();          // 走真实 Service 链路
    assertThat(o.getMessageBody()).isNotBlank();
}

测试的作用不是防住所有情况,是把这条口头规则变成一条会失败的用例。以后谁再改顺序,CI 会拦住。

我一开始想的是加一个启动期静态检查,后来放弃了------静态检查验不了运行时才能确定的事。想用启动断言解决运行时依赖,是我在这个项目里犯的第一个设计错误。

第 2 类:脏数据约定

几十张表没有外键,有一批记录是"逻辑删除但物理存在",用 status = 9 表示。还有一批 status = 0 的历史数据,含义和字段注释写的完全相反。

AI 重写查询的时候,会按注释来。结果新代码把 status = 0 当成"无效",实际上那是"最早期正常数据",删掉这批,报表直接对不上。

怎么处理:动手前,把每张表的状态分布跑一遍:

sql 复制代码
SELECT status, COUNT(*) cnt, MIN(create_time), MAX(create_time)
FROM orders GROUP BY status ORDER BY cnt DESC;

然后把结果贴给 AI,加一句:

status 的取值含义以下面的实际分布为准,不要相信字段注释。

这一句话让后面所有生成的 SQL 都对了。成本是两个晚上,收益是整个项目不返工。

第 3 类:时间与并发假设

有一个库存扣减,老代码用 SELECT ... FOR UPDATE,用得没错。但它只在一个入口里被调用,所以真正保护它的不是那把锁,是"只有一个入口"这件事。

后来加了一个 MQ 消费者进来,走的是另一条路径:直接 UPDATE stock SET num = num - n,FOR UPDATE 根本没覆盖到这条 SQL。库存开始出负数。

这类问题 AI 尤其难发现,因为它是"新增的东西"破坏了"旧的东西的隐含前提"。你在让它加功能的时候,它只关心新功能对不对,不会去检查新代码是否绕过了某个它不知道存在的约定。

怎么处理:每次让 AI 加一个会碰数据的入口,强制它回答:

sql 复制代码
这个改动会不会产生第二条修改同一张表的路径?
这条路径是否经过原有的加锁/校验逻辑?
如果没经过,需要补哪些约束(表级 CHECK、触发器、或在 SQL 里带上 where num >= n)?

它列得不全,但列出来的部分足够你去查。关键是把"必须经过同一条路径"这个隐含约定变成数据库层面的硬约束,而不是继续靠"只有一个入口"这种人来维持的默契。

第 4 类:业务口径

最麻烦的一类。同一个"有效订单",在 3 个模块里有 3 种算法,因为 3 拨人各自理解不同。谁也没错,但报表、结算、对账三个数永远对不上。

这类问题 AI 不但帮不上,还会放大它------你让它重写,它会自己发明第 4 种算法。

怎么处理:把口径统一这件事整个从 AI 任务里摘出来,单独人工做完,写成一份《口径定义表》,然后再动代码。这份表后来成了项目里唯一一份没被废弃的文档。


四、顺序反了,项目就废了

我一开始是"先重构,后补测试"------因为老系统没有测试,补测试听着像浪费时间。

第 9 天我就改了。原因是 AI 的改动大部分是对的,少数是错的,而你无法用肉眼分辨哪些是少数。

后来的顺序是这样的:

markdown 复制代码
1. 用真实数据把要动的模块跑一遍,把真实输出录下来当快照
2. 让 AI 按快照写特征测试(characterization test)
   → 测的不是"应该怎样",是"现在实际怎样"
3. 覆盖率到 40% 以上才允许 AI 改这块
4. 每次 AI 改动后立刻跑,红了就回滚,不尝试修
5. 一个模块稳定了,再动下一个

第 1 步和第 2 步的顺序不能反,这是我最想强调的一点。

如果你让 AI 读代码去推导"期望值",它推出来的是"代码想做什么",不是"代码实际做了什么"。 这两者在这个老系统里经常不一致------上面第 2 类和第 4 类的坑,就是这么来的。

正确的做法是:先跑,录真实输出,再让 AI 写测试去比对这个快照。顺序反了,测试就变成把 bug 固化成规范。

第 4 条也是关键。AI 改红了,不要让它自己修。 它会越修越远,最后把一个 3 行的 bug 修成 30 行的设计问题。回滚重来,成本更低。

特征测试有个额外好处:它把"当前行为"固化成了代码。后面再有人问"这块为什么这么写",测试就是答案。


五、什么情况下这套方法会失效

不写清楚这节,这篇文章就是在骗人。

  1. 没有版本控制,或者提交历史是垃圾。你没法判断哪块代码是稳定的核心、哪块是临时糊上去的。第一步就做不下去。
  2. 系统已经在崩,业务方天天催。这时候没有时间补测试,AI 加速的只是"制造更多故障的速度"。
  3. 业务规则本身在变。老系统的价值是"沉淀了 7 年的规则",如果规则明年就要推翻,重写的收益远低于重搭。
  4. 团队没人能判断 AI 的输出对不对。这是最危险的:你以为在重构,其实在把 4 万行可控的烂代码,换成 4 万行不可控的烂代码。

第 4 条是硬门槛。团队里得有人能判断它对不对,否则就是在加速返工。


六、结果

现在是第 4 个月。核心的两个模块已经切到新实现,跑了一个多月,出过两次小问题,都是数据口径,不是代码错。

剩下的模块还在老代码上。我没有一次全换------每次只换一块、能回滚,是我给自己定的死规矩。

时间构成的变化比总时长更值得看:

环节 用 AI 之前 用 AI 之后
写代码 约 60% 约 25%
审查与验证 约 15% 约 50%
读代码 / 理解 约 25% 约 25%

总工时省了大约 40%,但省下来的不是"不用干活",是"不用干重复的活"。 写的时间少了,看得多了。

有一类时间我要单独说,因为它不能这么比:

"改完没报错、但数字不对"这类排查。

在不用 AI 的情况下,这类问题基本不存在------不是老系统没 bug,是没人去改它,所以不会踩出来。用了 AI 之后,改动量大了一个数量级,这些坑被踩出来的概率跟着上去了。

所以正确的比法不是"半天 vs 两天",而是"以前 0 次,现在若干次"。我这一轮遇到 7 次,其中 3 次花了一天以上。

这就是 AI 的真实代价:显性 bug 少了,数不对的多了。 因为它不会报错,它只会安静地做一件你以为它没做的事。


七、如果重来一次

  1. 第一周不写一行代码,只做三张表:表的状态分布、模块的调用关系、口径定义
  2. 先给最不敢动的那三块补特征测试,而不是先重构最容易的
  3. 把 4 类隐含规则里的前两类写成集成测试 + 运行时兜底,让错误尽早暴露
  4. 给 AI 一份"禁止清单",明确哪些目录它不许碰
  5. 每天结束时让它自己写一份"今天改了什么、我没把握的是什么"

第 5 条是意外收获。我本来只是想要个日志,结果它自评没把握的地方,十次里能命中六七次。这个命中率不算高,但足以让我把那六七处单独再看一遍。


留个问题:你们在把老系统交给 AI 之前,有没有先把隐含规则显式写出来?还是直接开始让它改?

我猜大部分是直接开始。我一开始也是。


案例已脱敏,不含公司、客户与项目标识。 本文的素材与判断由我本人提供,AI 辅助整理成文。

相关推荐
吴汐10 分钟前
【CRMEB 社区版】二开第一问:这个需求该进 driver,还是进 modules?
后端
新*15 分钟前
Elasticsearch 7.17 的 SQL 到底能查到什么?一次踩坑记录
后端·sql·elasticsearch·查询
卷无止境39 分钟前
SALib敏感性分析的python库学习笔记
后端·python
ServBay41 分钟前
从 Val Town 停服看,代码能下载不代表项目能搬家
后端·sqlite
我不会起名字3222 小时前
seq 空洞排查:Mongo 的 51 只是预留上界,Redis CURR 才是已消耗边界
redis·后端·mongodb·go·即时通讯·im·故障排查
特立独行的猫A2 小时前
少 3 个环节、少 1 次 ABI、少 3500 行代码:cj-tauri 与 Ionic/Capacitor 的鸿蒙桥接机制对比
java·后端·python
学长毕设2 小时前
基于SpringBoot的高校后勤报修管理系统-附源码
java·spring boot·后端
特立独行的猫A2 小时前
cj-tauri vs Rust/Tauri 同款播放器实测:编译 21 秒 vs 11 分钟,磁盘 12 MB vs 1.2 GB
java·后端·python
乐橙开放平台3 小时前
海康和大华摄像头能统一管理吗?可以通过 GB28181 或视频融合平台接入
网络·后端·物联网·音视频