ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

方法调用结构样本:AI生成代码不跑偏的关键

方法调用结构样本:AI生成代码不跑偏的关键 先说一个我踩过的坑。几个月前我用人工智能编程工具写一个待办任务模块需求描述写得很清楚创建待办、列表查询、标记完成、删除。生成出来的代码单看每个方法都没问题真跑起来却发现一堆结构性问题——Controller里塞了状态判断Service直接写SQLRepository上方凭空多出一个“查询所有任务后在内存里排序”的逻辑。后来我换了输入方式先给AI一份方法调用结构样本再让它按样本生成。代码质量立刻提升了一个档次。这件事让我意识到一个关键点——所谓的人工智能编程真正决定产出上限的往往不是提示词写得多花哨而是你有没有给AI提供一份清晰的方法调用结构样本。这篇文章就围绕这个主题展开方法调用结构样本到底是什么、待办任务场景下怎么设计、按什么姿势喂给AI、实际踩过哪些坑。适合正在用AI写业务代码的开发者也适合想在团队里统一AI生成代码风格的读者参考。1. 为什么AI编程离不开方法调用结构样本1.1 一次待办任务代码的“翻车现场”我最初用的提示词大概是这样的“帮我写一个待办任务模块包含创建、列表、完成、删除功能用Spring Boot实现。”需求不可谓不清楚AI也不负众望地生成了一大堆代码。但你把构造函数一拉开问题全出来了Controller里的createTodo方法写了完整的参数校验和状态判断业务规则盘踞在HTTP层ApplicationService里没有业务编排反而出现了JdbcTemplate直接写SQLRepository层被绕开部分查询逻辑散落在Service和Controller里标记完成时直接setStatus(COMPLETED)没有校验当前状态是否允许流转。单看每个方法语法都没问题但组合在一起就是典型的“能跑但没法维护”的结构灾难。我当时的第一反应是“AI不行”但冷静下来复盘发现问题出在我自己身上——我没告诉AI这个项目的方法调用结构是怎么组织的。1.2 AI编程工具的真实工作方式它是模式补全器不是架构师市面上主流的AI编程工具本质上是基于海量代码训练出来的模式补全器。你给它一个指令它根据训练数据里的统计规律预测“最有可能的下一段代码是什么”。注意是最有可能不是最正确更不是最符合你项目架构。这意味着什么AI对“正确结构”的理解来自它见过的通用代码而不是你项目的特定约定。你的项目是Controller → ApplicationService → DomainService → Repository的四层结构AI脑子里却是五花八门的Controller直接操作DAO、Service既是事务边界又是业务逻辑集合的混乱模式。所以它生成的代码“看起来像那么回事”但细节全是结构硬伤。方法调用结构样本就是用来解决这个上下文缺失问题的。你提前把“这个项目里方法之间应该怎么调用、每层该干什么、不该干什么”写清楚AI才有依据生成符合架构的代码。没有样本AI在猜有了样本AI在按图施工。1.3 待办任务最适合当样本的业务场景有人可能会问为什么要拿待办任务做样本因为它是典型的“麻雀虽小五脏俱全”的业务场景。待办任务的功能不多但覆盖了完整的后端调用链路创建涉及参数校验和持久化列表涉及过滤排序分页标记完成涉及状态机和权限校验删除涉及存在性检查和级联约束。这四个操作放在一起基本把Controller、ApplicationService、DomainService、Repository四层能遇到的问题全暴露了一遍。更关键的是待办任务的业务规则足够简单容易把“调用链本身”和“复杂业务”剥离开让读者看清楚结构长什么样。你拿一个订单系统当样本光状态机就够写一篇长文反而冲淡了方法调用结构这个主题。所以待办任务是我推荐的首选样本场景学会了这里的调用关系换到订单、库存、任务流思路完全一致。2. 待办任务调用链的骨架设计先定边界再写代码2.1 一个用例对应一个方法而不是按CRUD划分设计方法调用结构样本的第一步不是打开IDE写代码而是先把用例边界划清楚。待办任务的标准用例至少有五个创建待办、查询待办列表、查询待办详情、标记待办完成、删除待办。这里最常见的错误是“按CRUD划分方法”。很多AI生成代码时天然喜欢把“创建”对应insert“列表”对应select“删除”对应delete一套数据操作四板斧。结果是标记完成这种典型的“状态变更用例”被硬生生拆成了“先把记录查出来再改个状态字段再存回去”的散装代码。按用例划分的意思是让ApplicationService的每个方法完整表达一个业务用户故事。createTodo就是“创建一个待办”从校验参数到落库到返回VO一气呵成。completeTodo就是“把一个待办标记完成”从查存在性到校验状态到变更到落库同样一气呵成。一个方法对应一个完整用例调用链清晰AI也好模仿人也好review。2.2 调用链四层职责拆分待办任务的方法调用结构我用四层来定义这也是我在团队里推荐的标准形态层职责允许做的事禁止做的事ControllerHTTP入口适配参数解析、调用ApplicationService、返回VO写业务判断、直接操作实体、访问RepositoryApplicationService用例编排调度DomainService和Repository开启事务写SQL、拼业务规则、直接操作实体内部状态DomainService领域规则校验状态流转校验、权限校验、业务不变量校验做持久化、编排多个用例、返回值里塞DTORepository持久化适配保存、查询、删除、分页过滤写业务规则、返回组装好的VO这套划分不是银弹但胜在简单可执行AI照着样本不容易跑偏。核心思想是每一层只做一件事层与层之间单向依赖不允许往回调用。拿DomainService举例它很容易和ApplicationService混淆。我的判据很简单ApplicationService管的是“这个用例要经过哪些步骤”DomainService管的是“这个领域规则允不允许”。前者是流程编排者后者是规则裁判员。裁判员只做判断不做操作所以DomainService的方法最好返回void校验不过就抛异常校验通过就静静返回。2.3 方法命名、返回值和异常的硬性约定骨架设计里最容易被忽略但恰恰是AI最需要明确指示的三件事方法怎么命名、返回值给什么、异常怎么抛。方法命名我要求“动词开头、表达业务意图”。创建待办就叫createTodo不叫insertTodo标记完成就叫completeTodo不叫updateStatus。命名是调用链的第一层语言命名烂了整个结构就开始松动。AI在这方面的能力比人更差因为它倾向从数据视角命名你必须在样本里替它立好规矩。返回值方面我的约定是“永不出实体一律出VO”。Controller返回给前端的必须是TodoVO不直接返回Todo实体。贫血模型还是充血模型之争先放一边至少对外输出要保持稳定。如果让AI放飞自我它会在三个不同的方法里分别返回实体、DTO、Mapreview到怀疑人生。异常处理更是一个坑。我要求领域规则校验统一抛InvalidTodoException、状态冲突抛TodoStateConflictException、未授权抛TodoPermissionException由全局异常处理器统一映射成HTTP状态码。一切为了不让AI在Controller里写try-catch。3. 一份可以直接复用的方法调用结构样本3.1 创建待办任务的调用链样本这部分我把实际在项目里用的样本贴出来代码用Java演示换成TypeScript或Golang只是语法差异调用结构完全一样。同时也是这段内容的核心——先看完整调用链再怎么拆// #SAMPLE:CREATE_TODO // // 调用链TodoController.createTodo // → TodoApplicationService.createTodo // → TodoDomainService.validateNewTodo // → TodoRepository.save // 约束 // - Controller 只做 HTTP 参数解析不写业务判断 // - ApplicationService 内部不出现 if 和校验逻辑 // - 领域校验统一放 DomainService // RestController RequestMapping(/api/v1/todos) public class TodoController { private final TodoApplicationService todoApplicationService; public TodoController(TodoApplicationService todoApplicationService) { this.todoApplicationService todoApplicationService; } PostMapping public ResponseEntityTodoVO createTodo(RequestBody TodoCreateRequest request) { TodoCreateCommand command request.toCommand(CurrentUser.id()); TodoVO vo todoApplicationService.createTodo(command); return ResponseEntity.ok(vo); } }Service public class TodoApplicationService { private final TodoDomainService todoDomainService; private final TodoRepository todoRepository; public TodoApplicationService(TodoDomainService todoDomainService, TodoRepository todoRepository) { this.todoDomainService todoDomainService; this.todoRepository todoRepository; } Transactional public TodoVO createTodo(TodoCreateCommand cmd) { Todo todo cmd.toEntity(); todoDomainService.validateNewTodo(todo); todoRepository.save(todo); return TodoVO.from(todo); } }Component public class TodoDomainService { private static final int MAX_TITLE_LENGTH 50; public void validateNewTodo(Todo todo) { if (todo.getTitle() null || todo.getTitle().trim().isEmpty()) { throw new InvalidTodoException(待办标题不能为空); } if (todo.getTitle().length() MAX_TITLE_LENGTH) { throw new InvalidTodoException(标题长度不能超过50个字符); } if (todo.getDeadline() ! null todo.getDeadline().isBefore(LocalDateTime.now())) { throw new InvalidTodoException(截止时间不能早于当前时间); } } }public interface TodoRepository { void save(Todo todo); OptionalTodo findById(Long id); ListTodo findByUserId(Long userId, Pageable pageable); ListTodo findByUserIdAndStatus(Long userId, TodoStatus status, Pageable pageable); void delete(Todo todo); }这段样本覆盖了完整链路。留意几个细节Controller连命令对象都不自己组装而是让Request的toCommand方法完成ApplicationService里没有任何if校验完全委托给DomainServiceRepository接口在样本里一并写好为的是堵住AI“发明新方法”的嘴。3.2 标记完成与删除的调用链样本创建是新增相对简单。标记完成是状态变更调用链要复杂一截因为它多了一个“先查后改”的过程// #SAMPLE:COMPLETE_TODO // 调用链TodoController.completeTodo // → TodoApplicationService.completeTodo // → TodoRepository.findById先查存在性 // → TodoDomainService.validateCompletion再校验规则 // → todo.markCompleted()执行状态变更 // → TodoRepository.save最后落库 PatchMapping(/{id}/complete) public ResponseEntityTodoVO completeTodo(PathVariable Long id, RequestBody TodoCompleteRequest request) { TodoCompleteCommand command new TodoCompleteCommand( id, CurrentUser.id(), request.getCompletedAt()); TodoVO vo todoApplicationService.completeTodo(command); return ResponseEntity.ok(vo); }Transactional public TodoVO completeTodo(TodoCompleteCommand cmd) { Todo todo todoRepository.findById(cmd.getTodoId()) .orElseThrow(() - new TodoNotFoundException(cmd.getTodoId())); todoDomainService.validateCompletion(todo, cmd.getUserId()); todo.markCompleted(cmd.getCompletedAt()); todoRepository.save(todo); return TodoVO.from(todo); }public void validateCompletion(Todo todo, Long userId) { if (!todo.belongsTo(userId)) { throw new TodoPermissionException(无权操作他人的待办任务); } if (todo.getStatus() TodoStatus.COMPLETED) { throw new TodoStateConflictException(该待办已完成不能重复操作); } if (todo.getStatus() TodoStatus.CANCELLED) { throw new TodoStateConflictException(已取消的待办不能标记完成); } }删除操作的调用链和完成操作很接近deleteTodo同样先findById查存在性再做归属和状态校验最后调TodoRepository.delete(todo)。没必要整段贴出来但要注意删除也要走“先查后删”不能让AI直接写deleteById(id)了事否则把别人的待办删了都没人知道。3.3 列表查询链路与状态流转约束查询链路和写入链路不一样写入是“实例方法调用序列”查询更接近“参数传递链”。但同样有结构约束// #SAMPLE:LIST_TODOS // 查询链路TodoController.listTodos // → TodoApplicationService.listTodos // → TodoRepository.findByUserIdAndStatus // → 组装 TodoVO // 约束排序、分页、过滤全部在 Repository 完成 // 禁止把全量数据拉到内存再过滤 GetMapping public ResponseEntityPageResultTodoVO listTodos( RequestParam(required false) TodoStatus status, RequestParam(defaultValue 0) int page, RequestParam(defaultValue 20) int size) { PageResultTodoVO result todoApplicationService.listTodos( CurrentUser.id(), status, page, size); return ResponseEntity.ok(result); }public PageResultTodoVO listTodos(Long userId, TodoStatus status, int page, int size) { Pageable pageable PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, createdAt)); ListTodo todos; if (status null) { todos todoRepository.findByUserId(userId, pageable); } else { todos todoRepository.findByUserIdAndStatus(userId, status, pageable); } return PageResult.from(todos.stream().map(TodoVO::from).toList()); }查询链路样本里最重要的一条约束是“不要在内存里过滤和排序”。AI特别容易在Repository没有提供对应方法时直接findByUserId拿到全量列表再在Service里用Stream做过滤排序。数据量小的时候没问题一旦单用户待办破千这种写法就是性能事故。所以样本里必须写死过滤排序分页是Repository的职责。状态流转约束是待办任务里最容易丢的一环。我把这张状态机表也放进样本当前状态允许到达状态触发操作NEWIN_PROGRESS开始任务NEWCOMPLETED直接完成NEWCANCELLED取消任务IN_PROGRESSCOMPLETED标记完成IN_PROGRESSCANCELLED取消任务COMPLETED无终态CANCELLED无终态4. 把样本正确喂给AI的三种姿势4.1 对话式AI样本前置 明确指令很多人用对话式AI编程的习惯是直接说需求这其实是把AI当成了“懂项目背景的老同事”。正确姿势是把需求描述和项目约定分开先给项目约定再给需求。我实际用的提示词结构是这样的你是一位熟悉分层架构的Java工程师。项目使用 Controller → ApplicationService → DomainService → Repository 四层结构方法调用约束如下 [此处粘贴上文的方法调用结构样本包括三个样本代码反例约定] 请按照以上调用链结构实现“编辑待办标题”功能。 编辑规则标题不能为空长度不超过50字符只能编辑自己的待办。注意顺序样本在前需求在后。这份提示词还有一个隐藏作用——当需求描述和样本矛盾时AI会优先遵守约束更强的指令而不是放飞想象力。4.2 IDE辅助编程样本目录与符号标记在Cursor、Copilot这类IDE工具里把全部样本堆在提示词里不现实更推荐的方式是把样本做成项目里的“可检索文档”。我在项目根目录维护了一个docs/ai-patterns/目录里面放method-call-patterns.md核心代码块统一用#SAMPLE:CREATE_TODO、#SAMPLE:COMPLETE_TODO这类大写标签开头。这样做有两个好处。第一编辑器里的AI补全能通过注释感知项目约定你在写createTodo时它会从历史代码和样本注释里领悟调用风格。第二用Cursor的对话功能时可以直接这个文件告诉AI“先读这个文件再干活”。4.3 输入输出对和反例标注只给“正向样本”还不够AI会把正向样本当成“参考风格”但它并不知道哪些做法是红线。所以我的样本文件里专门有一节叫“反例约定”明文禁止四种写法// 反例约定以下代码模式禁止出现在本项目中 // 1. Controller 里出现 if 判断业务状态 // 2. ApplicationService 里出现 SQL/JdbcTemplate // 3. Service 之间互相调用内部方法绕过 ApplicationService // 4. Repository 返回 List 后在 Service 内存中做过滤/排序/分页经验是AI对“禁止做什么”的遵守度远高于“应该做什么”。人也一样给一个明确禁止清单比对着一堆代码猜隐含规则要高效得多。4.4 样本也要纳入版本管理样本文件不是一次性产物它必须跟着项目架构演进。我见过最典型的场景是项目把Repository从MyBatis换成了Spring Data JPA但样本文件没更新AI继续按老接口生成代码编译都过不了。所以凡是涉及调用链调整的PR都要同步改method-call-patterns.md。这和我把样本当“团队编码宪法”的理念是一致的——它是活的约定不是存档的笔记。我在提交记录里甚至会写“doc: 更新方法调用结构样本新增软删除链路”让整个团队都能追溯架构约定的变化。5. AI按样本生成代码时的典型偏差与修正5.1 偏差一方法拆分粒度不对即使给了样本AI还是会在细节上跑偏。最常见的是方法拆分粒度问题要么把ApplicationService的多个用例塞进一个方法要么把一个用例拆得支离破碎比如把校验写成一个私有方法、把VO转换写成另一个私有方法、真正的业务逻辑只剩两三行。识别方法很清楚看方法体里是不是超过30行、方法参数是不是超过4个、方法命名是不是变成了processTodoData之类。修正方法也很简单让AI“分步骤生成不要一次性输出完整文件”。我通常让它先输出ApplicationService方法骨架再补DomainService校验规则最后写Repository方法每步之间人工看一眼。5.2 偏差二AI自己“发明”Repository方法这是最隐蔽的一个坑。AI需要“按标题模糊搜索待办”时如果样本里没有findByTitleContaining它会很自然地在Repository接口里新增这个方法而不是复用已有的findByUserId。如果项目用的是JPAAI甚至会发明一个映射关系完全不存在的查询方法编译期才发现错误。更严重的是它会引入findAll()然后把过滤逻辑写在Service里。刚才说了这是查询链路的红线。我的处理方式是样本里加一句硬约束“Repository接口方法以样本为准AI不得新增必须新增时先询问用户。”这句话看着简单实战里能省下大半review时间。5.3 偏差三事务边界跑偏事务是AI最理解不了的机制之一。它经常把Transactional加到DomainService上或者加到ApplicationService里一个被同类的其他方法调用的私有方法上事务直接失效。我吃过一次大亏让AI生成“批量完成待办”功能时AI在TodoApplicationService.batchComplete里调用了completeTodo方法但因为两个方法都在同一个类里completeTodo被同类内部调用时绕过了代理对象事务注解根本没生效。数据写了一半。所以样本里我固定了一条铁律事务只标注在ApplicationService的公开用例方法上DomainService和Repository一律不标注。内部方法调用一律通过注入的独立Bean进行。5.4 偏差四绕过领域方法直接改状态这个偏差最隐蔽也最致命。AI生成了todo.setStatus(TodoStatus.COMPLETED)就直接落库了完全跳过了todo.markCompleted()里的状态机校验逻辑。结果就是一个CANCELLED状态的任务可以被直接标记成COMPLETED业务上毫无意义还污染数据。我会在样本的实体示例代码里明确写出实体的内部状态变更方法public void markCompleted(LocalDateTime completedAt) { // 状态流转只在这里做 if (this.status ! TodoStatus.NEW this.status ! TodoStatus.IN_PROGRESS) { throw new TodoStateConflictException(当前状态不能标记完成); } this.status TodoStatus.COMPLETED; this.completedAt completedAt; }配合“Repository保存的必须是状态变更完成后的实体”这条约定AI基本不会再来硬改实体内部字段。5.5 实测下来最有效的修正口诀经过一段时间磨合我形成了一套最有效的修正检查每次AI生成完代码我都会跑一遍快速扫描搜索setStatus和setTitle这种直接setter确认没有绕过实体方法搜索new JdbcTemplate和jdbcTemplate确认Service层没写SQL数一数Controller里有没有if关键字有就多半是业务逻辑泄漏确认所有Transactional都在ApplicationService的方法上。口诀就四句先查后改校验在前仓储接口不新增事务只写在用例方法上。这十六个字基本上把AI在方法调用结构上90%的偏差都堵住了。最后再分享一个我个人的使用习惯在项目的README里加一行链接指向docs/ai-patterns/method-call-patterns.md让任何新加入项目的人以及任何AI编程工具都第一眼能看到这份样本。每当架构有变动先改样本再让AI生成代码。这个顺序一旦固定下来AI生成代码的返工率会肉眼可见地下降。希望这篇关于方法调用结构样本的拆解能让你在下次用AI写待办任务或者任何类似业务模块时少踩几个我已经替你踩过的坑。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表