撕开AI编程的遮羞布:我在CRM项目中踩过的那些坑

摘要: 本文基于真实的 CRM 实战 项目,深度剖析 AI 编程陷阱企业级开发 中的系统性风险。从“幽灵 Bug”、数据权限缺失到事务遗漏,我们揭示了 AI 生成代码在复杂业务场景下的典型缺陷。文章不仅分享踩坑实录,更探讨了为何 AI 代码“好难调试”,并为开发者提供了关键的 代码审查 清单与使用建议。旨在帮助你在享受 AI 编程效率红利的同时,有效规避潜在的生产事故。


在这里插入图片描述

前言:从“真香”到“真坑”

三个月前,我决定在一个中型 CRM 系统开发中全面引入AI 编程助手(Cursor + Claude/ChatGPT)。初衷很朴素:CRUD 代码占开发工作量的 60% 以上,让AI 来写,我专注于业务逻辑和设计,岂不美哉?

前两周确实爽——实体类、DTO、简单查询接口,唰唰唰就出来了。但进入核心业务模块后,画风突变。

今天这篇文章,我想把这段经历中最真实的糟心事摊开来聊。不是评测,不是广告,就是一个踩了一百多个坑的开发者,想告诉后来者:AI 写代码这件事,远没有宣传片里那么美好。


一、CRM 开发中的典型翻车现场

场景 1:客户分配逻辑的“幽灵 Bug”

需求:销售主管将客户池中的客户分配给销售人员,需满足:同一客户不能同时属于多人、分配记录需留痕、超额需审批。

我给 AI 的 Prompt 很详细,包含了上述所有规则。它生成的代码看起来也很完整——Controller、Service、Mapper、Entity 一应俱全。

问题出在哪?

// AI生成的分配逻辑(简化版)
public Result assignCustomer(Long customerId, Long salesId) {
    Customer customer = customerMapper.selectById(customerId);
    if (customer == null) {
        return Result.fail("客户不存在");
    }
    // 检查是否已分配
    if (customer.getSalesId() != null) {
        return Result.fail("客户已分配");
    }
    customer.setSalesId(salesId);
    customerMapper.updateById(customer);
    // 记录分配日志
    assignLogMapper.insert(new AssignLog(customerId, salesId));
    return Result.success();
}

生产环境跑了三天后,我们发现:同一个客户被分配给了两个不同的销售。

原因很简单——没有加分布式锁,也没有在数据库层面做唯一性约束。在并发场景下,两个请求同时通过了 if (customer.getSalesId() != null) 的检查,然后各自写入。

AI 当然不会主动告诉你“这里需要加锁”,因为它不知道你的系统是单实例还是集群部署,不知道你的 QPS 大概是多少,不知道你用的 MySQL 是什么隔离级别。

而我作为开发者,因为“代码看着没问题”,Review 时也大意了。

调试成本:花了整整一天才复现并定位到这个问题。因为本地单机测试几乎不可能触发,只有在上线后多人同时操作时才出现。


场景 2:数据权限的“形同虚设”

CRM 系统的数据权限是个经典难题:普通销售只能查看自己的客户,大区经理能查看本区的,管理员能查看全部。

AI生成的Service层方法:

public Page<Customer> getCustomerPage(CustomerQuery query, Long currentUserId) {
    QueryWrapper<Customer> wrapper = new QueryWrapper<>();
    wrapper.eq("status", query.getStatus());
    // ... 各种条件拼接
    return customerMapper.selectPage(page, wrapper);
}

它完全忘记了数据权限过滤。

我不得不在每个查询方法里手动加上:

DataScope scope = permissionService.getDataScope(currentUserId);
wrapper.in("sales_id", scope.getAllowedUserIds());

这类问题在AI生成的代码中极其普遍——它能写出正确的SQL语法,但写不出符合业务规则的SQL语义。

