多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

LLM结对编程实践指南:角色边界、协作原则与工程落地

LLM结对编程实践指南:角色边界、协作原则与工程落地 A Thought on LLM Pair Programming如果你最近一直在关注 AI 辅助开发大概率对“AI 结对编程”这个概念已经不陌生了。简单来说就是让大语言模型扮演结对编程Pair Programming中的“导航员”角色自己则专注于驾驶员的编码工作。平时写代码、做重构、补测试都可以通过对话让 LLM 参与进来从而把重复劳动压缩到一个很低的水平。不过真正在项目里连续使用一段时间后我发现这件事并不像 Demo 里展示得那么轻松。LLM 结对编程的确能提升效率但它同时会引入新的工程问题代码风格漂移、过度自信的错误建议、上下文窗口限制、需求理解偏差……这些问题单看都不严重组合在一起却会实实在在地影响交付质量。所以这篇文章不打算堆一堆“AI 多强大”的营销话术而是以实际工程视角聊一聊 LLM 结对编程的完整链路它解决什么问题、应该以什么角色存在、如何和它高效协作、有哪些坑以及最后落到团队协作中时应该以什么姿态引入它。1. LLM Pair Programming 到底是什么1.1 传统结对编程的启示传统结对编程通常是这样一种场景两个开发者坐在同一台电脑前一个人负责写代码Driver另一个人负责审查、思考、规划下一步Navigator。导航员会不断提问、指出边界情况、提醒潜在的设计缺陷驾驶员则专注于快速落地实现。这种模式的本质是实时脑力互补一个偏向执行一个偏向判断。LLM Pair Programming 借鉴了同样的分工逻辑只不过导航员从人类变成了大语言模型。你负责把需求拆解成具体步骤LLM 负责快速生成代码块、解释概念、生成测试用例、甚至帮你重构已有逻辑。你仍然掌握方向盘但副驾驶已经不是普通的人类同事而是一个拥有大量语料背景的 AI。1.2 LLM 结对编程的实操形态在实际项目里LLM 结对编程通常以三种形态出现形态交互方式典型工具编辑器内联式在 IDE 中直接调用补全、对话、代码生成GitHub Copilot、通义灵码、Codeium、Cursor对话窗口式在独立对话页面中粘贴代码、需求由 LLM 生成完整方案ChatGPT、Claude、文心一言命令行/Agent 式通过 CLI 工具执行任务LLM 自主读取文件、批量修改Cursor Agent、Aider、OpenAI Codex这三种形态各有利弊。编辑器内联式适合小步快走的场景随时提问、随时补全不打断编码节奏对话窗口式适合方案讨论、代码审查、知识问答上下文更宽信息组织更清晰Agent 式则适合批量重构、跨文件改造但风险也更高因为它会在你的命令行里执行命令、修改文件。1.3 它解决的三大问题先说清楚这一点你才知道应该在什么时候、以什么频率使用 LLM 结对编程。第一缩短从想法到脚手架的距离。以前要写一个 Spring Boot 服务先建工程、配依赖、写启动类、写配置文件半小时就过去了。现在你只需要把需求描述给 LLM它能在几秒内给出一个结构完整的最小项目模板你再根据团队规范调整。第二降低不熟悉领域的上手成本。遇到一个没接触过的库、一个不熟悉的算法、一段看不懂的 Shell 命令与其去搜索引擎反复找答案不如直接把场景丢给 LLM 解释效率高很多。第三减少重复样板代码的脑力消耗。DTO、VO 转换、CRUD 接口、单元测试骨架、复杂正则表达式这些对经验丰富的开发者来说难度不大但写起来很费时间。LLM 很适合承担这类“有规律但不费脑”的产出。2. 环境准备与协作工具选择在正式开始“LLM 结对编程”之前先看一套实际可用的环境组合。不同团队使用的工具差异很大下面这套以编辑器内联 对话窗口为主适合大多数中后台业务开发场景。2.1 基础环境类别建议说明操作系统Windows 10/11 / macOS / Linux无明显差异重点看团队协作平台兼容性代码编辑器VS Code / JetBrains 系插件生态最成熟LLM 工具编辑器插件 Web 对话内联补全负责小任务对话负责方案级讨论项目类型任意主流语言项目本文以 Python 和 Java 混合示例为主版本管理Git 单分支 频繁 Commit这是最关键的协作前提后面会解释唯一需要强调的是使用 LLM 结对编程时项目必须处于 Git 管理下。因为 AI 生成代码有概率引入逻辑错误你必须能够一键回退到修改前状态。没有版本控制AI 改错代码后你会非常被动。2.2 工具选择的三个判断维度如果你还没选定工具可以从以下三个维度判断上下文感知能力。好的结对编程工具应该能自动读取当前文件、工程结构、最近打开的文件而不是每次都要你把代码复制粘贴给它。补全质量与交互精度。在长函数、复杂逻辑上补全是否合理对话生成是否支持定位到具体文件位置。数据安全边界。企业项目代码如果涉及敏感逻辑要注意工具是否支持私有化部署、是否会存储你的代码片段以及你的团队是否允许使用外部 AI 服务。2.3 一个推荐的工作台就日常开发而言推荐组合是JetBrains 系 IDE 内联 AI 插件 浏览器对话窗口。内联插件负责写函数、补注释、生成重复代码浏览器对话负责更复杂的任务比如“帮我设计这个模块的表结构”“分析这段代码的性能瓶颈”“把这段逻辑抽象为策略模式”。两者配合基本覆盖了开发全流程。3. LLM 结对编程的角色边界与核心协作原则3.1 LLM 不是写代码机器而是“高密度知识助手”很多人刚接触 LLM 写代码时会天然地把 AI 当成外包程序员需求一丢等结果结果发现代码质量很随机然后得出“AI 是垃圾”的结论。这个认知是有问题的。LLM 在编程协作中的优势不是“独立完成需求”而是“高密度知识输出 即时代码生成”。你问它一个具体问题它能快速组织出结构完整的回答你让它写一个明确的小函数它能给出包含边界判断的完整代码。但一旦需求模糊、业务逻辑复杂、上下文不完整它就开始“自信地胡说”。所以合作时人的核心价值是拆解需求、定义质量、验证结果。把任务描述得越明确LLM 的输出就越可控。3.2 三条核心协作原则结合实际使用体验我认为有三条原则应该长期坚持。第一条把需求拆到“可执行”粒度。不要对 LLM 说“帮我写一个用户模块”而要说“帮我写一个用户注册接口参数包含 username、password、email密码需要 BCrypt 加密存储用户名和邮箱需要唯一校验返回 JSON 格式的注册结果异常时返回统一错误码”。前者会让 LLM 自由发挥生成的代码可能包含大量你不想要的额外逻辑后者会约束它的输出范围贴近团队代码规范。第二条让 LLM 先给方案再写代码。对于非小量级任务我倾向于先让 LLM 给出实现思路或代码结构自己审查后再让它生成完整代码。这样最大程度避免方向性错误。比如先不要写代码。我要为订单模块增加一个超时关闭功能订单状态包含 PENDING、PAID、SHIPPED、COMPLETED、CANCELLED。超时关闭只适用于 PENDING 状态的订单。请给出推荐的技术方案包括定时任务选型、扫描 SQL 逻辑、状态变更边界以及潜在并发风险。这种提问方式下LLM 给出的回答可以帮助你做方案评审如果你直接让它写代码它很可能把一堆定时任务框架全给你塞进去。第三条把“验证责任”留在自己这边。这点类似于代码 Review。LLM 生成的代码你要默认它有问题而不是默认它正确。尤其要注意边界条件、异常处理、空指针、并发安全、事务边界这几个高发问题哪怕代码能跑通也不能直接合入主干分支。4. LLM 结对编程的完整实战案例接下来用一个贴近业务的小项目把整套过程串起来。场景如下开发一个“文章标签管理”模块后端使用 Spring Boot MyBatis Plus数据库使用 MySQL前端只需要提供 HTTP 接口不需要页面。我们目标是通过 LLM 结对编程完成需求分析、数据库表设计、后端代码生成、单元测试编写、常见问题修复五个环节。4.1 第一步需求澄清与任务拆解在打开编辑器之前先把需求整理清楚。推荐先和 LLM 做一轮需求澄清比如在对话窗口中输入你是熟悉 Spring Boot 和 MyBatis Plus 的资深开发。请帮我分析“文章标签管理”模块需要哪些接口和功能。 已知约束 1. 标签属于文章体系但标签本身独立维护。 2. 标签需要支持分页查询、新增、修改、删除。 3. 删除标签时如果已经有文章引用了该标签不能物理删除只能标记为禁用。 4. 标签名称在同一个标签分组下唯一。 5. 需要记录创建时间和更新时间。 请给出接口设计列表、每个接口的入参出参、数据库表结构建议以及潜在的边界情况。LLM 会返回一套结构完整的接口设计。重点不是直接采用而是对照自己的业务经验做调整。比如它可能漏了“分页查询需要支持按创建时间倒序”或者没有考虑“禁用标签不能重新启用”的业务细节。这一步的价值在于把模糊业务语言翻译成工程术语为后续代码生成提供精准输入。最终我们的接口清单如下接口功能备注GET /api/tags分页查询标签列表支持名称模糊查询、分组精确匹配POST /api/tags新增标签校验标签名不能为空、分组不能为空PUT /api/tags/{id}修改标签名称、分组、排序值可修改DELETE /api/tags/{id}删除标签若有文章引用标记 disabled1GET /api/tags/{id}查询标签详情返回基础信息4.2 第二步数据库表设计把上面的接口设计作为上下文继续让 LLM 给出建表语句。输入根据上面的接口设计请给出 MySQL 建表语句。表名使用 tag需要包含以下字段 - id主键 - name标签名称 - group_name标签分组 - sort排序值 - status状态 0-正常 1-禁用 - article_count文章引用数量冗余字段 - create_time、update_time时间字段 要求引擎使用 InnoDB字符集 utf8mb4补充适合的索引。得到的 SQL 类似CREATE TABLE tag ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, name varchar(64) NOT NULL COMMENT 标签名称, group_name varchar(32) NOT NULL DEFAULT default COMMENT 标签分组, sort int NOT NULL DEFAULT 0 COMMENT 排序值, status tinyint NOT NULL DEFAULT 0 COMMENT 状态0-正常 1-禁用, article_count int NOT NULL DEFAULT 0 COMMENT 文章引用数量, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_name_group (name, group_name), KEY idx_group_status (group_name, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT文章标签表;注意这个 sql 里的uk_name_group唯一索引正好满足需求中“同一个分组下标签名称唯一”的约束。4.3 第三步生成后端核心代码进入编辑器创建项目结构后开始让 LLM 生成核心代码。假设项目已经通过 Spring Initializr 创建好依赖包括spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java、lombok。我们逐层生成。Controller 层// 文件路径src/main/java/com/example/tagdemo/controller/TagController.java RestController RequestMapping(/api/tags) RequiredArgsConstructor public class TagController { private final TagService tagService; GetMapping public ResultPageResultTagVO page(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String name, RequestParam(required false) String groupName) { return Result.success(tagService.pageTags(pageNum, pageSize, name, groupName)); } GetMapping(/{id}) public ResultTagVO detail(PathVariable Long id) { return Result.success(tagService.getTagDetail(id)); } PostMapping public ResultVoid create(RequestBody Validated TagCreateRequest request) { tagService.createTag(request); return Result.success(); } PutMapping(/{id}) public ResultVoid update(PathVariable Long id, RequestBody Validated TagUpdateRequest request) { tagService.updateTag(id, request); return Result.success(); } DeleteMapping(/{id}) public ResultVoid delete(PathVariable Long id) { tagService.deleteTag(id); return Result.success(); } }Service 层// 文件路径src/main/java/com/example/tagdemo/service/TagService.java Service RequiredArgsConstructor public class TagService { private final TagMapper tagMapper; public PageResultTagVO pageTags(Integer pageNum, Integer pageSize, String name, String groupName) { PageTag page new Page(pageNum, pageSize); LambdaQueryWrapperTag wrapper new LambdaQueryWrapperTag() .like(StringUtils.hasText(name), Tag::getName, name) .eq(StringUtils.hasText(groupName), Tag::getGroupName, groupName) .orderByAsc(Tag::getSort) .orderByDesc(Tag::getCreateTime); PageTag tagPage tagMapper.selectPage(page, wrapper); ListTagVO records tagPage.getRecords().stream().map(TagVO::from).collect(Collectors.toList()); return new PageResult(records, tagPage.getTotal(), tagPage.getCurrent(), tagPage.getSize()); } public TagVO getTagDetail(Long id) { Tag tag tagMapper.selectById(id); if (tag null) { throw new BizException(标签不存在); } return TagVO.from(tag); } Transactional(rollbackFor Exception.class) public void createTag(TagCreateRequest request) { long count tagMapper.selectCount(new LambdaQueryWrapperTag() .eq(Tag::getName, request.getName()) .eq(Tag::getGroupName, request.getGroupName())); if (count 0) { throw new BizException(同分组下标签名称重复); } Tag tag new Tag(); tag.setName(request.getName()); tag.setGroupName(request.getGroupName()); tag.setSort(request.getSort() null ? 0 : request.getSort()); tag.setStatus(0); tagMapper.insert(tag); } Transactional(rollbackFor Exception.class) public void updateTag(Long id, TagUpdateRequest request) { Tag tag tagMapper.selectById(id); if (tag null) { throw new BizException(标签不存在); } tag.setName(request.getName()); tag.setGroupName(request.getGroupName()); tag.setSort(request.getSort()); tagMapper.updateById(tag); } Transactional(rollbackFor Exception.class) public void deleteTag(Long id) { Tag tag tagMapper.selectById(id); if (tag null) { throw new BizException(标签不存在); } if (tag.getArticleCount() ! null tag.getArticleCount() 0) { tag.setStatus(1); tagMapper.updateById(tag); return; } tagMapper.deleteById(id); } }实体类// 文件路径src/main/java/com/example/tagdemo/entity/Tag.java Data TableName(tag) public class Tag { TableId(type IdType.AUTO) private Long id; private String name; TableField(group_name) private String groupName; private Integer sort; private Integer status; TableField(article_count) private Integer articleCount; TableField(value create_time, fill FieldFill.INSERT) private LocalDateTime createTime; TableField(value update_time, fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }4.4 第四步运行与验证项目启动后用 curl 验证接口是否正常# 新增标签 curl -X POST http://localhost:8080/api/tags \ -H Content-Type: application/json \ -d {name:Java,groupName:后端} # 分页查询 curl http://localhost:8080/api/tags?pageNum1pageSize10groupName后端 # 重复新增同名标签应返回业务异常 curl -X POST http://localhost:8080/api/tags \ -H Content-Type: application/json \ -d {name:Java,groupName:后端}预期的第一个请求返回成功第二个请求返回标签列表包含刚才新增的记录第三个请求返回“同分组下标签名称重复”的错误信息。这个阶段重点验证两点接口是否能通唯一索引是否生效。4.5 第五步发现 LLM 代码中的经典坑上面代码能直接运行但认真看你会发现几个隐患。这也是使用 LLM 结对编程时必须养成的排查习惯。坑点一更新标签时没有做重复名校验。updateTag方法只更新了名称和分组但没检查修改后的名称是否和同分组下的其他标签冲突。如果用户把一个标签改成和另一个标签完全相同的名字数据库唯一索引会抛出异常而不是返回友好的业务提示。修复方式long count tagMapper.selectCount(new LambdaQueryWrapperTag() .eq(Tag::getName, request.getName()) .eq(Tag::getGroupName, request.getGroupName()) .ne(Tag::getId, id)); if (count 0) { throw new BizException(同分组下标签名称重复); }坑点二删除标签时的 articleCount 不是实时数据。示例代码约定了 article_count 是冗余字段但是否保证实时准确取决于文章标签关系表是否同步更新。如果没有同步机制删除时会误判为“无引用”而物理删除标签导致历史文章展示异常。纠正思路是删除标签前实时查一次引用关系表而不是依赖冗余字段。Long refCount articleTagMapper.selectCount(new LambdaQueryWrapperArticleTag() .eq(ArticleTag::getTagId, id)); if (refCount 0) { tag.setStatus(1); tagMapper.updateById(tag); return; }这类问题说明 LLM 会按“通常的业务直觉”生成代码但它不了解你项目里真实的数据同步逻辑需要你把约束补进 Prompt或者在 Review 时主动发现。5. 常见问题与排查思路LLM 结对编程过程中以下问题出现频率较高我把现象、原因、解决思路整理成一张表方便收藏备查。问题现象常见原因解决思路生成的代码风格与团队规范不一致没有在 Prompt 中提供团队代码规范在初始 Prompt 中加入“方法注释使用 Javadoc 风格”“Controller 中不写业务逻辑”等规范描述代码能跑通但逻辑边界不对需求分析不完整LLM 按默认场景推断在对话窗口中做需求澄清明确空值、并发、事务边界反复生成相同的错误代码上下文不断被重置没有利用对话中的错误反馈不要频繁开新窗口把最近一次错误结果直接粘贴并指出问题让 LLM 基于当前上下文修正补全建议打断了思维内联工具使用时机不合适在关键算法、复杂业务处手动禁用补全只把它用于样板代码AI 生成了不存在的 API 或已废弃方法训练语料时效性不足检查官方文档不要信任 LLM 对版本的具体描述遇到不确定 API 时另开对话询问或直接查源码让 LLM 修改代码结果破坏了其他功能未明确告知修改边界在 Prompt 中约束“只修改 X 方法其他文件不动”生成的 SQL 在大数据量下性能堪忧索引缺失、查询条件顺序不合理用 EXPLAIN 分析执行计划基于结果决策索引5.1 围绕“AI 报错”的排查清单如果 LLM 生成的代码在项目里连续报错推荐按下面顺序排查先看报错堆栈确定是哪一层出错。把报错信息原样粘贴给 LLM让它先解释原因而不是直接让它改。对 LLM 的回答保持怀疑验证它说的错误原因是否和堆栈中的行号对得上。查看 Git diff确认它改动了哪些区域是否有“越权修改”。单独用最小测试用例复现问题缩小范围。修复后顺手补一条单元测试防止后续再次被 LLM 改动破坏。5.2 什么时候不该用 LLM 写代码LLM 结对编程不是任何时候都适用。以下场景我建议优先人工处理涉及核心交易链路、金额计算、状态机流转的代码你本身不懂但业务影响面很大的模块需要严格性能优化的热点代码带有复杂安全约束的认证、鉴权、数据权限代码团队内部积累了独有业务规则但文档没有完整覆盖的模块。这些代码一旦写错代价很高宁可慢一点自己写也不需要引入额外不确定性。6. 从个人效率到团队协作最佳实践与工程建议很多人使用 LLM 结对编程一段时间后会进入舒适区什么问题都先问一下 AI代码能过测试就觉得万事大吉。但从团队工程角度出发这种“单兵 AI 作战”会带来几个比较隐蔽的问题。6.1 建立团队级 Prompt 规范如果整个团队都使用 AI 编码工具一套共享的 Prompt 规范是值得投入的。它不是让大家用一模一样的模板而是把高频约束固定下来。例如后端接口统一返回 Result 结构异常必须抛出 BizException不允许返回裸 HTTP 状态码数据库操作必须使用 MyBatis Plus 的 LambdaQueryWrapper方法级注释必须说明入参、出参、异常场景。把这些规范写进团队文档并在每次和 LLM 对话时附在上下文开头生成的代码会更接近团队习惯。尽管前期阻力存在但收益会随着次数累积越来越明显。6.2 强制 Review 小步提交LLM 生成的代码尤其要遵守“小步提交”原则。每次修改尽量控制在一个功能点生成完代码后立即 review测试通过后马上 commit。这样做的好处是如果 AI 改出了隐藏问题通过 Git 回退到上一个干净版本成本极低。Review 时可以清晰地看到 AI 修改的范围不容易漏掉非预期改动。后续如果 AI 上下文丢失你依然能通过 Commit 历史快速重建上下文。6.3 把 LLM 当成无限耐心的 Code Review 搭档除了生成代码LLM 在 Code Review 场景下的体验同样不可忽略。你完全可以把自己的代码粘贴给它让它从以下维度审查是否存在空指针隐患并发场景下是否有线程安全隐患数据库访问是否存在 N1 查询事务边界是否合理是否需要补充参数校验。例如请 review 下面这段代码重点检查并发安全和事务边界不要修改代码只输出问题列表和修改建议。 —— 粘贴代码 ——它给出的意见不需要全盘接受但可以作为 Review 清单的起点。尤其是对刚接手的新项目让 AI 先过一遍代码往往能发现自己忽略的细节。6.4 保持人的设计主导地位这里要格外强调一点LLM 适合做实现者但不适合做架构师。它没有你的项目背景、团队协作记忆、线上故障教训。它在面对设计取舍时往往倾向于“最常见的方式”而不是“你的项目最合适的方式”。所以凡是涉及模块划分、接口协议设计、数据库表关系设计、技术选型决策都应该由人来主导。你可以让 LLM 提供候选方案、帮你列出优缺点但最终拍板必须是自己。6.5 知识积累与 Prompt 沉淀最后分享一个实用经验把经常用到的有效 Prompt 保存起来形成自己的“AI 协作片段库”。比如你是一名擅长编写单元测试的 Java 开发。请为下面的 Service 方法补全单元测试。要求 1. 使用 Mockito 模拟依赖。 2. 覆盖正常路径、参数为空、依赖异常三种场景。 3. 测试方法命名使用 given_when_then 风格。 4. 断言优先使用 AssertJ。一段时间之后你的 Prompt 片段库会越来越厚写代码的速度也会因为复用这些高质量 Prompt 而明显提升。这本质上是把自己的工程经验重新组织成了 AI 可以理解的语言。7. 总结与下一步学习建议这篇文章从 LLM 结对编程的角色定位讲起聊了它的核心价值、环境准备、协作原则、完整实战案例、常见问题和团队实践。核心观点可以整理成几句话LLM 是最好的知识检索器和样板代码生成器但它是副驾驶不是机长需求拆解越清晰AI 输出越可控Review 和版本控制是使用 AI 写代码的安全底线团队要沉淀自己的 AI 协作规范而不是让每个成员各自为战。如果你打算继续深入学习建议按以下路径推进先熟悉内联补全工具把重复代码生成效率提上来。再练习高质量 Prompt 编写针对业务需求拆解和代码生成建立自己的模板。接着尝试 AI Agent 类工具让小范围内的跨文件重构自动化但只在 Git 分支上试运行。深入研究代码 Review 类用法用它辅助日常 Review提高代码质量。最后在团队内分享你的使用经验和踩坑案例推动形成统一的 AI 辅助开发规范。在正式项目中始终记住一点AI 生成的每一行代码在合入主干前都应该经过人的审视。你可以信任它的速度但不要盲目信任它的判断。LLM 结对编程的价值不在于替代开发者而在于把我们从低价值、高重复的编码细节中解放出来把更多精力留给真正的设计和决策。如果这篇文章对你有启发建议收藏备用。后续我会再整理一期关于“如何把 LLM 接入团队现有代码评审流程”的实践笔记到时可以继续交流。
返回列表