☰
AI代码Review避坑指南:15个常见陷阱与检查清单
2026/10/19 3:58:05 网站建设 项目流程

1. 为什么AI写的代码需要专门Review

1.1 一个真实的翻车现场

去年下半年,团队里一个中级前端同学用AI辅助写了一个数据看板页面。功能跑起来没问题,图表渲染正常,交互也流畅。代码提交后我扫了一眼,觉得结构挺清晰,变量命名也规范,就点了通过。结果上线第二天,运营反馈说页面在低端安卓机上直接白屏。

排查了半天,发现问题出在AI生成的一段日期格式化逻辑里。它用了一个看起来非常优雅的链式调用,但那个写法依赖了某个较新的运行时特性,在目标机型的浏览器内核上根本不支持。更坑的是,这段代码被包裹在一个try-catch里,异常被吞掉了,控制台干干净净,什么线索都没有。

这件事让我意识到一个问题:AI写的代码,最大的风险不在于它写得烂,而在于它写得太“像样”了。语法正确、命名规范、注释齐全,一眼看过去甚至比人写的还整洁。但正是这种表面上的高质量,让Review的人容易放松警惕,跳过那些真正需要深究的地方。

1.2 AI代码的“隐形陷阱”特征

我后来复盘了一下,AI生成的代码有几个非常典型的特征,这些特征恰恰是Review时最容易忽略的盲区。

第一,过度自信的边界处理。AI倾向于假设输入永远是合法的、网络永远是通畅的、用户永远按预期操作。它很少主动写防御性代码,因为训练数据里的示例代码大多是为了演示功能,而不是为了处理真实世界的混乱。

第二,似是而非的API调用。AI对某些API的记忆是模糊的,它会“编造”一个看起来合理但实际不存在的方法名,或者把两个不同库的API混在一起用。这种错误在语法层面完全合法,只有运行时才会暴露。

第三,复制粘贴式的逻辑重复。AI很擅长生成结构相似的代码块,但它不会主动抽象。三个地方需要做类似的校验,它就老老实实写三遍,每遍还略有不同。这种代码Review时很容易被跳过,因为“看起来没问题”。

第四,缺乏业务语义的命名。AI生成的变量名往往很“通用”——data、result、temp、handleClick。这些名字在语法上没问题,但完全丢失了业务上下文,后续维护的人根本不知道这个data到底是什么data。

1.3 这篇博文能帮你解决什么

我把自己这两年Review AI代码踩过的坑、总结出来的检查清单,整理成了15个具体的识别点。每一个点都配有真实的代码示例、问题分析、以及修改建议。不管你是刚接触AI辅助开发的新手,还是已经用了一段时间但总觉得哪里不对劲的老手,这份清单都能帮你建立一套系统的Review方法论。

文章会按照代码的不同层面来组织:从最基础的数据类型处理,到异步逻辑,再到组件设计和性能优化。每个检查点我都会解释“为什么AI容易在这里犯错”,以及“你应该怎么快速识别和修复”。最后还会给出一份可以直接拿去用的Review速查表。

2. 数据类型与边界处理:AI最容易翻车的第一站

2.1 空值判断的“想当然”

AI在处理空值时有一个非常顽固的倾向:它默认数据总是存在的。比如下面这段代码,是从一个AI生成的用户信息展示组件里摘出来的:

const userName = user.profile.name; const avatarUrl = user.profile.avatar.url;

这段代码在演示环境里跑一万遍都不会出错,因为演示数据永远是完整的。但真实场景里,user可能为null,profile可能不存在,avatar可能是空对象。任何一个环节缺失,整个页面就崩了。

AI之所以这样写,是因为它训练数据里的示例代码大多假设数据是完整的。它没有“防御性编程”的意识,除非你在prompt里明确要求。

Review的时候,看到这种连续属性访问,就要条件反射地问一句:中间任何一层为空怎么办?正确的做法是用可选链加上合理的默认值:

const userName = user?.profile?.name ?? '匿名用户'; const avatarUrl = user?.profile?.avatar?.url ?? defaultAvatar;

注意:可选链不是万能的。如果user为null本身就是一个业务异常,那你不应该用可选链静默处理,而应该在上层就做好校验和错误提示。可选链只适用于“这个字段确实可能为空且为空是合理的”场景。

2.2 数字计算的精度陷阱