更可怕的是:这类问题在测试环境中不容易被发现。测试同学一般使用管理员账号操作,天然能看到所有数据,根本测不出权限漏洞。直到 UAT 阶段,业务方用一个普通销售账号登录,才发现“怎么看不到数据”或者“怎么看到了别人的数据”。


场景 3:事务处理的“薛定谔式提交”

客户跟进模块有一个操作:添加跟进记录的同时,更新客户的最新跟进时间和状态。

AI生成的代码:

public void addFollowUp(FollowUpDTO dto) {
    followUpMapper.insert(dto);
    Customer customer = customerMapper.selectById(dto.getCustomerId());
    customer.setLastFollowTime(LocalDateTime.now());
    customer.setStatus(dto.getNextStatus());
    customerMapper.updateById(customer);
}

没有 @Transactional 注解。

这意味着:如果第二步更新客户状态时抛出异常,第一步插入的跟进记录已经落库,数据就不一致了。

我知道你在想什么——“这么明显的错误,AI 怎么会犯?”

事实是:它真的会犯,而且频率比你想象的要高。 特别是在你让它“重构一下这段代码”或者“帮我拆分这个方法”的时候,原有的事务注解可能在重构过程中被“优化”掉了。


场景 4:枚举和状态机的“自由发挥”

CRM 中有大量的状态流转:线索 → 潜在客户 → 成交客户 → 流失客户。每个状态之间有严格的流转规则。

AI 特别喜欢做的一件事:自己发明状态值

我定义了 CustomerStatus 枚举:PROSPECT(0), INTENTION(1), DEAL(2), LOST(3)。

AI 在某个判断逻辑里写了:

if (customer.getStatus() == 1) { // 这里应该是INTENTION
    // 做一些事
}

硬编码“魔法数字”本身不算大问题。问题是它有时候会写成== 2,而2在我的枚举里是DEAL不是INTENTION。这种错误编译器不会报错,单元测试如果没覆盖到这个分支,就直接带到线上了。


二、为什么AI代码“好难调试”?

结合上面的案例,我总结了AI代码难以调试的四个根本原因:

1. 错误类型从“显性”变成了“隐性”

人工编码的典型错误 AI编码的典型错误
拼写错误、语法错误 → 编译失败,立即发现 逻辑遗漏、边界未处理 → 编译通过,运行时偶发
空指针 → 堆栈清晰,定位快速 并发竞态 → 只在特定条件下触发,极难复现
接口参数不匹配 → 启动时报错 业务规则违反 → 功能正常但数据错误

AI生成的代码语法正确率极高,但这恰恰是陷阱——它会让你放松警惕,跳过本该有的代码审查和测试环节。

2. 认知负担的反转

调试自己写的代码:你知道每一步的意图,顺着逻辑就能找到问题。

调试 AI 写的代码:你首先需要理解AI的思路。而AI没有“思路”——它是基于概率生成的token 序列。这意味着它的代码可能没有统一的风格、可能混用不同的设计模式、可能在同一个文件中出现三种不同的异常处理方式。

你需要花大量时间做“代码考古”,才能搞清楚这段逻辑到底想干什么。

3. 缺乏“防御性编程”的本能

一个有经验的开发者写代码时会本能地想:“如果这个接口超时了怎么办?”“如果这条数据被删除了怎么办?”“如果有人并发点击了这个按钮怎么办?”

AI没有这种本能。它写的是"理想世界"里的代码——所有依赖都可用、所有输入都合法、所有操作都串行执行。

而生产环境恰恰相反。

4. 测试代码的“自欺欺人”

AI 很擅长写测试——但你仔细看就会发现,它写的测试往往是“为了让代码通过而写的测试”,而不是“为了发现问题而写的测试”。

@Test
public void testAssignCustomer() {
    // 准备
    Customer customer = new Customer();
    customer.setId(1L);
    customer.setSalesId(null);
    // 执行
    Result result = service.assignCustomer(1L, 100L);
    // 断言
    assertEquals("success", result.getCode());
}

