1. 项目背景16 万行代码从“手写”到“AI 协同”的转折点先交代一下背景。这个项目是一个中大型业务系统涵盖管理后台、用户端 API、定时任务、消息推送、数据对账等多个模块。按传统开发方式估算16 万行代码大概是一个 6 到 8 人团队忙活大半年的产出。但这次我们实际投入的核心开发只有 3 个人周期压缩到了 4 个月而且代码量还比最初预估的多出了将近 20%。这 16 万行不是硬凑出来的。它包括了前端 TypeScript 代码约 4.8 万行、后端 Go 代码约 6.2 万行、Python 脚本与数据迁移约 1.6 万行、SQL 与数据库迁移文件约 1.2 万行以及测试代码、配置文件和流水线定义约 2.2 万行。其中由 AI 直接生成或辅助生成的比例我后来粗略统计了一下大概在 70% 以上。但真正有意思的不是这个数字而是从“让 AI 写代码”到“把 AI 写代码这件事工程化”之间我们踩出来的那条路。如果你是刚接触 AI Coding 的开发者或者正在犹豫要不要把 AI 引入团队日常开发这篇文章会比较适合你。我会把从工具选型、规范制定、多智能体协作到代码审查、质量兜底、常见坑位排查的完整过程都拆开讲尽量少讲虚的多给能直接落地的东西。先说一个核心观点AI Coding 不等于“AI 帮你按回车”。早期我们确实走过一段“让 AI 自由发挥”的弯路结果就是代码风格混乱、模块边界模糊、接口调用像迷宫甚至出现同一个业务逻辑在三个文件里各写一遍的情况。后来才意识到AI Coding 能不能产生价值依赖的不是模型有多强而是你给它划定的工程边界有多清晰。当我们把这件事从“写代码”升级成“定义问题、编排任务、审查产物”的工程流程后效率才真正起了飞。这个过程就是所谓的从 AI Coding 到 AI Engineering。2. 整体设计拆解为什么 AI 写代码需要一套“工程宪法”2.1 没有约束的 AI Coding代码会失控到什么程度先聊聊我们吃过的一个亏。项目启动第一周我们图省事直接把一个模块的需求文档扔给 AI 工具让它“看着写”。产出的代码确实能跑单测也过了但问题出在几个隐蔽的地方。第一个是代码风格分裂。同一个项目的不同文件里有的用 interface 定义数据类型有的用 type 别名有的干脆直接 any错误处理一部分返回 error一部分返回 null一部分直接 panic。代码 review 的时候每个人都觉得“这不太对”但又说不出具体哪里不对因为每一段单独看都说得通。第二个是重复造轮子。用户权限校验这个逻辑AI 在我们没提醒的情况下先后写了三份一份在中间件里一份在 service 层还有一份直接写在 handler 里。表面上每份都能跑但改一个需求就要改三处漏改一处就是线上 bug。第三个是过度设计。AI 很喜欢把简单的功能包装成复杂的抽象。比如一个简单地按 ID 查询用户的操作它给封装了接口 实现类 泛型仓库 缓存装饰器还加了一层事件发布。看起来“干净”实际维护成本高得离谱。这些问题的根源在于AI 的训练数据来自海量开源项目而开源项目里既有写得好的也有写得烂的。如果不给它一套明确的、跟当前项目绑定的约束规范它就会在这些风格之间随机游走。所以我们的第一个结论是上 AI Coding 之前必须先有一份“工程宪法”。2.2 我们定义的“工程宪法”包含什么这份工程宪法不是泛泛而谈的开发规范而是围绕 AI 工作方式定制的。主要包括五个方面第一目录结构与分层约定。规定每个模块的标准结构是 handler、service、repository、model 四层谁可以依赖谁谁不能反向调用数据模型统一放在 model 下DTO 转换只在 handler 层做。这些约束写进文档只是第一步更重要的是要让 AI 在每次生成代码前都“读到”这份约定。第二命名风格与代码风格。统一使用小驼峰命名、类型定义用 interface、错误必须返回 error 值而非 panic、日志必须走统一封装、禁止直接打印。这些规则不难理解难的是让 AI 每次都遵守。我们的做法是把规范浓缩成一段固定提示词附在每个模块的生成请求里稍后会细说。第三接口优先原则。先定接口再写实现。这个原则特别适合 AI 协作因为接口一旦定死AI 生成的实现代码就很难跑偏。哪怕是临时的内部模块我们也强制要求先写接口签名和主要数据结构评审确认后再让 AI 填充实现。第四测试先行。每个业务逻辑模块必须附带至少一个测试文件测试用例由我们定义关键场景AI 负责补充边缘情况。我们不在 review 时讨论“要不要写测试”只讨论“测试够不够”。第五变更记录。AI 每次生成或修改代码必须输出一份简短的变更说明格式固定在每个代码文件头部注释里包含变更人AI、变更日期、变更动机。这个习惯刚开始觉得没必要后来排查问题时帮了大忙。2.3 为什么“工程化”比“选哪个工具”更重要很多人问我用的哪个 AI Coding 工具。说实话工具重要但没重要到决定成败。我们尝试过好几款主流产品包括 GitHub Copilot、Cursor、以及其他基于大模型的代码生成服务最终并行使用的是一个支持多文件上下文理解的 IDE 插件和一个可以在终端对话的 CLI 工具。前者负责写代码后者负责分析和重构。真正决定产出的是我们在每个工具外面包了一层“工程壳”。也就是上面说的规范、任务拆解方式、审查流程、反馈闭环。同样的模型有人拿它写出来的东西是一堆漂亮的垃圾有人拿它写出来的东西可以直接进生产环境。差距不在模型在工程流程。这也是从 AI Coding 到 AI Engineering 的第一层转变从“我能用 AI 写代码”变成“我能让 AI 稳定地产出符合项目标准的代码”。前者是个人技巧后者是团队能力。3. 核心细节与实操要点任务拆解、提示词模板、代码审查3.1 任务拆解把“大需求”切成“AI 能理解的中块”AI 上下文窗口越来越大但并不意味着你可以把整个项目的需求一次性扔进去。实践中我们发现最有效的拆解粒度是“一个能独立交付的业务模块”比如 “用户注册接口”、“订单列表查询”、“定时对账任务”。粒度太大AI 容易迷失生成代码与已有架构脱节粒度太小频繁切换上下文消耗大量时间效率反而不如手写。以用户注册接口为例我们拆成下面这些子任务定义 User 结构体和数据库迁移文件字段包括 ID、手机号、密码哈希、昵称、状态、创建时间、更新时间。编写注册接口的 handler负责解析请求参数、调用 service、返回统一响应结构。编写注册逻辑的 service包含手机号查重、密码哈希、验证码校验、创建用户、发送欢迎通知。编写 repository 层负责用户数据的增查事务处理由 service 层传入的 transaction 对象控制。补充注册流程的测试用例覆盖验证码过期、手机号已注册、密码强度不足等场景。每一个子任务都对应一次独立的 AI 会话。会话开始前我们先把接口定义和相关的 model 结构贴进去然后才是具体的任务描述。这样 AI 生成的代码基本能跟已有代码对齐。实际操作中我用的是这种三段式提示词结构第一段是上下文包括项目分层说明、相关文件路径、本模块要遵循的接口定义。第二段是任务描述包括输入输出格式、功能点、异常场景处理要求。第三段是约束条件包括禁止使用某种写法、必须补充哪些日志、测试要求等。举个例子生成用户注册 service 时的提示词大致长这样背景这是一个使用 Go 语言开发的管理系统标准分层为 handler / service / repository / model。 现有文件 - internal/model/user.go - internal/repository/user_repo.go 请实现 internal/service/user_service.go 中的 Register 方法。 需求 - 输入参数为 RegisterRequestPhone、Password、VerifyCode - 校验验证码验证码从 Redis 读取key 为 verify:register:手机号不匹配返回业务错误 ErrVerifyCodeInvalid - 校验手机号是否已注册已注册返回 ErrPhoneExists - 密码使用 bcrypt 哈希后存储 - 创建用户后通过消息队列发送一条欢迎通知消费方在其他模块 - 返回 userId 约束 - 错误必须返回业务错误码禁止直接返回裸 error - 日志统一使用 logger.Info / logger.Error禁止使用 fmt.Println - 不修改 handler 与 repository 接口这种结构下AI 生成的第一版代码质量已经比之前“自由发挥”高了不止一个档次。原因也好理解它看到了边界知道哪些事情不能做自然不会跑偏。3.2 多智能体协作架构师、实现者、审查者分工项目中期我们引入了多智能体协作的玩法。所谓多智能体其实就是让不同角色定位的 AI 各司其职而不是一个 AI 从头包到尾。概括起来分为三类架构师型 AI、实现者型 AI、审查者型 AI。架构师型 AI 的任务是根据需求文档生成模块设计方案。它不写具体业务代码只产出数据结构定义、接口签名、模块边界、关键流程描述。我们用的是带较长上下文理解能力的对话模型把项目全局规范和本次需求贴进去让它输出一份设计文档再进行人工评审。实现者型 AI 就是我们前面说的主力输出它会严格按照评审通过的设计文档生成代码。这里有很关键的一点设计文档一旦确认实现环节不允许 AI 擅自改动接口签名和数据结构。如果它发现设计中存在问题可以提出来但修改必须回到架构师环节重新评审。审查者型 AI 则专门挑毛病。在正式代码审查开始前我们先用 AI 对 AI 产出的代码做一轮静态检查。检查内容包括是否漏了错误处理、是否有重复代码、是否遵循分层规范、是否有明显的并发安全问题、是否有资源未释放。审查者输出的是一份问题清单标出问题位置、原因、修改建议。这三类角色由同一个底层模型驱动的不同配置组合来承担区别在于上下文附加内容和输出格式要求不同。分角色之后一个很直观的改变是实现者不会一边写代码一边“想太多”去调整架构审查者不会因为“代码是我自己写的”而手下留情。不过这里要提醒一句不要期待全自动闭环。目前的多智能体协作流程编排还是得靠人来控制。我们就用过工作流引擎把这些 Agent 串起来但很快发现需求理解偏差、边界情况处理、规范冲突这些问题当前模型还做不到自我仲裁。最终我们的做法是“人定规则AI 跑流程人工管异常”。执行层面的重复劳动交给 AI涉及判断和决策的场景必须有人把关。3.3 AI 辅助生成代码后的快速自查清单AI 生成代码后不是直接提交就完事。我们总结了一份快速自查清单每次提交前过一遍能挡住大部分低质量问题。清单如下接口签名是否与设计文档一致有没有偷偷改了参数类型或返回值错误处理是否完整有没有直接把 err 忽略掉比如_ doSomething()是否引入了超出项目依赖范围的新库如果引入了是必要的吗日志有没有包含足够上下文比如用户 ID、订单号、请求追踪 ID有没有直接用魔法数字或魔法字符串而不定义常量有没有把敏感信息打日志数据查询是否可能出现 N1 问题并发场景下有没有共享可变状态新加的代码有没有对应的测试覆盖这份清单不是给人“朗诵”的而是作为输入条件喂给审查者型 AI让它按清单逐项检查。同时人在做代码审查时可以只关注逻辑设计层面不用浪费时间挑风格和数据格式问题效率提升明显。4. 实操过程与核心环节实现从空目录到一个模块完整落地的全记录4.1 实操场景做一个月度统计报表模块为了把过程说得更具体我拿一个实际做过的“月度统计报表模块”作为例子完整走一遍实操流程包括所有关键代码生成、配置方法和组织方式。模块需求一句话就能说清每个月 1 号凌晨统计上个自然月的订单金额、订单量、用户增量、退款量、退款率产出汇总数据并写入报表表。先分析一下这个需求的特点逻辑不复杂但涉及从业务库读取大量数据、做聚合计算、清洗去重、最后写表。如果让 AI 直接写它很可能会写出一个个非常直观但性能糟糕的查询比如在循环里关联查询用户资料、统计订单时把全月订单一次性捞到内存里再慢慢算。所以我们先让架构师型 AI 产出模块设计。设计文档文档包含四个部分数据源说明、计算口径、表结构定义、调度触发方式。数据源这边订单表、用户表、退款表都在同一个 MySQL 集群里但订单表数据量比较大月度统计时全表扫描会拖慢主库所以统计逻辑不再直接查业务表而是从提前同步好的数据仓库宽表读取。计算口径方面订单金额按照支付成功这个状态来统计退款金额按照退款完成这个状态统计用户增量按每天新增用户数去重后求和。表结构是核心我们统一使用月度、订单总额、订单量、用户增量、退款总额、退款率、统计时间这几个字段。调度方式用系统自带的多节点定时任务串行执行避免重复统计。这份设计文档前后走了一轮人工评审主要是我们把统计口径里“订单金额按支付成功状态”确认了一遍。这个过程比较重要如果口径错了后面 AI 写出来的查询再漂亮也没用。4.2 分步生成代码与配置设计定稿后开始实现。整个模块的代码量大概在 800 行左右分成这样几个文件internal/model/monthly_report.go定义 MonthlyReport 结构体字段同上。internal/repository/monthly_report_repo.go负责查询宽表数据、写入报表表。internal/service/monthly_report_service.go核心计算逻辑、数据组装与落库。cmd/job/monthly_report.go定时任务入口调用 service。internal/service/monthly_report_service_test.go配套测试。我们用一次会话让 AI 生成 model 和 repository另一次会话生成 service再一次会话生成定时任务入口和测试。每次生成前都把上一轮生成的代码文件内容贴进去保证上下文连续。举个例子repository 层的关键代码在生成时我们额外要求考虑分页查询与合并避免一次加载全量数据。AI 当时给出的方案是把订单数据按天分批查询每天的数据汇总后合并到内存中而不是拉全月数据。这个方案符合我们的要求也符合数据量实际情况。代码片段大致是这样func (r *MonthlyReportRepo) FetchDailyOrderStats(ctx context.Context, start, end time.Time) ([]DailyOrderStat, error) { var stats []DailyOrderStat for day : start; !day.After(end); day day.AddDate(0, 0, 1) { dayStart : time.Date(day.Year(), day.Month(), day.Day(), 0, 0, 0, 0, time.Local) dayEnd : dayStart.AddDate(0, 0, 1) rows, err : r.db.QueryContext(ctx, SELECT COUNT(*) AS order_count, COALESCE(SUM(total_amount), 0) AS order_amount, COUNT(DISTINCT user_id) AS user_count FROM order_info WHERE pay_status ? AND pay_time ? AND pay_time ? , payStatusPaid, dayStart, dayEnd) ... } return stats, nil }service 层里拿到这些每日统计之后再进行求和与聚合。这里 AI 一开始忘记处理空数据场景也就是某天完全没有订单时的除零问题。我们通过审查者 AI 的检查捕捉到了这个点要求它补上当订单量为 0 时的默认处理逻辑。关键部分在于它是怎么被发现的。审查者型 AI 在检查的时候看到stat.OrderCount计算退款率时的除法直接标注“分母可能为 0”并给出修改建议。我们确认后让实现者 AI 改了计算方式加上了空数据保护。类似这种节点上的“机器把关”如果完全靠人肉 review 也不是不行但十几万行代码下来注意力会被消耗得很厉害漏网之鱼会越来越多。4.3 流水线与任务的衔接代码写完只是第一步定时任务要接入流水线、把依赖的配置、编译产物、部署方式都做好。我们用的 CI/CD 流水线是 Jenkins项目里新增了一个从上而下的任务配置把cmd/job/monthly_report.go编译为独立二进制部署到任务机上然后注册到调度中心。调度时间配置为每月 1 日凌晨 1 点避开业务高峰期。首次上线时我们还手动触发了一次任务核对报表数据与手工统计结果确认无误后才放开自动调度。这一步不能省AI 写出来的计算逻辑哪怕代码层面全对也得拿真实业务数据验证“算得对不对”。我们第一次跑就发现了口径不一致的问题宽表里的订单状态枚举跟线上业务库的不完全一致导致部分订单没有被统计进去。这个坑不是代码逻辑问题而是数据口径对齐问题但如果不实际跑一次靠看代码根本发现不了。4.4 任务重试机制的设计定时任务还有一个特殊场景需要处理如果任务执行到一半挂了比如数据库连接超时、依赖的数据源还没就绪重跑时不能重复统计、不能漏统计。为了让 AI 写出符合预期的任务代码我们在提示词里明确要求使用“先清后插”的策略统计开始前清空目标月份历史数据全部重新计算。同时在报表表上对month字段建了唯一索引重复跑的话同一月份只会有一条记录用INSERT ... ON DUPLICATE KEY UPDATE保证幂等。这个设计是我们在做第一个定时任务模块时踩过坑之后总结出来的。当时没有幂等设计结果任务重跑一次后报表数据翻倍排查了很久才发现是重复插入。后来所有批量写数据的任务都统一按这个模式来省了很多事。5. 常见问题与排查技巧实录AI Coding 项目里绕不开的坑5.1 代码质量下降是 AI 的锅还是流程的锅相关热搜里有人问“AI Coding 的到来会不会让代码质量下降”。我的答案是会如果你不干预的话。AI 默认产物通常有“能用但混沌”的特征它擅长在一个文件里完成你要求的功能但不擅长在十几个文件的复杂项目里维持风格统一和逻辑一致性。我们做了一次实验同一个模块一组用 AI 直接生成并提交另一组用我们前面说的完整流程规范约束 设计先行 审查闭环来做。两周后对比直接生成那组的缺陷密度大概是完整流程组的 2.6 倍其中很大一部分集中在重复代码、遗漏错误处理、违反分层依赖这些方面。这不是 AI 能力问题而是工程系统设计问题。如果你把 AI 当成一个水平不差但容易忘事的新同事那你要做的就是给它明确的工牌、统一的代码规范、清晰的任务清单、认真的 code review。如果你直接让它放飞自我那代码质量只能靠运气。所以与其问“AI 会不会让代码质量下降”不如问“我的交付流程是否已经适配了 AI 协作”。5.2 提示词写了AI 还是乱来怎么办我们经常收到类似的抱怨“我已经把规范写得很清楚了AI 还是无视乱定义接口。”这种情况很常见尤其是在上下文窗口较长、中间穿插多轮修改的时候。排查思路有三步第一步确认提示词是否在“生成动作”附近。把规范放对话开头AI 生成到一半时它可能已经把早期上下文淡忘了。我们的做法是核心约束在每次输入中都重复一遍宁可牺牲一点 token也要保证关键规则在生成的前部出现。第二步确认是否存在规则的互相矛盾。比如你既说“所有错误统一返回业务错误码”又在示例代码里写了return err。AI 会倾向学习示例代码而不是抽象规则。所以给示例代码的时候必须保证示例本身就是符合规范的否则就是自己拆自己的台。第三步确认输出的可验证性。如果 AI 遵守了规则你要能通过某个方式确认。我们的做法是加脚本检查比如通过正则扫描新代码文件检查是否有裸fmt.Println、是否有未注册的路由函数、是否缺少注释头。只要反馈是一个自动化的“通过/不通过”AI 的遵守率会明显提升因为模型天然会避免被判定为“错误”。5.3 上下文窗口不够用模块之间开始打架做大型模块的时候AI 的上下文窗口确实紧张。我们遇到过一个情况用 AI 维护一个包含十几个函数的 service 文件前面几轮还能正常修改到后面它突然把之前约定好的某个方法名改了因为它们之间的关联信息超出了上下文记忆范围。应对方式有几个。第一尽量拆小文件一个文件职责单一、行数控制在 300 行以内。第二修改之前先把关键接口定义和调用关系贴给 AI让它重新加载。第三重要结构用单独的文件存一份“契约快照”每轮操作开始时让 AI 先读快照在快照基础上修改而不是靠对话历史记忆。我们最终的规范是当单次会话无法承载整个模块时不要硬刚直接换一个新会话把必要的上下文重新粘进去。这样多花几秒但产出的质量稳定得多。5.4 测试覆盖率虚高但业务漏洞不少AI 很会写测试写出来的测试往往覆盖了所有方法。但问题是这些测试大多是按实现逻辑生成的用例实现如果有逻辑漏洞测试也发现不了因为测试是和实现一起生成的天然同源。比如一个统计函数把写成AI 生成的测试用例里也跟着用跑起来全绿但实际边界值少算了一条。可以靠审查者型 AI 从业务场景独立生成测试用例尽量从需求出发而不是从实现出发。最简单的办法是让两个不同会话分别生成实现和测试测试会话只知道需求文档不看实现代码。这个“测试与实现隔离”的做法我们强力推荐。5.5 线上出了问题如何快速定位是 AI 写的还是人为埋的还有个实际场景。项目后期代码量上来了真出问题的时候第一反应总是“是不是 AI 写的这段有问题”。但实践经验告诉我们除非是低级的逻辑边界错误否则 AI 生成的代码出问题的原因大概率跟人写代码出问题的原因是一样的——需求理解偏差、接口约定变更、数据异常导致状态机走到未预期分支。我们的排查流程是这样的先看变更注释前面提到 AI 生成代码会把变更信息写进文件头这里派上了用场确定这段代码是什么时候新增/修改的、由哪个会话产生的然后看关联的测试用例判断测试是否覆盖了出问题的分支再看线上日志结合 trace ID 把请求链路串出来。这套流程跟排查人工代码差别不大只是 AI 代码的文件头注释更规整变更有据可查反而比某些同事的提交记录更清晰。5.6 常见问题速查表症状可能原因解决方式AI 生成的代码风格飘忽缺少统一规范约束在提示词中附上项目规范通过脚本检查风格改了需求 AI 却按旧逻辑实现上下文窗口记忆丢失新开会话重新粘入关键接口和现状描述代码全绿但业务数据不对实现与测试同源缺陷测试生成与实现生成隔离按需求描述独立写测试重复代码过多任务拆解粒度不合适把公共逻辑先抽象成独立模块再让 AI 引用使用了未声明的新依赖依赖声明约束缺失在提示词里写明“禁止新增依赖”或通过依赖扫描工具拦截接口调用链断裂设计稿与实现稿不同步设计阶段先定接口并评审实现阶段禁止改接口6. 一点个人体会做这个项目之前我对 AI Coding 的态度是“能用但只能用来补点工具脚本”。做完 16 万行之后我的想法变成了AI Coding 不是“帮你写代码”那么简单它其实是逼着你把工程流程想得更清楚。因为当产出代码的速度被 AI 放大了十倍你花在定义问题、划定边界、审查结果上的精力也得以同样幅度提升否则混乱和 bug 也会被放大十倍。具体到团队落地如果让我给一个最简短的建议那就是先把“工程宪法”写好再让 AI 开工先让一个人做全流程试点把规范调顺了再推广到团队。不要在没有任何约束的情况下直接把 AI 扔给全员也别指望招一个“会用 AI 的人”就能解决所有问题AI 只是放大器你原来的工程水平决定它放大的是价值还是风险。最后再分享一个小技巧。如果团队里有人觉得 AI 生成的代码“不靠谱”又说不清哪里不靠谱那就让他做一次审查者型 AI 的提示词设计。当他把“哪些代码是不可以接受的”逐条写清楚时他其实已经把自己对工程质量的理解系统化了一遍。这比任何培训都有效也是我们从 AI Coding 走向 AI Engineering 过程中最意外的收获。
