夜雨聆风学习资料网

ARTICLE · 1162277

AI 写的代码"看起来太对",才是它最危险的地方:识别 AI 写代码的 15 个常见坑

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 痛点:

痛点
占比
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.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 时代的代码质量,靠的是团队一起把关。

相关学习资料