周一早上的代码评审,我把一个AI生成的PR翻来覆去看了几遍,鼠标停在Merge按钮上悬空了十几秒,最后还是收回来了。队友在群里问我:现在有AI编程了,写代码几秒钟的事,你怎么合并反而变慢了?我一下子不知道怎么回答。用AI写代码,从输入提示词到拿到一版能跑的代码,现在确实只要几分钟;可到了真正要Merge的那一刻,我心里比手写代码的时候更没底。这种反差应该不少人有共鸣:生成代码的门槛归零了,合并代码的心理门槛却越来越高。
这篇文章围绕这个矛盾展开,聊聊AI编程到底改变了什么、没有改变什么,以及我这两年是怎么处理AI生成代码、让它能真正进入仓库的。我先后用AI写过接口、算法脚本、模型推理代码和内部工具,踩过的坑不算少,沉淀下来的流程也不复杂,但确实管用。适合正在用AI编程的开发者、独立接项目的自由开发者,以及需要审核AI代码的技术负责人参考。
1. 真实处境:门槛降了,但审核门槛没降
1.1 说“代码门槛归零”时,我们到底指什么
先承认一个事实:AI确实把“写出一段代码”这件事变得极其便宜。过去一个新入行的同学要上手一个业务模块,得先熟悉语言语法、框架约定、调试手段,没有三五天进不了状态。现在只要把需求描述清楚,AI几十秒就能交出一份看起来完整的实现,甚至带注释带测试。很多热词里的场景,比如让AI写mobilenetv2的demo、写LSTM模型、写Python量化回测脚本,确实就是几行提示词的事,生成结果很多时候运行起来很顺畅。
但这里要分清一个概念:“写出代码”和“让代码正确运行并长期维护”是两个维度。AI生成的是文本,是静态的产物;而工程要面对的是运行环境、并发冲突、异常分支、依赖版本、团队协作这些动态信息。翻译软件出现以后,看外文资料的门槛也降了,但没有人会把机器翻译的合同直接拿去签字盖章。代码比合同更复杂,因为它不是给人看的一份说明,而是要交给机器执行的指令序列,任何一个隐藏的错误都可能在生产环境里放大。
所以我说门槛归零,其实只归零了“打字层”的门槛,让一个不懂语法的人也能产出格式规范的代码。但“读代码”的能力没有因此变得无关,反而变成了更核心的瓶颈。很多新人拿到AI生成的代码,第一反应是好厉害、能跑,第二反应是如果出了问题该改哪一行,完全没有头绪。这不是他的问题,而是工具特性决定的:生成速度快,不代表理解成本低。
1.2 从“写作者”到“审查者”,责任没有转移
我再往深处说一个感受:以前手写代码,每一行逻辑都经过自己的脑子和手,Merge之前对代码是有“肌肉记忆”的。哪怕不回头细看,也知道这个函数在哪个模块、依赖了哪个数据、哪里可能兜底不足。现在AI几十分钟生成几百行代码,我变成了一个面对陌生代码的审查人员,既没有“这是我自己写出来”的熟悉感,却依然要承担“这是我提交上去的”的全部责任。
这种“所有权错位”是很多人越来越不敢Merge的第一层心理根源。我在实际项目里见过不止一次:功能开发得很顺利,AI把主体代码都写出来了,但到了合并阶段,负责的同事突然变得很审慎,反复改注释、反复跑测试,进度反而比纯手写更慢。你说他偷懒吗?不是,他是真的在努力搞懂这段他并不熟悉的代码。团队复盘出问题时,流程只认提交人,不会认“这段是AI写的”。所以责任从来没有被AI分担走,它只把“写”的动作外包了,“负责”这两个字还在人身上。
想通这一点之后,我的心态反而平稳了。既然角色已经从写作者变成审查者,那就按审查者的思路来:不追求一次看懂所有代码,而是建立一套检查流程,让AI的产出在流程里被验证、被修整,直到它配得上我按下Merge按钮。接下来要谈的三类问题,是我认为“不敢Merge”的最具代表性根因,也是这套流程要解决的目标。
2. 不敢Merge的根因:AI生成代码的三大盲区
2.1 幻觉:一本正经地埋雷
AI生成代码最危险的地方不是它写不出来,而是它会一本正经地编写不存在的API、不存在的工具类,甚至编出一段逻辑自洽但完全错误的算法。我用过一个很典型的例子:让AI写一个分页查询函数,生成的代码里有一个工具方法调用DataPageUtil.trimPage,类名、参数、返回值风格看起来都很规范,注释也写得很完整,可整个项目里根本没有这个类。这种错误编译期就会暴露,还算运气好。
更麻烦的是那种编译能过、测试能过,但逻辑上有细微偏差的代码。比如让AI实现一个排序算法,它在循环里加了一个看起来是优化的判断,其实改变了排序的稳定性;又比如让AI解析日期字符串,它默认了某种时间格式,遇到用户输入的其他格式就静默返回空值。AI本质上是概率式地生成符合训练数据风格的文本,它不是在执行严格的编译推导,而是在“填词”。这意味着幻觉发生的概率不是零,而是分布在任何一个语句里,你无法预判它藏在哪一行。
所以我的第一个结论是:永远不要相信AI的“自检”。不少工具会给生成结果附上“我已经检查过”的说明,但在当前的工程实践里,这种自检对语义级错误的敏感度非常有限。Diff审查和独立测试,依然是人类该承担的工作。
2.2 环境与依赖:AI看不到你的“这栋楼”
第二个盲区是环境割裂。代码不是悬空的文字,它要在特定的操作系统、编程语言版本、第三方库环境、运行时配置里跑起来。AI能读到你的提示词,但读不到你机器上装的是Python 3.9还是3.11,也读不到项目里用的是Spring Boot 2.7还是3.2,更读不到团队的其他成员各自在什么环境下工作。
我遇到过一个很典型的场景:AI生成的数据导出脚本里,随手import了新版pandas的某个接口。我在开发机上跑得好好的,但同事在Windows机器上拉下来一跑,直接弹窗提示“由于找不到msvcp140.dll无法继续执行代码”,排查了半天才发现是Python版本和VC运行库不一致导致的。还有一次AI给我生成了一段请求封装,它默认新版本库的API风格,但项目锁文件里还是老版本,一运行就AttributeError。这种问题在代码审查里很难发现,因为它的逻辑没有错,错在“生态契约”上。
在多人协作的团队里,这种环境问题足以让一个PR躺三天。AI不像人,它不会问你“咱们项目用的是什么依赖管理工具”,它只会基于训练数据里的最常见做法给你一个“最合理猜测”。所以我现在对AI代码的依赖变更特别敏感,合并前先看依赖锁文件有没有悄悄多出来或者被改动的东西。
2.3 只写“理想路”,不写边界路
第三个盲区和稳定性有关。训练语料里的教程代码,绝大多数是理想输入下的happy path,输入合法、网络正常、返回值存在。但真实系统最吃紧的恰恰是边界情况:空列表、None值、超时、重入、并发写、文件不存在、编码不一致。
比如让AI写量化交易策略代码,回测阶段用前复权数据跑得收益曲线很漂亮,但没有处理停牌、没有处理涨跌停、没有处理数据缺失,实盘一跑就是另一种结果。又比如让AI写一个读取文件并解析的脚本,它大概率不会处理文件不存在、超大文件内存溢出、编码混乱的情况。这些东西不是AI故意漏掉,而是训练数据里根本没有足够的负样本,AI不知道“工程代码”需要覆盖大量异常路径,因为教程里通常不写那些。
这些边界恰恰是Merge真正需要守护的东西。很多时候我盯着AI生成的主逻辑觉得没问题,却越想越心虚,就是因为我知道:真正让它崩掉的不会是那行漂亮的列表推导式,而是一个没被兜住的极端输入。下面这套工作流,核心就是把这三大盲区一个个堵上。
3. 驯服AI代码:让它配得上Merge按钮的安全工作流
3.1 提示词不是“帮我写个功能”,而是“给我一组验收条件”
很多人的提示词是“帮我写一个带重试的HTTP请求函数”,这种宽泛描述等于把全部决策权交给了AI,然后人类再去猜它做出了什么决策。我的做法是把提示词写成验收标准,把所有可验证的约束提前列清楚。一个实际的例子:
请用Python写一个带重试的HTTP请求函数: - 入口参数:URL、headers、timeout、max_retries - 行为要求:状态码为5xx或连接超时时,按指数退避(1秒、2秒、4秒)重试,最多3次 - 输出约定:成功返回响应文本;最终失败抛出自定义的RequestFailedError - 依赖约束:只能使用requests库,不得引入未声明的第三方依赖 - 附加要求:生成pytest测试用例,覆盖超时和5xx两种场景这样写提示词有两个好处。第一,AI生成的代码不再是“自由发挥”,而是往一个能被人类检查的框架里填肉;第二,审查时我能拿着这些验收条件逐条对着代码打钩,而不是漫无目的地通读。如果你觉得AI的输出经常不符合预期,可以先反省一下提示词里到底有没有给出可衡量的标准。“AI编程提示词”这个热词在社区里很火,看得多了就会发现,真正拉开差距的不是提示词写得花哨,而是约束写得足够具体。
当然,提示词不是银弹。模型仍然可能曲解你的意图,但至少失误的方向会被压缩到一个可控范围。我习惯在提示词末尾加一句“不要修改我未要求改动的部分”,这句话能在不少场景里拦住AI顺手重构别人代码的冲动。
3.2 小步提交:让AI代码以“可理解的增量”进入仓库
第二个关键习惯是拆分。很多人让AI一次性生成一个几百行的大模块,然后当作一个巨大PR提交。这种做法的后果是评审者面对一大堆陌生代码,几乎不可能逐行吃透;任何一处幻觉都可能被淹没在海量diff里,Merge的阻力自然居高不下。
我的做法是把需求拆成多个可独立验证的小步骤:先让AI生成函数签名和类型标注,审一眼结构再继续;接着生成主逻辑,单独跑一遍主路径;再补异常分支和边界处理;最后才让AI补测试用例。每一个增量都保持在可理解的尺寸内,出了任何问题都能很快定位。这本质上是代码解耦的实践,AI只是替我把每一小块更快地写出来,但“切碎”这个动作必须由人来主导。
在git操作层面,小步提交也能显著减轻Merge冲突的规模和次数。一个只改了一个函数的小分支,比一个横跨十几个模块的大分支更容易同步主干、更容易解决冲突。我给自己定过一个很粗的经验值:AI单次生成的代码如果超过300行,就要主动停下来做一次拆分和审查,而不是往下继续堆功能。
3.3 Merge前的四道关卡
不管AI生成的代码看起来多顺畅,我会在Merge前强制走四道关卡,缺一不可。
第一关,diff审查。不直接看整个文件,而是用git diff只看本次改动引入了哪些行。对每个新出现的符号、函数调用、依赖引用做一次“溯源”:项目里有没有这个定义?调用链是否走得通?在这个环节,现代IDE的“跳转到定义”“查找所有引用”“查看调用层级”功能非常好用,类似source insight在大型C/C++工程里做全局浏览的能力,能快速定位AI生成的代码在真实调用关系里的位置。工具本身不是难点,关键是你要顺着调用链真的走一遍,而不是只看这一个文件。
第二关,测试补丁。运行项目现有的测试套件,同时把AI生成的测试用例也跑起来。这里我有个具体操作:让AI先生成测试用例,再让我自己额外补两三个边界用例,比如空输入、异常参数、并发调用。如果AI的测试只覆盖happy path,说明它对边界仍然没有概念,需要人工补上。
第三关,边界枚举。针对改动涉及的核心函数,列一个极值清单:输入为null、空字符串、超长字符串、重复调用、高并发、外部依赖超时,逐项确认行为是否符合预期。这一步不用全部自动化,可以用临时脚本或者测试用例快速跑,关键是覆盖“AI想不到”的那些路径。
第四关,性能抽查。对涉及循环、大数据量、频繁IO的改动,做一个粗略的性能对比,确认没有明显退化。AI在某些场景下会生成看起来很优雅但复杂度爆炸的写法,比如不该有的双层循环、频繁的深拷贝,这一步能拦住这类问题。
这四关做完,我才会把注意力放到Merge本身的操作上。
3.4 处理AI代码的Merge冲突:rebase和merge的取舍
回到“Merge”这个核心操作,先说一个被问过很多次的问题:功能分支要并入主干时,到底用git merge还是git rebase。我的建议是按团队协作习惯来,但如果是合入AI生成的大段代码,我比较倾向先把功能分支rebase到最新主干上,而不是直接merge主干进分支。原因很实际:rebase能把冲突提前拆成一个个小块,在一次rebase过程中逐个处理,比最后统一merge时面对一个巨大的冲突集合要可控得多。
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 功能分支长期开发、需保留完整上下文 | git merge | 历史完整,便于回溯实验记录 |
| 频繁同步主干、希望历史线性清晰 | git rebase | 冲突提前分步处理,降低一次合入的风险 |
| AI生成的多轮修改准备进主干 | squash合并 | 将杂乱的中间提交压缩成一个原子提交,便于回滚 |
| 共享分支、多人同时基于它开发 | 不要rebase | 改写历史会导致其他人的本地状态错乱 |
用rebase处理冲突时有一个重要原则:不要盲目选择ours或theirs。AI生成的代码里“看似合理的死代码”太多,冲突的一侧可能是同事认真调试过的实现,另一侧是AI根据它的语料生成的替代方案,哪个语义更正确必须逐个判断。我见过有人图省事,在冲突界面直接选了AI那侧,结果把一个同事已经修正过的配置覆盖掉,上线后功能开关直接异常。处理冲突时,我会把两边的代码打开对比,确认每一处保留的语义都说得通。
另外强调一点:推送代码时不要随便用“强制覆盖本地代码”的组合拳,也尽量不要对共享分支做force push。如果本地代码和远端有分叉,先stash或另开分支,再在干净的worktree上处理,否则队友的提交可能人间蒸发。这些git纪律和AI无关,但AI流程里尤其容易在混乱中被忽略。
4. 实战记录:差点让Merge翻车的三个事故
4.1 事故一:一个JSON Merge Conflict吞掉同事配置
有一次,两个功能分支同时往主干的配置文件里加内容。一个分支是同事手工加的开关,另一个分支是AI生成代码时顺手补的默认配置。两个分支合到一起时,同一把JSON key下存在不同默认值,自动合并失败,需要人工解决。负责解决的人对AI生成的那段配置更眼熟,就直接保留了那侧,结果把同事的开关名删掉了。功能上线后,相关模块的开关一直读不到配置,查了好久才发现是Merge冲突时误删导致的。
这个事故的教训有两个层面。第一,配置文件尽量单独提交、单独走PR,不要混在功能代码里;并行编辑同一文件是冲突的根源。第二,解决JSON conflict时,要回到语义层面看这个key是给谁用的、默认值从哪里来,而不是机械地二选一。现在团队里有一个约定:凡是AI生成的代码里包含配置信息,我会重点检查它是否新增了“多余但看似有理”的字段,因为那些字段往往是冲突埋点。
4.2 事故二:依赖悄悄升级,队友机器上“无法继续执行代码”
另一个印象深刻的坑和依赖有关。AI生成的数据处理脚本在开发机上运行正常,但提交后同事拉下来跑,报了一个“由于找不到msvcp140.dll无法继续执行代码”的提示。刚开始大家以为是Windows运行库的问题,让同事装了VC运行库还是不行,最后才发现AI生成的代码import了新版pandas,依赖版本和我本地的环境不一致,我的requirements.lock没有及时更新进去。
这个问题最麻烦的地方在于:它不在逻辑审查的视野内。diff看起来只是多了几行import,但背后是依赖生态的漂移。那次之后我形成了一个习惯:AI生成的任何代码进了本地工作区,第一步不是看逻辑,而是看依赖文件有没有发生变化,发生变化就必须单独说明理由。在团队层面,后来也推进了线上运行环境的镜像化,把依赖固定到镜像里,这类事故就几乎不再出现了。
4.3 事故三:AI引入旧版库,让流水线被安全扫描拦截
还有一个来自第三方的坑。当时要让程序调用一个Spring工具类,AI在pom里自动加了一个旧版本的依赖。逻辑上没问题,编译也过了,但持续集成里的依赖安全扫描扫出了问题,具体是一个公开披露的漏洞,编号CVE-2024-38819。构建流水线直接拦了下来,PR就挂在那边进不去。后来人工一看,那个依赖根本没必要新增,删掉就解决了。
这件事给我的提醒是:AI的依赖选择常常是“训练集里的陈旧偏好”,它对项目当前的安全基线一无所知。现在我在提示词里会明确写“不得新增依赖,如果必须新增,先说明理由并确认与项目基础设施兼容”,这一条加进去之后,依赖变更的次数明显减少了。CI里的安全扫描和依赖体检一定要保留,不能为了让AI代码“顺利合并”而跳过这些检查,节省两分钟可能换来运维上的大麻烦。
4.4 排故速查表:AI代码合并问题诊断
把实战中遇到的高频问题整理成一张速查表,方便排查:
| 症状 | 可能原因 | 排查方向 |
|---|---|---|
| Merge冲突集中在配置文件 | 多分支并行改同一文件 | 把配置独立PR;解决冲突时看语义而不是机械二选一 |
| 合并后功能开关异常 | 冲突解决时误删/误改配置 | 检查冲突记录,重启分支对比 |
| 测试通过但运行时报缺模块 | 依赖版本漂移或新增依赖未锁定 | 检查依赖锁文件、环境差异,固化运行环境 |
| CI构建被安全扫描拦截 | AI引入过期依赖 | 检查依赖版本,优先删除不必要依赖,升级到修复版本 |
| 代码风格正常但逻辑边界漏处理 | AI不擅长覆盖异常路径 | 手写边界用例,枚举极值输入 |
| 本地与远端分叉无法推送 | 误用rebase/force push | stash后另开分支处理,不要强推共享分支 |
这些事故看起来各不相同,背后的逻辑却指向同一件事:AI生成代码只是起点,从起点到Merge之间还隔着一层用自己的判断力补上的安全网。这张网只能由人来搭。
我个人在实际操作中的体会是,Merge之前的“不敢”并不是坏事。它说明你还知道自己对这段代码没有完全负责,还知道自己漏掉了什么。我现在把这种犹豫当成一个信号:凡是让我犹豫的改动,我都回去再走一遍3.3节里的四道关卡,走完还虚,就把AI生成的代码扔掉重写一小段。直到我能对着白板把这个函数讲清楚,我才会按下合并键。AI编程帮我省掉了大量打字时间,但真正守住Merge按钮的,依然是那个愿意对代码负责的人。