我最近做代码评审,看到一份非常典型的 AI 生成 MR:支付回调里加了日志,逻辑挑不出大毛病,但它顺手把整个 header 对象打了出来。如果合入,access token 会跟着日志一起流进日志系统。这不是个例,而是 AI 编码时代的典型翻车方式——AI 生成的代码越来越“像样”,但它不会主动告诉你它改了哪些文件、加了哪些依赖、埋下了哪些自己察觉不到的问题。所以我想认真聊一个很多人忽略的话题:工程级 AI 编码边界与防护规则设计。
如果你正在用 AI 写代码,或者正准备把 AI 编码工具引入正式项目,这篇文章适合你。我会结合自己踩过的坑,讲清楚为什么边界比效率更重要,再给出一套可以直接拿走的防护规则设计,从规则文件到 Git Hook、再到 CI 流水线,尽量做到每一步都可落地。
1. 为什么 AI 代码总会“暗度陈仓”:失控场景复盘
1.1 主路径正确,边缘条件一碰就碎
让 AI 写一个用户上传头像的功能,它生成的文件处理函数看着漂漂亮亮,文件名处理、格式校验都有。但仔细一检查,能发现三个问题:文件名直接用原始文件名拼进路径,没有做归一化,路径穿越风险;MIME 校验用了includes('image'),image/svg+xml也能混进来,如果 SVG 里塞了脚本,就成了存储型 XSS;文件大小检查放在读入内存之后,一个超大文件能把内存打满。
这不是个例。AI 的训练数据让它特别擅长模仿“正常代码的长相”,但项目特有的威胁模型和边界条件,它没有感知。它写出来的代码主流程基本能跑,一到异常分支、恶意输入、极端参数,就开始露馅。这些代码混进主干之后,不是“有个 bug 没修”这么简单,而是直接给系统开了一个口子。
1.2 AI“一本正经”编造 API 和配置
更让人头疼的是 AI 的“幻觉”。我问过 AI 某个第三方 SDK 的用法,它给我写出了一个根本不存在的函数签名,参数名、返回类型看起来都合理,一编译就报错。还有一次,它在一个 Node 项目里自行添加了一个 npm 包,没做任何版本锁定,理由是“这个场景用这个库比较方便”。
这种“自信地犯错”比不会写更危险。人工写代码时,作者通常会查文档、跑测试确认一下;AI 生成代码后,如果开发者在 review 时也没多想,这些不存在的 API、未锁版本的依赖就会直接进入代码库。等部署环境一换,或者依赖一更新,问题才会集中爆发。
1.3 失控的真正代价:责任和信任都模糊了
代码是资产,也是负债。AI 生成的代码在没有边界约束时,会变成一种特殊的负债——你不知道它在什么场景下会失效,也不知道它背后的设计依据是什么。人工写代码时,作者至少踩过哪些坑、为什么绕开某个方案,都会有印象;AI 代码没有这些上下文。出问题后,你不得不从需求反推,重新理解一遍代码逻辑,甚至重写。
更麻烦的是责任模糊。团队一旦养成“AI 写、我随便看”的习惯,review 就会流于形式。到时候出了线上事故,谁都觉得不是自己的问题,但谁都无法快速定位问题。结果就是信任崩塌:一部分人开始把 AI 编码拉黑,另一部分人继续无边界地依赖 AI,团队割裂成两个极端。
2. 工程级 AI 编码边界设计:三层边界模型与工作流改造
2.1 动手前先回答三个问题
我建议你在给 AI 编码立规矩之前,先认真回答三个问题:
- 第一,你允许 AI 写哪些文件?哪些文件是绝对不让 AI 碰的?比如鉴权模块、支付逻辑、部署脚本,这些改动一旦出错,影响的是账号和钱。
- 第二,每一段 AI 代码进入主干前,要经过哪几道验证?单测、lint、安全扫描、人工 review,缺了哪一道都不算完整闭环。
- 第三,万一 AI 改错了,你能在多长时间内回滚?影响范围到哪里?
这三个问题看着简单,但绝大多数团队答不上来。答不上来,说明边界是模糊的。别让这种模糊延续到代码库里。
2.2 入口边界:任务拆到多细才算稳
入口边界解决的是“给 AI 什么活儿”的问题。很多开发者习惯一句话丢给 AI:“帮我重构这个模块”。任务太大,AI 的自由发挥空间就大,产出不可控性也大。工程级的做法是把需求拆成小而明确的“任务单元”,比如“把 utils/format.ts 里的手机号格式化函数拆成两个函数,并补充单元测试”这个粒度就比“重构整个 utils 模块”稳得多。
我给团队的提示词模板大概是这样的:先说明目标任务,再限定文件范围,再列出约束条件。比如“只修改 src/utils/date.ts 文件;不要新增依赖;不要改变现有函数签名;不要格式化无关代码;提交前跑一遍相关测试”。这些约束看起来啰嗦,但对 AI 来说,约束越明确,它的行为就越可控。
2.3 过程边界:文件范围、变更粒度和可观测性
过程边界解决的是“AI 干活时怎么限制它”的问题。我习惯从三个维度去卡:
- 文件范围:用规则文件把关键目录拉黑,比如
src/auth/**、deploy/**,凡是 AI 生成的 diff 碰了这些目录,直接拦截。 - 变更粒度:AI 一次只能提交一个任务单元对应的改动,不允许夹带无关重构。判断方法很简单,看 diff 里有没有跟当前任务无关的改行。
- 可观测性:AI 生成的提交必须在提交信息里带
[ai]或类似标签,比如feat: add phone formatter [ai]。标签的作用不是区别对待,而是让评审者心里多一道校验线,也让日后的代码考古能够快速定位这段代码的来源。
2.4 出口边界:没有验证就不许合并
出口边界解决的是“AI 写的东西怎么验收”的问题。我规定 AI 相关 MR 必须满足三个条件才能合并:单测和 lint 全绿;密钥扫描和依赖审计通过;至少一个负责该模块的人做了 review。
如果有 AI 改动的文件属于关键模块,比如鉴权、支付、权限模型,我会要求两个人 review,其中一个人专门看安全影响。这个环节不能省。很多项目出事,都是因为 AI 写着顺手,review 看着也顺手,结果上线后一打就碎。
2.5 把 AI 编码改成“时间流式”工作流
现在很多 AI 编码工具都强调“一键生成”,但工程上我更推荐“时间流式”推进。所谓时间流,就是把开发过程看作一系列按时间顺序排列的可回放步骤,每个步骤只做一件事,做完就提交,提交就能回滚。
举个例子,让 AI 重构一个父组件,我不会让它一次性输出完整的新文件。我会拆成三个时间片:第一个时间片,把被内联的子组件抽成独立组件;第二个时间片,替换父组件的调用方式;第三个时间片,跑测试并清理死代码。每个时间片单独提交、单独 review。这样就算第二个时间片的方案有问题,也不会把第一个时间片的好代码拖下水。
工具类型上,现代 AI 代码编辑器、命令行 Agent、自动评审机器人可以组合成一条流水线。哪怕只是先用免费的基础工具,也应该让它们遵循同一套工作流,而不是各干各的、最后人工补救。AI 代码编程工具的效率差异其实没有想象中大,真正拉开差距的是“输出有没有被有效校验和控制”。
3. 防护规则落地:规则文件、Git Hook 与 CI 强校验
3.1 规则要防的不是 AI,而是五类高发风险
规则不是用来防 AI 的,防的是 AI 放大后的工程风险。从我实际看到的上线事故来看,有五类风险优先级最高:
| 风险类型 | 典型表现 | 防护手段 |
|---|---|---|
| 敏感信息泄露 | 硬编码密钥、日志打印 token 和 header | 密钥扫描、正则黑名单 |
| 危险 API 调用 | eval、child_process.exec、shell 拼接 | 危险调用黑名单 |
| 依赖失控 | AI 自行新增第三方包、不锁版本 | 依赖白名单、lock 文件检查 |
| 权限过宽 | 文件权限 0o666、越权写操作 | 代码风格检查、人工审核 |
| 非幂等操作 | 数据库迁移脚本被重复执行、批量写操作 | 单独人工审核 |
AI 的很多错误是“单个点看着没问题,整体破坏系统稳定性”。所以规则要偏保守,宁可用黑白名单,也不要让 AI 在关键路径上自由探索。
3.2 用一份规则文件统辖所有检查
我习惯把防护规则收敛到一份 YAML 文件里,比如ai-code-rules.yml。这份文件同时供本地脚本和 CI 读取,避免在两处各写一套规则、最后不一致的问题。一个比较典型的文件长这样:
version: 1.0 project: demo-web blocked_paths: - src/auth/** - src/payment/** - deploy/** - .github/workflows/** allowed_paths: - src/components/** - src/utils/** - src/pages/** forbidden_patterns: - "(AKIA[0-9A-Z]{16})" - "eval\\(" - "password\\s*=\\s*['\"][^'\"]+['\"]" dangerous_calls: - "child_process.execSync" - "child_process.exec" - "shell: true" dependency_policy: mode: whitelist allow: - lodash - axios - react deny: [] check_level: strict review_required: true这份文件里,blocked_paths是保护名单,allowed_paths是鼓励 AI 发挥的范围,forbidden_patterns是正则黑名单,dependency_policy则是依赖白名单。实际操作中要根据项目情况调整,但核心思路一致:把规则写成代码,而不是写在团队协作说明里。
3.3 本地第一道网:pre-commit 扫描脚本
本地防护的最佳位置是pre-commithook。我写过一个 Node 脚本,核心逻辑是读取当前暂存的 diff,然后依次检查刚才那份规则文件里的条目。以下是一个简化版:
#!/usr/bin/env node // scripts/guard-ai-diff.js const { execSync } = require('child_process'); const fs = require('fs'); const yaml = require('yaml'); const rules = yaml.parse(fs.readFileSync('ai-code-rules.yml', 'utf8')); const diff = execSync('git diff --cached', { encoding: 'utf-8' }); const problems = []; // 1. 检查是否触碰未允许修改的文件 rules.blocked_paths.forEach((pattern) => { if (diff.includes(`diff --git a/${pattern}`)) { problems.push(`禁止修改文件: ${pattern}`); } }); // 2. 检查危险正则 rules.forbidden_patterns.forEach((pattern) => { const re = new RegExp(pattern, 'gm'); if (re.test(diff)) { problems.push(`检测到危险模式: ${pattern}`); } }); if (problems.length) { console.error('AI 防护规则拦截到以下问题:'); problems.forEach((p) => console.error(' -', p)); process.exit(1); }这里有一个很实际的坑:扫描 diff 时要过滤掉删除行。如果你删掉一行坏代码,正则却从删除的内容里命中了问题,就会误报。更稳妥的做法是解析 diff,只扫描新增行。另外,YAML 解析和 JSON 解析一样,加错一个空格就会挂,所以规则文件建议配一个rules.schema.json做校验,或者用 TypeScript 写规则,让编译期帮你看住类型。
3.4 CI 做强制关卡:密钥扫描、依赖审计、diff 校验
本地 hook 可以被绕过,真正强制的是 CI。我一般在流水线里放四类任务:先跑 lint 和单元测试,再跑依赖漏洞检查,接着跑密钥扫描,最后跑刚才那份规则文件里的 diff 校验。GitHub Actions 的简版配置大概是这样的:
name: ai-guard on: [pull_request] jobs: ai-guard: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npm run lint - run: npm test - run: npm audit --audit-level=high - run: npx gitleaks detect --source . --redact - run: node scripts/guard-ai-diff.jsgitleaks是目前比较普及的密钥扫描工具,对常见云厂商密钥格式都有默认规则;npm audit则负责依赖漏洞。两者配合 diff 校验,基本能覆盖我前面列的五类高发风险中的四类。关键是,CI 里的规则必须和本地脚本用同一份配置,否则本地测完 CI 又拦一道,体验会很割裂。
3.5 人的 review 升级成“决策点抽查”
加入防护规则后,人工 review 的价值没有降低,而是变化了。要求 reviewer 一行行重读 AI 生成的几百行代码,不现实,也没有必要。我更推荐把 review 重心放在三个决策点上:
- 第一,AI 为什么做这个设计?有没有被忽略的更简单方案?
- 第二,边界条件覆盖了吗?尤其是输入校验、日志脱敏、依赖变化。
- 第三,AI 是不是顺手改多了?有没有夹带与当前任务无关的重构?
这三个决策点一看就是“人在回路”才会关注的,单纯靠自动化工具判断不了。Reviewer 不需要把 AI 当成不信任对象,而是把它当成一个经验丰富但偶尔冒进的协作伙伴,你要做的,是把住设计层面的关键关口。
4. 实操记录:给前端 Node 项目加一道 AI 代码安全闸门
4.1 项目背景和规则目标
我在一个由 3 人维护的 Node 前端项目上完整跑过这套方案。项目有几十个页面,核心业务集中在几个大模块里,团队三个人都在用 AI 编码工具。刚开始大家觉得 AI 效率很高,直到有一天在日志里发现测试环境密钥被打了出来,才意识到没有边界的 AI 编码有多危险。
这次整改的目标很简单:第一,防止敏感信息进仓库;第二,保护 auth 和支付这类核心目录不被 AI 随意改动;第三,让 AI 新增依赖必须有意识、有记录;第四,保证所有 AI 生成代码都经过 review。
4.2 第一步:建立 ai-code-rules.yml 并锁死
我先在仓库根目录建了ai-code-rules.yml,并规定这个文件本身不允许 AI 修改,改动必须走人工提交。然后做了三个设置:把src/auth、src/payment、deploy目录加入blocked_paths,因为账号和钱相关的模块一旦被改坏,影响不可控;把src/components、src/utils加进allowed_paths,在这个范围内允许 AI 自由发挥;在forbidden_patterns里放入密钥正则、eval、child_process.exec和密码硬编码规则。依赖策略我用了白名单模式,新依赖必须人工确认后加进allow列表。
这个文件建好之后,我会在团队里同步一份说明文档,把每个规则的解释写清楚。因为规则如果不解释,大家只会觉得“我改了代码,结果被机器人拦了”,不知道拦的是什么、为什么拦。
4.3 第二步:写本地 pre-commit 防护
我按照前面的示例,写了scripts/guard-ai-diff.js,挂在pre-commithook 上。脚本做三件事:检查 blocked_paths 是否出现在暂存的 diff 里;扫描新增代码中的危险模式;检查 package.json 里的新增依赖是否在白名单内。如果发现问题,脚本直接输出错误并退出进程,提交被中断。
第一周跑下来,误报率确实不低,比如eval的正则会命中测试代码里的字符串。后来我按文件类型做了分级:测试文件和数据模拟文件走宽松规则,业务代码走严格规则。误报降下去之后,团队对这个脚本的态度从“烦人”变成了“还挺靠谱”。
4.4 第三步:在 CI 里跑同一套规则
本地 hook 只是缓解,CI 才是强制关卡。我把项目接到 GitHub Actions,在 PR 合并前跑了npm run lint、npm test、npm audit、gitleaks,最后再执行一次node scripts/guard-ai-diff.js。develop 分支设为保护分支,merge 前必须全部通过。
这里一个重要经验是:不要让本地和 CI 各实现一套逻辑。我之前见过有团队本地 Shell 脚本、CI 用插件,结果两边规则不同步,本地通过、CI 报错,或者反过来 CI 通过、本地拦截,非常浪费时间。我直接把本地脚本复用到 CI,这样只要规则文件更新,全链路立刻生效。
4.5 第四步:两个星期的实测数据
运行两周后,我看到一些比较有意思的数据:防护脚本在合并前拦下了 2 次密钥泄露,拦下 1 次对 auth 目录的越权修改,有 3 个 PR 因为新增依赖没走白名单被打回。这个结果直接证明,规则拦截的“潜在事故”数量是真实的,而不是空转。
更微妙的变化是团队对 AI 编码的信任度提升了。以前大家心里没底,不知道 AI 有没有埋雷;现在规则跑完一遍,大家默认“只要过了规则,大概率没问题”。信任体系建立起来之后,AI 编码的效率红利才真正释放出来。
5. AI 防护规则避坑指南:误报、绕过与团队阻力
5.1 误报太多,规则会被“内部和谐”
第一次上规则的时候,不要试图把所有检查调到最强。我第一周收集了大量误报,发现若干正则写得太宽,比如密钥正则把代码里示例 token 当成真密钥,危险调用正则把注释里的字符串也算了进去。处理方式是按文件类型分规则:文档、测试文件、业务代码分开配置,严苛度不同。
还有一个技巧是给开发者留显式的豁免注释,比如在代码行尾写// ai-guard:ignore-line,告知规则这里已经人工确认过。这样既不会卡住正常开发,也能保证豁免行为有记录、可追溯。如果豁免次数太多,就要回头看看是不是规则本身有问题,而不是开发者想绕规则。
5.2 本地 Hook 能绕过,CI 必须兜底
git hooks 可以被绕过。git commit --no-verify一用,本地脚本直接失效。所以我要强调这个原则,CI 才是最后的兜底。我见过不少团队把心思都花在给开发者装 hook 上,结果绕过之后毫无感知。更好的做法是:本地 hook 做体验优化,CI 做强制校验,两者各司其职。
如果团队用企业级代码托管平台,建议把主干分支设为保护分支,要求 PR 合并前必须全部检查通过。这样就算有人强行 push,也会被拦截在服务器端,而不是靠个人自觉。
5.3 AI 喜欢顺手改没关的代码,怎么治
AI 编码工具最普遍的问题就是“顺手优化”。它改一个函数时,会把相邻代码也重排一遍,顺手改掉几个缩进、几个变量名,然后让整个 MR 看起来像个大重构。这种情况不仅增加 review 负担,还会掩盖真正有风险的改动。
我的处理方式有两个:第一,在提示词里显式要求“只修改与任务相关的区域,不要格式化无关代码”;第二,在 diff 检查里增加行数阈值,AI 相关 MR 改动超过 400 行时自动标记为“需要人工拆分”。同时,Reviewer 的检查单里要有“删除与本任务无关的改动”这一项,从制度上逼着 AI 的产出更聚焦。
5.4 规则文件本身被改动,等于保护失效
规则文件本身也要被保护,这一点最容易忽略。如果 AI 可以顺手修改ai-code-rules.yml和 CI 配置,那它相当于解锁了所有限制,防护形同虚设。我会把这些文件加进blocked_paths,并且要求对规则文件的任何改动走单独的 MR,说明变更原因,由负责人确认后再合入。
另外,规则文件最好放在.ai/目录下,让所有 AI 工具和所有团队成员都读取同一份配置。不要靠某个成员在个人提示词里“约定俗成”,那个东西没法审计,也没法传承。
5.5 团队觉得审查负担重,怎么把阻力降到最低
任何新流程都会遇到阻力,防护规则也一样。我最常听到的抱怨是“审查太多了,本来 AI 是为了解放人,现在又要搭进去时间”。我的经验是,不要把防护做成对人的考核,而是把它做成对代码的体检。
在落地时,我会明确传达一个原则:这套规则是帮助开发者在交付前发现问题的,不是事后追责的依据。然后把绝大多数检查自动化,让 AI 生成的代码自动跑扫描,只有扫描结果异常时才需要人工介入。这样团队成员感受到的就不是流程变重,而是安全感变强:AI 写的代码有人帮忙把关,自己可以放手试。我在实践里最有用的一句话是:“这套规则不是不让你用 AI,是让 AI 的产出不会在深夜上线的时候反咬你一口。”
我自己的体会是,AI 编码边界不是对 AI 的不信任,而是对软件交付这件事的尊重。AI 工具只会越来越强,但工程系统的价值从来不是由“生成速度”决定的,而是由“可预测性”决定的。你给 AI 设置边界、设计防护规则,本质上是把对交付质量的控制权重新拿回自己手里。最后分享一个小技巧:把这份规则文件放到仓库的.ai/目录下,让所有 AI 工具和所有成员都读取同一份配置,团队就不需要靠口口相传来约定 AI 使用规范。规则会过时,记得每两周 review 一次,跟项目一起演进。