ARTICLE · 1162277
AI 写的代码"看起来太对",才是它最危险的地方:识别 AI 写代码的 15 个常见坑
今年我们做了 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 痛点:
如果你的团队正在使用 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.ts
app.config.errorHandler = (err) => {
// 上报到监控 + 显示错误页
}
识别信号:.then() 后没有 .catch()、JSON.parse 无 try/catch、模板里直接访问对象属性无空值保护。
陷阱 3:硬编码密钥 / 凭证
// AI 写的"快速对接"
const API_KEY = 'sk-xxxxxxxxxxxxxxxx'
const API_BASE = 'https://internal-api.company.com'
AI 不知道你的密钥策略。它可能直接复制训练数据里的"示例值"——但更多的情况是它会"贴心"地帮你写完整密钥路径,让你更倾向于直接使用。
修复:构建时注入或后端代理。
// Vite 项目:使用 import.meta.env
const API_KEY = import.meta.env.VITE_API_KEY
const API_BASE = import.meta.env.VITE_API_BASE
// 或:完全走代理,前端不接触密钥
const API_BASE = '/api/proxy'
识别信号:字符串以 sk- / pk- / Bearer 开头;看到内网域名但不在环境变量里。
陷阱 4:localStorage 滥用存敏感数据
// AI 写的"登录态持久化"
function login(token: string) {
localStorage.setItem('token', token)
localStorage.setItem('user', JSON.stringify(userInfo))
}
token 长期存 localStorage 容易被 XSS 窃取。AI 不懂"token 应该走 httpOnly cookie"这条 web 安全基本规则。
修复:token 由后端 set-cookie 控制,前端只存非敏感信息。
function login(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 只在以下场景使用:
1.该值会作为 props 传给子组件
2.该值会被 watch / watchEffect 依赖
3.模板中多处使用,且依赖计算昂贵
识别信号:computed 里只有一行基础运算;数据根本不会变化但包了 ref/reactive。
陷阱 8:滥用 any
async function loadUser(id: string) {
const res = await fetch(`/api/users/${id}`)
const user = (await res.json()) as any
return user
}
any 让 TypeScript 失去价值。user.name 拼错不会报错,等同于写 JS。AI 遇到"类型对不上"时最常见的处理方式就是 as any。
修复:定义具体类型 + 错误检查。
interface User {
id: string
name: string
email: string
}
async function loadUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) throw new Error('Failed to load user')
return res.json() as Promise<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'
const cloned = _.cloneDeep(obj)
import moment from 'moment'
const formatted = moment(date).format('YYYY-MM-DD')
AI 倾向给出"教科书式"答案。lodash、moment、jquery 这些"老牌"库在它的训练数据里出现频率极高,因此 AI 容易直接引入。但现代 JS / TS 原生 API 已经覆盖了 90% 场景。
修复:
// 深拷贝
const cloned = structuredClone(obj)
// 日期格式化
const formatted = new Intl.DateTimeFormat('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit'
}).format(date)
识别信号:PR 中新增 lodash / moment / jquery;import * as _ 但只用了 1 个函数。
陷阱 12:静默吞错
async function saveData(data: any) {
try {
await api.save(data)
} catch (e) {
console.log('error', e) // 仅打印
}
}
用户点击保存,以为成功了,实际没存。AI 给的 catch 块经常是空的或者只有 console.log。
修复:
async function saveData(data: Data) {
try {
await api.save(data)
showToast('保存成功')
} catch (e) {
showToast(`保存失败:${e.message}`, 'error')
}
}
识别信号:catch (e) {} 空块;用户关键操作(保存、提交、支付)相关代码无错误反馈。
🟢 P2 | 建议修复(代码质量)
陷阱 13:过度抽象——抽出只调用一次的 util
// 只有 ProductCard 调用了它
function formatDate(date: Date): string {
return new Intl.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:无意义的注释
function getUserName(user: User) {
// 获取用户名字
return user.name
}
注释应为"为什么"而非"是什么"。AI 倾向生成"做了什么"的注释,因为它见过的训练数据里这种注释占比很高。
修复:
function getUserName(user: User) {
// 后端返回的 name 字段可能为空字符串(已注销用户)
return user.name || '匿名用户'
}
陷阱 15:命名不一致
interface UserData {
user_id: string
userName: string
uid: number
}
同一个概念三种命名。AI 不会主动遵守你的命名规范。
修复:与团队规范保持一致。
interface User {
id: string
name: 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.js
export default [
{
rules: {
'vue/no-v-html': 'error', // v-html 必须人工 review
'@typescript-eslint/no-explicit-any': 'warn', // 警告 any
}
}
]
工具只能发现约 30% 的 AI 代码问题,人工 Review 仍是核心。
几条带走的工程心法
1.AI 写代码易如反掌,Review 才是真功夫。AI 把"写"的成本降到接近 0,Review 的重要性反而提升。
2.AI 代码"看起来太对"是最大的危险。Review 时保持质疑感,不要被风格迷惑。
3.错误处理是 AI 代码的"重灾区"。过去 6 个月我们遇到的 AI 代码 Bug 里,70% 以上是错误处理缺陷。优先 Review 这一块。
4.安全代码永远不直接 merge。涉及 XSS、密钥、权限、资金的代码,AI 输出仅供参考。
5.把 AI 当作一个"非常熟练但判断力不稳定的初级工程师"——它的产出值得 review,但不值得盲信。
你团队里 Review AI 代码最大的阻力是什么?欢迎评论区聊聊。
如果本文对你有帮助,欢迎转发给你们的 Reviewer 同事——AI 时代的代码质量,靠的是团队一起把关。