尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

AI代码生成总翻车?用提示词约束让AI不再自作聪明

AI代码生成总翻车?用提示词约束让AI不再自作聪明 1. AI为什么会好心办坏事先看清自作聪明的底层逻辑1.1 一次简单的排序引发的血案我先讲一个上个月真实发生的事。当时我需要一个函数把一组按创建时间记录的事件排个序我随手在AI编程工具里敲了一句写一个函数把事件按时间排序。结果AI像被点醒了什么天赋哐哐生成了两百多行代码定义了一个SortableEvent抽象接口搞了一个EventSorter工厂类引入了全局日期解析库来处理时区还附带了一个自定义比较器配置项。我盯着那一屏幕代码愣了半天——我只要一个简单的array.sort()啊。这不是AI蠢恰恰是AI太聪明了。大模型的本质是概率补全它不是在理解需求而是在接续你给的文字预测接下来最可能出现的代码是什么。训练数据里包含了大量企业级、工程化的代码片段模型天然倾向于生成结构更复杂、考虑更周到的实现因为在它的统计分布里复杂的代码比简单的代码更常见。所以当你只说写个排序它默认你要处理各种时间格式、要支持扩展、要遵循设计模式——这些默认就是自作聪明的来源。1.2 自作聪明的几种常见临床表现根据我这一年多高强度使用AI编程工具的经验自作聪明大概有以下五种典型症状你可以对照一下自己有没有遇到过。过度设计是最普遍的。明明5行代码能解决的问题AI给你写成50行接口、抽象、工厂模式全上。它不是在完成你的任务而是在展示它的工程能力。你在评审代码时看到那些完全不需要的继承结构多半就是这个原因。幻觉依赖也很要命。AI会凭空import一个包或者调用一个根本不存在的API。因为大模型在训练时见过无数种库的用法但它分不清你当前项目里装了什么也记不清某个库的具体版本、确切函数签名。结果代码一跑直接module not found查半天发现是AI自己编的。擅自重构发生在你只让它改一个函数它却把整个文件都重构了一遍。很多AI编程工具在收到修改指令时会自作主张顺手优化周边代码。这非常危险因为它的优化可能破坏你已经调试好的逻辑。编造实现更隐蔽。你的项目根本没有数据库AI却写出一套ORM调用你只是要一个简单的返回逻辑它默认你后面要接MySQL、Redis提前把基础设施代码都编好了。固执己见最让人崩溃。你明确告诉它这里不要用缓存它嘴上说你说得对下一轮生成还是加了缓存甚至还会在代码注释里解释一句这里加入缓存以提升性能。它在自我说服而你要跟这种温柔的固执反复拉扯。搞清楚这些症状之后你会发现一个核心事实AI不是坏是太能脑补。它的大脑一直在补全你话语背后的潜台词而你恰恰没告诉它哪些是不存在的要求。所以治标的第一步是把需求本身钉死。2. 需求讲清楚AI才不会自由发挥一条提示词的进化过程2.1 别再聊天式对话像写验收单一样描述需求我见过太多人把AI编程工具当聊天机器人用开口就是帮我写个用户模块做个登录功能处理一下数据。这种交流方式在人类同事之间可能行得通因为人类会追问用户模块具体包含什么登录是账号密码还是手机验证码数据存在哪。但AI不会追问——它只会顺着你的话往下编。你模糊它就自由发挥你具体它才收敛。所以我现在给团队定的规矩是每次让AI写代码必须按三层结构写需求。第一层是背景与目标。告诉它这段代码给谁用、放在什么项目里、解决什么问题。比如这是公司内部运营后台的用户管理模块使用Vue 3 TypeScript Element Plus你需要生成一个用户列表页面的查询逻辑。第二层是约束与边界。这是最关键的一层必须写清楚不能做什么。比如不新增第三方依赖不修改其他文件不要做分页之外的任何扩展功能不要写注释。约束是给AI划的警戒线没有警戒线它一定越界。第三层是验收标准。告诉它输入什么、输出什么、异常时怎么办。比如输入一个array数组返回按时间降序排列后的新数组原数组保持不变元素为空时返回空数组不抛异常。这三层写下来看起来像一段简约的需求文档但它能直接过滤掉AI八成的脑补空间。2.2 一个需求从模糊到清晰的实战对照来做个直观的对比。假设你要一个处理用户数据的函数三种说法三种结果。原始版写一个处理用户数据的函数。AI的表现基本是玄学。它可能返回一个包含20个方法的类把用户数据的增删改查全包了还附带一个UserManager的单例。原因很简单你的需求是开放的模型把所有可能性都当作你的需求了。改进版实现一个函数接收用户数组只返回状态为active的用户。这一版AI能正常完成核心逻辑但它仍然会自由发挥可能把active解释成今天有登录记录可能顺手给返回的数组加了缓存可能改变了原数组的顺序甚至可能把输入参数直接sort()了。收敛版实现 getActiveUsers(users: User[]): User[] - 只返回 users 中 status active 的用户 - 数组顺序保持原样不修改入参数组 - 不引入任何第三方依赖不写注释不要抽象接口 - 输入为 null 或 undefined 时直接返回 []这个版本的代码质量就很稳定了。AI知道边界在哪知道什么样算合格也知道哪些事绝对不能做。你会发现约束写得越细AI生成的代码越接近你脑中预期的那个版本最终diff审查时花的时间也就越少。2.3 上下文瘦身别让AI被历史对话带偏除了把需求写清楚还有一个很多人会忽略的坑上下文污染。AI编程工具的多轮对话有个特点——它会参考前几轮的对话内容。你半小时前问过这个项目的缓存应该怎么设计接下来你让它写一个新模块它很可能带着缓存的思路来写。对话历史越长AI就越容易被早先的内容带偏。我自己踩过这个坑。有一次我让AI先分析一个接口的性能瓶颈它分析了半天给出了关于数据库索引的建议。紧接着我让它把这个接口改成返回JSON格式结果它顺手给代码加了读写分离的数据库连接池方案理由是根据之前的分析这个接口可能面临性能问题。我花了两分钟删掉了一堆多余的代码。现在的做法很简单每个任务新开一个对话只把必要的参考文件关键片段贴进去。如果非要在一个会话里连续做多个任务就在每个新任务开始时加一句忽略之前所有对话内容以下是一个全新任务。这个会话重置的动作比很多人想象的更有效。3. 给AI套上枷锁五类约束手段的实际用法3.1 禁止项为什么比期望项更管用我在第2节提到了约束这里展开讲一个核心原理禁止项 期望项。你可能觉得奇怪都是约束为什么不要做什么比要做什么更有效原因是模型的注意力机制。当你写要高性能、要健壮、要易扩展这类期望时模型会把它理解成需要更多设计所以它往复杂方向走。但当你说不做缓存、不做异步、不要引入第三方库时这些具体名词会直接命中模型的决策节点它知道哦这些选项被排除了反而会回到最简单的实现路径上来。我现在写需求的固定套路是这样的任务实现 [具体功能] 输出要求 - 只输出代码不要解释 - 代码放在一个代码块内 - 不要写注释 - 不要引入第三方库 - 不要修改其他文件 - 不要做输入校验之外的防御性编程 - 不要使用设计模式除非问题本身需要 - 禁止使用async/await、Promise、缓存、定时器最后那条禁止使用非常关键。你越是列出你敢想到的坑AI就越不容易踩。它在生成代码时相当于有一张负面清单在约束概率分布。虽然不能保证100%不出格但至少能把最常见的自作聪明点全部掐死。3.2 风格锁定给AI一段标准答案当榜样约束只解决了做什么不做什么但AI写出来的代码风格可能还是跟你团队规范不一致。比如你的项目用camelCaseAI写了snake_case你的团队不用分号AI每行都加分号你习惯用const定义常量AI全用let。这些细节不致命但累积起来就是灾难性的代码漂移。最有效的办法是在提示词里附上一段你项目中的真实代码片段那段代码最好是风格规范、结构清晰的典型文件。告诉AI按照这个文件中的代码风格来实现。大模型对样例的模仿能力非常强一段样例比十句风格描述都管用。我在实际项目中试过给AI一段大约20行的真实工具函数作为风格锚点它生成的代码在命名习惯、缩进方式、错误处理模式上基本能和原项目融为一体。这种方法对于新项目尤其有用——你第一次让AI生成整个目录结构的时候先在提示词里附上两个参考文件效果立竿见影。3.3 输出格式锁定别让AI在代码外自由发挥AI很爱在代码外面加戏。你让它写个函数它给你来一段好的这是一个根据您的需求实现的函数该函数用于……之类的废话解释。你让它写个配置文件它把YAML格式转成了JSON还在开头加了一行注释。这些都是输出格式失控的表现。解决方案是在提示词里硬性规定输出格式。我常用的格式模板输出格式 - 只输出一个代码文件的内容 - 不要输出任何解释、说明、前言、后语 - 不要输出 Markdown 代码块标记以外的任何内容 - 如果无法完成直接输出 ERROR: 原因这个模板的好处是它把AI的表达欲关进了笼子。因为AI在训练时学过遵循用户对输出格式的明确要求你越是给出精确的格式指令它越倾向于严格服从。这里我推荐一个技巧在提示词结尾加上严格按照以上要求输出否则回答将被视为错误。这句否则惩罚的措辞比单纯的请遵守更有效。3.4 任务拆分一次只让AI做一件小而具体的事避免自作聪明的另一个有效原则是任务颗粒度控制。如果你让AI帮我做一个登录系统它一定会给你设计出用户表、验证码、权限模型、Token机制、记住密码——一个需求衍生出八个功能点。但如果你让它只生成一个验证用户密码并返回布尔值的函数它就没那么多戏可以加。所以我现在习惯把大需求拆成小任务每个任务控制在10到30行代码能完成的范围内。需求越小模型的确定性越高AI的自由度就越低。更进一步你甚至可以把一个完整的模块拆成多个独立小任务分多次生成每生成一次你评审一次。这种做法虽然慢一点但质量稳定可控特别适合对代码质量要求高的场景。3.5 代码评审配合git diff只审查它改了什么最后一道约束手段其实不在提示词里而在你自己的工作流中。无论你对AI约束得多好合并代码前必须看diff。我见过太多人被AI带翻车不是因为AI写错了而是因为自己没检查。我的习惯是合并前执行git diff --stat看看改了哪些文件然后逐个文件查看diff。重点不是AI新增了什么而是它改动或者删除了什么原本不该动的代码。AI修bug的时候经常顺手清掉一段看似无用但实际上是边界处理的逻辑这种坑在diff里一屏就能看出来在运行时可能折腾你几天。4. 一个签到接口的完整翻车与修复实录4.1 场景设定一个看起来很普通的需求空讲理论容易抽象我们实战走一遍。假设我现在的项目是一个微信小程序的积分系统需要给用户做一个每日签到接口。需求本来很简单用户在某个打卡页面上点击签到后端记录一条签到记录返回当天的签到状态和连续签到天数。我故意不写清需求直接让AI生成这个接口看看它会整出什么幺蛾子。然后我再带着约束重新让它生成对比两次的差异。整个过程是真实发生的只是我把项目细节简化了一下。4.2 第一轮没有约束的全方位翻车我给出的原始提示词是帮我写一个签到接口用户每天可以签到一次判断用户是否已经签到如果没有生成签到记录返回连续签到天数。技术栈是Node.js Express数据库用MySQLORM用Sequelize。AI给出的代码结构是这样的一个完整的签到路由文件里面包含了Redis分布式锁、消息队列延时任务、批量签到表设计、连续签到奖励配置表甚至还有一个针对签到高峰流量的异步队列削峰处理。整份代码大约300行其中大约270行是我完全不需要的。连续签到的天数计算它确实做了但它设计了一个SigninStreakCache类把连续签到数据缓存到Redis里理由是连续签到查询频繁需要缓存优化。问题是项目现在还没上线用户量是个位数这个缓存完全没必要还引入了额外的Redis依赖。更离谱的是它为了确保每天只能签到一次这个约束给数据库设计了两张表一张签到记录表、一张签到唯一约束表然后用事务和锁来保证并发场景下的唯一性。这就是典型的无约束生成结果。它不是在满足我的需求而是在满足一个它想象中的千万级用户产品经理的需求。用大白话说它把门口装个门铃这个任务做成了设计一整套小区门禁安防系统。4.3 第二轮把约束砸上去之后我重新写了一份提示词这次把约束全部带上任务实现一个每日签到接口 技术栈Node.js Express Sequelize MySQL 项目路径该项目是积分系统中的回调接口已有 models 目录其中 User 模型已定义 功能要求 - 接收 userId 参数 - 根据 userId 查询今天的签到记录如果有则返回 { success: false, message: already signed } - 如果没有创建一条签到记录返回 { success: true, streak: N } - streak 计算规则查询该用户最近的一条签到记录如果昨天有签到则 streak 昨天记录中的 streak 1否则 streak 1 - 当天没有签到记录时才执行创建操作创建后返回 约束禁止 - 不要引入任何第三方依赖Redis、消息队列、缓存统统不要 - 不要修改 models 目录下的任何文件 - 不要写额外的方法只实现接口逻辑本身 - 不要使用 async/await 之外的并发控制 - 不要创建新表只使用已有的 SigninRecord 模型 - 不需要考虑并发重复请求这个接口由按钮触发 - 不写注释不要输出任何解释 输出格式只输出代码文件内容这一轮AI生成的代码大概只有40行逻辑清晰完全符合预期。它没有再设计缓存没有再引入队列只做了最朴素的查询、判断、插入、计数四件事。虽然它仍然在边界判断上多写了一个输入校验我要求了输入校验之外的防御性编程不要做但校验本身还算合理但整体已经收敛到可评审的程度。4.4 这次实操暴露出来的三个关键教训第一所有自作聪明都有明确诱因。第一轮AI设计Redis缓存是因为我没说不让用Redis它设计两张表是因为我没说不让建新表它引入消息队列是因为我没说不让引入中间件。每一个多余的设计都在我的提示词里找到了对应的空子。AI不是随机抽风是全自动寻找你没有设防的区域。第二技术栈描述要精确到有没有。我在第一轮只说了数据库用MySQLORM用Sequelize但没说项目里有没有Redis、有没有消息队列、有几个模型。AI就默认这些都有。第二轮我把已有模型、已有目录都写清楚它的生成结果立刻收敛了。第三接口型任务的输出会主动夸大。因为它觉得你后面可能接更多功能所以自觉做了前瞻性设计。对付这个最简单的说法是这是一个一次性使用的接口后续不会扩展——虽然没有程序员会信这种话但AI信了它就真的只写当前需求。5. 兜底机制即使AI聪明过头系统也不会崩5.1 让AI先写测试再写实现讲一个我用得非常多的反向操作让AI先写单元测试再写实现代码。这个方法之所以好用是因为测试代码本身就是一整套行为说明书。测试里的每个断言都钉死了一个行为规则AI在写实现时会被测试牵着走很难自作聪明地乱加功能。具体做法是在提示词里要求AI先输出测试用例再输出满足测试的实现。你不需要真的按测试驱动开发的流程走只要让AI自己提前把验收标准写出来它生成实现代码的时候就会默认收敛。而且测试代码还能作为你后续验收的自动化依据。我在团队里实践一轮后发现AI生成测试代码时通常会更保守——因为测试里已经定义了输入输出、边界情况和异常返回实现代码自然只能围绕这些行为展开。此后代码评审的成本直线下降因为挑毛病变成了跑测试。5.2 git分支保护与AI代码隔离如果你的团队不止你一个人在用AI编程工具还有一个值得推行的做法AI生成的代码必须走单独的分支经过人工评审后才能合入主分支。不要让AI直接往主干代码上推。分支隔离看起来只是流程问题但它实际上是一个安全网——如果AI生成的代码有严重的副作用你随时可以放弃整个分支而不是在主干上一层层找问题。我见过不少团队直接在主分支上让AI改代码AI一旦顺带重构了三个文件你连回滚都很难精准操作。单独分支配合git diff对比才能让AI的工作成果处于可审查、可回退的范围内。此外我强烈建议在仓库里加一条Shell脚本或者CI检查扫描新增代码里是否有禁用依赖列表中出现的包名或API。这是最后一层硬性防线专门拦截AI的幻觉依赖。5.3 常见问题与排查技巧速查表最后把这一年在AI写代码上踩过的坑整理成一张速查表遇到问题直接查。症状根本原因对应策略AI把简单需求做成了复杂框架需求描述太开放未定义边界增加禁止使用设计模式/缓存/异步等负面清单明明说了不要Redis还是被加上负面清单不够具体只说了不要缓存直接点名禁止引入redis、ioredis等任何缓存相关依赖修改一个函数结果整个文件被重构修改型任务的提示词不够窄明确只修改第X行到第Y行的函数体其他内容不得改动生成的代码一运行就报错说模块不存在幻觉依赖AI不知道项目装了什么在提示词里贴出package.json或依赖列表让它写A功能结果连B功能也写了任务颗粒度过大拆任务一次只给一个不超过30行的具体函数需求AI坚持用错误方案怎么纠正都没用会话上下文污染它还在按之前逻辑执行新开对话粘贴干净的需求描述重置上下文生成的代码每次风格都不一样没有提供风格锚点附上一段项目真实代码作为风格样例要求按此风格实现代码虽然能跑但总有一种哪里不对的感觉它遵守了显式需求但在隐性细节上自由发挥把验收标准写具体输入输出格式、返回结构、异常行为全都写明5.4 几个能把AI自作聪明概率降到最低的通用技巧除了上面这些最后再分享几个零散但特别实用的技巧。第一个是给AI一个版本号或上下文开关。在每次新任务开始时加上一句独立任务忽略之前所有对话。当前项目代码版本为v1.2所有代码基于这个版本进行编写不要设想任何未来功能。这句话看起来像废话但它能有效重置模型的隐性偏好尤其是你在一个会话里跑了七八轮之后的场景。第二个是善用单一职责关键词。在需求描述里明确写这个函数只做一件事其他功能通过参数传递由调用方决定。这个语义对模型有天然约束力因为训练数据里的单一职责原则意味着不要塞额外功能。第三个是对AI生成代码做检查清单式验收。我给自己列了一个最小验收清单代码行数是否在合理范围、有无多余依赖导入、有无超出任务范围的函数定义、有无对入参的意外修改、有无被AI删掉的原有用例分支。每次合并前对着清单打勾打不完不合并。第四个也是最重要的不要迷信AI编程工具的品牌。市面上有免费的工具也有价格不低的商用工具但就自作聪明这个维度来说没有哪个工具敢承诺100%不脑补。你真正要优化的不是你用哪款AI写代码而是你对AI下指令的能力。工具只是执行者你才是架构师。在我个人经验里AI写代码这件事最让人痛苦的从来不是它不会写而是它写得太多了。这种太多源于模型天性里的过度补全倾向但好消息是这种倾向是可以通过约束技术压制的。把需求钉死、把边界划清、把验收写细、把兜底做严AI可以从一个热情过度的实习生变成一个靠谱的初级开发。当然AI并不会因为你有了一次成功的约束体验就永远听话。每一次让它写代码都是一场新的博弈。保持警惕、坚持审查、持续沉淀自己的提示词模板是我目前能给出的最实在的建议。
返回列表