作为一个常年和AI编程工具打交道的人,说实话,工具确实让写代码快了不少,但最近一年我越来越明显地感觉到一件事:AI生成的代码,正在悄悄给团队制造一种新的“技术债”。这种债不是跑不通的bug,而是更隐蔽的——风格碎片化、边界处理缺位、注释废话连篇、异常逻辑随手一抛。代码能跑,但没人敢碰,改一行要心惊胆战半天。
所以当我开始做“CleanCode AI编程标准代码生成器”这个系列的时候,想解决的就是这个最根本的问题:能不能让AI在生成的源头就输出符合团队规范的代码,而不是等代码写完了、合并了、上线了,才在Code Review里追着人改。我的答案是把“规范”内置到生成链路里,让生成即规范,从源头杜绝技术债,而且保证易调测、易维护。这一弹,我就把整套方案的设计思路、核心实现和落地实操完整地拆开讲清楚,第三十五弹,聊点真正能落地的东西。
1. 先想清楚:AI生成代码的技术债到底从哪里来
1.1 问题不在AI,在“没有标准的生成链路”
很多团队刚开始用AI编程时都很兴奋,觉得效率翻倍,但用上两三个月就沉默了。UI能跑、接口能通、单测能过,可是当你真的去读这些代码、去改这些代码的时候,心里的火气就上来了。
我总结了一下,AI无约束生成代码普遍存在四个问题。
第一,风格碎片化。同一个项目里,有人习惯返回Result包装对象,有人直接返回裸实体;有人用构造器创建对象,有人用链式setter;有人Controller里做参数校验,有人Service里才想起来校验。AI每次生成都是“概率采样”,下一次生成的结果可能跟上一段代码完全不是一个风格。代码风格不统一,看起来像四五个人没有沟通就各写各的。
第二,结构性缺漏。AI很擅长把主流程写完整,但不擅长处理边界分支——事务里漏了回滚条件,缓存更新没考虑一致性,批量接口没做分页保护。这些结构性的东西如果不在生成阶段就卡死,光靠事后review,人眼很难全看出来。
第三,异常逻辑散漫。生成代码里最常见的通病是catch (Exception e) { log.error(e.getMessage()); }直接吞掉异常,或者干脆不catch,让异常一路抛到前端。真正规范的异常处理应该是分层、分类、有兜底策略的。这需要一套固定的写法约束。
第四,注释与文档质量低。AI生成的注释,绝大多数是在重复代码本身——“把用户的姓名设置为姓名”,这种注释一点信息量都没有。更糟的是,AI还会编造不存在的业务逻辑,然后写进注释里,误导后来维护的人。
这四类问题,本质上都是同一个根源:AI生成代码的时候,预测目标里只有“像代码”,没有“合规范”。模型根据海量语料做概率采样,什么风格的代码都有可能出现,你让它“自由发挥”,它就真的自由发挥。
1.2 为什么坚持“源头治理”而不是“事后修”
关于怎么解决,行业里其实有三种路线。
第一种是事后强制。代码生成完了,交给lint工具和Code Review去挑刺,发现违规再让人工修。这种方案最省事,但问题在于——修复成本在后边是呈指数上升的。生成阶段改一个命名可能只要一分钟,但合入主干后再改一个公共接口的命名,要牵连调用方、测试用例和文档,半天都未必够。
第二种是生成后自动格式化/自动修复。比如eslint --fix、gofmt这类工具能不能兜底?可以解决一部分空白、引号、缩进问题,但解决不了结构性问题。gofmt可以把代码排整齐,但排不齐“事务是否包裹了完整业务操作”这种逻辑层面的问题。
第三种,就是我这个系列一直在贯彻的“源头治理”:在生成链路里内置规范约束,让AI从第一行代码开始就按照固定模板输出。就像装修房子,不是等墙砌歪了再请人拆掉重砌,而是在施工之前就把结构施工图定好,每一块砖该放哪里都有据可查。
这是我这套方案价值观最核心的部分——技术债要靠“设计”来消除,不能靠“事后打补丁”来缓解。
2. 方案的整体设计与核心架构
2.1 三层架构:不是让AI自由发挥,而是给AI一套约束框架
整个生成器采用三层架构:规范配置层、代码生成层、结果校验层。
规范配置层负责把“我们团队的规范”变成机器可读的规则集。这一层是纯配置化的,团队里每个人都可以阅读、评审、修改。代码生成层负责把需求描述转化为具体代码,但它不是让模型裸奔,而是给它喂一套“带约束的提示词+模板骨架”。结果校验层负责对生成结果做最终检查,用AST解析、规则匹配等手段确认输出真的符合规范,而不是停留在“看起来符合”。
这一层架构我从一开始就想得很明白:不是搞一个“更聪明的AI”来替代现有模型,而是把模型包在一个规范框架里,让它的能力被引导到正确方向。底层模型可以用主流的大模型API或本地模型,但上层必须是一个确定性的工程系统。
2.2 为什么规范必须可配置
早期我也走过弯路。最开始我尝试把所有规范硬编码在生成器的代码里——Java文件必须带@author、方法必须写Javadoc、Controller必须返回Result。但是很快发现一个问题:不同团队、不同项目、不同技术栈,规范差异非常大。
我遇到过一个场景:给A团队做的规范里,持久层要求用MyBatis-Plus的LambdaQueryWrapper;B团队用的是Spring Data JPA,这个规范对B团队就是无效甚至有害的。如果规范是硬编码的,每接一个团队都要改一遍生成器源码,那这个工具就没有推广价值了。
所以我最终选择了YAML DSL做规范配置。把“命名规则”“注释要求”“异常处理策略”“事务边界规则”等都定义成配置文件。一个团队接进来,只需要提供一份自己的规范描述,生成器就能按这套规范输出代码。
2.3 模板 + 规则 + 模型的协作机制
这套方案里,生成逻辑是“三层约束”的关系。
第一层是模板骨架,它定义了代码的结构骨架。比如一个Service接口的实现类,骨架就是类声明、字段注入、公共方法、私有方法这个顺序。模板把结构锁死,模型就没有机会把结构写乱。
第二层是填充规则,它定义了模板里“变量部分”怎么写。方法名怎么取、参数怎么校验、异常怎么抛、返回值怎么包装。规则把自由度降到可接受的范围,模型只能在这个范围内发挥。
第三层才是模型的语言表达能力。它负责把自然语言需求转化为模板里的具体业务逻辑。比如“查用户订单列表”转换为listOrdersByUserId(Long userId, PageParam page)的实现细节。
优先级是模板 > 规则 > 模型。当模型采样的结果跟模板冲突时,模板赢;跟规则冲突时,规则赢。这个先决条件非常重要,如果反过来,那这套方案就退化成普通的“AI生成后人工修”了。
3. 核心实现:规范引擎与代码增强细节
3.1 规范引擎怎么做好“生成前、生成中、生成后”三道约束
先说生成前约束。这是把规范“翻译”成提示词结构的过程。把“团队规范”中的硬性要求,比如必须包含的import列表、必须继承的基类、必须实现的接口,直接拼进System Prompt。这能让模型从第一轮生成时就看到规范。
然后是生成中约束。这一层我实现了一个模板解析器,代码是Jinja2风格的模板加占位符。模型输出的时候,我要求它按模板逐段填充,而不是一次性生成整个文件。这样占位符之间的依赖关系就是可控的,生成错误也能准确定位到某个片段。
最核心的是生成后校验。我实现了一个轻量级的AST解析器,而不是用正则表达式去匹配文本。为什么要用AST?因为AST才是代码真正的语义结构。正则只能看到“有没有return”,AST能看到“return是不是在try块内”“有没有在finally里做了return”“事务注解是不是标在了private方法上”。校验规则每条都对应AST上的一种节点模式。
3.2 规范配置的YAML示例
我直接给一段我实际项目里用的规范配置片段,大家可以感受一下配置化的方式。
# clean-code-rules.yaml project: name: order-service basePackage: com.example.order language: java style: classDocRequired: true authorTagRequired: false lineLengthLimit: 120 methodNameCamelCase: true structure: controller: extends: BaseController resultWrapper: Result validationAnnotation: true forbidSystemOut: true serviceImpl: interfaceSuffix: Service implSuffix: ServiceImpl transactionAnnotation: true forbidSystemOut: true methodLengthLimit: 60 mapper: interfaceSuffix: Mapper extendsBaseMapper: true exception: strategy: layer-based controller: throw BizException(ErrorCode.PARAM_ERROR) service: throw BizException(ErrorCode.BIZ_ERROR) mapper: translateToDataException comment: classLevel: summary methodLevel: required assertCommentMustExplain: true forbiddenComments: - "TODO fix" - "此处代码很烂"这一段配置里,structure和exception是最关键的。structure决定了三层代码的骨架约束,exception决定了异常怎么分层处理。有人可能会问,forbidSystemOut这种小事有必要写进规范吗?太有必要了,AI特别喜欢在排查问题时顺手写个System.out.println,如果没有这一条约束,生成出来100个类里能藏30个控制台输出。
3.3 校验规则的落地——把“规范意识”变成代码判断
校验层最花时间的不是写校验规则,而是定义“什么是违规”。
我举一个真实的例子。团队规范要求:Service实现类的public方法必须开启事务。这个规则落到AST上,需要检查三件事:方法是不是public、方法所在类是不是Service实现类、方法上是否标注了@Transactional。这三件事在AST里分别对应不同的节点层级,只要从类节点往下找到方法节点,再检查方法节点的注解列表即可。
还有一条更隐蔽的规则:Controller方法不允许直接返回实体类。AST校验要检查返回类型是不是com.example.order.entity包下的类。如果返回了实体类,直接给出一条修改建议——改为返回VO对象。
整个校验器跑完一次,会输出一份“违规报告”,而且这份报告是结构化JSON,可以直接集成到CI流水线里,当作代码门禁的一部分。
4. 实操全流程:从配置到落地的完整复现
4.1 接入与初始化
我用一个真实的操作场景来说:假设我要在“订单服务”里生成一个“分页查询用户订单”的功能模块。
第一步是初始化规范环境。把生成器下载到本地,执行初始化命令,生成clean-code-rules.yaml配置文件。然后根据团队规范修改配置。我们团队用的是Java和Spring Boot,所以我在配置里把language设为java,把resultWrapper设为Result。
第二步是配置大模型API。这里我推荐使用支持函数调用(Function Calling)的模型接口,因为生成过程中需要模型按结构化格式返回内容,比如返回JSON片段。函数的强约束能力是纯文本输出比不了的。
第三步是跑一次自检。执行generator self-check,生成器会输出一个“规范环境自检报告”,检查配置文件格式、模型连接是否正常、模板文件是否存在。这一步一定要做,很多人跳过之后,后续排错会非常痛苦。
4.2 典型场景:订单分页查询接口的完整生成过程
配置好之后,直接输入需求描述:
“生成用户订单分页查询接口,按创建时间倒序,返回订单号、商品名、金额、状态,支持按状态过滤。入参:userId、pageNum、pageSize、status。”
生成器内部执行流程是这样的:
第一步,解析需求,抽取关键要素。生成器先把这段自然语言拆出来:资源是“用户订单”,操作是“分页查询”,排序是“创建时间倒序”,过滤条件是“状态”,返回字段是“订单号、商品名、金额、状态”。
第二步,匹配模板。根据“分页查询”这个关键词,从模板库里选中“分页查询模板”,这个模板定义了Controller、Service、Mapper三层的代码骨架。
第三步,模型按模板逐段填充。生成器告诉模型:“我现在给你一个Controller方法的模板,其中方法名占位符为空,请你根据需求补充方法名。”模型返回listUserOrders,通过命名规则校验后填入占位符。
第四步,调用校验器。生成完一个完整模块,校验器跑一遍规则,发现所有规则都通过了,才会把代码输出到磁盘,同时附带一份“规范符合性说明”。
生成的Controller代码大概是这样的:
@RestController @RequestMapping("/api/orders") public class UserOrderController extends BaseController { private final UserOrderService userOrderService; public UserOrderController(UserOrderService userOrderService) { this.userOrderService = userOrderService; } @GetMapping("/user/{userId}") public Result<PageResult<UserOrderVO>> listUserOrders( @PathVariable Long userId, @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) Integer status) { PageParam pageParam = new PageParam(pageNum, pageSize); return success(userOrderService.listUserOrders(userId, pageParam, status)); } }这段代码的规范点在哪里?Controller继承了BaseController、参数校验用注解、返回统一包装Result、构造器注入而不是字段注入。这些全部是模板和规则锁定的,模型没有机会自由发挥。
Service层的实现,事务注解、参数校验、分页逻辑也都由模板锁定。生成的Mapper接口继承了BaseMapper,分页用了分页插件,排序字段由规则校验为“白名单中的字段”,防止SQL注入。
整个生成过程,我记录下来的总耗时大约是90秒左右——模型生成占大头,校验和纠正在秒级。
4.3 生成后的调测与维护体验
代码生成完,不是就完事了。我把生成结果导入现有工程,跑了一遍单元测试和集成测试,结果一次通过。
后面有个小需求变更——要在查询结果里加上“商品图片URL”字段。这个改动通过生成器来做就非常顺了。我只需要在需求描述里追加“增加商品图片URL字段”,生成器会识别这是“字段追加变更”,自动定位到之前的生成记录,只更新VO类、组装逻辑和SQL查询字段,而不是重新生成整个模块。
这就是“易维护”的体现。生成器不是一次性工具,它有“增量更新”能力,这是因为每次生成都会在本地保留一个生成快照,包含这次的完整模板、配置和生成结果。下次变更时,基于快照做局部重生成,不会影响没有变更的代码块。
这个设计是经历过教训后才想明白的。我早期做生成器时,每次都是全量生成覆盖,有一次改了个字段名,生成器直接把另一个服务里手写的一段复杂定制逻辑也覆盖了,直接导致线上故障。从那以后我就坚持做增量更新,而且强制要求“生成器只改自己生成过的代码”,这让维护风险降到了一个可控水平。
5. 常见问题与排查技巧实录
5.1 生成结果偏离模板怎么办
这是使用中最常遇到的问题。模型根本没按模板填占位符,直接输出了一整个类。我排查过一段时间,原因有几种。
最常见的是提示词里模板和需求的位置顺序不对。模板放在后面、需求放在前面,模型容易被需求描述带跑。解决办法是模板必须放在System Prompt中,需求放User Prompt中,并且用分隔符明确划分区域。
其次是模板占位符命名太模糊。原来我用{methodName}这种命名,模型经常猜不准;后来改成{methodName:分页查询方法名}这种“占位符名+语义提示”的双重约束,效果提升特别明显。模型知道这个位置该干什么,不会乱填。
再有一种情况是上下文污染。如果之前生成过其他风格的代码,模型可能延续那种风格。解决办法是每次生成会话都重置上下文,不要在同一会话里连续让模型生成不同模块。
5.2 校验器误报与漏报
AST校验器最大的坑是误报——把合法代码判成违规。我遇到过一个非常典型的例子:校验“Controller不允许返回实体类”时,老代码里有一个@Deprecated的旧接口,确实返回了实体类,但它是历史遗留,团队明确不做修改。校验器一跑就报错,把CI卡死了。
后来我增加了“规则豁免名单”。在配置里维护一个白名单:针对精确匹配的方法名或路径,可以不执行某一条规则。这不是逃避规范,而是实现对存量债务的灰度治理——新代码100%合规,老代码列计划逐步整改。
另一个坑是漏报,多发生在泛型和反射场景。比如BaseMapper<T>的接口,AST里看到的是泛型T,不是实际类型。如果规则要校验“Mapper是否继承BaseMapper”,泛型擦除会让校验器分析不出实际类型。解决方案是做类型消解,利用泛型层级关系把T解析为具体实体类。
5.3 生成器与人工协作的边界怎么划
我必须实话实说,这套生成器不是万能的。经过几十个版本的迭代,我明确划了一条边界。
适合生成器做的:CRUD接口、分页查询、简单的业务校验、基础的工具类、规范的单测骨架、领域模型的代码结构。这些代码有模式可循,模板能发挥很大作用。
不适合生成器做的:核心复杂业务的算法逻辑、涉及多个系统深度交互的编排流程、需要依赖人脑业务判断的规则引擎配置。这些代码,模板锁不住,规则也无法穷举,硬套生成器只会生成一个“看似完整实则没法用”的架子。
实际的协作流程是:生成器负责“骨架+常规”,人工负责“核心+异常”。也就是先把常规代码全部生成好,开发者集中精力写那20%真正需要人脑判断的核心逻辑。这个比例在实践中验证下来,是性价比最高的。
最后分享两个实战心得
我在这套生成器上踩过很多坑,最核心的一条体会是:规范先于模型。别指望模型懂规范,先把规范的边界划出来,模型的能力才有发挥的空间。任何“让AI生成完再说”的思路,最终都会被技术债反噬。
再分享一个小技巧。把团队Code Review里反复出现的高频问题,固化成一条一条的校验规则。我们团队曾经因为“事务注解写在private方法上”的问题反复review出问题,后来我在规则库加了一条“事务方法不可为private”,这个问题就再也没出现过。编码规范的最终形态,不应该是一份没人读的文档,而应该是一套跑在代码生成链路上的硬性校验工具。
这就是我坚持做“CleanCode AI编程标准代码生成器”这个系列最核心的原因——把规范内嵌到生成链路里,让AI从一开始就写出对的代码。