这个测试有什么用?它只验证了“正常流程能走通”。没有验证并发场景、没有验证参数非法的情况、没有验证事务回滚。

但它看起来很像那么回事,很容易骗过不做深入审查的眼睛。


三、媒体评测为什么总是“最佳战例”?

这个问题值得单独聊聊。

几乎所有AI编程工具的评测文章都有一个共同特征:展示最成功的案例,隐藏最失败的尝试。

原因不难理解:

  1. 幸存者偏差:作者尝试了20次,选出效果最好的那一次截图展示。你看到的不是AI的平均水平,是它的天花板。
  2. Demo效应:评测通常用"写一个贪吃蛇"、"实现一个TODO应用"来证明AI的能力。这些场景的共同特点是——无状态、无并发、无权限、无复杂业务逻辑。这正是AI最擅长的领域。
  3. 商业利益:评测作者往往有联盟营销链接,或者与工具有合作关系,没有动力展示负面体验。
  4. 认知偏差:人们更愿意分享"AI帮我节省了两小时"的故事,而不是"AI浪费了我三小时debug"的经历。

但企业级开发不是写贪吃蛇。 CRM、ERP、交易系统——这些系统的复杂度不在于代码量,而在于业务规则的交织、数据一致性的要求、权限体系的精细以及高可用性的约束。在这些维度上,AI目前的水平远不足以让人放心。


四、我的反思与建议

经历了这次CRM 项目的“洗礼”,我对AI 编程的定位有了更清醒的认识:

AI适合做什么?

场景 推荐度 理由
样板代码生成(Entity / DTO / VO 转换) ★★★★★ 模式固定,出错概率低
正则表达式、SQL 查询编写 ★★★★☆ 有明确输入输出,容易验证
代码注释和文档生成 ★★★★☆ 不涉及逻辑正确性
单元测试用例草稿 ★★★☆☆ 需要人工审查和补充边界 case
复杂业务逻辑实现 ★★☆☆☆ 容易遗漏非功能性需求
并发和分布式场景 ★☆☆☆☆ 几乎必出问题

AI 不适合做什么?

  • 核心业务链路:涉及资金、数据一致性、权限控制的代码
  • 系统架构决策:AI 不了解你的团队规模、技术栈约束、业务发展阶段
  • 性能敏感的代码:AI 不知道你的数据量级和访问模式
  • 需要长期维护的代码:AI 生成的代码往往缺乏可扩展性设计

给正在使用 AI 编程的朋友几点建议

  1. 把 AI 当高级搜索引擎,别当架构师。 让它帮你查API用法、写正则、生成模板代码,但核心设计和关键逻辑必须自己把控。
  2. AI 生成的代码,一律当作“不可信代码”来 Review。 不要因为它是 AI 写的就放松标准——恰恰相反,应该更加严格。
  3. 强制要求 AI 写测试,但永远不要相信它写的测试足够充分。 把它当起点,不是终点。
  4. 在Prompt中注入尽可能多的上下文。 你的并发模型、数据量级、权限体系、编码规范——说得越细,AI翻车的概率越低。但即便如此,也不能完全信任。
  5. 建立团队级的AI代码审查清单。 列出AI最容易犯的错误类型(缺事务、缺锁、硬编码、权限遗漏),Review时逐一核对。

结语

AI编程工具是强大的生产力加速器,这一点毫无疑问。但它不是银弹,更不是替代品。

“能跑”和“能用”之间的距离,就是专业开发者的价值所在。 那些边界处理、异常处理、并发控制、权限校验、数据一致性保障——这些枯燥的、不起眼的、不出彩的工程细节,才是区分“玩具代码”和“生产代码”的真正分水岭。

AI目前跨越不了这道分水岭。也许未来某天它可以,但至少在今天,我们需要诚实地面对它的局限性。


如果你也在用AI开发企业级项目,欢迎在评论区分享你的踩坑经历。让更多人看到真实的一面,比任何评测都更有价值。


Logo

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

更多推荐