
上周我帮同事查一段刚由某个AI编程助手写好的模块。编译通过测试也全绿可我问了他三个问题输入为空列表会走到哪里金额出现负数应该报错还是忽略这个函数为什么放在工具类里而不是订单服务里他愣了很久最后说“AI生成的我也没细看”。那一刻我意识到这段代码从合并进主分支起就已经没有主人了。这事不是个别现象。Vibe Coding这几年被开发者社区反复讨论大家喜欢它是因为能把脑子里的想法直接用自然语言吹给机器机器立刻还你一段能跑的代码。但问题恰恰藏在这股爽感里代码写了一堆能跑通演示路径却没人在真正理解它。于是出现了一个几乎所有团队都绕不开的怪圈——AI写完没人敢碰。这篇文章我想把这件事彻底掰开为什么“写完就没人敢碰”是代码本身的问题还是团队责任链的问题有哪些Vibe Code可以救哪些必须推倒以及我自己在项目里怎么立规矩让AI产出的代码仍然有人敢改、敢维护。适合正在重度使用AI编程的人也适合做代码评审的技术负责人。1. Vibe Coding的爽感从哪里来坑就埋在哪里1.1 自然语言、即时反馈和“先跑起来再说”的快感所谓Vibe Coding核心工作流可以浓缩成三步用自然语言描述需求让AI生成初版代码然后运行、观察、再改提示词微调。反复循环几次一段能跑的功能就出来了。比起传统开发要写设计文档、画类图、手动定义接口这个过程几乎没有任何语法负担。这种协作方式的吸引力是实实在在的。写一个几十行的脚本过去我要查半天库文档现在直接说“读这个JSON按字段求和输出报告”几秒钟就有结果。尤其是做原型验证和一次性工具时效率提升非常明显。你不需要完整确认每个API细节只要把意图说清楚AI会基于训练数据里的常见模式帮你补齐实现。我见过不少团队最初引入Vibe Coding的时候开发速度肉眼可见地快了起来。新功能从需求到演示以前要一两天现在可能一两个小时就出demo。这个阶段所有人都是兴奋的没人想去追问“这段代码到底是怎么来的”因为结果看起来完全可用。1.2 对话过程不会进代码库留下的只有一坨结果问题就出在这个“怎么来的”上。传统开发里一段代码凝结着人的决策过程为什么用这个算法而不是那个、为什么这个分支放在这里、为什么这个字段允许为空。这些决策会通过命名、注释、提交信息、代码评审意见散落在代码库里后续维护者顺着这些痕迹就能理解代码。AI生成的代码只保留了最终产物。你和AI来回沟通过十几轮调整过需求细节、改过字段格式、绕过某个报错这些过程全部停留在聊天窗口里代码库里一个字都不会留下。等这段代码合并进主分支后续接手的人看到的是一堆没有来源、没有理由、不知道经历过什么修改的代码。更麻烦的是几乎没有人会把和AI的对话过程导出成文档放进仓库。大家都默认“代码就是结果结果能跑就行”。可一旦代码需要维护你面对的不再是一个有上下文的人而是一个不透明的黑盒。这个黑盒不会解释自己为什么做出这些选择也不会告诉你哪些地方是临时妥协的结果。1.3 AI产出速度与人类审查速度的剪刀差还有一个被低估的问题数量。AI写代码的速度实在太快了。你花二十分钟描述完一个模块它十分钟就吐出来上千行代码而这还只是一轮对话。如果团队里几个工程师同时用这种方式工作一周内仓库里就能多出几千行甚至上万行AI代码。人类的代码审查速度远远跟不上这个节奏。一个熟练的工程师认认真真看一千行别人写的代码可能半天就没了如果还要检查边界条件、跑场景、验证错误处理时间还得翻倍。当AI代码以这个速度涌入仓库审查就只能流于形式大概扫一遍编译过了、测试绿了、commit信息正常就合并了。于是这里出现一个很尴尬的组合代码量巨大、无人理解、还缺少有效审查。仓库里的“无人敢碰”区域就在这种剪刀差里扩张等你想回头治理的时候范围已经大到无从下手。2. 代码里的四颗定时炸弹边界、幻觉、隐式状态和假测试2.1 全绿路径与隐藏假设演示级代码混进生产环境AI训练数据里大量代码来自教程、博客、开源示例和编程社区的问题回答。这些代码天然有一个特点它们是为“演示”写的不是为了“生产”写的。演示只需要一条正常路径跑通而生产环境面对的是一大堆异常输入、边界条件和状态变化。举个例子AI非常容易写出这样的代码def calculate_total(items): return sum(item[price] for item in items)看着没错空列表返回0正常数据能算总额。但你去追问它如果某个item里没有price键呢直接抛KeyError。如果price是字符串不是数字呢运行时类型错误。如果金额需要保留两位小数做四舍五入呢浮点累计直接把精度丢掉。这些场景在演示里永远不会出现因为演示数据都是模型自己“脑补”出来的干净数据。维护者之所以不敢碰这类代码是因为他们心里清楚这段代码从来只验证过一条路径那条路径之外全都是雷。你改任何一个输入格式或者接入一条新的业务规则都需要先在一堆“看起来正常的默认行为”里猜到底有没有坑。这种不确定性才是碰它之前最让人犹豫的东西。2.2 API调用像开盲盒不存在的方法和跑偏的签名AI生成代码时对API的调用依赖的是训练数据里的统计规律。它知道这个库大概有哪些函数函数大概长什么样但很容易在细节上出偏差有的函数名字正确但参数顺序写反了有的参数名根本不存在有的函数已经在新版本里废弃了它还在按照旧版用法调用。编译阶段的错误还算好办一跑编译器就能发现。真正致命的是运行时才暴露的错误。比如AI调用了某数据处理库的merge方法它写的是data.merge(keyid)但真实签名是data.merge(onid)代码不会在导入时报错运行到那一行才炸。更麻烦的是如果AI同时生成了对应的测试代码测试也会按错误的调用方式来写导致编译检测和人工审查都被这个错误用法带偏。这种问题我见过很多次它就像开盲盒表面上代码逻辑完整实际上对外部API的理解是概率性的。维护者改这段代码时必须反复验证每一个外部调用的真实签名、真实行为和真实返回结构。这个工作量比直接看人写的代码高出一个数量级于是大家干脆选择“能不动就不动”。2.3 隐式状态和全局副作用改一行配置炸一片模块AI在生成代码时倾向于使用最近出现的上下文而不是认真设计依赖关系。这会导致一个现象它非常喜欢隐式状态。明明可以显式传参的两个值它会选择去读全局配置明明应该由调用方提供的上下文它会在函数内部偷偷访问环境变量明明可以写成纯函数的逻辑它会去修改一个外部单例对象。我接手过一个AI写的定时任务模块。函数内部从环境变量里读取了一个目录路径而这个环境变量在另一个模块里被顺带设置。单独看定时任务代码完全正常可一旦运行顺序变了或者环境变量被别处覆盖定时任务就会开始读错目录。维护者排查的时候根本不会第一眼去看环境变量因为没人会想到一个内部函数居然隐式依赖无关模块的运行时状态。这种代码的可怕之处在于改动影响无法被IDE的静态调用搜索发现。你改了一个环境变量的初始值所有隐式读取它的AI代码全部跟着变你添加了一个全局状态写入口莫名其妙就有好几个模块被影响。维护者手里的依赖图是残缺的所以每一次改动都像在黑夜里走悬崖边。2.4 测试写着“绿”其实就是“自问自答”更隐蔽的炸弹是AI生成的测试。AI很擅长“自问自答”它写一个函数再写一套测试测试的断言会顺着函数实现去写而不是从业务行为去定。这意味着测试通过只能说明“代码没有发生变化”不能说明“代码行为符合预期”。典型表现是断言过于单薄。一个计算订单总额的函数AI生成的测试用例通常就是构造两个正常订单调用函数断言结果等于3。它不会测试空列表、不会测试负数金额、不会测试键缺失、不会测试类型异常。更糟的是这些测试通常只走快乐路径对错误处理逻辑完全没有覆盖。当一个人准备修改AI代码时他会先看测试。测试全绿会给他一个虚幻的安全感让他误以为“有测试兜底我可以放心改”。真正动手后才发现测试什么都没验证任何一次重构都可能引入没人察觉的回归。测试不但没有降低风险反而成了错误的信心来源。3. 比技术债更麻烦的是责任链彻底断裂了3.1 孤儿代码没有作者也就没有人背责任传统代码维护体系里有个不成文的规则谁写的谁改谁合并的谁负责。哪怕代码质量差只要它有一个明确的作者改动出了问题就可以找到人来解释当初的设计意图。AI代码打破了这条规则。它没有作者“作者”只是把提示词发给AI的人而这个人对代码内部逻辑的理解往往并不比接手者更深。我见过不少工程师对自己提交上来的AI代码一无所知你问他某个分支为什么存在他只能说“AI这么生成的我试过它是对的”。这种代码就像孤儿没人认领。在责任链条缺失的情况下局面对维护者是极其不利的。改这段代码出了问题承担后果的是改动者但不改它只能眼睁睁看着技术债积累。理性的个人选择往往是别碰它让它继续跑着。在一个多人协作的项目里这种决策最终会汇聚成大片无人敢动的区域。3.2 影响面评估从“推导”变成“猜谜”可靠的代码修改建立在一个前提上修改者心里有清晰的依赖图。我知道这个函数被谁调用改了返回值会影响哪些模块我知道这段逻辑依赖什么状态动了它会导致什么行为变化。有了这张图我评估影响面是推导不是猜测。AI生成的代码规模一大这张图就彻底失效。一方面人脑无法同时跟踪上千行AI拼装出来的逻辑另一方面AI代码里大量借用隐式状态、动态属性和魔法值IDE的静态分析只能覆盖很小一部分。修改者最后只能靠“应该没问题吧”来推进工作这已经不是技术能力的范畴而是赌博。赌博的可怕之处在于它会赢很多次。前面十次改动都安然无恙就会强化“这块代码其实还挺稳”的错觉直到某一次隐式依赖被触发整个模块崩掉排查成本远远超过之前所有省下的时间。3.3 不敢删不敢重写坏代码被沉没成本保护起来既然AI代码这么难维护为什么不能删掉重写答案是重写成本焦虑。AI生成这段代码花了一上午人肉重写可能要一周重写过程中还可能漏掉某些AI已经处理过的隐藏细节。于是大家倾向于保留原样哪怕这段代码写得再烂至少目前看起来是能跑的。这种心态会给坏代码加上一层“存在即合理”的保护色。它的真实维护成本在不断增加但账面上唯一的代价只是“没人敢碰”。等项目扩张到一定程度这段代码已经嵌入无数调用链重写成本变得更高形成恶性循环。到这一步问题就不仅仅是技术债务了而是整个团队对这部分代码的逃避心理。每个成员都知道它不妥但每个人都不愿意做那个站出来挨打的人。技术债可以靠重构清还这种责任缺失所制造的不动资产会导致代码库出现一块持续扩大的僵尸地带。4. 分诊一下哪种Vibe Code能留哪种必须推倒4.1 用三个维度给Vibe Code分诊生命周期、副作用半径、数据正确性不是所有AI生成的代码都是垃圾也不是所有都能救。我建议用三个维度做判断代码预期生命周期、副作用影响半径、数据正确性要求。这三个维度决定了一段代码值不值得人工接管。代码类型生命周期副作用半径数据正确性要求风险结论一次性脚本、本地小工具天级本地可随时删低可用无所谓独立模块、纯函数无外部副作用月级模块内中可留但必须补测试和审查核心业务链路、支付金额、权限控制年级整个系统极高必须推倒重写或深度人工重构第一个维度很好理解脚本跑完就扔坏了重新生成没有人会为它睡不着觉。第二个维度看的是改动它之后爆炸范围有多大。第三个维度看的是错误后果是否可逆算错一笔钱的代价和日志文件名打错一个字的代价完全不同。4.2 低危代码可以躺平中危必须先补观察点再补测试如果你的AI代码属于纯函数、独立模块或者只在内部调用、不涉及外部数据共享那它其实有救。前提是不要直接信任。我的做法是先补观察点在入口加日志、在关键分支加断言、在返回出口记录输入输出摘要。跑一段时间收集真实世界的数据确认这些边界条件到底会发生什么。观察点没问题之后再针对真实场景补测试。这时候补测试的目的不是证明代码正确而是锁定现有行为防止后续重构时不知道哪里变了。测试可以不全但至少要把你在观察期里见过的异常输入覆盖进去。这一套做完中危代码基本就变成了一段“有人理解且有人看管”的普通代码。它可能仍然不够优雅但至少不再是黑盒维护者敢动手改。对大多数项目来说改造到这一步已经够用了。4.3 高危代码必须推倒重写能跑不是留下来的理由涉及金额计算、权限判断、并发控制、数据持久化的核心链路我不建议给AI代码留机会。哪怕它跑得再稳、测试再全也不行。理由很简单这类代码出错的后果很严重而AI代码内部逻辑是概率生成出来的你无法穷尽它出错的所有可能。推倒重写不等于把代码删了从零开始。正确姿势是把它当参考实现先读代码提炼出它做了哪些事区分出哪些是业务需求哪些是AI自己加的边界处理然后由人按架构规范重新实现一份。重写过程中每一行都要能说出“为什么这么做”每一个异常分支都要有人能解释。这个过程确实费时间但它有一个不可替代的产出人工对这段代码的完整理解。只有理解到位后续维护才谈得上风险可控。把高危代码从Vibe Coding的产物里挑出来重写不是为了否定AI的效率而是为了给系统留一块“有人敢碰”的核心地带。5. 让AI写代码但代码库还敢碰我的五条硬规矩5.1 人当翻译机AI当打字员先写规格书再让它动手很多人用Vibe Coding的方式不对是让AI当架构师自己当验收员。这等于让一个没有业务上下文的模型帮你做技术决策出问题再头疼也正常。我现在要求团队先写规格书AI只负责把规格翻译成代码。规格书不需要复杂但至少包含输入是什么、输出是什么、异常行为怎么处理、边界条件怎么定义、性能上限是多少。别嫌麻烦这一步花十五分钟能省掉后续十五天的猜谜时间。更重要的是有了规格书代码评审就有一份对照文件AI跑没跑偏一目了然。5.2 测试前置让AI同时给出“测试用例实现”既然AI生成的测试最爱自问自答那就在生成实现之前先逼它立军令状。我会在提示词里加一句“先列出这个函数的边界条件列表再针对每个边界条件写测试用例最后才写实现。”如果边界条件漏了我在审查的时候一眼就能看出来然后让它补上再继续。这个做法的本质是把问题排查从“事后碰运气”挪到“事前定义清楚”。测试用例先行也给后续人工复审提供了一个明确标准只要跑这几个用例就能知道程序对边界条件的反应是不是符合预期。我喜欢让AI先写测试还有一个原因它写的测试如果和它写的实现配合得天衣无缝没有任何独立断言那这段代码本身就是可疑的应该退回重做。5.3 解释性回写讲不清的代码不允许合并现在我对所有合并进主分支的AI代码强制要求AI输出一段“人话解释”说清楚每个非平凡分支存在的理由。审查者必须把这段解释和代码逐条对照凡是解释不通的、解释跟代码对不上的、AI自己都含糊其辞的片段全部删掉重写。这个规矩背后的逻辑很直接代码可以在短时间内被生成但理解必须在合并前真实发生。如果审查者无法用自己的话说清这段代码为什么这样写那代码就是没人理解的它后续一定会变成维护黑洞。AI写出来的解释如果质量过关就让它作为注释留存在代码里这相当于把对话里的“为什么”沉淀回了代码库。5.4 模块隔离每一次AI生成的代码先住进“隔离舱”新生成的AI代码很难保证质量所以我会控制它进入系统的半径。规矩是AI代码默认只能以独立函数或模块形式落地不允许要求它直接修改现有核心模块。入口要显式定义参数要显式传递禁止AI代码访问全局单例、环境变量或隐式共享状态。这么做的目的是把故障范围框在一个可控的隔离舱里。哪怕它内部逻辑再糟糕出问题也只炸这一小块不影响主链路。等这段代码经过观察、测试和人工理解证明它确实可靠了再决定要不要把它往更核心的位置迁移。隔离舱的代价是增加了一点接口定义的功夫换来的却是主系统永远不被AI代码的隐性依赖渗透。5.5 合并前的可维护性清单六项过不了就退件最后我们团队在代码评审阶段固定使用一份可维护性清单。这条清单不是用来卡人的而是用来把“敢不敢碰”这种模糊感受变成具体的验收标准这段代码是否每个函数都能有人当众讲清楚每个异常分支是否有明确且合理的处理关键路径是否有针对边界条件的独立测试是否依赖无法追溯的全局状态命名是否准确表达业务概念而不是看出处编号代码库中是否已有完整的人工审查记录任何一项不满足这段代码就退回到生成前状态由人和AI协作重新完成而不是直接合并。这套清单执行了半年多仓库里新增的AI代码质量大幅提升更关键的是团队重新赢回了维护的信心。我个人现在的默认工作流还是离不开AI它给我写初稿、帮我写测试脚手架、帮我整理日志格式都干得又快又好。但每一段要进主仓库的代码我必须自己先读懂再看不懂的片段坚决删掉重写。这个习惯改起来并不难真正难的是承认一件事AI能替你写代码但没人能替你完成“理解”这一步。跨过这道坎Vibe Coding才真正从玩具变成了趁手的工具。