先说结论: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 行的设计问题。回滚重来,成本更低。
特征测试有个额外好处:它把"当前行为"固化成了代码。后面再有人问"这块为什么这么写",测试就是答案。
五、什么情况下这套方法会失效
不写清楚这节,这篇文章就是在骗人。
- 没有版本控制,或者提交历史是垃圾。你没法判断哪块代码是稳定的核心、哪块是临时糊上去的。第一步就做不下去。
- 系统已经在崩,业务方天天催。这时候没有时间补测试,AI 加速的只是"制造更多故障的速度"。
- 业务规则本身在变。老系统的价值是"沉淀了 7 年的规则",如果规则明年就要推翻,重写的收益远低于重搭。
- 团队没人能判断 AI 的输出对不对。这是最危险的:你以为在重构,其实在把 4 万行可控的烂代码,换成 4 万行不可控的烂代码。
第 4 条是硬门槛。团队里得有人能判断它对不对,否则就是在加速返工。
六、结果
现在是第 4 个月。核心的两个模块已经切到新实现,跑了一个多月,出过两次小问题,都是数据口径,不是代码错。
剩下的模块还在老代码上。我没有一次全换------每次只换一块、能回滚,是我给自己定的死规矩。
时间构成的变化比总时长更值得看:
| 环节 | 用 AI 之前 | 用 AI 之后 |
|---|---|---|
| 写代码 | 约 60% | 约 25% |
| 审查与验证 | 约 15% | 约 50% |
| 读代码 / 理解 | 约 25% | 约 25% |
总工时省了大约 40%,但省下来的不是"不用干活",是"不用干重复的活"。 写的时间少了,看得多了。
有一类时间我要单独说,因为它不能这么比:
"改完没报错、但数字不对"这类排查。
在不用 AI 的情况下,这类问题基本不存在------不是老系统没 bug,是没人去改它,所以不会踩出来。用了 AI 之后,改动量大了一个数量级,这些坑被踩出来的概率跟着上去了。
所以正确的比法不是"半天 vs 两天",而是"以前 0 次,现在若干次"。我这一轮遇到 7 次,其中 3 次花了一天以上。
这就是 AI 的真实代价:显性 bug 少了,数不对的多了。 因为它不会报错,它只会安静地做一件你以为它没做的事。
七、如果重来一次
- 第一周不写一行代码,只做三张表:表的状态分布、模块的调用关系、口径定义
- 先给最不敢动的那三块补特征测试,而不是先重构最容易的
- 把 4 类隐含规则里的前两类写成集成测试 + 运行时兜底,让错误尽早暴露
- 给 AI 一份"禁止清单",明确哪些目录它不许碰
- 每天结束时让它自己写一份"今天改了什么、我没把握的是什么"
第 5 条是意外收获。我本来只是想要个日志,结果它自评没把握的地方,十次里能命中六七次。这个命中率不算高,但足以让我把那六七处单独再看一遍。
留个问题:你们在把老系统交给 AI 之前,有没有先把隐含规则显式写出来?还是直接开始让它改?
我猜大部分是直接开始。我一开始也是。
案例已脱敏,不含公司、客户与项目标识。 本文的素材与判断由我本人提供,AI 辅助整理成文。