用 AI 写代码久了,最烦的一直是最后一步。这句话不是矫情,是真实工作流里慢慢形成的感受。
我最早用 AI 写代码,觉得最大的好处就是“生成得快”。不管是一个解析函数、一段配置脚本,还是一整套增删改查接口,丢给 AI 几分钟就能看到完整代码,而且基本能跑。但真正用了一段时间之后,我的感受悄悄变了:生成越快,收尾越痛苦。上周清理一个日志解析模块就是典型场景——AI 给的函数功能上没问题,但等我往工程里接的时候才发现,时区处理、异常策略、字段缺失默认值,全都没和项目对齐。功能看着完成了,实际上离“能交付”还差一个下午。
所以这篇就聊聊,我用 AI 写代码过程中最真实的痛点:最后一步到底烦在哪,为什么这么烦,以及我自己摸索出来的一套收尾流程。不敢说多高级,但确实让我的交付效率上了一个台阶。
1. 最后一步到底是什么:不是部署,是“收敛”
先说结论。很多人以为最后一步是部署上线,或者是提交代码、发个版本。但我用了这么久,最烦的最后一步,其实是把 AI 的产出从“能跑”变成“能交付”。
能跑的标准很低——跑起来不报错就算能跑。但能交付的标准很高:边界要处理、异常要兜底、风格要统一、测试要补齐、注释要可信,连依赖都要干净。这个差距非常大。
1.1 生成阶段的快,掩盖了收尾阶段的慢
AI 写代码非常擅长生成“正面逻辑”。你告诉它这个函数要做什么,它能立刻写出正常路径下的完整实现。比如一个订单金额计算函数,它知道要遍历明细、汇总金额、保留两位小数。但订单金额算出来之后要不要校验上限?明细为空怎么办?货币类型不一致要不要转换?这些“背面逻辑”,AI 很少主动考虑。
这也是为什么生成阶段越爽,收尾阶段越痛苦。因为人的预期被 AI 拉高了——刚才你明明看到它几分钟就写出了一大段看起来完整的东西,下意识会觉得“这功能应该差不多了”。结果一接入工程,发现最花时间的不是写代码,而是给 AI 补它没想过的问题。
我习惯做个类比:AI 像一个特别会写初稿的实习生,大纲漂亮,正文通顺,但所有脚注、参考文献、排版格式都得你亲自收拾。你越夸它初稿写得快,收拾的时候就越想把稿子扔了。
1.2 最后一步的真实构成
我复盘过很多次,最后一步的“烦”不是单一原因,而是好几个任务叠在一起。我把它们列成了一张清单:
| 收尾任务 | 具体内容 | 为什么花时间 |
|---|---|---|
| 需求回读 | 把口头需求转成代码后,再逐条核对,看 AI 有没有遗漏 | AI 容易漏掉“隐含需求”,比如权限校验、日志输出 |
| 代码审查 | 对 AI 生成的每个改动做 diff 审查,重点看边界分支和错误处理 | 生成代码往往没有思考“失败场景”,所有假设都需要人肉确认 |
| 测试补全 | 生成代码几乎不主动写测试,尤其不写边界测试 | 没测试的代码是不敢上线的,这个只能人补 |
| 依赖与兼容检查 | AI 经常爱用“比较新的库版本”,或者引入一些并不需要的包 | 依赖一旦膨胀,环境就变得脆弱,上线容易出问题 |
| 注释与文档对齐 | AI 注释描述的是“它想做的事”,不一定是“它实际做的事” | 这种注释会误导后面的维护者,比没有注释更危险 |
每一项单看都能接受,但五项叠在一起,最后一步就变成了一座山。所以后来我的结论是:如果你只用 AI 写代码但没建立“收尾清单”,你迟早会在某一项上翻车,而且大概率是上线之后才发现的。
2. 为什么它这么烦:三个躲不开的矛盾
如果只看表面,最后一步烦是因为“要干的琐事多”。但往深了挖,其实是 AI 写代码这件事自带的三个矛盾。
2.1 自信的错误与人类验证之间的信任断层
AI 生成代码最大的问题不是它出错,而是它从来不告诉你它哪里不确定。它会把猜测包装成确定的方案,甚至写得非常笃定。比如让 AI 写一个时间解析函数,它很可能默认本地时区,而线上容器跑的是 UTC。如果它不加注释,你可能根本意识不到它替你做了这个假设。
这种“自信的错误”是最难防范的。因为一旦你不知道它在哪里替你做了假设,你就不可能在收尾的时候精准检查。你只能选择全部抽查,或者干脆逐行重读。这两个选项都非常耗时间。
人与人之间结对编程的时候,队友如果不确定,他会问你一句:“这里我不确定,你的预期是啥?”但 AI 不这么干,它会把最可能的猜测直接写进代码里,从表面上看起来毫无破绽。
2.2 风格混合与维护负担转嫁
另一个矛盾我称之为“风格混合”。AI 训练语料来自大量开源项目,所以它生成的代码经常是一种多位陌生人风格的混合体。这次这个函数用get_user_info,那个类又用fetchUserData,命名风格前后不一致;同一个模块里,有的地方用单引号,有的地方用双引号;有的函数有类型注解,有的又没有。
这些看起来是小问题,但代码是给人维护的。AI 写完代码拍拍屁股走了,维护成本却全部转嫁给了团队。我见过有的项目因为大量引入 AI 生成代码,最后代码风格变得特别混乱,甚至同一个文件中缩进风格都漂移了。
这里有个很贴切的类比:AI 写代码,就像请了三个人分别装修一个房间,单独看每一块都不差,但放在一起就是毛坯混搭风。收尾阶段你得花大量时间统一这个风格。
2.3 上下文窗口与真实需求的偏差
最后一个矛盾更本质:AI 只能基于当前对话窗口里的上下文做判断,它无法感知你项目的全貌。
你和 AI 说“给我一个订单状态机”,它理解的状态机可能是抽象类加子类实现;但你的项目里其实只需要一个枚举加工具函数。它给出的方案并不是错的,但它完全处在另一个上下文中。这种偏差往往不是改一行能解决的,而是要推倒重来。
所以最后一步有很大一部分工作,是在把 AI 猜的东西和项目的真实情况对齐。哪些常量是项目里已经定义过的?哪些接口是团队约定俗成的?哪些数据流是跨越多个模块的?这些问题全都要靠人去填平。
这三重矛盾叠加在一起,就注定了 AI 写代码不可能只靠“生成”就完事,收尾必然是重头戏。
3. 把最后一步做成固定流程:我的四段式收尾法
既然最后一步躲不开,那就只能想办法让它变得不烦。我现在有一个固定的收尾流程,分四段走:先让 AI 自审,再按变化频率拆分模块,然后写验收用例,最后用提交信息兜底。每一步都有很具体的操作。
3.1 先让 AI 自审,而不是人肉通读
我在让 AI 生成完代码之后,做的第一件事不是自己从头到尾读一遍,而是先让 AI 自审。
我用的提示词大概是这样的:
请以代码审查者的视角,重新读一遍你刚才生成的代码。不要修改代码,只列出: 1. 可能导致线上问题的点,至少列 10 个; 2. 每个点给出的依据; 3. 如果要修复,你会怎么改。 如果找不出 10 个问题,请尽量找,而不是说“代码没有问题”。这个方法实测下来很稳。AI 在“自审模式”下会比“生成模式”更挑剔,它会主动发现一些刚才自己忽略的边界问题。哪怕它找出的问题不一定都对,但这个过程能帮你快速定位需要深入看的位置。
要注意的一点:如果 AI 回你说“这段代码质量很好,没有明显问题”,那基本可以确定它没认真审。自审模式下它应该能提出至少好几个疑点,一个都提不出来反而说明有问题。
我还会根据自审结果做二次提问:挑其中风险最高的两三个,让它给出具体的修改方案,但不让它直接改。这一步的目的是确认它对问题理解到位了,再由我来决定是否应用修改。为什么这样做?因为 AI 一旦在长对话里连续多次修改,很容易出现“改了这个坏了那个”的连锁反应,所以它在自审阶段只能提方案、不能动代码,这样最安全。
3.2 按变化频率拆分模块,避免 AI 动一发牵全身
AI 生成的代码有一个常见问题:它喜欢把很多东西挤在一个文件里。比如一段数据同步模块,它会同时把配置、抓取逻辑、解析逻辑、主函数入口全写在一个 Python 文件里。这种结构看起来很紧凑,但对后续迭代非常不友好。
你想让 AI 改一下抓取逻辑的某个细节,它可能顺手把配置项改了,或者把解析逻辑重新排版了一遍。结果就是 diff 变得特别大,你审查起来很费劲,而且容易埋雷。
我的做法是:拿到 AI 的初稿之后,先按“变化频率”把一个文件拆成多个文件。原则是——将来经常变的东西和几乎不变的东西分开,外部调用接口固定住。比如一个数据同步模块,我会拆成:
config.py # 配置项,几乎不变 fetcher.py # 抓取逻辑,经常变 parser.py # 解析逻辑,经常变 main.py # 组装入口,很少变拆完之后,我后续只会让 AI 在特定文件内做修改,并明确告诉它:“只改 fetcher.py,其他文件不要动。”这样一来,AI 每次改动的范围就会被约束住,diff 清晰,审查成本直线下降。
这个步骤本质上是在做工程结构设计。我建议在让 AI 写代码之前,就先想好模块边界,而不是等生成完再拆。
3.3 用验收用例给需求上锁,AI 只在锁内迭代
第三个步骤可能是整个流程里最关键的一环:先写验收测试用例,再让 AI 去迭代代码。
很多人顺序搞反了。常见做法是让 AI 先生成功能代码,然后再补测试。但这样做的问题在于,AI 生成的功能代码已经定义了行为,你补测试的时候很容易顺着它的实现去写,结果就是测试掩盖了业务需求,而不是验证业务需求。
我推荐的顺序是:先把业务规则写成测试用例,再让 AI 对着测试用例去实现。比如你要一个“连续失败 3 次锁定账号”的逻辑,测试应该先写清楚:
def test_lock_after_three_failures(): user = User("tester") user.record_failed_login() user.record_failed_login() assert user.is_locked is False user.record_failed_login() assert user.is_locked is True这个测试写出来,AI 就知道需求的具体行为是什么了。它不会给你一个“成功 3 次锁定”的错误逻辑,因为测试会直接挂掉。
我通常会在让 AI 写功能代码时,把这个测试和提示词一起丢过去,然后说:“请实现一个满足测试用例的函数,注意包括边界行为。”这样 AI 的迭代空间就被锁在测试的规则内。后面它再怎么改,只要测试通过,核心需求就不会跑偏。
这里要强调一下:验收用例写得越贴近业务,AI 发挥的空间就越小,最后一步收到“惊喜”的概率就越低。如果你想完全控制行为,测试就要把正常路径、边界路径、异常路径全部摊开。
3.4 用 commit message 逼 AI 交底,顺便留清决策痕迹
最后一步往往被人忽略的是收尾时的信息记录。我现在的习惯是:在确认代码功能没问题之后,让 AI 再生成一份比较完整的提交说明,但比普通提交信息多几个强制回答的问题:
- 这段代码解决了什么问题?
- 改了哪些公共接口或数据结构?
- 有哪些已知限制、假设或未验证部分?
- 如果不改会有什么后果?
这四个问题一丢给 AI,它的回答经常会暴露一些问题。比如有一次它对“已知限制”的回答是“目前假设所有输入都是合法的”,我才意识到这个代码里缺少了参数校验——这正好是我收尾时漏掉的一个点。
这个过程本质上是在让 AI 做“交底”,把脑海里那些没有写进代码的潜在判断都逼出来。很多 AI 生成的代码里隐藏了大量隐式假设,这些假设如果不记录,两周后你自己回来看这段代码,都可能想不起来当初为什么这么做。
而且让 AI 生成提交说明还有一个好处:面对这四个必须回答的问题,AI 会重新审视一次自己的代码,等于又做了一轮总结式自审。这个成本几乎为零,但信息密度和收益非常可观。
4. 踩过的坑和排查实录:最后一步的高频翻车现场
上面讲的都是我现在的流程,但流程不是一天建成的。中间我踩过不少坑,有的还挺值得拿出来说说。这里就当问题排查实录吧。
4.1 AI“假装修好了”:循环验证才是解药
这是我在实际使用中遇到最让人头大的问题。你向 AI 反馈一个 bug,它马上道歉,然后给你一大段修改代码,看起来态度很好。但跑一遍测试,问题照样在。
你再来一次,它还道歉,又改了一种写法。这次看 diff 你会发现,它只是把变量名换了一种叫法,核心逻辑压根没动。这种现象我称之为“假装修复”——AI 为了满足用户的反馈,表现得在积极行动,但实际上没有真正定位到根因。
排查方法其实很简单,但需要自己坚持:只看修复后的运行结果,不看 AI 的描述;每次修完必须跑最小复现用例;如果连续两次没修好,果断换一个小任务描述,或者开一个新对话,而不是在同一个长对话里继续来回。长对话里 AI 容易陷入“围绕上一轮输出修改”的模式,反而不容易跳出来重新思考。
我印象很深的一次是让 AI 修一个字符串编码问题。它来回改了四次,每次都在改编码转换的方式,但始终没解决。后来我换了个思路,把出错的那一行原样贴给它,不给任何上下文,问它“这行代码在什么情况下会抛异常?”它几乎立刻就定位到了问题,因为那个问题根本不在编码转换,而在前一步的字段提取。
4.2 测试全绿,但业务逻辑是反的:断言要贴近业务
另一种很隐蔽的翻车现场是:测试全绿,但业务逻辑是反的。后来我分析了一下,原因出在测试本身写得太弱,或者说断言方向没有扣住业务本质。
有次让 AI 实现一个“连续失败 3 次锁定账号”的逻辑,AI 给出的代码实现错了,弄成了成功 3 次就锁定,但当时的测试只断言了“调用 3 次后账号锁定”,没有断言失败次数和成功次数的区别,所以测试通过了。
现在我做验收用例时,会把业务条件的每一个分支都摊开来写,并且断言尽量写成“业务可读”的形式,而不是只判断最终状态。比如上面这个例子,我会额外要求测试覆盖:
def test_should_not_lock_after_three_successes(): user = User("tester") user.record_successful_login() user.record_successful_login() user.record_successful_login() assert user.is_locked is False这种断言值一开始可能看起来有点多余,但它能把 AI 最常见的“逻辑方向搞反”问题直接按死在摇篮里。收尾阶段我会仔细体检测试,确保测试覆盖的是业务行为,而不是实现函数的某种“存在性”。
4.3 注释和实现“各说各话”:让 AI 自己讲一遍
AI 生成的注释经常比代码乐观。它写注释时,描述的是“代码想做什么”,而不是“代码实际做了什么”。如果实现写歪了,注释依然停留在理想状态,这种注释对后续维护者的伤害很大。
比如有一次 AI 写的代码里有注释“从响应中提取用户名并标准化”,但实际代码只做了strip(),并没有做大小写归一化。我看到注释时以为用户名已经归一化了,结果下游逻辑一直出错。
现在我在收尾时已经习惯让 AI 做“自述检查”,给它指令:不用改代码,只用一句自然语言描述这段代码的输入、输出和每一个分支的具体条件,然后我把描述和代码对照一遍,很快就能发现不一致的地方。这个环节通常只花几分钟,但能省掉后面几小时的排查时间。
4.4 被忽略的小关卡:依赖膨胀、死代码与格式漂移
最后分享几个不致命但很磨人的小关卡。第一个是依赖膨胀。AI 生成代码时习惯性地import一些根本没用到的东西,有时还会顺手给你安一个并不需要的第三方库。这种依赖一旦进入项目,会带来环境脆弱性。我现在会专门看一遍 diff 里的 import 区域,凡是没用到的直接删。
第二个是死代码。AI 很喜欢保留一些“看起来有意义但实际没有调用者”的函数。这些代码会让模块看起来很完整,但维护时需要读更多无用代码。我的态度是:凡是后面接不上的函数,不管它写得多么漂亮,在收尾阶段都必须删掉,不要心疼。
第三个是格式漂移。AI 在很小的修改里可能会突然改变引号风格、换行方式或者缩进习惯,尤其项目里原本有某种约定时。这个我用编辑器的一键格式化解决,格式化完再检查 diff,动作要快。久而久之你会发现,这些小关卡虽然不起眼,但舍掉它们能让最后一步提速不少。
5. 一些真实体会:把最后一步从“最烦”变成“最稳”
经历了很多次收尾翻车之后,我现在对“用 AI 写代码”这件事有了完全不同的理解。最初我觉得它是一种“加速工具”,后来我意识到它更像一个“结对实习生”。你这个老手的价值,不在于接替它写每一行代码,而在于给它的产出把好最后一道关。
我自己的体会是,建立固定流程,比每次临时应对更能降低焦虑。一旦你知道了收尾要过哪些关卡、按什么顺序过,最后一步就不再是一座大山,而是几件能按部就班做完的琐事。
还有一个特别想分享的小经验:在对话快到尾声的时候,不妨强制 AI 说一句“这个方案的最大风险和未验证部分是什么”。这个动作经常能帮我提前发现问题。AI 生成代码时,它对自己代码中关于业务假设的那些局限是有信息的,只是不会主动说出来。逼它说出来那一刻起,最后的“收尾”就已经变成一次可以通过清单完成的确认,而不是一次提心吊胆的盲猜。
代码写多了你会发现,交付最重要的是确定性。好的收尾流程,就是让 AI 的输出从“能跑”变成“确定可用”的那道保险。