我拿真实 Java 项目测了下 AI 修 BUG:真正难的不是写代码,是做工程判断

9 阅读7分钟

我拿真实 Java 项目测了下 AI 修 BUG:真正难的不是写代码,是做工程判断

最近我拿一个真实前后端项目测了下 XunOPC。

不是刷题,也不是让它写一个独立函数,而是直接放进真实工程里,让它自己找问题、判断风险、修改,再复核。

后端一共有 583 个 Java 文件。

这次测完以后,我最大的感受不是“AI 现在写代码真快”,而是另一件事:

Coding Agent 真正难的,可能已经不是代码生成,而是工程判断。

现在让模型写 CRUD、补接口、改 SQL,甚至生成测试,都不算特别新鲜。

真正进到一个老项目以后,问题通常不是:

这一行代码怎么写?

而是:

为什么这里会出问题?

以及:

还有哪些地方可能有同样的问题?

这次有两个细节,我觉得特别典型。

一个 BUG,不能只修报告里点名的那几个文件

其中一个问题出在审批流程里。

最开始报告已经定位到 3 个服务存在同类风险,核心都和 handleRejectTarget 的调用有关。

如果只是按任务修 BUG,正常逻辑应该是:

找到 3 个文件
↓
修改
↓
验证
↓
结束

任务也算完成了。

但 XunOPC 没停。

它继续全局搜索 handleRejectTarget 的所有调用,一共找到了 6 处,然后逐个检查调用上下文。

最后又发现两个原报告没有点名的服务:

IpChanges
LineApply

实际上也存在同一种风险。

也就是说,报告最开始说 3 个,最后实际覆盖到了 5 个服务。

这个地方让我印象挺深。

因为它做的不是简单的“多改两个文件”。

而是一件我们平时修线上 BUG 很常见的事情:

从一个 BUG,反推一个 BUG 类型。

比如线上报一个 NPE。

最省事的修法当然是:

if (obj != null) {
    ...
}

然后提交。

但做过一段时间项目的人一般都会继续搜:

这个方法还有谁在调用?

这个错误写法是不是在别的 Service 里也复制过?

是不是同一个设计问题已经散在其他模块里,只是还没炸?

真实工程里,很多 BUG 都不是一个点。

只是某一个点先暴露出来而已。

所以我现在看 Coding Agent,已经不太在意最后有没有一句:

Task completed.

我更想看的是:

它是在修“这一条 BUG”,还是已经理解了“这一类 BUG”。

这两个能力差很多。

@Transactional 这个坑,更能看出有没有工程脑子

第二个问题是定时任务重复生成续签记录。

问题链路大概是:

插入续签记录
↓
更新“已复制”状态

如果第一步成功,第二步失败,而且两个动作不在同一个事务里,那么数据库就会留下这种状态:

续签记录:已经生成
原记录:仍然显示未复制

下一次定时任务再跑:

发现未复制
↓
再次生成续签记录

重复数据就出来了。

看到这里,Java 开发第一反应应该都差不多:

@Transactional
protected void doExecute() {
    ...
}

看起来一行就解决。

但这次偏偏不能这么干。

XunOPC 往上继续看了基类和调用关系,发现 doExecute 是 protected,而且存在同类内部调用。

这时候 Spring 事务那个经典坑就出来了。

声明式事务依赖代理。

正常情况是:

调用方
↓
Spring Proxy
↓
目标 Bean@Transactional 方法

代理在中间帮你开启、提交或者回滚事务。

但如果类内部这么调:

public void execute() {
    doExecute();
}

实际相当于:

this.doExecute();

这次调用并不会重新经过 Spring Proxy。

于是就可能出现一个很坑的情况:

注解写了,代码也完全正常,但事务压根没有按你以为的方式生效。

这种代码其实比直接报错更危险。

因为它“看起来对”。

最后 XunOPC 没有硬套 @Transactional,而是换成了 TransactionTemplate

大概就是把两步操作显式包进同一个事务:

transactionTemplate.execute(status -> {

    createRenewRecord();

    updateCopiedStatus();

    return null;
});

这样结果就清楚了:

两步都成功
=> commit

中间任何一步失败
=> rollback

我觉得这里真正有意思的,不是 AI 会不会用 TransactionTemplate

这个 API 模型当然见过。

真正难的是:

它知道什么时候“最标准的答案”不能用。

这就是工程判断。

真实项目里,最花时间的往往不是写,而是读

现在很多 AI 编程 Demo 都很爽。

一句话输入进去,几百行代码直接出来。

但真实项目里,最浪费时间的部分经常不是写代码。

是读代码。

读调用链。

读基类。

读历史实现。

读事务边界。

读状态变化。

读为什么五年前有人要这么设计。

有时候一个 BUG 只看当前文件,五分钟就能给出修法。

但顺着调用关系再看两层,会发现刚才那个修法根本不成立。

所以我现在反而比较关注 Coding Agent 有没有这些动作:

  • 会不会主动扩大搜索范围;
  • 会不会顺着调用关系继续看;
  • 会不会翻基类和已有实现;
  • 会不会理解框架机制,而不是只匹配代码模式;
  • 找到一个明显答案以后,会不会再确认这个答案在当前工程里真的生效。

这些能力其实已经不太像“代码生成”。

更像在理解一个系统。

我现在最怕 AI 给我“看起来完全正确”的代码

AI 如果写了一段根本编译不过的代码,其实不可怕。

IDE 红了。

CI 挂了。

测试失败了。

马上就能发现。

真正危险的是这种:

@Transactional
public void process() {
    ...
}

看起来很合理。

或者:

if (result != null) {
    ...
}

也很正常。

但它可能完全没有解决根因。

因为真正的问题藏在代理机制、调用链、并发、数据库状态或者业务流程里。

这种 Patch 最容易混过去。

Reviewer 第一眼甚至会觉得:

“嗯,应该就是这么改。”

上线以后继续炸。

所以我现在越来越觉得,AI Coding 真正进入生产环境以后,评价标准不能只是:

能不能写出代码?

而应该变成:

它知不知道自己为什么这么改?

这次测试也不是“AI 已经能替程序员”

这次就是一次真实项目测试,样本量也只有 1。

不能因为一次表现就直接得出“某个 Agent 一定比另一个强”。

而且这次也有明显不足。

一共修改了 7 个文件,最后做了静态复查,但当时测试环境没有 Maven,所以没有完成完整编译验证。

这个问题我觉得该写就得写。

真实工程测试如果最后只剩:

“全自动。”

“完美修复。”

“全面领先。”

基本就没什么参考价值了。

我真正觉得有意思的是,这次开始看到一些过去更像人类工程师才会做的动作:

发现一个问题以后,继续往外扩。

看到标准修法以后,没有立刻套模板。

因为 Spring 的真实调用机制,放弃了最直觉的 @Transactional

最后换成更符合当前工程结构的实现。

这些东西,其实已经不是“AI 会不会写代码”的问题了。

更像是:

AI 有没有开始理解工程。

代码生成会越来越便宜。

真正难的,可能一直都是判断。

尤其是那种:

知道一段代码看起来没问题,但其实不能这么写。