过去一年里,我以“救火队员”的身份接手了好几个 AI 辅助开发出来的半成品项目。它们的起点几乎一模一样:某团队用代码大模型把原型功能搭得飞快,两周干完了过去一个月的活;然后上生产、加需求、堆功能,代码量膨胀到几千行之后,某次改一个接口,牵连出一串服务一起报错;再后来,没有人能完整讲清楚核心链路,项目停在一种“能跑,但谁都不敢动”的状态。
刚开始我也以为是模型能力不够,换个更强的模型就能解决问题。后来反复对比了好几个项目才发现,真正的分水岭从来不在模型,而在使用姿势。那些能长期迭代、持续重构的 AI 辅助项目,都早早给 AI 上了一套工程约束。如果你问我“那件事”到底是什么,我会直接说:让 AI 在一个有上下文快照、有接口契约、有自动验收的体系里按工单施工,而不是让它对着整个项目自由发挥。
这句话听起来不惊艳,但它几乎解释了所有 AI 项目的生死差异。下面我把背后的机理、落地步骤和踩坑经验详细拆开讲。
1. 先别急着怪模型:AI 编程项目是怎么一步步“烂”掉的
1.1 AI 生成的代码,本质是“概率续写”,不是系统理解
很多人对 AI 写代码有一个误解,以为它像人一样先把整个代码库读懂,再基于“完整理解”做设计。实际上,主流代码大模型在做的事,是根据当前提示词和已有代码片段,逐字预测“下一个 token 最可能是什么”。它的输出在局部看起来非常合理,但不代表它心里有整张依赖图、模块边界和演进历史。
这个区别在项目早期会被掩盖。几百行代码时,整个项目几乎可以塞进上下文窗口,模型能看到全部内容,生成的代码自然全局协调。可一旦项目成长到几千行、横跨几十个文件,模型能看到的只剩局部。它会在局部信息里做“看起来最合理”的补全,至于这个补全会不会破坏另一个模块的约定,它根本不知道。
我把这个过程叫“僵尸智能”:每一个局部都活灵活现,合起来却没有一个统一的意志。初期一顿操作猛如虎,中期步步改、步步惊心,末期改一个字段要动六个文件,就是这种模式带来的典型体验。
1.2 项目失控的三张典型面孔
我在好几个项目里反复见到同样的失控信号,基本可以归纳成三张面孔。
第一张面孔是“稳定期急剧缩短”。项目前两周一切都很顺,加功能半小时搞定;到了第五六周,加一个小功能可能要回归大半天。不是 AI 变笨了,而是它早期生成的代码彼此之间的隐式耦合开始显形。你改 A 模块,B 模块莫名其妙挂掉,因为 B 之前“顺手”依赖了 A 的内部函数,而当时没有任何测试和文档标记过这种依赖。
第二张面孔是“模块边界彻底消失”。为了让某个局部功能快速跑通,AI 常常会绕过分层约定,router 层直接查数据库,service 层直接拼 SQL,工具函数里藏着一堆业务逻辑。越界的代码越积越多,最后没人说得清一个请求到底经过哪些层、碰了哪些表。
第三张面孔是“知识只活在对话里”。架构决策、依赖理由、命名约定,全都没有沉淀成任何形式的文档或规则文件。一旦原来的会话过期,下一段对话又是从零开始。AI 开始凭记忆猜项目结构,写出来的东西自然和真实情况南辕北辙。人也好,模型也好,没有可继承的知识库,项目就不可能稳定。
1.3 为什么这比带一个新手工程师更危险
有人会说,AI 写出来的烂代码,和刚毕业的工程师写出来的烂代码也差不多吧?差很多。
新手工程师写得慢,但他们会问问题,会在你引导下建立全局观,会记住“这个模块不能碰”的警告。AI 没有这个学习闭环。它的训练目标是补全出像样的代码,而不是理解业务目标。你给它一个模糊任务,它不会追问需求边界,只会交出一份“看起来很专业、实际暗藏隐患”的实现。
更要命的是,它会以极高速度把同样的坏习惯复制到全项目。一个错误的设计模式从出现到扩散成灾,可能只需要一个下午。这相当于你把一支施工队请进了房间,但没给他们图纸、工艺规范、验收标准,还告诉他们“速度优先”。他们当然能垒起一座歪歪扭扭的大楼——问题从来不出在工人,而出在管理方没给约束。
2. 你要做的“那件事”:把 AI 从自由发挥改成按契约施工
2.1 先摆正定位:AI 是“受监督的施工队”,不是独立架构师
很多人用 AI 编程时的默认姿势,是把它当成一个可以完全交底的项目经理:“帮我重构这个模块”“帮我加一个购物车功能”。这个姿势在 Demo 和小工具上没问题,但放到长期项目上,本质是让 AI 自己定义目标、自己设计结构、自己验收,等于雇主消失了。
正确姿势是把 AI 当“受监督的施工队”:它可以在你划定的道路内高效干活,但没有权限决定边界、接口、依赖方向、验收标准。你是那个发图纸的人。图纸越清楚,施工队越不容易跑偏;图纸模糊,施工队一定会自由发挥,而自由发挥的代价就是项目烂掉。
我见过一位开发者给 AI 下达任务时只写了一句“做一个库存预警功能”,半小时后 AI 交上来一套包含数据库建表、静态工具函数、直接读写文件的“完整实现”。表面看效率很高,实际它自己定义了数据模型、自己决定绕过 service 层、自己引入了不该有的依赖。这就是把 AI 当架构师的必然结果。
2.2 工程约束的三根支柱:上下文快照、接口契约、自动验收
为了把“按契约施工”落到实处,我一般会立三根支柱。缺一根,另外两根撑不住。
第一根是“上下文快照”。AI 对项目的理解,完全来自你喂给它的材料。你需要在仓库里放一份固定路径的项目说明,内容覆盖技术栈、目录结构、核心模块、数据模型、启动和测试命令。这份说明就是 AI 的入职手册。没有它,AI 永远是那个只看局部的新员工。
第二根是“接口契约”。项目里哪些层可以互相调用、数据模型长什么样、错误处理约定是什么,都要成文。比如“router 层不得直接操作数据库”“新增第三方依赖必须在工单里写明理由”“公开函数必须带类型注解”。契约不一定要很厚,但必须是明确的、可检查的。宁可少而硬,不要多而软。
第三根是“自动验收”。 AI 生成的代码不是成品,只是候选品。它能不能进主线,应该由脚本说了算,而不是由模型自己拍胸脯。我常用的最低配置是一套本地检查脚本:类型检查、lint、单测、构建。代码过不了这套脚本,话说得再漂亮也不合并。
这三根支柱合在一起,就是“那件事”的最小实体。它不需要你有多么复杂的平台,一个目录、一份规则、一个脚本,就足以把 AI 从“自由发挥”切到“按契约施工”。
2.3 为什么好提示词也替代不了这些约束
可能会有人说,我把规则全部写进提示词里,不也等于给了 AI 约束吗?实际效果差很远。
提示词是一次性指令,它只在当前这段对话里起作用。项目里同时有三个人都在用 AI,每个人各写各的提示词,约束就会分裂。更关键的是,提示词无法强制执行。你让 AI “不要绕过分层”,它这次可能遵守了,下次换个会话、换个模型,它又忘了。约束要真正生效,必须沉淀在仓库里、脚本里、可执行的检查里。
也有人迷信更大的上下文窗口,觉得“把整个仓库都喂进去,AI 自然就理解了”。我实测过这个思路,效果反而更糟。全量代码塞进上下文后,模型确实“看”到了更多文件,但它分不清哪些是核心约束、哪些是历史遗留的坏味道,于是产出会变得更平均、更平庸。上下文不是越多越好,而是越有结构越好。把核心契约单独提取出来,比盲目堆量有效得多。
3. 从 0 到 1 落地:给项目上约束系统的完整步骤
3.1 第一步:先把项目现状拍成“AI 入职手册”
我刚接手一个 AI 辅助项目时,做的第一件事永远是打开仓库,看看有没有类似CONTEXT.md的文件。如果没有,我会先花半小时把它补上,然后才允许 AI 动代码。
这份手册放在context/CONTEXT.md,内容不需要很长,但必须覆盖以下五块:
# CONTEXT - 技术栈:Python / FastAPI / PostgreSQL - 入口:`src/main.py`;启动命令:`python -m uvicorn app.main:app` - 分层约定:`router -> service -> repo`,禁止 router 直接操作数据库 - 核心模块:`inventory`、`order`、`user` - 测试命令:`pytest tests/`;代码检查:`ruff check src/`你可以先用项目目录树快速生成初稿,再人工补充关键约定。命令很简单:
find src -type f -name "*.py" | head -50 tree -L 2 -I node_modules这个文件的价值在于,它把 AI 的“视野”从随机抓取变成了有主次的阅读。模型每次开工前先读它,至少知道项目是怎么分层、怎么跑测试、哪些模块是核心。没有它,AI 每次都在盲人摸象。
3.2 第二步:把聊天里的需求,变成带验收标准的任务工单
项目里最危险的习惯,是在聊天框里直接说“帮我改一下库存功能”,然后 AI 开始发挥。正确的做法是先把需求写成一个任务工单文件,例如tickets/2024-028-inventory-alerts.md:
# 任务 2024-028:新增库存预警查询接口 - 影响范围:`src/modules/inventory/`、`tests/inventory/` - 边界:不改动订单模块;不新增第三方依赖 - 验收标准: 1. `GET /api/inventory/alerts` 可返回预警列表 2. 超时/缺参场景返回约定错误结构 3. 已有单测与新增单测全部通过 - 注意事项:查询必须走 service 层,禁止 router 直接访问数据库为什么要落到文件而不是留在对话里?因为对话是易失的,工单是可持续的。今天你让 AI 改 A 模块,明天另一个会话里 AI 可能因为不知道 A 的边界而踩进去。工单上写了“边界:不改动订单模块”,下次任何人或模型翻开这个文件,都能看到同样的约束。提交代码时还可以在 commit message 里引用工单号,未来的排查就有据可查。
我的实操习惯是“一个工单对应一个会话”。这个工单没做完,不开启下一个会话;这个工单做完了,把结论记录到工单末尾,再进入下一个任务。这样 AI 的每一次工作都被完整归档,项目知识不会丢在聊天历史里。
3.3 第三步:写规则不是用嘴说,让检查脚本当裁判
有了手册和工单,还需要最后一道闸门:自动化检查。我通常在仓库里放一个scripts/check_all.sh,把本地验证统一收口:
#!/usr/bin/env bash set -euo pipefail echo "[check] types..." yarn tsc --noEmit echo "[check] lint..." yarn lint echo "[check] tests..." yarn test --watchAll=false然后在提交前自动执行它。最粗粒度的方案是直接在 Git 钩子里调用:
cat > .git/hooks/pre-commit << 'EOF' #!/usr/bin/env bash set -euo pipefail ./scripts/check_all.sh EOF chmod +x .git/hooks/pre-commit从这之后,任何通过 AI 生成、再由你提交的代码,都必须先过这套脚本。过不了就说明当前状态不满足项目基线,没有资格进入历史。这套机制等于把“验收”从人的口头判断,变成了一个反复执行、不近人情的裁判。
3.4 第四步:小步合并,让每次改动都留有回滚点
约束系统的最后一块拼图是“小步提交”。用 AI 写代码时,人们很容易陷入一种心态:让它一次生成一个完整的大功能,觉得这才是效率。但真正的效率是出了问题还能快速回到上一个稳定点,而不是一口气写两千行然后不知道怎么修。
我给自己定的规则是:一次 AI 会话只允许修改一个逻辑块,改动尽量控制在 200 行以内,并且必须单独跑测试、单独提交。大重构会被拆成十几个小工单,每个小工单都能独立验证、独立回滚。提交信息也写成能自解释的格式:
feat(inventory): 新增库存预警接口 refs: ticket#2024-028 docs: context/CONTEXT.md 同步更新这条规则看起来慢,实际上在项目后半程会带来巨大回报。真正决定项目会不会烂掉的,不是 AI 一次能写多少行,而是你允许代码库在没有回滚点的情况下前进多远。回滚点越密,AI 犯错的代价就越低;试错成本低了,你才敢让它持续干活。
4. 维护这套约束系统:避坑、自律与坏项目整改
4.1 把约定固化成钩子和脚本,别指望自觉
任何约束系统最大的敌人都是“这次先跳过”。人总有状态不好的时候,模型也有“温度”波动的时候,所以我不把规则停留在文档,而是想方设法让它进入自动执行路径。
一个简单有效的手段是给 Git 仓库加上 shared hooks。除了上面提到的 pre-commit,还可以在 CI 里跑同一套check_all.sh,实现“本地过不了、服务器也过不了”的双保险。如果你用的是托管仓库,可以把检查脚本纳入合并请求流水线,跑不过直接拦掉,这样就彻底绕开了“人情”。
这里有一个我踩过的坑:钩子脚本如果写得有副作用,会很讨人嫌。比如动不动就自动改代码、自动git add,会把工作台搞得一团糟。我的原则是“只读校验,不自动改”。脚本只负责报告问题,真正动手修改还是交给人做,这样每个改动都留有人工确认的痕迹。
4.2 让 AI 反过来维护上下文,省掉手工同步
很多人一听到要维护CONTEXT.md,第一反应是“我又不是专业文档写手”。这个抗拒我理解,但解法不是自己咬牙写,而是把维护工作也外包给 AI。
我会在规则文件里给 AI 加一条硬性要求:任何修改触碰了接口签名、模块边界、数据模型或启动方式,必须在同一份提交里同步更新context/CONTEXT.md和对应模块文档。没有同步文档的 diff 视为不完整。
AI 本来就很擅长总结和改写,让它记录“自己改了什么”再容易不过。真正需要你做的,只是 review 它写的更新是否属实。这样上下文快照就不是一次性投入,而是随项目演进不断长出来的活文档。我见过不少人担心维护成本高,实际上一旦让 AI 自己维护文档,成本会低到可以忽略,而收益是整个项目随时都有一个“知道全局的人”。
4.3 项目已经烂了,怎么“止损”和逐步重构
如果项目已经出现改不动、不敢动的情况,不用推翻重来,先按止损三步走。
第一步是补观测。找到最常出问题、最影响业务的核心链路,给它们加上冒烟测试和关键用例。哪怕是一条主链路的健全性测试,也会让你后续的重构有信心。
第二步是切边界。用静态分析或简单脚本,把那些“跨层 import”“路由直接访问数据库”的位置全部搜出来,打印成一份清单。然后按工单一个一个修理,每次只处理一条越界依赖,把接口恢复到约定边界内。
第三步是让 AI 只干“窄活”。在边界没有切干净之前,绝不让 AI 做任何跨模块的大改动。你可以让它补测试、补文档、修静态检查问题,或者在小范围内实现某个已经被完全定义好的功能接口。等模块边界清晰了,再逐步放大它的工作范围。
有些项目确实烂到无法修复。真到那一步,我会选择保留原来 AI 生成的业务梳理、接口清单和测试用例,把它们当作新项目的需求素材,但核心代码不会直接搬过去。可以这么说,AI 生成的代码很多不是“不可用”,而是“不可维护”;不可维护的代码转作脚手架,剩余价值也很高。
5. 常见问题速查:我踩过的坑和排查顺序
5.1 AI 每次改动都“牵一发动全身”,先查什么
项目里最让人头秃的问题,就是“明明只改了一个字段,为什么订单和库存全部报错”。我现在的排查顺序基本固定。
第一,看工作区的实际 diff,而不是听 AI 的解释。很多牵一发动全身的起因,是 AI 为了完成一个局部需求,顺手改掉了另一个文件里它认为“相关”的常量或函数。你只要看一眼 diff,真相立刻清楚。
第二,跑一次依赖关系分析。如果仓库里出现了循环引用,或者接口调用跨过了层级,优先处理环上的那条边。我一般用静态工具打印依赖图,找出环,然后一个提交剪断一条边,不追求一次全部清零。
第三,用git log --graph快速看提交历史。如果某个时间点之后故障变多,多半是那个提交引入了结构性坏味道。定位之后,宁可回滚这个提交,也不要带着坏味道继续往上盖楼。
5.2 AI 嘴上解释得很清楚,代码却对不上,怎么识别
这个问题出现得太频繁了。AI 被问“为什么这么写”时,会生成一段听起来很顺滑的解释,但它的解释同样是“概率续写”,不保证和实际代码一致。
我的方法是要求 AI 把解释和 diff 一一对应。我在工单里固定一条规则:任何涉及逻辑变更的回答,必须列出对应的代码位置和改动行。如果它说“这里做了缓存”,我就要求它指出具体是哪个函数、哪个分支、哪次调用发生了缓存行为。点到文件级和行级之后,大部分“嘴上泡沫”会自然暴露。
还有一个技巧:给任务设置“最小改动”约束。当 AI 给出的改动超过合理范围,但又解释不出来为什么有这么多 diff 时,多半是在用无关改动掩盖真实问题。这时候我会直接退回,要求它给出更小、更精确的 diff。
5.3 一个人维护这套系统会不会本末倒置
这是我在把约束系统推广给其他开发者时最常听到的质疑。我的答案是:约束量要和项目规模匹配,不要让流程变成官僚。
如果你只是在写一次性脚本、临时 Demo,那不需要完整全套,只要一个最轻量的闭环就够了:一份临时的说明文档 + 一条跑通的主流程测试。半小时投入就能挡住大部分低级错误。
如果项目已经进入长线迭代、多人协作或准备商业化,那 context、工单、检查脚本的成本是必须付的。因为服务端不等人,代码库长期存在的每一天,失去约束带来的复杂度都会成倍增长。与其等烂透了再花三倍时间补课,不如一开始就花半天把约束立起来。
不要让“流程”变成目的。每一条规则都应该能回答“它到底在防止哪个具体问题”,凡是回答不上的规则,都可以删掉。我保留的永远是三种:防止漏测试的、防止越界的、防知识丢失的。
我个人现在接手任何 AI 辅助项目,第一件事永远是打开仓库,确认有没有context目录和工单文件夹。没有的话,我会先补上,再让 AI 动代码。因为维持一个项目的生命期,靠的不是某个模型格外聪明,而是仓储里的结构、约束和回滚点一直活着。趁着项目还没有完全走到“不敢动”的那一天,停下来补一份 CONTEXT、写一张工单、接一道检查脚本,可能就是你捞回这个项目最便宜的一次机会。