前端做金额计算时,浮点数精度问题是个老生常谈的话题。但AI生成的代码里,这个问题出现的频率高得惊人。比如:

const total = price * quantity; const discount = total * 0.8; const finalPrice = discount.toFixed(2);

看起来没问题对吧?但0.1 + 0.2在JavaScript里等于0.30000000000000004,这是IEEE 754浮点数的经典问题。AI生成的代码很少主动处理这个,它默认数学运算就是精确的。

更隐蔽的是,AI有时候会用toFixed来做四舍五入,但toFixed的舍入规则并不是标准的四舍五入,它在某些边界值上的行为会让财务同学直接来找你喝茶。

Review的时候,凡是看到涉及金额、比例、税费的计算,一律要求用整数分或者专门的精度库来处理。如果项目里已经有decimal.js或者big.js,直接要求替换。如果没有,至少要把计算逻辑封装成一个工具函数,统一处理舍入规则。

2.3 数组操作的边界遗漏

AI对数组的操作有一个经典问题:它经常忘记处理空数组和单元素数组的情况。比如这个从AI生成的分页逻辑里摘出来的片段:

const lastItem = list[list.length - 1]; const middleItem = list[Math.floor(list.length / 2)];

如果list是空数组,lastItem就是undefined,middleItem也是undefined。后续如果对这些值做属性访问,直接报错。

还有一种更隐蔽的情况:AI生成的排序逻辑有时候会假设数组至少有两个元素,然后用list[1]去取第二个元素做比较基准。空数组或者单元素数组直接越界。

Review的时候,看到数组索引访问,特别是用length做运算的,一定要确认空数组的情况。最简单的防御是在函数入口加一个if (!list.length) return的守卫。

2.4 类型转换的隐式陷阱

JavaScript的隐式类型转换是出了名的坑多,AI生成的代码经常在这上面栽跟头。比如:

