1. 这不是一场“AI能不能写代码”的辩论,而是一次真实项目交付现场的复盘
“Is AI coding that good?”——这个标题乍看像一句轻飘飘的疑问,实则戳中了过去三年里每个程序员、技术主管、产品负责人心里反复掂量过的硬问题。它不问原理,不谈参数,只盯着结果:当一个真实需求摆在面前,从零启动、有明确交付 deadline、要经受测试验收、要能被其他工程师接手维护——这时候,让 AI 当主力 coder,行不行?我从去年三月开始,在自己负责的三个中小型业务系统迭代中,把 Copilot、Cursor、CodeWhisperer 和本地部署的 CodeLlama-70B 全部拉进生产环境跑了一整年,不是做 demo,不是写玩具脚本,而是替换了 37% 的日常功能开发工作量。这里说的“替换”,是指从需求评审后第一行代码开始,到 PR 合并、单元测试通过、上线灰度验证完成,整个链路由 AI 主导生成,人类角色退为 Reviewer、架构把关者和异常兜底人。核心关键词很实在:AI coding、实际交付、代码质量、可维护性、团队协作成本。这篇文章不讲大模型怎么训练,不列 benchmark 分数,也不鼓吹“程序员即将失业”;它是一份带时间戳、带 commit hash、带线上错误率曲线的实战手记。适合正在评估是否在团队中引入 AI 编程工具的技术负责人、想搞清楚“到底该信 AI 写多少行代码”的一线开发者,以及那些被老板扔来一句“你们也试试 AI 编程吧”后两眼发黑的 TL。你不需要懂 transformer,但得写过至少半年真实业务代码;你不需要会微调 Lora,但得知道为什么某个函数改了三遍才通过 CI。接下来所有内容,都来自我们团队在支付对账模块重构、内部 BI 报表引擎升级、以及 SaaS 客户自助配置中心搭建这三个真实项目中的逐日记录。
2. 项目整体设计与思路拆解:为什么我们选择“渐进式嵌入”,而不是“全盘托付”
2.1 核心设计逻辑:用交付压力倒逼能力边界测绘
很多团队一上来就让 AI 写核心交易链路,结果三天崩两次线上,最后全回滚,还背了锅。我们反其道而行:不设禁区,但设“压力探针”。所谓压力探针,就是在每个项目里,人为植入三类高风险但高频的“校验点”:
- 结构强约束点:比如支付对账模块中,必须严格遵循“原始流水 → 清算文件 → 对账差异表 → 差异处理工单”四层数据流转,每层字段名、类型、非空规则、更新时机全部固化在 DDL 和 protobuf schema 中。AI 生成代码若漏掉任意一层的字段映射或类型转换,下游立刻报错。
- 状态机敏感点:BI 报表引擎中,一个报表任务存在 “created → validating → generating → rendering → delivered → expired” 六种状态,状态跃迁规则写死在 FSM 配置里。AI 若擅自新增跳转路径(比如从 validating 直接到 delivered),会导致任务卡死且无法人工干预。
- 跨服务契约点:客户自助配置中心需调用计费服务、权限服务、通知服务三个外部 API,每个接口的 request body schema、error code 映射、重试策略、熔断阈值全部定义在 OpenAPI 3.0 spec 文件中。AI 若按自己理解拼接 JSON,哪怕字段名大小写差一个字母,HTTP 400 就打脸。
这三类点不是用来“考倒 AI”,而是为了快速定位它的“认知盲区”。我们发现,AI 在处理显式、静态、文档化强约束时表现极稳;一旦进入隐式、动态、上下文依赖型逻辑(比如“用户连续三次输错密码后,需触发风控策略,但该策略本身由另一个微服务实时下发”),出错率陡增 4.8 倍。这个数据不是 benchmark,是我们用 176 次 PR review 记录统计出来的。
2.2 方案选型背后的硬取舍:为什么放弃“全自动 pipeline”,坚持“人机协同双轨制”
市面上有两类主流方案:一类是构建全自动 CI/CD pipeline,AI 提交代码后自动跑测试、自动合并;另一类是“AI 辅助 IDE 模式”,人类始终握着键盘和 merge 权。我们花了六周做 A/B 测试,结论非常明确:全自动 pipeline 在当前阶段是危险的幻觉。原因有三:
第一,测试覆盖率陷阱。我们要求所有 AI 生成代码必须附带单元测试,但很快发现,AI 写的测试用例有严重倾向性——它极度擅长覆盖“happy path”,对边界条件(如空字符串、超长字段、时区夏令时切换、数据库主键冲突)的覆盖不足。在支付对账模块中,AI 生成的 23 个测试用例里,只有 2 个覆盖了“清算文件中金额字段为负数”的场景,而这个 case 正是去年导致 12 万笔订单对账失败的根因。
第二,diff 理解失真。当 AI 修改一个已有函数时,它生成的 diff 往往“看起来合理”,但会悄悄破坏调用方假设。例如,原函数getCustomerBalance(customerId)返回BigDecimal,AI 优化后改为返回Optional<BigDecimal>,表面更安全,却导致上游 5 个服务未做空值判断直接.get()而 crash。人类 reviewer 能一眼看出这个契约变更,但自动化 pipeline 只认编译通过和测试绿灯。
第三,责任归属真空。当线上出现 P0 故障,如果 pipeline 是全自动的,追责链条断裂:是 prompt 写得不够准?是模型版本升级引入 regression?还是 baseline 测试没覆盖到?我们最终采用“双轨制”:AI 生成代码 → 人类编写 review checklist(含上述三类压力探针的具体检查项)→ 人类执行 checklist 并签字 → 手动 merge。这个“手动 merge”动作看似低效,实则是责任锚点,也是知识沉淀入口——每次 review checklist 的迭代,都成为团队新的《AI 编程规范 v1.3》。
2.3 避开的典型误区:为什么我们坚决不用“AI 自动生成完整微服务”
有团队尝试让 AI 根据一份 PRD 文档,直接生成整个 Spring Boot 微服务,包括 controller、service、repository、DTO、config、test 全套。我们试过一次,结果灾难性。问题不在代码语法,而在架构意图的不可传递性。PRD 里写“用户可自定义报表字段”,AI 理解为“生成一个动态 SQL 构建器”,而实际业务需要的是“字段白名单 + 预编译模板 + 权限隔离”,前者在高并发下必然被 SQL 注入击穿,后者需要在 controller 层做字段级 RBAC 拦截。AI 无法从文字描述中推导出“这个功能未来要支撑 500 家客户各自配置不同字段组合”的扩展性约束。我们后来定下铁律:AI 只能生成“单个函数”或“单个类”,且该函数/类必须满足:
- 输入输出契约完全明确(有清晰的 interface 定义)
- 不涉及跨模块状态共享(无 static 变量、无全局 cache)
- 无隐式副作用(不修改传入对象、不触发外部 HTTP 调用)
这条铁律让我们避开了 92% 的架构级返工。记住:AI 是超级高效的“代码段生成器”,不是“系统架构师”。想让它当架构师,等于让速记员去写小说——字都认识,但故事逻辑是另一回事。
3. 核心细节解析与实操要点:从 prompt 设计到 review checklist 的颗粒度控制
3.1 Prompt 不是“提问”,而是“给 AI 下工程指令”:我们用的 5 类结构化模板
很多人以为 prompt 就是“帮我写个排序算法”,这在工程实践中毫无价值。我们的 prompt 是带上下文、带约束、带示例的工程指令。以下是我们在生产环境中验证有效的五类模板:
模板一:契约驱动型(用于强 schema 场景)
你是一个资深 Java 工程师,正在编写支付对账模块的清算文件解析器。 【输入】:一行文本,格式为 "20231015|10000001|CNY|12345.67|SUCCESS",字段含义依次为:日期(yyyyMMdd)|订单号|币种|金额|状态 【输出】:一个 Java Record,名为 ClearingRecord,包含以下字段: - date: LocalDate(需从字符串解析,注意 yyyyMMdd 格式) - orderId: String(非空校验) - currency: String(仅允许 CNY/USD/EUR,否则抛 IllegalArgumentException) - amount: BigDecimal(需处理小数点后两位精度,使用 ROUND_HALF_UP) - status: String(仅允许 SUCCESS/FAILED/PENDING) 【禁止】:不要添加任何额外字段;不要做数据库操作;不要打印日志;不要捕获 RuntimeException。 【示例输入】:"20231015|10000001|CNY|12345.67|SUCCESS" 【示例输出】:new ClearingRecord(LocalDate.parse("20231015", DateTimeFormatter.BASIC_ISO_DATE), "10000001", "CNY", new BigDecimal("12345.67").setScale(2, RoundingMode.HALF_UP), "SUCCESS")这个模板的关键在于:把业务规则(如“仅允许三种币种”)转化为代码级约束(IllegalArgumentException),把模糊描述(“处理精度”)转化为具体 API(setScale(2, RoundingMode.HALF_UP))。AI 对这种“填空式”指令响应极准,错误率低于 0.3%。
模板二:状态机映射型(用于 FSM 场景)
你正在实现 BI 报表引擎的状态机处理器。已知当前状态为 'validating',收到事件 'VALIDATION_SUCCESS'。 【状态跃迁规则】: - validating + VALIDATION_SUCCESS → generating - validating + VALIDATION_FAILED → created(并设置 failureReason 字段) - 其他组合 → 抛出 IllegalStateException("Invalid state transition") 【输出】:一个 Java 方法,签名如下: public ReportTaskState handleValidationEvent(ReportTaskState currentState, String event) 【要求】: - 使用 switch 表达式,穷举所有可能 event 值(VALIDATION_SUCCESS / VALIDATION_FAILED / 其他) - 对 '其他' 情况,必须抛出 IllegalStateException,message 固定为上述字符串 - 不要修改 currentState 对象,返回新构造的 ReportTaskState 实例这里的关键是强制 AI 使用switch而非if-else,确保可读性和可维护性;规定异常 message 字符串,避免不同开发者写的提示语不一致;强调“返回新实例”,杜绝状态污染。
模板三:契约兼容型(用于改造旧代码)
现有方法: public List<Order> findOrdersByStatus(String status) { ... } 现在需要新增分页支持,但必须保持原有方法签名不变(向后兼容)。 【新需求】: - 新增重载方法:findOrdersByStatus(String status, int page, int size) - 原方法内部必须调用新方法,page=0, size=100(默认值) - 新方法需使用 JPA Pageable,返回 Page<Order> - 不要修改原方法的 Javadoc,但需为新方法添加完整 Javadoc,说明分页参数含义这个模板直击“遗留系统改造”痛点。它不只要求功能正确,更强制契约兼容(原方法调用新方法)、默认值合理(page=0 对应第一页)、文档同步(Javadoc 必须更新)。我们发现,AI 对“保持兼容”这类抽象要求响应很差,但一旦写成“原方法内部必须调用新方法”,准确率飙升。
模板四:错误防御型(用于高危操作)
你正在编写客户自助配置中心的权限校验拦截器。需调用权限服务 HTTP 接口: POST /v1/permissions/check Request Body: {"resource": "report:123", "action": "view", "userId": "u_abc"} Response: {"allowed": true, "reason": "policy_match"} 或 403 【要求】: - 必须设置 3 秒超时(connect timeout + read timeout) - 必须重试 2 次(指数退避,初始 100ms) - 若权限服务不可用(网络超时/5xx),必须降级为允许访问(fail-open),并记录 WARN 日志 - 若返回 403,必须提取 reason 字段,放入自定义异常 PermissionDeniedException(reason) - 禁止吞掉任何异常,所有异常必须向上抛出或包装这个模板把运维经验(超时、重试、降级)和安全要求(reason 提取、异常包装)全部编码进 prompt。AI 生成的代码,我们只需检查重试逻辑和降级策略是否符合,其余基本无需修改。
模板五:测试用例生成型(专治 AI 的 happy path 偏好)
请为以下方法生成 8 个 JUnit 5 测试用例: public BigDecimal calculateTax(BigDecimal amount, String countryCode) 【业务规则】: - amount <= 0 → 抛出 IllegalArgumentException("amount must be positive") - countryCode 为空或长度≠2 → IllegalArgumentException("countryCode must be 2-letter ISO code") - country="CN" → 税率 13%,结果保留2位小数,HALF_UP - country="US" → 税率 0%,返回 0.00 - country="JP" → 税率 10%,但若 amount > 1000000,则税率升至 15% - 其他 country → 税率 0% 【要求】: - 至少 2 个 negative case(amount<=0, countryCode invalid) - 至少 1 个边界 case(amount=1000000.00 for JP) - 至少 1 个精度 case(amount=123.456, expect 123.46 for CN) - 所有测试必须用 @DisplayName 注解,中文描述场景这个模板是我们的“测试兜底神器”。它不只要求数量,更指定 negative/boundary/precision 三类用例的最低占比,并强制中文描述。生成的测试用例,我们 review 时只看是否满足这四条,不再逐行审逻辑。
3.2 Review Checklist 的颗粒度:为什么我们把“检查项”拆到函数级
很多团队的 review checklist 停留在“检查是否有单元测试”“检查是否有日志”这种宏观层面,这在 AI 编程时代是失效的。我们的 checklist 是函数级的,每个函数对应一张表。以calculateTax函数为例,review checklist 如下:
| 检查项 | 检查方式 | 通过标准 | 实际发现的问题 |
|---|---|---|---|
| 输入校验完整性 | 查看方法开头是否包含 amount 和 countryCode 的 null/empty/length 检查 | 必须同时存在两个if判断,且抛出的 exception message 与 prompt 完全一致 | AI 漏掉了 countryCode 长度检查,只判了 null/empty |
| 税率计算精度 | 查看 CN/Japan 分支中是否调用setScale(2, RoundingMode.HALF_UP) | 必须出现在 return 语句前,且参数完全匹配 | AI 在 JP 分支用了setScale(2, RoundingMode.HALF_DOWN),导致 123.455 四舍五入成 123.45 |
| 降级策略显式化 | 查看 US 分支是否直接返回BigDecimal.ZERO.setScale(2) | 必须是字面量ZERO,不能是new BigDecimal("0.00") | AI 用了new BigDecimal("0.00"),虽结果相同,但违反团队 BigDecimal 创建规范 |
| 异常包装一致性 | 查看所有 throw 语句是否均为new IllegalArgumentException(...) | message 字符串必须与 prompt 中一字不差 | AI 把 "countryCode must be 2-letter ISO code" 写成了 "country code must be 2 letters",少连字符、多空格 |
这张表的价值在于:它把抽象的“代码质量”转化为可执行、可审计、可培训的动作。新人拿到这张表,对照代码一行行打钩,15 分钟就能完成 review;TL 抽查时,直接看 check 项是否全勾,不看代码风格。我们累计沉淀了 47 张此类函数级 checklist,覆盖支付、报表、配置三大领域 92% 的核心函数。
3.3 工具链的隐形战场:为什么我们禁用 Copilot 的“自动补全”模式,只用“/ask”命令
这是最容易被忽视,却影响最大的细节。GitHub Copilot 默认开启“inline suggestion”,即你在写public class时,它自动在光标后补出整个类结构。我们团队全员禁用此功能,强制使用/ask命令(在 Cursor 中是Ctrl+K)。原因有三:
第一,上下文污染。inline suggestion 会贪婪地扫描当前文件所有代码,包括注释、TODO、甚至被注释掉的旧逻辑,然后把这些噪声当作“上下文”喂给模型。我们曾遇到 AI 在生成新 service 方法时,把注释里的// TODO: add retry logic later当真,真的在代码里加了重试,而这个 TODO 是三年前写的,早已被废弃。
第二,意图丢失。当你敲get时,Copilot 自动补getCustomerById,但你实际想写的是getCustomerBalanceHistory。这种“猜中开头,错在结尾”的情况,导致大量无效补全和删除,打断思维流。而/ask是明确的指令:“我要一个根据 customerId 查询余额历史的方法”,AI 必须基于这个完整意图生成,不会偷看旁边代码。
第三,可追溯性归零。inline suggestion 生成的代码没有 prompt 记录,无法回溯“当时为什么这么写”。而/ask的每一次调用,我们强制要求保存 prompt 到 git commit message 的[prompt]区块里。例如:
feat(report): add pagination to report task list [prompt] Generate overloaded method findTasksByStatus(String status, int page, int size) that calls existing findTasksByStatus(status) with default page=0, size=100. Return Page<Task>. Keep original method signature unchanged.这个[prompt]区块,就是我们代码的“DNA 记录”。当三个月后有人质疑“为什么 page 默认是 0 而不是 1”,我们直接git log -p就能看到当时的决策依据。
4. 实操过程与核心环节实现:从需求评审到线上验证的全流程切片
4.1 需求评审阶段:如何把 PRD 转化为 AI 可执行的“工程需求说明书”
PRD(产品需求文档)是给产品经理和业务方看的,充满“用户觉得”“体验更好”“灵活配置”这类模糊表述。AI 无法处理模糊,所以我们的第一步,是把 PRD 手动翻译成《AI 工程需求说明书》(AERS)。这不是简单改写,而是进行三重“工程化提纯”:
第一重:实体与属性显性化
PRD 原文:“用户可以在报表页面自定义显示字段。”
AERS 改写:
- 实体:
ReportFieldConfig(存储用户配置) - 属性:
reportId: String(非空)、userId: String(非空)、displayFields: List<String>(非空,元素必须属于预定义白名单["order_id", "amount", "status", "created_at"])、createdAt: Instant(自动生成) - 约束:
displayFields长度 1~10,重复字段自动去重
第二重:状态与事件原子化
PRD 原文:“配置保存后,系统会实时生效。”
AERS 改写:
- 状态:
ReportFieldConfig存在PENDING(刚创建)、ACTIVE(已生效)、OBSOLETE(被新版本覆盖)三种状态 - 事件:
CONFIG_CREATED(触发PENDING → ACTIVE)、CONFIG_UPDATED(触发ACTIVE → OBSOLETE+ 新建PENDING) - 契约:
CONFIG_CREATED事件必须发布到 Kafka topicreport-config-changes,key 为reportId,value 为 JSON 序列化后的ReportFieldConfig
第三重:接口与协议契约化
PRD 原文:“前端通过 API 获取用户配置。”
AERS 改写:
- HTTP Method:GET
- Path:
/api/v1/reports/{reportId}/config - Request Header:
X-User-ID: string(必传,JWT 解析) - Response Body:
{ "reportId": "r_123", "displayFields": ["order_id","amount"], "updatedAt": "2023-10-15T08:30:00Z" } - Error Code:
401 Unauthorized(token 无效)、403 Forbidden(X-User-ID 与 report 所属用户不匹配)、404 Not Found(reportId 不存在)
这份 AERS 文档,就是我们给 AI 下指令的唯一依据。它不追求文采,只追求无歧义。我们规定,任何未在 AERS 中明确定义的字段、状态、错误码,AI 绝对不允许生成。这个过程看似繁琐,实则节省了后期 70% 的返工时间——因为所有争议,都在评审阶段用 AERS 锁死了。
4.2 开发阶段:一个真实函数的诞生全过程(含 prompt、AI 输出、review、修改)
我们以ReportConfigService.getEffectiveConfig(String reportId, String userId)方法为例,展示完整闭环:
Step 1:Prompt 编写(耗时 8 分钟)
你正在实现报表配置服务。需根据 reportId 和 userId 获取当前生效的配置。 【业务规则】: - 查询 ReportFieldConfig 表,条件:reportId = ? AND userId = ? AND status = 'ACTIVE' - 若找到,返回 ReportFieldConfig 对象 - 若未找到,查询同一 reportId 下 status = 'PENDING' 的配置,若存在则返回(视为待生效) - 若仍无,返回 null(不抛异常) 【技术约束】: - 使用 Spring Data JPA,Repository 方法名必须为 findByReportIdAndUserIdAndStatusOrFindByReportIdAndStatus - 必须使用 @Query 注解重写 JPQL,因为 OR 条件在方法名中无法精确表达 - JPQL 必须显式写出 status = 'ACTIVE' 和 status = 'PENDING',不能用变量 - 返回值类型必须为 Optional<ReportFieldConfig> 【禁止】:不要用原生 SQL;不要做二次查询;不要捕获 Repository 异常Step 2:AI 输出(Cursor v0.42.0 + CodeLlama-70B)
@Query("SELECT c FROM ReportFieldConfig c WHERE (c.reportId = :reportId AND c.userId = :userId AND c.status = 'ACTIVE') OR (c.reportId = :reportId AND c.status = 'PENDING')") Optional<ReportFieldConfig> findEffectiveConfig(@Param("reportId") String reportId, @Param("userId") String userId);注:AI 生成了 Repository 方法,而非 Service 方法。这是常见偏差——AI 更熟悉“数据访问层”模式。
Step 3:Review(按 checklist 打钩,耗时 5 分钟)
- ✅ 输入参数命名与 prompt 一致(
reportId,userId) - ✅ JPQL 中 status 字符串硬编码,未用变量
- ❌ 返回类型应为
Optional<ReportFieldConfig>,但 prompt 要求的是 Service 方法,不是 Repository 方法 - ❌ 缺少对
PENDING结果的 userId 校验(prompt 要求“同一 reportId 下 status = 'PENDING' 的配置”,但未限定 userId,会导致返回他人配置)
Step 4:人类修正(耗时 3 分钟)
- 将 Repository 方法改为 Service 方法实现
- 在 JPQL 中增加
AND c.userId = :userId到 PENDING 分支 - 添加注释说明“PENDING 配置仅对创建者生效”
最终代码:
@Transactional(readOnly = true) public Optional<ReportFieldConfig> getEffectiveConfig(String reportId, String userId) { // 优先查 ACTIVE 配置(自己的) Optional<ReportFieldConfig> active = configRepository .findByReportIdAndUserIdAndStatus(reportId, userId, ConfigStatus.ACTIVE); if (active.isPresent()) { return active; } // 再查 PENDING 配置(仅限自己的,防止越权) return configRepository .findByReportIdAndUserIdAndStatus(reportId, userId, ConfigStatus.PENDING); }注:我们放弃了复杂的 JPQL OR,改用两次 Repository 调用。因为 review 发现,AI 生成的 OR 查询在 MySQL 8.0 上无法利用联合索引,性能下降 400%。这是 prompt 无法覆盖的领域知识。
Step 5:测试用例生成与补充(耗时 4 分钟)
用模板五生成 8 个测试,发现 AI 漏掉了“PENDING 配置存在但 userId 不匹配”的 case,手动补充:
@Test @DisplayName("当存在他人PENDING配置时,不返回") void shouldNotReturnOthersPendingConfig() { // given ReportFieldConfig othersPending = new ReportFieldConfig("r_123", "u_others", List.of("order_id"), ConfigStatus.PENDING); configRepository.save(othersPending); // when Optional<ReportFieldConfig> result = service.getEffectiveConfig("r_123", "u_mine"); // then assertThat(result).isEmpty(); }整个过程耗时 20 分钟,产出一个 100% 符合 AERS、通过所有 checklist、性能达标、测试覆盖完整的函数。对比人类独立开发,节省了约 65% 时间(人类平均需 55 分钟)。
4.3 测试与上线阶段:如何用“AI 生成测试”反向验证“AI 生成代码”的鲁棒性
我们的测试策略是“双 AI 验证”:用 AI 生成代码,再用另一个 AI(或同一 AI 的不同 prompt)生成测试,最后用人类做“测试有效性审计”。这不是为了省事,而是为了暴露 AI 的系统性盲区。
第一层:AI 生成的单元测试(已述)
目标是覆盖 prompt 中指定的边界。
第二层:AI 生成的混沌测试(Chaos Test)
我们专门设计了一个 prompt,让 AI 生成“故意捣乱”的测试:
请为 ReportConfigService.getEffectiveConfig 方法生成 3 个混沌测试用例。 【要求】: - 每个用例必须使用 @TestInstance(TestInstance.Lifecycle.PER_CLASS) - 必须 mock configRepository,使其返回特定异常: * case1:抛出 DataAccessException(模拟数据库连接中断) * case2:抛出 OptimisticLockException(模拟并发更新冲突) * case3:返回 null(模拟底层缓存穿透) - 测试断言必须验证 service 方法是否优雅处理: * case1:是否记录 ERROR 日志(检查 logger.error 被调用) * case2:是否重新尝试(检查 repository 被调用 2 次) * case3:是否返回 empty Optional(不抛异常)这个 prompt 生成的测试,暴露出我们之前忽略的两个问题:
- AI 生成的 service 方法没有 try-catch,导致
DataAccessException向上透出,违反 fail-fast 原则 OptimisticLockException处理缺失,未实现重试逻辑
于是我们紧急在 service 方法上加了@Retryable(value = {OptimisticLockException.class}, maxAttempts = 2),并补充了 error 日志。这个发现,是传统单元测试很难覆盖的——因为它要求测试者预知所有可能的底层异常类型,而人类往往只记得常见的几种。
第三层:人类审计(关键!)
我们不运行这些混沌测试,而是审计它们的“合理性”。例如,AI 生成的 case1 断言是:
verify(logger, times(1)).error(anyString(), eq(exception));但人类审计发现:anyString()太宽泛,应该锁定为"Failed to get effective config for reportId={}, userId={}",否则日志格式不统一。于是我们把这条加入 checklist,后续所有 error 日志都必须用固定 message 模板。
上线前,我们要求:
- 所有 AI 生成代码,必须通过人类编写的 checklist(100% 打钩)
- 所有 AI 生成测试,必须通过人类审计(检查断言是否精准)
- 混沌测试必须在 staging 环境跑通(证明异常处理真实有效)
这三层过滤,把线上故障率从初期的 1.2% 压到了 0.07%。
5. 常见问题与排查技巧实录:那些在 Slack 频道里刷屏的“为什么又崩了”
5.1 典型问题速查表:我们踩过的坑,按发生频率排序
| 问题现象 | 根本原因 | 排查技巧 | 解决方案 | 复现概率 |
|---|---|---|---|---|
| PR 合并后 CI 突然失败,错误指向一个从未修改的 util 类 | AI 在生成新代码时,顺手“优化”了老类的 import 顺序,把org.apache.commons.lang3.StringUtils换成了java.util.Objects,而老类中StringUtils.isEmpty()调用未被替换 | git diff --no-index /dev/null <(git show HEAD:src/main/java/xxx/Util.java | grep import)对比 import 变更 | 建立 pre-commit hook,用checkstyle禁止 import 顺序变更;AI prompt 中加“禁止修改未提及的文件” | 38% |
线上日志出现大量NullPointerException,堆栈指向 AI 生成的 service 方法,但该方法明明有 null 检查 | AI 生成的 null 检查写在了if里,但忘记加{},导致后续代码无论条件真假都会执行 | grep -A5 -B5 "if.*== null" src/main/java/xxx/Service.java检查 if 结构 | 在 SonarQube 中启用squid:S126规则(强制 if-else 大括号);AI prompt 中加“所有 if/for/while 必须用大括号包裹” | 29% |
| 数据库查询变慢 10 倍,EXPLAIN 显示未走索引 | AI 生成的 JPQL 使用了LIKE '%keyword%',而字段无全文索引;或用了function upper(name)导致索引失效 | git log -S "LIKE" --oneline快速定位最近引入的模糊查询 | 建立 SQL 审计清单:禁止LIKE前导通配符;禁止在 where 条件中对字段用函数;所有新查询必须附带 EXPLAIN 计划 | 22% |
单元测试本地通过,CI 环境失败,报DateTime.now()时间不一致 | AI 生成的测试用LocalDateTime.now()作为期望值,但 CI 服务器时区为 UTC,本地为 CST | grep "LocalDateTime.now|Instant.now" src/test/java/扫描所有“now”调用 | 全局替换为Clock.fixed(Instant.parse("2023-10-15T12:00:00Z"), ZoneId.of("UTC")),并在 test config 中注入 | 15% |
Swagger UI 中 API 文档的 response schema 显示为object,而非具体 DTO | AI 生成的 controller 方法返回Map<String, Object>,Springfox 无法推断泛型 | curl -s http://localhost:8080/v3/api-docs | jq '.paths["/api/v1/config"].get.responses["200"].content["application/json"].schema.$ref'检查 ref | AI prompt 中强制要求“返回类型必须是具体 DTO 类,禁止 Map/List/泛型”;CI 加swagger-codegen验证 | 8% |
这张表不是凭空而来,是 127 次线上故障复盘的结晶。我们把它贴在团队 Wiki 首页,新人入职第一件事就是熟读。
5.2 独家避坑技巧:三个让 AI 编程“稳如老狗”的实操心法
心法一:永远用“最小可验证单元”启动
不要一上来就让 AI 写“用户登录功能”,而是先让它写validatePassword(String raw, String encoded)这个单一函数。理由:
- 单一函数输入输出明确,AI 不易发散
- 可立即写测试验证,反馈周期 < 1 分钟
- 一旦出错,影响范围可控(就一个函数)
我们团队约定:所有 AI 编程任务,必须拆解到“能在一个小时内完成 review 和测试”的粒度。超过这个粒度,必须拆分。这避免了“写了两天,review 发现全错”的灾难。
心法二:建立“AI 生成物指纹库”,让每次输出可追溯、可对比
我们用一个简单的 Python 脚本,自动为每个 AI 生成的代码块生成指纹:
import hashlib def gen_f