去年下半年我们做了 6 个月的 AI 编程试点,期间 Review 了 200 多个 PR。我整理出一组反直觉的数据:AI 代码 PR 的首次 Review 通过率比人工代码高 23%,但 merge 后缺陷率只低 4%。
换句话说,AI 代码看起来"更对",但实际并没有更安全。
更扎心的是,有一次我自己 Review 了一个 AI 生成的 PR,里面有个v-html直接渲染用户评论。代码风格无可挑剔,三秒通过。merge 第二天,生产环境被注入了一段挖矿脚本。
从那次之后,我把所有 AI 生成的 PR 都当成"陌生人的代码"重新看。这篇文章整理了过去 6 个月里我反复遇到的 15 个真实陷阱,给所有 Reviewer 一份可立即上手的速查手册。
AI 代码最大的问题,是"看起来太对"
AI 生成的代码有一个特征:语法、风格、可读性往往都很"标准"。这恰恰让它危险——一个写得"漂亮"的危险代码,比一个写得"粗糙"的安全代码更难被发现。
我们调研了 6 支前端团队,列出了 AI 时代 Review 工作的 Top 5 痛点:
| 痛点 | 占比 |
|---|---|
| Reviewer 看不懂 AI 为什么这么写 | 78% |
| AI 代码"看起来太对",不好意思挑刺 | 65% |
| 找不出具体问题,但又觉得哪里不对 | 58% |
| AI 引入的 Bug 排查时间比人工代码长 2 倍 | 47% |
| 团队成员开始依赖 AI Review,缺乏独立判断 | 41% |
如果你的团队已经引入或正在考虑引入 AI 编程工具,这 5 个痛点大概率都会遇到。
在展开 15 个具体陷阱之前,先建立 5 个"心智模型"——AI 代码几乎所有问题,都可以归到这 5 个过度行为之一。
1. 过度防御:到处是if (!x) return null、try/catch、?? defaultValue。AI 见过太多防御性代码,机械模仿,但分不清"必要"和"过度"。
2. 过度抽象:抽出只调用一次的 util、提前组件化、引入设计模式。AI 倾向"做通用解",但缺乏对"现在是否需要"的判断。
3. 过度优化:到处computed、提前加缓存、加防抖节流。AI 知道"性能优化"是个好东西,但不知道"何时不该优化"。
4. 过度类型化:类型体操、复杂泛型、过度as、滥用any。AI 训练语料里有大量 TypeScript "展示型"代码。
5. 过度设计:引入根本不需要的状态管理库、状态机、依赖注入。AI 倾向给出"工业级"解,但你的项目可能只是个内部工具。
记住这 5 个模式,胜过逐条对照 Checklist。
15 个陷阱(按严重程度排序)
🔴 P0 | 必须修复(涉及安全、数据、稳定性)
陷阱 1:XSS——直接渲染未转义内容
<!-- AI 写的:处理"富文本"展示 --> <script setup lang="ts"> defineProps<{ content: string }>() </script> <template> <div v-html="content"></div> </template>用户输入、API 返回、Markdown 渲染结果都可能包含<script>或事件属性。AI 不知道你的内容来源是否可信——它只关心"完成渲染"。
修复:用 DOMPurify 处理。
<script setup lang="ts"> import DOMPurify from 'dompurify' import { computed } from 'vue' const props = defineProps<{ content: string }>() const sanitized = computed(() => DOMPurify.sanitize(props.content)) </script> <template> <div v-html="sanitized"></div> </template>识别信号:搜索v-html/bypassSecurityTrustHtml,看到默认假设不可信。
陷阱 2:错误处理缺失——fetch / JSON.parse 必崩
<script setup lang="ts"> import { ref, onMounted } from 'vue' const props = defineProps<{ userId: string }>() const user = ref(null) onMounted(() => { fetch(`/api/users/${props.userId}`) .then(r => r.json()) // 非 JSON 会 throw .then(data => user.value = data) // null 时崩溃 }) </script> <template> <div>{{ user.name }}</div> </template>AI 默认假设"一切顺利"。但 fetch 失败、JSON 解析失败、user 为 null,任何一个发生都会让页面变白屏。
修复:完整三态 + 错误处理。
<script setup lang="ts"> import { ref, onMounted } from 'vue' interface User { id: string; name: string } const props = defineProps<{ userId: string }>() const user = ref<User | null>(null) const loading = ref(true) const error = ref<Error | null>(null) onMounted(() => { loading.value = true fetch(`/api/users/${props.userId}`) .then(r => r.json()) .then(data => user.value = data) .catch(e => error.value = e) .finally(() => loading.value = false) }) </script> <template> <Skeleton v-if="loading" /> <ErrorMessage v-else-if="error" :error="error" /> <Empty v-else-if="!user" /> <div v-else>{{ user.name }}</div> </template>外层用app.config.errorHandler做兜底。
// main.tsapp.config.errorHandler=(err)=>{// 上报到监控 + 显示错误页}识别信号:.then()后没有.catch()、JSON.parse无 try/catch、模板里直接访问对象属性无空值保护。
陷阱 3:硬编码密钥 / 凭证
// AI 写的"快速对接"constAPI_KEY='sk-xxxxxxxxxxxxxxxx'constAPI_BASE='https://internal-api.company.com'AI 不知道你的密钥策略。它可能直接复制训练数据里的"示例值"——但更多的情况是它会"贴心"地帮你写完整密钥路径,让你更倾向于直接使用。
修复:构建时注入或后端代理。
// Vite 项目:使用 import.meta.envconstAPI_KEY=import.meta.env.VITE_API_KEYconstAPI_BASE=import.meta.env.VITE_API_BASE// 或:完全走代理,前端不接触密钥constAPI_BASE='/api/proxy'识别信号:字符串以sk-/pk-/Bearer开头;看到内网域名但不在环境变量里。
陷阱 4:localStorage 滥用存敏感数据
// AI 写的"登录态持久化"functionlogin(token:string){localStorage.setItem('token',token)localStorage.setItem('user',JSON.stringify(userInfo))}token 长期存 localStorage 容易被 XSS 窃取。AI 不懂"token 应该走 httpOnly cookie"这条 web 安全基本规则。
修复:token 由后端 set-cookie 控制,前端只存非敏感信息。
functionlogin(userInfo:UserInfo){localStorage.setItem('theme',userInfo.preferences.theme)}识别信号:localStorage.setItem('token';存身份证 / 银行卡 / 手机号。
陷阱 5:竞态条件——搜索结果错乱
<script setup lang="ts"> import { ref } from 'vue' const query = ref('') const results = ref<any[]>([]) const handleSearch = async (q: string) => { const data = await api.search(q) results.value = data // 输入 "abc",但 "a" 的请求后返回 → 覆盖 } </script> <template> <input v-model="query" @input="handleSearch(query)" /> </template>用户连续输入 “a” → “ab” → “abc”,“a” 的请求最后返回,覆盖了正确结果。AI 不会主动考虑这种"用户实际操作可能比代码异步逻辑快"的场景。
修复:watch + AbortController。
<script setup lang="ts"> import { ref, watch } from 'vue' const query = ref('') const results = ref<any[]>([]) watch(query, async (newQuery, _, onCleanup) => { const controller = new AbortController() onCleanup(() => controller.abort()) const data = await api.search(newQuery, { signal: controller.signal }) results.value = data }) </script> <template> <input v-model="query" /> </template>识别信号:搜索框 / 自动补全组件;多个 await 后紧跟赋值;无 abort / cleanup 逻辑。
🟠 P1 | 应当修复(架构、性能、可维护性)
陷阱 6:watch / watchEffect 滥用
<script setup lang="ts"> import { ref, watch } from 'vue' import { useRouter } from 'vue-router' const router = useRouter() const productId = ref('') // 错误:把"点击跳转"放进 watch watch(productId, (id) => { if (id) router.push(`/product/${id}`) }) </script> <template> <button @click="productId = '123'">查看</button> </template>watch 是"同步外部系统到状态",不是"事件处理"。事件应该直接在@click里完成。
修复:
<script setup lang="ts"> import { useRouter } from 'vue-router' const router = useRouter() </script> <template> <button @click="router.push('/product/123')">查看</button> </template>识别信号:watch里只有if (cond) doSomething();依赖里有router/route。
陷阱 7:computed 滥用 / 过度响应式化
<script setup lang="ts"> import { computed } from 'vue' const props = defineProps<{ product: { id: string; price: number } }>() // 错误:基础计算根本不需要 computed const formattedPrice = computed(() => `¥${props.product.price.toFixed(2)}`) // 内联函数:Vue 没有 useCallback 的需求 const handleClick = () => console.log(props.product.id) </script>Vue 3 的响应式系统天然有依赖追踪,没有 React useMemo/useCallback 那种"每次都重算"的开销。AI 倾向"以防万一"加 computed,但忽略了 computed 自身也有依赖追踪成本。
修复:
<script setup lang="ts"> const props = defineProps<{ product: { id: string; price: number } }>() const formattedPrice = `¥${props.product.price.toFixed(2)}` const handleClick = () => console.log(props.product.id) </script>判断原则:computed 只在以下场景使用:
- 该值会作为 props 传给子组件
- 该值会被 watch / watchEffect 依赖
- 模板中多处使用,且依赖计算昂贵
识别信号:computed里只有一行基础运算;数据根本不会变化但包了 ref/reactive。
陷阱 8:滥用 any
asyncfunctionloadUser(id:string){constres=awaitfetch(`/api/users/${id}`)constuser=(awaitres.json())asanyreturnuser}any 让 TypeScript 失去价值。user.name拼错不会报错,等同于写 JS。AI 遇到"类型对不上"时最常见的处理方式就是as any。
修复:定义具体类型 + 错误检查。
interfaceUser{id:stringname:stringemail:string}asyncfunctionloadUser(id:string):Promise<User>{constres=awaitfetch(`/api/users/${id}`)if(!res.ok)thrownewError('Failed to load user')returnres.json()asPromise<User>}识别信号:单文件any> 5 次;as any/as unknown as;// @ts-ignore无说明。
陷阱 9:列表 key 用 index
<!-- AI 写的:用 index 作 key --> <script setup lang="ts"> import type { Todo } from './types' defineProps<{ todos: Todo[] }>() </script> <template> <ul> <TodoItem v-for="(todo, index) in todos" :key="index" :todo="todo" /> </ul> </template>删除中间项时,index 不变但内容变了,导致输入框光标错位、动画错乱、内部状态错位。
修复:
<template> <ul> <TodoItem v-for="todo in todos" :key="todo.id" :todo="todo" /> </ul> </template>识别信号:搜索:key="index"/:key="i"/:key="idx";列表项有内部状态(输入框、勾选状态)。
陷阱 10:不处理 loading / error / empty 三态
<!-- AI 写的 --> <script setup lang="ts"> import { ref, onMounted } from 'vue' import type { User } from './types' const users = ref<User[]>([]) onMounted(() => { api.getUsers().then(data => users.value = data) }) </script> <template> <ul> <li v-for="u in users" :key="u.id">{{ u.name }}</li> </ul> </template>用户请求未完成时看到空白;失败时无提示;空数据时无反馈。AI 给的代码经常只处理"成功"这一种状态。
修复:
<script setup lang="ts"> import { ref, onMounted, computed } from 'vue' import type { User } from './types' const users = ref<User[]>([]) const loading = ref(true) const error = ref<Error | null>(null) const isEmpty = computed(() => !loading.value && !error.value && users.value.length === 0) onMounted(() => { loading.value = true api.getUsers() .then(data => users.value = data) .catch(e => error.value = e) .finally(() => loading.value = false) }) </script> <template> <Skeleton v-if="loading" /> <ErrorMessage v-else-if="error" :error="error" /> <Empty v-else-if="isEmpty" /> <ul v-else> <li v-for="u in users" :key="u.id">{{ u.name }}</li> </ul> </template>识别信号:组件调用了 async API 但无 loading 状态;空数据时显示空白页面。
陷阱 11:引入不必要的依赖
import_from'lodash'constcloned=_.cloneDeep(obj)importmomentfrom'moment'constformatted=moment(date).format('YYYY-MM-DD')AI 倾向给出"教科书式"答案。lodash、moment、jquery 这些"老牌"库在它的训练数据里出现频率极高,因此 AI 容易直接引入。但现代 JS / TS 原生 API 已经覆盖了 90% 场景。
修复:
// 深拷贝constcloned=structuredClone(obj)// 日期格式化constformatted=newIntl.DateTimeFormat('zh-CN',{year:'numeric',month:'2-digit',day:'2-digit'}).format(date)识别信号:PR 中新增lodash/moment/jquery;import * as _但只用了 1 个函数。
陷阱 12:静默吞错
asyncfunctionsaveData(data:any){try{awaitapi.save(data)}catch(e){console.log('error',e)// 仅打印}}用户点击保存,以为成功了,实际没存。AI 给的 catch 块经常是空的或者只有 console.log。
修复:
asyncfunctionsaveData(data:Data){try{awaitapi.save(data)showToast('保存成功')}catch(e){showToast(`保存失败:${e.message}`,'error')}}识别信号:catch (e) {}空块;用户关键操作(保存、提交、支付)相关代码无错误反馈。
🟢 P2 | 建议修复(代码质量)
陷阱 13:过度抽象——抽出只调用一次的 util
// 只有 ProductCard 调用了它functionformatDate(date:Date):string{returnnewIntl.DateTimeFormat('zh-CN').format(date)}<!-- ProductCard.vue --> <script setup lang="ts"> import type { Product } from './types' const props = defineProps<{ product: Product }>() const dateText = formatDate(props.product.createdAt) </script> <template> <div>{{ dateText }}</div> </template>抽象的成本(理解、跳转、修改)大于收益。
修复:内联即可。
<!-- ProductCard.vue --> <script setup lang="ts"> import type { Product } from './types' const props = defineProps<{ product: Product }>() const dateText = new Intl.DateTimeFormat('zh-CN').format(props.product.createdAt) </script> <template> <div>{{ dateText }}</div> </template>经验法则:同一逻辑出现 3 次以上再抽象。
陷阱 14:无意义的注释
functiongetUserName(user:User){// 获取用户名字returnuser.name}注释应为"为什么"而非"是什么"。AI 倾向生成"做了什么"的注释,因为它见过的训练数据里这种注释占比很高。
修复:
functiongetUserName(user:User){// 后端返回的 name 字段可能为空字符串(已注销用户)returnuser.name||'匿名用户'}陷阱 15:命名不一致
interfaceUserData{user_id:stringuserName:stringuid:number}同一个概念三种命名。AI 不会主动遵守你的命名规范。
修复:与团队规范保持一致。
interfaceUser{id:stringname:string}识别信号:搜索user_id|userId|uid,同一 PR 出现 2 种以上说明 AI 没注意规范。
一份精简的 Review Checklist
把上面 15 个陷阱压缩成 11 个必查项,贴在工位或加到 PR Template 里。
🔴 P0(任意 1 项不过 = 拒绝 merge)
- 无
v-html/bypassSecurityTrustHtml,或已用 DOMPurify - async 操作有
.catch()或try/catch - 渲染前有空值保护
- 无硬编码密钥 / 内部 URL
- 搜索 / 自动补全有 AbortController
- 用户关键流程有 loading + error 反馈
🟠 P1(任意 2 项不过 = 拒绝 merge)
watch/watchEffect数量合理,无滥用- 无
as any/// @ts-ignore - 列表
:key使用稳定 ID,不用 index - loading / error / empty 三态完整
- 无新增
lodash/moment等大依赖
团队怎么落地
三件小事,比写十篇规范更有效。
1. PR Template 加一段 AI 声明
## AI 代码声明 - [ ] 本 PR 包含 AI 生成的代码 - [ ] P0 检查项全部通过 - [ ] P1 检查项至少 80% 通过2. 每月一次"AI 代码找 Bug 挑战赛"
从近期 AI 生成的 PR 里选 3-5 个,让团队成员盲测找出问题。亲测发现,这种方式比培训更有效——大家会主动建立"质疑 AI"的本能。
3. ESLint 加几条规则拦截明显问题
// eslint.config.jsexportdefault[{rules:{'vue/no-v-html':'error',// v-html 必须人工 review'@typescript-eslint/no-explicit-any':'warn',// 警告 any}}]工具只能发现约 30% 的 AI 代码问题,人工 Review 仍是核心。
几条带走的工程心法
AI 写代码易如反掌,Review 才是真功夫。AI 把"写"的成本降到接近 0,Review 的重要性反而提升。
AI 代码"看起来太对"是最大的危险。Review 时保持质疑感,不要被风格迷惑。
错误处理是 AI 代码的"重灾区"。过去 6 个月我们遇到的 AI 代码 Bug 里,70% 以上是错误处理缺陷。优先 Review 这一块。
安全代码永远不直接 merge。涉及 XSS、密钥、权限、资金的代码,AI 输出仅供参考。
把 AI 当作一个"非常熟练但判断力不稳定的初级工程师"——它的产出值得 review,但不值得盲信。
你团队里 Review AI 代码最大的阻力是什么?欢迎评论区聊聊。
如果本文对你有帮助,欢迎转发给你们的 Reviewer 同事——AI 时代的代码质量,靠的是团队一起把关。