我拿一个真实 Java 项目测了下 AI 修 BUG,真正拉开差距的不是会不会写代码

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

不是刷 LeetCode,也不是让它写一个孤立函数,而是直接把一个实际工程扔进去,让它自己找问题、判断风险、修复,再做复核。

项目后端一共有 583 个 Java 文件。整个过程中我没有一步一步告诉它“这里怎么改”,只给了任务目标。

这次测完以后,我最大的感受反而不是“AI 现在代码写得真快”。

现在模型会写 CRUD、补接口、改 SQL、生成测试,其实已经没那么稀奇了。真正有区别的,是它发现一个问题以后,会不会继续往下追,以及它能不能判断一个“看起来正确”的修法,在当前工程里到底真的生不生效。

这次有两个地方,我觉得特别典型。

1. 报告只指出 3 个问题,它自己又往外扩了

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

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

如果只是按报告修 BUG,正常流程其实很简单:

  1. 找到这 3 个文件;
  2. 修改对应逻辑;
  3. 验证;
  4. 提交。

任务也算完成了。

但它没有停在这里,而是继续全局搜索 handleRejectTarget 的所有调用。

最后一共找到了 6 处,再逐个看调用上下文,结果发现另外两个原报告里没有点名的服务——IpChangesLineApply——其实也存在同类问题。

也就是说,最初报告只指出了 3 个风险点,最后实际覆盖到了 5 个服务。

这个地方我觉得挺有意思。

因为它做的不是“多改了两个文件”,而是做了一件真实开发里很常见的事:

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

比如线上报了一个空指针,我一般不会只在报错那一行加个判空。

第一反应通常是继续搜:

这个方法还有谁在调用?

这个写法是不是在其他 Service 里也复制过?

是不是同一个设计问题已经散落在多个模块?

真实项目里很多 BUG 都是这样。你今天修掉一个,只能说明这个点先爆了,不代表其他地方没有同样的问题。

所以我现在看 Coding Agent,越来越不太在意它最后弹出来一句“任务完成”。

我更关心的是:

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

这两个能力差别挺大。

2. @Transactional 看起来能解决,实际上可能根本不生效

第二个问题更典型。

有一个定时任务会重复生成续签记录。

问题链路大概是这样:

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

如果第一步成功,第二步失败,而两个操作又没有处在同一个事务里,那么数据库最终会留下一个很尴尬的状态:

续签记录已经插入了,但原记录还是“未复制”。

下一次定时任务再执行:

检测到未复制
    ↓
再次生成续签记录

于是重复数据就出来了。

看到这里,Java 开发第一反应基本都会想到:

@Transactional
protected void doExecute() {
    // insert
    // update
}

看起来没问题。

但这里刚好踩到了 Spring 事务里一个很经典的坑。

XunOPC 没有直接加 @Transactional,而是继续往上看了基类和调用关系,发现目标方法 doExecuteprotected,而且存在同类内部调用。

这时候问题就来了。

Spring 声明式事务本质上依赖代理。

正常情况下,调用链应该类似:

外部对象
   ↓
Spring Proxy
   ↓
目标 Bean
   ↓
@Transactional 方法

代理有机会在方法执行前开启事务,执行完成后提交或回滚。

但如果是类内部直接调用:

public void execute() {
    doExecute();
}

本质上是:

this.doExecute();

这个调用并没有重新经过 Spring Proxy。

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

注解写了,代码也能编译,IDE 也不报错,但事务压根没有按你想的方式工作。

这种 BUG 比直接报异常更麻烦。

因为它“看起来完全正确”。

最后它没有硬套 @Transactional,而是改成了 TransactionTemplate

逻辑大概类似:

transactionTemplate.execute(status -> {

    createRenewRecord();

    updateCopiedStatus();

    return null;
});

把两步操作显式放到同一个事务回调里。

结果就是:

插入成功 + 更新成功
=> 提交

插入成功 + 更新失败
=> 整体回滚

这才真正解决了重复生成的问题。

我觉得这个地方比“AI 会不会写 Spring”更有意思。

因为 @Transactional 怎么用,模型肯定见过无数遍。

真正难的不是:

这个注解怎么写?

而是:

当前这个调用链里,这个注解到底有没有用?

这已经不是代码补全问题了,而是工程判断。

3. Coding Agent 真正难的是读工程,不是生成代码

现在很多 AI 编程 Demo 看起来都很爽。

输入一句需求,几秒钟生成几百行代码。

但真实项目真正费时间的部分,经常根本不是“写”。

而是读。

读调用链。

读基类。

读已有实现。

读事务边界。

读数据库状态。

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

同样一个 BUG,如果只看当前文件,可能五分钟就能给出一个修法。

但把上下游都看完以后,可能会发现这个修法根本不能用。

所以我现在会比较关注 Coding Agent 有没有几个能力:

  • 会不会主动扩大搜索范围;
  • 会不会顺着调用关系继续往上看;
  • 会不会检查项目里已经存在的类似实现;
  • 会不会判断框架机制,而不是只匹配语法;
  • 会不会在发现“常规答案”后,再验证一下这个答案在当前工程里是否成立。

这几个能力,其实都不像传统意义上的“代码生成”。

更像一个程序员在理解系统。

4. 为什么我觉得“看起来正确”比直接写错更危险

AI 写错一个方法,其实没那么可怕。

编译失败了,一眼就能看到。

测试挂了,也很好定位。

真正危险的代码一般长这样:

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

看着没问题。

或者:

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

也很合理。

但是因为调用链、状态机、并发、事务传播或者框架机制的问题,它实际上没有解决根因。

这种代码最容易混进真实项目。

因为 Reviewer 看第一眼也会觉得:

“嗯,像是这么回事。”

然后上线以后继续炸。

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

能不能把代码写出来。

而应该是:

它能不能知道自己为什么这么改。

5. 这次测试也有明显不足

这次测试只是一次真实项目实测,样本量就是 1,不能用一次结果证明某个 Agent 永远更强。

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

这一点我觉得也应该明确写出来。

真实工程测试如果最后只剩“完美修复”“全面领先”,其实参考价值反而不高。

工具有做得好的地方,也一定有没覆盖到的地方。

我更关心的是它到底表现出了什么能力。

这次让我比较意外的,不是它修掉了几个 BUG。

而是它开始表现出一些我原本更习惯在人类工程师身上看到的动作:

发现一个问题以后继续扩大搜索范围;

看到一个标准解法以后,没有直接套模板;

因为 Spring 的实际调用机制,放弃了看起来最简单的 @Transactional

最后换成了更符合当前工程结构的事务方案。

这些东西,我觉得才是 Coding Agent 接下来真正值得看的方向。

代码生成本身可能会越来越便宜。

工程判断,才是下一阶段真正难复制的部分。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