上个月我review一个新人的PR,500多行代码横跨十几个文件,乍一看挺唬人,泛型套泛型、工厂包观察者,注释比代码还密,一股子“AI香水味”扑面而来。问了一句“这代码怎么来的”,对方很坦诚:让AI全量生成的。我把那段代码里三层外三层扒了一遍,发现它确实能在测试环境跑通,但一旦牵扯到项目里的老接口、历史字段、权限边界,处处都是雷。那一刻我意识到,AI辅助开发这件事,很多人从一开始姿势就错了。
这篇内容我想掰开揉碎讲清楚:AI这个工具到底该怎么用,才能吃到效率红利又不给项目埋雷,以及为什么“让AI全量生成代码”是当前团队协作里最危险的习惯之一。适合正在用AI写业务代码、或者正打算把AI引入日常开发流程的工程师参考,也适合团队技术负责人拿去做内部约定。
1. 全量生成的诱惑与陷阱:为什么这条路走不通
1.1 看上去很美:AI全量生成带来了什么错觉
很多开发者第一次用AI编程助手时,都会被它的“输出能力”震住:丢一个需求进去,几十秒出来一份结构工整、注释齐全、还带单元测试的代码。尤其当你面对一个不熟悉的框架、一种没写过的语言时,这种感觉几乎像捡到宝。
我见过太多人从此养成了“需求来了直接贴给AI,跑通了就提PR”的习惯。表面上开发速度起飞了,但实际上这是个巨大的错觉:AI生成代码的“完成度”不等于“正确性”,更不等于“可维护性”。它只是按照训练数据里最常见的模式,拼出了一段概率上合理的代码,至于这段代码是不是符合你项目的实际情况、有没有踩你项目里的历史包袱,它根本不知道。
更隐蔽的问题是认知让渡。当一段代码不是经过你的思考过程产出的,你对它的理解就停留在“能跑”的层面。一旦线上出了问题,你面对的不是一段你能讲清楚来龙去脉的代码,而是一个黑盒。这个黑盒在代码评审时也极其尴尬:review的人看不懂为什么要这么设计,写的人也说不出所以然,最后要么集体妥协合并,要么整个模块推倒重写。这两个结果,都是技术债的开端。
1.2 上下文窗口的天花板:AI看不到你项目的全局
为什么AI全量生成代码这么容易翻车?底层原因要从它的工作方式理解。现在的AI编程助手无论模型多大,本质上都是“读完你给的上下文,然后预测下一个token”。它没有真正“跑过”你的项目,不知道你的接口文档长什么样、数据库里某张表有多少历史字段、某个老模块里藏着多少兼容逻辑。
举个例子。我之前在某后台系统里让AI帮忙补一个“导出用户列表”的功能,它直接对着我贴进去的列表接口生成了全套代码,看起来完美。结果测试时发现,导出的CSV里把软删除的用户也全带出来了。原因很简单:接口返回值里有deleted_at字段,真正的业务规则是导出时默认过滤掉这些记录,但AI并不知道“deleted_at=记录已删除”这个隐含约束,它眼里那只是一个字段名。
这就是全量生成的痛点:你没法把整个项目的隐性知识都塞进一次对话里。就算模型支持超长上下文,你能把多少个文件完整地贴给它?贴完它又能“理解”多少?上下文窗口再大,也装不下业务在这个项目里积累的六七年历史。所以AI看到的世界,永远是你的“局部投影”,让它在局部投影上做全局决策,翻车是必然的。
1.3 伪重构与技术债:好看代码背后的隐形代价
还有一种全量生成特别容易诱发的问题:AI会主动“优化”你的代码结构。你明明只是让它给一个老模块加个新按钮,它觉得你原来的代码风格太旧,于是顺手把整个文件改造成了新写法,还重构出一个抽象层。表面上代码更“现代”了,实际上一场灾难刚刚开始。
团队里其他成员对这块代码的认知还停留在旧结构上,突然来一个完全陌生的新结构,所有人的阅读成本都上去了。更麻烦的是,这个“伪重构”没有经过正常重构该有的流程:没有配套测试覆盖、没有分步验证、没有迁移兼容方案。三个月后老接口下线、新需求迭代,接手的人面对这段漂亮但根底不稳的代码,只能骂娘。
我自己踩过类似的坑:某次让AI帮忙改一个内部工具类的异常处理逻辑,它改完之后把整个类从回调风格重构成了async/await。单测能过,性能没问题,但当时项目里所有调用方全是按回调风格传参的,这一重构导致六个调用点全部需要联动修改。最后那个PR改了四个多小时才收尾,效率反而比手写慢了三倍。从那以后我就立了个规矩:AI只能改我指定范围内的代码,不允许顺手牵羊动别的部分。
2. 正确姿势的核心:从“让AI写代码”变成“和AI结对编程”
2.1 AI的定位:初级工程师,不是架构师
想清楚AI在开发流程里的角色,很多问题就迎刃而解。我一直跟团队说,把AI当成一个“随叫随到、基础扎实但完全不懂项目背景的实习工程师”就对了。实习工程师什么特点?你给他明确的小任务,再给他足够的背景和样例,他能干得又快又好;但你如果让他独立负责一个模块的整体设计,他大概率会写出一些理论上成立、实际上到处碰壁的方案。
所以正确的分工应该是:人负责定义问题、拆解任务、确认边界、设计接口与数据结构;AI负责在这套明确框架里,快速生成实现代码、做格式转换、补样板逻辑、写正则表达式和测试骨架。一句话,人做架构师和产品经理,AI做执行者。这个分工一旦颠倒,你的项目就开始失控。
我见过一个团队,把“微服务拆分方案”这种架构级任务也丢给AI,让它一口气生成服务划分、接口定义、数据库表结构。生成的文档看起来很完整,但没有任何人真正思考过这个拆法对当前团队的技术储备和业务节奏是否合理。方案评审会开了三场,最后推倒重来。这不是AI不好用,是把工具用错了层级。
2.2 喂上下文的方法论:Sample锚点、约束清单、边界说明三件套
既然定位是“带实习工程师”,那怎么下指令就变得非常关键。我总结了一套给AI下编程指令的三件套:风格锚点、约束清单、边界说明。
第一件,风格锚点。想让AI写出来的代码跟你的项目风格一致,就给它贴一小段现有代码。不是把整个文件塞过去,而是选一个“结构最典型”的函数或模块,告诉它“请照着这个风格写”。人的模仿能力来自于见过足够多的样本,AI也一样。你给的示例里自然包含命名习惯、缩进风格、错误处理方式,比你在Prompt里写一百字“请保持代码风格统一”有效得多。
第二件,约束清单。明确告诉AI“不要做什么”,往往比“要做什么”更重要。比如“不要引入新的依赖”“不要改现有函数签名”“不要使用class,保持函数式写法”“不要生成单元测试,测试我自己写”。这些约束直接决定AI输出的代码能不能无缝融入你的项目,能省掉大量返工时间。
第三件,边界说明。把输入输出形状说清楚,把特殊边界列出来。比如“输入是对象数组,字段可能是null”“如果字符串包含逗号、引号、换行符需要转义”。AI是概率模型,你不说它就想不起来,一旦想不起来,它就会用最常见的“理想情况”去写,而真实世界里到处都是不理想的输入。
下面是一个我常用的Prompt模板,包含这三件套:
【任务】 在 /src/utils/export.js 里新增一个函数 convertToCsv(rows, headers),把对象数组转成CSV字符串。 【风格锚点】 参考同文件里已有函数 downloadTextFile 的写法:函数式、命名用camelCase、不写类。 【约束】 - 不要引入任何依赖 - 不要改动 downloadTextFile 函数 - 不要生成注释 - 保持纯函数 【边界】 - rows 是对象数组,字段值可能为 null / undefined - headers 是 { key, label } 数组 - 单元格内容若包含逗号、双引号、换行符,需要按CSV规范转义 - null / undefined 输出为空字符串这套写法一开始会让人觉得“要求怎么这么多”,但用久了就知道,把上下文喂清楚花掉的五分钟,能省下后面改代码加沟通的半小时甚至两小时。
2.3 任务切分:大需求拆成AI能扛的小任务
另一个核心技能是任务切分。AI生成代码的质量,和任务的粒度强相关。你扔给它一个“做一个报表导出模块”,它写出来的东西一定是一个又大又全的万能模块;你扔给它一个“写一个将二维数组转成CSV字符串的纯函数”,它写出来的东西大概率又小又准。这就是任务粒度对输出质量的影响。
怎么切?我一般按“可独立验证”来切。一个任务切分出来之后,要能回答三个问题:输入是什么、输出是什么、怎么验证正确性。如果这三个问题回答不了,说明这个任务还不够小,继续切。
拿一个实际需求举例。某个后台管理页面要实现“导出当前筛选条件下的用户为CSV文件”,我不会把这个需求直接丢给AI,而是拆成下面四步:
- 第一步:确认数据来源。前端已经有一个列表页,接口返回JSON数组,这一步是人工确认的,不需要AI参与。
- 第二步:写纯函数,把对象数组转成CSV字符串。输入输出清晰,适合交给AI。
- 第三步:写触发浏览器下载的代码。这个逻辑虽短但有细节,需要人确认Blob类型、文件名等。
- 第四步:把前后端串起来,加权限校验和异常处理。这一步涉及业务规则,人工主导,AI只做参考。
这样切完之后,你会发现AI真正参与的部分只是一些小而明确的实现,而不是整个功能。而整个功能的设计和拼接,始终掌握在你自己手里。这正是“AI辅助开发”和“AI替代开发”的本质区别。
3. 一个需求全流程实操:用AI实现“导出CSV”功能
3.1 先切任务,再定接口:人工先做设计
这一节我用一个完整的例子,带你走一遍我平时用AI做一个功能的实际操作流程。需求很简单:某后台系统的用户列表页,加一个“导出CSV”按钮,把当前条件下的用户数据导出成文件。
拿到需求,我首先确定接口方案。列表页已经有一个接口返回用户数据,我会让前端导出时复用同一个接口,而不是另开新接口。这样做的原因有两个:一是保证导出数据的过滤逻辑和列表展示完全一致,二是权限控制也复用同一套,不需要额外维护一套导出权限规则。这个决策必须人来做,因为涉及对现有接口语义的理解。
接着定义前端数据结构。列表页渲染时会有一个用户数组,数组中每个对象包含用户ID、姓名、邮箱、状态、注册时间等字段。我要做的是加一个纯函数转换和一次下载触发,其他全部复用现有逻辑。
界面侧还需要加一个按钮。按钮的事件处理函数可以人写,也可以让AI写,但核心的数据处理逻辑我建议独立出来做成纯函数,这样方便单测,也方便以后复用。功能虽小,但结构上要干净。
3.2 让AI干它擅长的活:写纯函数与生成样板
设计确定之后,我才开始请AI动手。我给它发的第一轮Prompt,就是前面模板里那个convertToCsv任务。这个任务足够小而明确,AI很快就给了实现:
function convertToCsv(rows, headers) { const escapeCell = (value) => { if (value === null || value === undefined) return ''; const str = String(value); if (/[",\r\n]/.test(str)) { return `"${str.replace(/"/g, '""')}"`; } return str; }; const headerLine = headers.map((h) => escapeCell(h.label)).join(','); const dataLines = rows.map((row) => headers.map((h) => escapeCell(row[h.key])).join(',') ); return [headerLine, ...dataLines].join('\r\n'); }拿到这段代码后,我不会直接信任它,而是花两分钟人工review。重点看几件事:转义规则是否覆盖了逗号、双引号、换行;空值处理是否符合预期;表头是否用的是label而不是key;输出换行符用的是不是\r\n。这些点都是导CSV常见的坑,AI这次都处理对了,但假设它处理错了,这一步就能发现并修正,而不用等到测试阶段炸出来。
这就是我强调的“小任务好审查”原则。如果AI刚才生成的是一个500行的完整模块,我根本没精力逐行review,大概率会扫一眼就提交,然后带着隐患上线。但现在这段函数只有几十行,我有充分的把握确认它的逻辑是否正确。
3.3 人工缝合与边界打磨:BOM头、转义、Blob下载
接下来是人工缝合环节。纯函数转换好之后,前端点击导出按钮的完整逻辑是这样的:
const downloadUsersCsv = (users) => { const headers = [ { key: 'id', label: '用户ID' }, { key: 'name', label: '姓名' }, { key: 'email', label: '邮箱' }, { key: 'status', label: '状态' }, { key: 'created_at', label: '注册时间' }, ]; const csv = convertToCsv(users, headers); // 加BOM是为了防止Excel打开CSV时中文乱码 const content = '\uFEFF' + csv; downloadTextFile(content, '用户列表.csv', 'text/csv;charset=utf-8;'); };这里有一个我踩过很多次的坑:不加BOM头。纯UTF-8的CSV文件用Excel打开时,很多情况下默认按本地代码页解析,中文直接乱码。加一个\uFEFF字节序标记,Excel才会正确识别为UTF-8。这个细节AI大概率不会主动帮你加,因为它默认你用的是最标准的文本工具,而现实世界里打开CSV最多的工具就是Excel。
下载逻辑本身比较简单:用Blob包裹CSV字符串,创建一个临时a标签,设置download属性,触发click,然后撤销URL。这些代码在绝大多数现代浏览器里都能稳定工作,我通常会直接让AI生成这个样板函数,然后人工确认一下就好。注意文件名不要写死,最好根据导出时间生成,避免用户反复下载时被浏览器重命名。
3.4 测试用例先行:让AI的产出变得可验证
纯函数写好之后,我一般会先写一个测试用例验证,再接入按钮事件。这是防止CSV逻辑回归的最有效手段,也符合“先有验证,再有信任”的原则。我会拿几组边界数据——包含逗号的字段、包含双引号的字段、包含换行的字段、null字段、中文字段——全部过一遍。
const rows = [ { name: '张三', tag: 'a,b', desc: '第一行\n第二行' }, { name: '李四', tag: '"quoted"', desc: null }, { name: '王五', tag: '普通', desc: '中文内容' }, ]; const headers = [ { key: 'name', label: '姓名' }, { key: 'tag', label: '标签' }, { key: 'desc', label: '备注' }, ]; console.log(convertToCsv(rows, headers));运行结果会像下面这样:
姓名,标签,备注 张三,"a,b","第一行 第二行" 李四,"""quoted""", 王五,普通,中文内容这个输出看起来平常,但每一行的细节都值得琢磨:a,b被引号包住避免分列、"quoted"中的双引号被转义成两个双引号、换行符被保留在引号内、null字段输出为空。测试通过后,我才把这个函数接入按钮事件。这种“先验证再集成”的习惯,让我用AI写代码时踩雷的概率大幅降低。
4. 常见翻车现场与排查经验:我的避坑清单
4.1 AI引用了不存在的依赖或过时API
这是最常遇到的问题。AI生成代码时很自信地写一个import xxx from 'some-package',结果那个包根本不在package.json里。或者更隐蔽的,它用一个库的过时API,而这个API在新版本里已经废弃了,代码跑起来全是warning。
我的处理办法分两步。第一步,在Prompt的约束清单里明确写“不要引入任何新的依赖,优先使用标准库或项目已有依赖”。第二步,如果AI还是引用了什么,我不急着装包,而是返回去问它“这个依赖是干什么的?项目里有没有现成的替代?”然后基于项目已有依赖手工改写。装一个不熟悉的第三方依赖进生产项目,必须经过完整的安全审查,这个流程成本远高于手写几十行工具函数。
顺带多说一句,AI生成的代码里如果出现lodash、moment.js这类常见但早就有原生替代的库,我会格外警惕。不是这些库不好,而是为了一个isEmpty或日期格式化去引入一个几千行的依赖,性价比太低,而且容易埋下供应链风险。
4.2 风格与项目脱节:用风格锚点解决
项目里全是一堆函数式写法,AI丢出来一个Java味十足的Class;项目命名习惯是user_name,它写出来全变成userName;项目错误处理喜欢返回Result对象,它擅自改成抛出异常。这些风格冲突在代码Review时会被放大,因为风格问题不像逻辑错误那么致命,但会让团队每个人都很不舒服。
解决这个问题最直接的方式就是我前面强调的“风格锚点”——粘贴一小段现有代码让AI模仿。我还有一个小技巧:如果项目用了ESLint或Prettier,我会直接把项目的lint规则文件路径告诉AI,让它参考规则写代码。虽然AI不会真的去读你本地文件,但这个约束会触发它对常见风格规范的对齐,实测效果比空口说“保持代码风格”好得多。
4.3 AI大改文件破坏了原有逻辑
有一种情况让开发者欲哭无泪:AI接收了你“修改这个函数”的请求,结果它把整个文件都重写了,改完之后其他几个无关函数的行为全变了。这是因为在很多AI编程场景里,模型会倾向输出一个“完整自洽的文件”,而不是只输出你要求的那一小块差异。
遇到这种情况,我的操作规范是:让AI修改既有文件之前,先手动commit一次。改完之后用diff工具逐块审查,任何不属于本次任务的改动全部撤销。这个流程听起来繁琐,但踩过一次坑就知道有多重要。当然更稳妥的方案是,让AI“创建新文件”而不是“修改旧文件”,然后人来做文件合并。新文件写坏了不影响原有的,diff起来也清清楚楚。
4.4 过度设计综合征
AI有个很有意思的倾向:给定一个特别简单的需求,它也喜欢生成一个特别复杂的结构。明明只是写个格式转换,它非要引入Factory模式、抽象一个DataExporter接口、预留一个“可扩展的插件机制”。这种过度设计看得人血压升高。
我的处理方式是在Prompt里直接声明:“保持简单和直接,不允许引入设计模式,除非我明确要求。”另外我会额外加一句:“用最直白的写法实现功能。”有了这两句约束,AI生成的东西大多回归朴素。如果它还是忍不住抽象,我就只保留它生成的核心逻辑,把多余的结构全部删掉。毕竟,对一个12行的纯函数来说,最好的结构就是12行,不需要一个抽象层。
4.5 常见问题速查表
| 现象 | 原因 | 处理方式 | 预防手段 |
|---|---|---|---|
| 引用了不存在的依赖 | AI基于训练样本“编造”依赖 | 不急着装包,先问AI依赖用途,用项目已有代码替代 | Prompt里写明“不要引入新依赖” |
| 代码风格与项目不一致 | 缺少风格参照 | 贴风格锚点代码,少用空泛描述 | 提供项目里典型函数作为参考 |
| 修改文件时破坏了无关逻辑 | AI输出的是完整文件而非差异 | commit后diff逐块审查,撤销无关改动 | 尽量让AI新建文件,人工做合并 |
| 简单需求被过度设计 | 模型倾向生成“通用的架构” | 删掉多余抽象,保留核心逻辑 | Prompt明确“保持简单,不要设计模式” |
| 导出中文乱码 | 缺少BOM头或使用了错误编码 | 手工加\uFEFF,指定charset=utf-8 | 这类业务常识要记在团队文档里 |
| 生成的测试用例不覆盖边界 | AI按理想输入生成测试 | 人工补充空值、特殊字符、边界数据 | 测试用例设计是人的职责,别外包 |
5. 什么样的活适合甩给AI,什么样的活必须先自己上
5.1 适合AI干的事:样板、转换、正则、注释
AI编程助手在哪些场景下效率最高,哪些场景下风险最低,我觉得可以总结成一张任务分派表,团队内部也一直按这张表对齐预期。
- 样板代码。CRUD接口、标准表单校验、DTO字段映射这类重复度高的代码,AI写起来又快又好。人的精力应该留给这些样板之外的业务判断。
- 代码格式转换。TypeScript和JavaScript互转、ES5到ES6语法升级、对象数组结构改造,AI做这种机械性的转换工作可以说零失误率,比人手稳得多。
- 正则表达式。写正则是个体力活,尤其是可读性低的复杂正则。让AI生成并配几个测试用例,效率极高,但建议了解它生成的每部分含义,别当黑盒。
- 单元测试骨架。AI很适合根据函数签名生成测试用例骨架,但测试数据的构造和断言逻辑本身,需要人来补齐,毕竟只有人知道业务上什么结果才算“正确”。
- 代码注释和文档生成。对已有函数生成JSDoc注释、README示例,AI几乎不会出错,能省大量时间。
这些任务的共同特点是:输入输出明确,评判标准清晰,不需要太多项目背景。把它们交给AI,相当于把重复劳动外包,自己专注设计。
5.2 不能全甩的事:架构、安全、性能、业务规则
反过来,有几种类型的任务我会强烈建议你“让AI辅助,但绝不放手”。第一类是架构设计。服务怎么拆分、模块边界在哪、数据流怎么走,这些决策依赖对当前团队节奏和未来业务方向的综合判断,AI再智能也给不出“你这家公司最适合”的答案。
第二类是安全与权限逻辑。凡是涉及身份认证、数据权限、越权校验的代码,我建议每一行都由有经验的工程师亲手写。AI并不理解“这个接口的数据谁能看”背后的业务规则,它大概率会把所有数据一视同仁地返回出来,问题在测试环境根本发现不了,上线就是数据泄露事故。
第三类是性能与并发控制。限流、降级、并发安全的写法看起来就那么几行,但容错设计的质量全在经验里。AI可以帮你生成一个基于分布式锁的样板,但它无法判断你的锁粒度合不合理、会不会在高并发下把接口拖死。
第四类是核心业务规则。比如计费逻辑、库存扣减、状态机流转。这些代码出bug的代价极高,而且业务语义极其微妙,机器是完全不可能替代人来判断的。
我用一句话总结自己的原则:AI可以用在“执行路径清晰”的地方,而所有需要“做决定”的地方,人都必须亲自在场。你把决定权交出去,就是把自己对项目的掌控力交出去。
5.3 团队协作中推广AI辅助开发的几条约定
最后聊聊团队层面。AI辅助开发不是说每个开发者各自野路子使用,而是需要有共同约定,否则代码库很快就会风格崩坏。
我目前的团队里有几条硬性约定。第一,AI生成的代码必须经过和手写代码同等严格度的Code Review,不允许“AI写的所以不用细看”这种心态。第二,AI生成代码需要标注来源,我们会在PR描述里注明哪些文件是AI辅助生成的,这样评审人心里有数。第三,任何涉及权限、资金、核心业务规则的关键路径代码,不允许由AI独立产出,必须由人先手写,AI只做审查或测试补充。第四,所有AI生成代码合并前,必须有配套测试覆盖关键路径。
这几条约定不是限制AI的使用,恰恰相反,它们让AI的使用变得更加可持续。一个代码库里偶尔出现几段AI生成的代码很正常,但如果整个代码库变成了AI生成代码的堆积场,不出半年,你的团队对代码库的掌控力就会全面退化,而那时再想重建认知,成本远高于省下来的那点开发时间。
说回我个人的体验,现在每天动手写代码前,我会先花几分钟看一眼AI之前生成的产出,问自己三个问题:这段代码我能不能讲清楚它在干什么?它符不符合现有项目的风格?如果三个月后有人接手改它,会不会觉得莫名其妙?三个问题的答案都确定之后,它才有资格进入主干。这个方法帮我稳住了效率红利和可维护性之间的平衡,也是我这段时间最想分享给同行的一条经验。