if (value == '') { // do something }

用==而不是===,这是AI代码里非常高频的问题。AI似乎对宽松相等有一种偏好,可能是因为训练数据里老代码比较多。

还有一种情况是AI会用+来拼接字符串和数字,但它没有意识到如果其中一个操作数是字符串,另一个数字会被隐式转换:

const total = count + price; // 如果count是字符串'5',price是10,结果是'510'而不是15

Review的时候,看到==直接标记为需要修改,看到+操作符要确认两边类型是否一致。如果项目有ESLint配置,eqeqeq规则应该早就打开了,但AI生成的代码有时候会带着// eslint-disable-next-line eqeqeq这样的注释,这种注释本身就是危险信号。

3. 异步逻辑与状态管理:AI的“时序盲区”

3.1 Promise链的断裂与吞异常

AI写异步代码时,最典型的问题是Promise链的异常处理不完整。比如:

fetchData() .then(data => processData(data)) .then(result => updateUI(result));

这段代码没有.catch。如果fetchData失败,或者processData抛异常,整个链就断了,错误被静默吞掉。用户看到的是页面一直loading,控制台什么都没有。

AI之所以这样写,是因为它生成的示例代码通常只关注“成功路径”。它不会主动加上错误处理,除非你明确要求。

Review的时候,看到.then链没有.catch,直接标记为必须修改。但要注意,加.catch也不是随便加:

fetchData() .then(data => processData(data)) .then(result => updateUI(result)) .catch(error => { console.error(error); showErrorMessage('加载失败,请重试'); });

提示:catch里不要只写console.error。用户看不到控制台,你需要给用户一个明确的反馈。同时,catch的位置也很重要——如果多个then共享一个catch,你要确认错误处理逻辑是否对所有步骤都适用。

3.2 async/await的并发陷阱

AI非常喜欢用async/await,因为语法看起来更同步、更直观。但它经常犯一个错误:把本可以并行的异步操作写成了串行。比如:

async function loadDashboard() { const userInfo = await fetchUserInfo(); const orderList = await fetchOrderList(); const noticeList = await fetchNoticeList(); // ... }

这三个请求之间没有依赖关系,完全可以并行。但AI生成的代码里,它们被串行执行了,总耗时是三个请求之和。在弱网环境下,用户要多等好几秒。

正确的写法是用Promise.all:

async function loadDashboard() { const [userInfo, orderList, noticeList] = await Promise.all([ fetchUserInfo(), fetchOrderList(), fetchNoticeList() ]); }

Review的时候,看到连续的await,要问一句:这些请求之间有依赖吗?如果没有,就应该并行。但也要注意,Promise.all有一个特性:只要有一个失败,整个就失败了。如果某个请求失败不影响其他功能,应该用Promise.allSettled。

3.3 竞态条件的忽视

竞态条件是AI代码里最隐蔽的坑之一。比如一个搜索框,用户快速输入时,会触发多次请求。AI生成的代码通常是这样:

async function handleSearch(keyword) { const result = await searchAPI(keyword); setSearchResult(result); }

如果用户先输入“abc”,再输入“abcd”,两个请求几乎同时发出。如果“abc”的响应比“abcd”晚回来,最终显示的结果就是“abc”的,而不是用户最后输入的“abcd”。这就是典型的竞态条件。

AI不会主动处理这个问题,因为它没有“时序”的概念。Review的时候,看到这种“输入触发请求,请求更新状态”的模式,就要考虑加防抖或者请求取消。防抖可以用lodash的debounce,请求取消可以用AbortController。

3.4 状态更新的批处理与闭包陷阱

React的setState是异步批处理的,AI生成的代码经常在这个上面出问题。比如:

function handleIncrement() { setCount(count + 1); setCount(count + 1); setCount(count + 1); }

这段代码的本意是让count加3,但实际上只加了1。因为三次setCount拿到的count都是同一个值,React把它们批处理了。

AI之所以这样写,是因为它把setState当成了同步赋值。正确的写法是用函数式更新:

function handleIncrement() { setCount(prev => prev + 1); setCount(prev => prev + 1); setCount(prev => prev + 1); }

还有一种更隐蔽的闭包陷阱:在useEffect或者事件回调里引用了外部的state,但依赖数组没有正确声明。AI生成的useEffect经常漏依赖,导致回调里拿到的是旧的state值。

Review的时候,看到setState直接用当前state做计算,一律要求改成函数式更新。看到useEffect,要仔细检查依赖数组是否完整。如果项目有React Hooks的ESLint插件,exhaustive-deps规则应该打开,但AI有时候会加// eslint-disable-next-line来绕过检查。

4. 组件设计与代码结构:AI的“抽象缺失症”

4.1 重复代码的“三遍定律”

AI生成代码时,如果遇到类似的功能,它会倾向于复制粘贴然后微调,而不是抽象成可复用的组件或函数。比如一个表单页面,三个输入框的校验逻辑几乎一样,AI会写三遍:

const validateName = (value) => { if (!value) return '姓名不能为空'; if (value.length > 20) return '姓名不能超过20个字符'; return ''; }; const validateEmail = (value) => { if (!value) return '邮箱不能为空'; if (value.length > 50) return '邮箱不能超过50个字符'; return ''; }; const validatePhone = (value) => { if (!value) return '手机号不能为空'; if (value.length > 11) return '手机号不能超过11个字符'; return ''; };

三段逻辑结构完全一样,只是字段名和长度限制不同。AI不会主动抽成一个通用函数,因为它是在“逐段生成”,没有全局视角。

Review的时候,看到结构相似的代码块出现两次以上,就应该考虑抽象。上面这个例子可以抽成:

const createValidator = (fieldName, maxLength) => (value) => { if (!value) return `${fieldName}不能为空`; if (value.length > maxLength) return `${fieldName}不能超过${maxLength}个字符`; return ''; };

注意:抽象不是越多越好。如果两段代码只是看起来像,但业务含义完全不同,强行抽象反而会增加耦合。判断标准是:它们的变化原因是否相同?如果字段名变了另一个也要变,那就该抽象;如果只是巧合相似,那就保持独立。

4.2 组件职责的“上帝化”

AI生成的组件经常承担了太多职责。一个组件里既有数据请求,又有表单处理,还有UI渲染和错误提示。比如:

function UserProfile() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [editMode, setEditMode] = useState(false); const [formData, setFormData] = useState({}); useEffect(() => { fetchUser().then(setUser).catch(setError); }, []); const handleSave = async () => { // 校验、提交、更新状态... }; // 一大堆JSX... }

这个组件把数据获取、状态管理、业务逻辑、UI渲染全揉在一起了。AI这样写是因为它是在“完成一个功能”,而不是在“设计一个架构”。

Review的时候,看到组件超过200行,或者一个组件里有超过3个useState,就要考虑拆分。数据获取可以用自定义Hook抽出来,表单逻辑可以单独封装,UI部分可以拆成展示型组件。

4.3 命名缺乏业务语义

AI生成的变量名和函数名往往很“技术化”,但缺乏业务含义。比如:

const data = await fetchData(); const result = processData(data); const list = result.filter(item => item.status === 1);

data是什么数据?result是什么结果?list是什么列表?status === 1代表什么状态?这些命名在语法上没问题,但完全丢失了业务上下文。三个月后回来看这段代码,你根本不知道它在干什么。

AI之所以这样命名,是因为它不知道你的业务领域。它只能用最通用的词汇来填充。

Review的时候,看到data、result、list、item、info、temp这类命名,一律要求改成有业务含义的名字。比如:

const orderList = await fetchOrderList(); const validOrders = orderList.filter(order => order.status === OrderStatus.PAID);

提示:魔法数字也是同样的问题。status === 1里的1应该用枚举或者常量替代。AI经常直接写数字,因为它不知道这个数字的业务含义。

4.4 注释的“废话文学”

AI生成的注释有一个特点:它注释的是“代码在做什么”,而不是“为什么这样做”。比如:

// 设置loading为true setLoading(true); // 遍历数组 list.forEach(item => { // 处理item processItem(item); });

这些注释完全没有信息量,代码本身已经说清楚了。真正有价值的注释应该解释“为什么”:

// 这里必须用setTimeout而不是直接调用,因为DOM还没更新完 setTimeout(() => { scrollToBottom(); }, 0);

AI不会主动写这种注释,因为它不知道你的代码里有哪些“反直觉”的设计决策。

Review的时候,看到“废话注释”可以直接要求删除。同时,如果某段代码的逻辑比较绕,或者有特殊的历史原因,应该要求作者补充“为什么”的注释。

5. 性能与安全:AI的“无意识盲区”

5.1 不必要的重渲染

AI生成的React组件经常有性能问题,最典型的是useEffect依赖数组里放了对象或数组:

useEffect(() => { fetchData(options); }, [options]); // options是一个对象,每次渲染都是新的引用

如果options是在组件内部定义的,每次渲染都会创建一个新对象,导致useEffect无限执行。AI不会意识到这个问题,因为它不理解JavaScript的引用相等性。

还有一种情况是AI在render函数里创建新函数或新对象,然后传给子组件,导致子组件的memo失效:

<ChildComponent onClick={() => handleClick(id)} />

每次父组件渲染,onClick都是一个新的函数引用,ChildComponent的React.memo就白加了。

Review的时候,看到useEffect依赖里有对象或数组,要确认它是否稳定。看到传给子组件的props里有内联函数或内联对象,要考虑用useCallback或useMemo包裹。

5.2 内存泄漏的隐患

AI生成的代码经常忘记清理副作用。比如:

useEffect(() => { const timer = setInterval(() => { fetchLatestData(); }, 5000); }, []);

这个定时器在组件卸载后还在跑,如果fetchLatestData里更新了已经卸载的组件的state,就会报warning,严重的话还会导致内存泄漏。

AI不会主动加清理函数,因为它只关注“功能实现”,不关注“生命周期”。

Review的时候,看到setInterval、setTimeout、事件监听、WebSocket连接,一律要求确认是否有对应的清理逻辑。useEffect的return函数就是用来做这个的。

5.3 XSS与注入风险

AI生成的代码有时候会直接把用户输入渲染到DOM里:

<div dangerouslySetInnerHTML={{ __html: userContent }} />

dangerouslySetInnerHTML这个名字已经够警告了,但AI有时候还是会用,因为它想实现“富文本展示”的功能。

还有一种情况是AI用eval或者new Function来动态执行代码,这在任何情况下都应该被禁止。

Review的时候,看到dangerouslySetInnerHTML、eval、new Function、innerHTML,一律标记为高风险。如果确实需要渲染富文本,应该用专门的sanitize库先做净化。

5.4 敏感信息硬编码

AI生成的代码里有时候会出现硬编码的密钥、token或者API地址:

const API_KEY = 'sk-xxxxxxxxxxxx'; const BASE_URL = 'https://internal-api.example.com';

这些信息一旦提交到代码仓库,就是安全事故。AI这样写是因为它在演示“如何调用API”,但真实项目里这些应该从环境变量读取。

Review的时候,看到任何看起来像密钥的字符串,或者内部域名,一律要求移到环境变量或者配置中心。同时要检查.gitignore是否包含了.env文件。

6. 15个常见坑速查表与Review流程

6.1 速查表

序号坑点典型表现快速修复
1空值未防御连续属性访问无?.加可选链和默认值
2浮点精度金额直接乘除用整数分或精度库
3数组越界空数组取[0]或[length-1]加长度守卫
4宽松相等用==比较改成===
5Promise无catch.then链没有错误处理加.catch并给用户反馈
6串行await无依赖的请求顺序执行改Promise.all
7竞态条件搜索请求未取消加防抖或AbortController
8setState闭包直接基于当前state计算改函数式更新
9useEffect漏依赖依赖数组不完整补全依赖或重构
10重复代码相似逻辑复制三遍抽象成通用函数
11组件过大超过200行或3个useState拆分成子组件或Hook
12命名无业务含义data、result、temp改成业务语义命名
13无限重渲染useEffect依赖对象用useMemo稳定引用
14内存泄漏定时器/监听未清理useEffect返回清理函数
15硬编码敏感信息密钥写在代码里移到环境变量

6.2 我自己的Review流程

经过大量实践,我总结了一套针对AI代码的Review流程,分三步走。

第一步是快速扫描。先看整体结构,确认没有明显的架构问题。重点看有没有硬编码密钥、有没有eval、有没有dangerouslySetInnerHTML。这一步大概花2-3分钟。

第二步是逐行细读。按照上面的15个检查点,逐个过一遍。重点关注异步逻辑、状态更新、边界处理。这一步是最耗时的,但也是最关键的。我通常会对照速查表,每发现一个问题就在代码旁边标注。

第三步是运行验证。把代码跑起来,重点测试边界情况:空数据、慢网络、快速操作、异常输入。AI代码在正常路径上通常没问题,问题都藏在边界里。

提示:Review AI代码时,不要被它的“整洁外表”迷惑。越是看起来优雅的代码,越要仔细检查它的边界处理和错误处理。AI很擅长写“happy path”,但真实世界很少是happy的。

6.3 给团队的建议

如果你在团队里推广AI辅助开发,我建议把这份检查清单纳入Code Review的必查项。可以在PR模板里加一个checkbox列表,提交者自己先过一遍,Review的人再重点检查。

另外,ESLint和TypeScript能在很大程度上拦截AI的低级错误。把strict模式打开,把eqeqeq、no-eval、no-implicit-coercion这些规则都配上,能省掉很多人工检查的时间。

但工具只能解决语法层面的问题,业务逻辑、架构设计、性能优化这些,还是得靠人来看。AI是很好的“打字员”,但它不是很好的“工程师”。Review的人需要承担起“工程师”的角色,对代码的质量负责。

6.4 一个真实的修复案例

最后分享一个我最近修复的案例。AI生成了一个文件上传组件,代码看起来很正常:

async function handleUpload(file) { setUploading(true); const formData = new FormData(); formData.append('file', file); const result = await uploadAPI(formData); setUploading(false); setFileUrl(result.url); }

这段代码有几个问题:第一,没有try-catch,如果上传失败,setUploading(false)不会执行,按钮永远处于loading状态。第二,没有文件大小和类型的校验。第三,没有处理用户重复点击的情况。

修复后的版本:

async function handleUpload(file) { if (!file) return; if (file.size > MAX_FILE_SIZE) { showError('文件大小不能超过10MB'); return; } if (!ALLOWED_TYPES.includes(file.type)) { showError('不支持的文件类型'); return; } setUploading(true); try { const formData = new FormData(); formData.append('file', file); const result = await uploadAPI(formData); setFileUrl(result.url); } catch (error) { showError('上传失败,请重试'); } finally { setUploading(false); } }

这个案例很典型:AI只写了“成功路径”,把所有异常情况都留给了你。Review的时候,你要做的就是把这些缺失的边界一个一个补上。

我在实际使用中发现,AI辅助开发确实能提升效率,但它把“写代码”的门槛降低了,却把“Review代码”的门槛提高了。以前你只需要看人写的代码,现在你还要识别AI特有的那些坑。这份清单会随着我遇到的新问题持续更新,也欢迎你把自己踩过的坑补充进来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询