用 AI 写代码快一年,最舒爽的永远是“它把代码写出来的那一瞬间”,最烦躁的永远是“代码写完之后的那最后一步”。这一步在不同人项目里长成不同的样子:有人是构建镜像推上去,有人是合并分支过测试,有人是交付给运维跑通接口。我最近又在一个内部数据看板服务上被这一步折磨了一下午,才决定把这套经验仔细写下来。
AI 写代码这件事,真正体验过的人都会觉得“效率确实高”,但效率越高,心里越容易出现一个错觉:代码都写好了,剩下的收尾应该很快吧。错。最后一步恰恰是花时间最多、出问题最密、最让人想摔键盘的地方。这篇就来聊聊为什么 AI 写代码的收尾永远最麻烦,以及我后来怎么让这件事不再那么折磨人。
1. 为什么“最后一步”总是最烦?先看清 AI 做事的边界
1.1 AI 写代码的本质是“按概率生成”,不是“按需求交付”
很多人以为 AI 写代码是站在一个资深工程师的肩膀上思考,其实它更像一个知识面极广、但完全没在你现场待过的外包专家。你给它一句“实现一个导出 CSV 的接口”,它根据海量历史代码的概率分布,挑出一段最像样的回答。这段回答可能在语法上是完整的,在语义上也是吻合的,但它从来没有在你的环境里运行过哪怕一次。
这就是最后一步容易崩的根源:AI 不是在“交付”一个可运行的服务,它是在“生成”一段看起来正确的代码。它不知道你服务器的 Python 版本是 3.8 还是 3.10,不知道你的数据库连接串长什么样,不知道输出目录有没有写权限,更不知道你项目里已经存在哪些依赖版本。所有这些信息,它都靠默认值脑补。
我用一个不太严谨的类比:AI 像一位速度惊人的文书助理,你让它起草一份合同,它唰唰写完了,而且文体结构挑不出毛病。但这份合同要真正生效,还得你去核实条款、确认金额、补齐签名和公章。最后一步的“审核盖章”,就是代码写完之后的部署与验证环节。
1.2 最后一步是“真实接入”,AI 生成的代码默认活在真空中
在开发环境里写代码,最舒服的一点是所有资源都已经就绪:Python 包装在全局,路径也按你电脑上的习惯来,环境变量在 shell 配置里早就设好了。你感觉一切都很自然,但 AI 看不到这些自然。
它生成的代码默认几个假设:需要的依赖已经装好、文件路径可以直接创建、网络连接不会有问题、上游服务一定在线、磁盘空间永远充足、权限完全放行。这些假设在“生成代码”这个维度完全没问题,但一旦进入“真实接入”这个维度,就一个接一个地爆掉。
我见过太多案例:AI 生成了一段pd.read_csv('/data/input.csv'),本地跑没问题,部署到服务器上发现根本没有/data这个目录;或者生成了一段需要openpyxl的代码,却没把它写进requirements.txt,服务器一启动直接ModuleNotFoundError;再或者返回值里带了本地开发时的调试日志,完全没有业务监控所需的上下文。
所以你会发现一个反直觉的规律:越是用 AI 写得流畅的项目,最后一步越容易翻车。因为你越快拿到一个“看起来完整”的代码,就越容易跳过那些本该手动确认的环境事实。最后一步负责把所有这些被默认掉的事实重新翻出来,逼你逐一面对。
1.3 你看到的“写完了”,其实是“写完了主体”
还有一个容易被忽略的心理因素:AI 生成完代码的那个瞬间,你会产生一种“任务快做完了”的满足感。主体代码从无到有,往往只要几分钟,这极大地拉高了你的预期——这玩意都写出来了,收尾撑死半小时吧。
但现实是,AI 的生成结果往往只覆盖了功能主链路:输入参数、处理逻辑、返回结果。它不太会主动考虑健康检查接口、异常规范化、依赖清单的精确性、日志分级、权限校验、超时重试这些工程细节。这些细节恰恰占据了最后一步工作量的七成以上。
所以与其说“最后一步”是一个时间点,不如说它是一个缓冲区,把所有 AI 没有考虑到的真实工程问题都堆积在那里。问题越堆越多,最后一步就越令人烦躁。理解了这一点,下面的坑也就不难解释了。
2. 最典型的“最后一步”翻车现场:模拟项目X的上线清理实录
2.1 项目背景与当时的最后一步任务
前阵子我在做一个内部数据看板服务,就叫它模拟项目X吧。技术栈是 FastAPI 加 SQLAlchemy,数据库里存了不少业务数据。其中一个核心需求是:前端页面可以通过筛选条件,把结果导出成 CSV 文件。
这部分逻辑我用 AI 写得非常顺畅。我把表和筛选条件描述清楚,AI 很快就给我生成好了路由、查询逻辑和 CSV 处理代码。本地一跑,接口能通,文件也能正常生成。当时我心里想的是:再花半小时做一个 Docker 镜像,推到测试服务器上,这功能就算收工了。
结果这个“半小时”变成了一个下午。最后一步遇到三个大坑,一个比一个隐蔽,每一个都看似是小事,但串联起来就非常搞心态。
2.2 第一个坑:依赖版本全凭猜,一装就崩
第一次构建镜像时,我用的基础镜像是python:3.10-slim,项目里的requirements.txt是 AI 根据它自己的“经验”生成的,里面确实有fastapi、uvicorn、sqlalchemy、pandas,但偏偏漏掉了导出 CSV 时要用的openpyxl。
为什么本地没发现?因为我的本地环境是 conda 管理的,openpyxl早就在基础环境里装好了,所以import openpyxl不会报错。到了容器里就不一样了:容器按requirements.txt执行pip install,装完根本没有openpyxl。服务启动后,一旦调用导出接口,直接抛ModuleNotFoundError: No module named 'openpyxl'。
这类问题特别坑:它不是逻辑错误,而是环境差异。AI 生成代码时只看到了import,没有看到“这个库是否在依赖清单里”。所以我后来给自己立了一个规矩:AI 写出来的代码,第一步就是把所有import逐条和requirements.txt对齐,缺一条补一条,不要信任它的“经验”。
2.3 第二个坑:路径写得又死又随意,换个环境就找不到文件
依赖问题修完,镜像能启动了。我再调用导出接口,发现日志里报FileNotFoundError: [Errno 2] No such file or directory。打开 AI 生成的代码一看,它把输出路径写成了/Users/someone/data/export.csv。
这个路径怎么看怎么别扭:第一,它不是项目目录下的相对路径;第二,它明显是 AI 从我 prompt 里的一些零散信息猜出来的;第三,在容器里这个目录根本不存在,也没有任何代码在导出前自动创建它。
修复倒也简单:把输出目录改成通过配置项export_dir传入,然后在函数开头用Path(export_dir).mkdir(parents=True, exist_ok=True)确保目录存在。但这里让我意识到一个更关键的问题:AI 生成代码时,非常容易把“看起来合理但没经过验证的环境路径”写死,因为这是它在语料里见过的高频写法。你必须明确告诉它路径要可配置,否则它默认给你一个绝对路径。
2.4 第三个坑:只写了主流程,异常分支全裸奔
前两个坑填完,我以为已经稳了,没想到最花时间的是第三个:当筛选条件查不到任何数据时,接口直接返回 500,而且错误信息是一坨没有业务上下文的堆栈。
翻日志发现,AI 生成的代码里调用df.to_csv(output_path)之前,完全没有判断df是否为空。本来这也不是特别大的问题,但更难受的是:整个导出函数没有一层统一的异常处理,任何一个小问题都会变成浏览器端的Internal Server Error。前端同事根本没法判断是参数错了、数据为空,还是服务器内部出了故障。
后来我把接口的异常映射重写了一遍:数据为空时返回422加上“没有可导出的数据”这样的提示;文件写入失败时返回500并记录完整日志;数据库查询超时单独捕获并返回504。这套东西 AI 最初完全没有生成,因为它只盯着“主流程成功”这个目标,典型的只见树林不见树木。
2.5 排查链路:从报错到定位的完整过程
如果你也遇到类似情况,建议按这条链路排查,不要凭感觉乱试:
- 先看服务日志,而不是直接看代码。以这个项目为例,容器起来的场景下,第一件事就是
docker logs <容器名> --tail 100。日志里明确写了FileNotFoundError,这直接把我引向了路径问题,而不是去怀疑 SQL 写错或路由写错。 - 根据日志定位到具体函数,再去看 AI 生成的代码。路径问题的特征很明显:如果日志里的报错路径和你本地开发路径不一致,基本就是硬编码。
- 修复路径后重新构建镜像,此时依赖问题才真正暴露。因为构建过程中
pip install会重新检查依赖清单,而本地开发环境不会。所以要趁早用干净的构建流程验证。 - 最后用“边界参数”测试接口,比如空数据、超大日期范围、加密文件路径,把 AI 没写的异常分支一个个逼出来。
整个过程里,最花时间的是第三步和第四步,因为它们不是一次性的,要反复改、反复构建、反复验证。但只要你经历过一次完整的排查链路,后面再遇到 AI 生成的代码,就会下意识先想“这代码在全新环境里能不能跑起来”。
3. 把“最后一步”的问题,提前到 prompt 里解决
3.1 给 AI 加“执行上下文”,而不是只给功能描述
很多人在用 AI 写代码时,prompt 给得太抽象,比如“写一个导出 CSV 的接口”。AI 收到这种需求,只能按最通用的模板生成代码。通用模板的默认假设太多,最后一步自然容易翻车。
我现在会把执行上下文直接写进 prompt,比如:
我要求它“这是一个运行在 Docker 容器里的 FastAPI 服务,Python 版本是 3.10,依赖由 requirements.txt 管理,输出目录必须通过配置获取,接口需要处理空数据并返回带业务含义的错误信息”。这些上下文并不难写,但 AI 得到它们之后,生成的代码会立刻从“通用演示版”变成“可部署版”。
你不用一次写全,但至少要覆盖三个维度:运行环境、配置方式、错误处理。这三点几乎是最后一步翻车的重灾区。
3.2 让 AI 输出“可验证的最小闭环”,而不是“看起来完整的代码”
还有一招很有效:不要只让 AI 给代码,还要让它同时给“验证方式”。我一般会追加这样一句:请同时给出依赖清单、一个最小的调用示例、一个冒烟测试用例。这样做的好处是,AI 为了写出测试用例,必须先考虑自己的代码应该被怎么调用,这会让它把接口设计得更规范,而不是在函数内部堆一堆魔法参数。
另外,要求它给出依赖清单还有一个附加收益:它会主动检查自己到底import了什么。你甚至可以明确说“所有 import 必须出现在 requirements.txt 中”。这句话能减少大量依赖漏装的问题。
3.3 三个可以直接抄的 prompt 模板
这里分享三个我实际用下来效果不错的模板,直接复制改一下就行。
模板一:依赖明确版
请用 FastAPI 实现一个 CSV 导出接口。运行环境是 python:3.10-slim 容器,请同时给出一份完整的 requirements.txt,只列这个接口必需的依赖,并在注释里说明每个依赖的用途。不要使用我未提及的库。
这个模板的关键在于“在注释里说明每个依赖的用途”。AI 一旦开始解释依赖,就会认真思考每个包是否真的在代码里被用到,而不是凭印象堆一个万能清单。
模板二:路径可配置版
导出文件的输出目录不能写死,必须通过配置项 export_dir 获取。请使用 pathlib 构造路径,并在函数开头确保目录存在。请提供配置示例。
如果你不想用配置项,也可以用环境变量。重点是“不能写死”这四个字要明确出来,否则 AI 非常容易回到绝对路径的写法。
模板三:异常覆盖版
请列出这个导出接口所有可能的异常分支,包括空数据、权限不足、磁盘满、数据库连接失败等,为每个分支设计对应的 HTTP 状态码、错误信息和日志。不要只写 try-except 的占位。请用表格输出异常场景、状态码、日志内容。
这个模板的价值在于逼着 AI 去把“没考虑到的部分”变成“明确的结构化输出”。它列出来的异常分支,就是你后续写测试用例的清单。
4. 我后来改用的三步收尾工作流
4.1 第一步:让 AI 先生成“收尾清单”,再生成代码
有一次我突发奇想,在 prompt 里先不给功能细节,而是让 AI 直接列一份“这个功能上线前需要完成的收尾清单”,包括依赖、配置、路径、异常、日志、性能、安全。结果发现它列出的那十几项,几乎就是我自己以往最后一步会遇到的所有问题。
从此之后,我的工作流变成了:先要清单,再要代码。拿到清单后,我会把其中的关键项直接作为约束条件,再次要求 AI 在生成代码时逐一满足。这个过程看起来多了一步,但它把“最后一步”的验收标准前置到了开工前,后面反而更省时间。
你可以根据自己的项目特点让 AI 定制清单,比如后端项目让它关注迁移脚本、接口鉴权、限流;数据分析项目让它关注内存占用、大文件处理、超时设置。关键是先让 AI 把问题空间打开,而不是直接闷头写代码。
4.2 第二步:本地沙盒里模拟最糟糕的部署环境
我吃过“本地能跑就行”的亏,所以现在验证 AI 生成的代码时,一定会在一个干净的 Docker 容器里跑一遍,而不是直接用本地环境验证。
一个很实用的命令是:
docker run --rm -v "$PWD":/work -w /work python:3.10-slim \ bash -c "pip install -r requirements.txt && pytest -q"这样做的意义在于:容器里只有requirements.txt里明确声明的依赖,没有任何历史缓存包。如果在这个环境里能通过测试,那说明依赖清单至少是完整的;如果报错,那正好把“本地能跑”的幻觉提前戳破。
如果项目没有测试,至少也要跑一次启动命令,比如:
docker run --rm -p 8000:8000 -v "$PWD":/work -w /work python:3.10-slim \ bash -c "pip install -r requirements.txt && uvicorn app.main:app --host 0.0.0.0 --port 8000"这一步会把路径问题、权限问题、端口绑定问题全部暴露出来。不要嫌它慢,比起在测试服务器上反复构建,本地沙盒已经是效率最高的方案了。
4.3 第三步:用验收测试把最后一步拖进开发循环
最后一步容易积累问题的另一个原因,是它通常排在最后才做。为了改变这个局面,我要求自己给每个 AI 生成的功能补一个最基础的冒烟测试,不用覆盖全逻辑,只验证“能装、能启动、核心接口能通”这三件事。
比如导出接口的冒烟测试长这样:
def test_export_csv_smoke(client, tmp_path): response = client.get("/api/export", params={"date": "2025-01-01", "format": "csv"}) assert response.status_code == 200 assert response.headers["content-type"] == "text/csv"这段代码里虽然只测了正常路径,但它已经隐含了一个要求:路由要通、依赖要全、序列化要对。以后每次构建镜像时顺手跑一遍,如果挂了,说明最后一步的问题已经被检测到了,根本轮不到部署阶段再炸。
把这三步固定成习惯之后,AI 生成的代码再也没有像第一次那样把我困在部署上一下午。因为收尾工作不再是一切的最后,而是从第一天就开始的部分。
5. 收尾这件事,本质上是时间管理问题
5.1 为什么我总把最后一步的耗时低估一倍
回看这几年的效率变化,我发现 AI 写代码最大的风险不是代码质量,而是时间错觉。当 AI 能在十分钟内写完一个功能时,你会下意识认为“整个需求两小时够了”。但实际上,代码生成只占很少一部分,后面接踵而至的是依赖、配置、部署、验证、修异常。这些事每一件都不难,但它们都很琐碎,加在一起就非常耗时间。
我现在的经验是:凡是用 AI 高质量生成的功能,总耗时要按“生成耗时 x 2.5”来做计划。如果 AI 五分钟写完了,我至少要留二十分钟给收尾;如果 AI 半小时写完,我就当自己还要再花一个多小时。别嫌这个比例夸张,等你真正踩过几个坑,会明白这算法已经很保守了。
5.2 让 AI 当收尾助理:把日志和报错丢给它
遇到报错时,别急着自己在代码里翻,先把完整日志丢给 AI,让它帮你判断可能原因和排查顺序。我常用的说法是:
请根据这段报错日志,列出可能的原因,按概率从高到低排序,并说明每一步该怎么验证。不要直接给我改好的代码,我想先自己确认问题。
这个 prompt 很有用,因为 AI 在“分析问题”时比“生成代码”时更克制,也更少出现幻觉。它会真正根据日志里的关键字,比如ModuleNotFoundError、Permission denied、FileNotFoundError,给出对应的排查路径。你只需要照着它的清单逐项验证,效率比来回试要高很多。
5.3 收尾是长期习惯,不是一次性补救
我现在不再把“最后一步”当成一个要靠临时加班解决的阶段,而是把它拆成了每天十五分钟的例行练习:每次写完一个 AI 辅助的功能,就立刻用干净的容器跑一次、看一眼日志、补一条冒烟测试。这些动作的颗粒度很小,但积累起来,你会发现 AI 生成的代码越来越“像话”,也越来越少在部署阶段给你惊喜。
说到底,AI 写代码只是把“从零到一”的创作过程压缩了,但“从一到十”的工程化过程并不会消失。最后一步之所以最烦,是因为它承载了所有被压缩掉的不确定性。与其等它集中爆发,不如把它们平摊到每一天,每一次 prompt,每一条测试里。这样到真正上线的时候,你甚至会觉得最后一步平静得有些不真实。