首页 / 资讯中心 / 文章详情

AI代码Review避坑指南:15个常见陷阱与检查清单

AI代码Review避坑指南:15个常见陷阱与检查清单 ★ FEATURED ARTICLE
1. 为什么AI写的代码需要专门Review1.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可能为nullprofile可能不存在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就是undefinedmiddleItem也是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是字符串5price是10结果是510而不是15Review的时候看到直接标记为需要修改看到操作符要确认两边类型是否一致。如果项目有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.allasync 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.all7竞态条件搜索请求未取消加防抖或AbortController8setState闭包直接基于当前state计算改函数式更新9useEffect漏依赖依赖数组不完整补全依赖或重构10重复代码相似逻辑复制三遍抽象成通用函数11组件过大超过200行或3个useState拆分成子组件或Hook12命名无业务含义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特有的那些坑。这份清单会随着我遇到的新问题持续更新也欢迎你把自己踩过的坑补充进来。
阅读完成 · 觉得有帮助?
咨询建站