聊天讨论 AI 生成的代码能跑就行?这 5 个坑迟早炸

193577746(kyriewen) · 2026年06月22日 · 15 次阅读

AI 写代码的效率毋庸置疑,但"能跑"和"能维护"之间隔了一道鸿沟。这篇文章总结了 AI 生成代码最常见的 5 个质量问题,每个都附反面案例和修复方案,最后给出一份 5 条验收 checklist。

2026 年了,AI 写代码已经不是新鲜事。但我发现一个普遍现象:很多人的工作流是——

  1. 让 AI 生成代码
  2. 能跑就提交
  3. 出了 bug 再让 AI 修

这个循环看似高效,实际上在代码仓库里埋了一堆定时炸弹。下面是我 review AI 代码时反复遇到的 5 个问题,从高频到低频排列。


第一坑:过度工程化——3 行能解决的事写了 30 行

AI 特别喜欢上设计模式。你让它写一个"判断用户是否登录"的函数,它能给你整出工厂模式 + 策略模式 + 抽象基类:

// AuthStrategyFactory.js
class AuthStrategyFactory {
  static createStrategy(type) {
    const strategies = {
      jwt: new JwtAuthStrategy(),
      session: new SessionAuthStrategy(),
      oauth: new OAuthStrategy(),
    };
    return strategies[type] || new JwtAuthStrategy();
  }
}

// JwtAuthStrategy.js
class JwtAuthStrategy extends BaseAuthStrategy {
  validate(context) {
    const token = context.getToken();
    const decoder = new TokenDecoder(this.config);
    const result = decoder.decode(token);
    if (!result.isValid) {
      return AuthResult.failure(AuthErrorCode.INVALID_TOKEN);
    }
    return AuthResult.success(result.payload);
  }
}

// 另外还有:BaseAuthStrategy.js、TokenDecoder.js、AuthResult.js、AuthErrorCode.js

6 个文件,200 多行,就为了判断用户有没有登录。而这个项目从头到尾只用了 JWT 一种方式,不可能有 session 和 OAuth。

实际需要的代码:

export function checkAuth() {
  const token = localStorage.getItem('token');
  if (!token) {
    window.location.href = '/login';
    return false;
  }
  try {
    const payload = JSON.parse(atob(token.split('.')[1]));
    return payload.exp * 1000 > Date.now();
  } catch {
    localStorage.removeItem('token');
    window.location.href = '/login';
    return false;
  }
}

一个文件,15 行,功能完全一样。

为什么会这样: AI 倾向于"展示它知道的所有设计模式",而不是"用最简单的方式解决问题"。它没有偷懒的本能,而偷懒有时候是好事——逼你写出最简方案。

怎么防: 拿到 AI 代码先问自己——"删掉一半代码还能跑吗?"如果能,那删掉的就是过度设计。


第二坑:幽灵代码——大量函数从未被调用

让 AI 写一个"订单模块的工具函数",它会把所有它能想到的都给你:

// utils/orderHelpers.js

export function calculateOrderDiscount(order, discountRules, userLevel) {
  // ... 45 行折扣计算逻辑
}

export function formatOrderForExport(orders, format = 'csv') {
  // ... 60 行导出格式化逻辑
}

export function validateOrderTransition(currentStatus, targetStatus) {
  // ... 30 行状态机校验
}

写得都挺好,逻辑也对。但问题是——项目根本没有折扣功能,没有导出功能,状态机也没用到。

这些就是"幽灵代码":活在仓库里,从未被执行,但你维护的时候会把它们当成有用的代码去读、去理解。实际有效代码 5000 行的模块,可能躺了 3000 行死代码。

为什么会这样: AI 不知道 YAGNI(You Ain't Gonna Need It)。它基于"这个场景可能需要什么"生成代码,而不是"现在到底需要什么"。

怎么防: 前端用 knip 检测,比 depcheck 更准:

npx knip --include exports

一条命令揪出所有未被使用的导出函数、类型、组件。


第三坑:假注释——注释和代码说的不是同一件事

这个坑最阴,因为你只看注释的话,逻辑是通的:

// 检查用户是否有该订单的操作权限
async function checkOrderPermission(userId, orderId) {
  const order = await db.orders.findById(orderId);
  const formattedOrder = {
    ...order,
    createdAt: dayjs(order.createdAt).format('YYYY-MM-DD'),
    updatedAt: dayjs(order.updatedAt).format('YYYY-MM-DD'),
    items: order.items.map(item => ({
      ...item,
      price: (item.price / 100).toFixed(2),
    })),
  };
  return formattedOrder;
}

注释说"检查权限",代码在做数据格式化。返回值不是 boolean,是一个格式化后的对象。

调用方写成了这样:

const order = await checkOrderPermission(userId, orderId);
if (!order) throw new Error('无权限');

恰好,当 orderId 不存在时 findById 返回 null!null === true,所以"权限检查"碰巧能跑。纯属巧合。

为什么会这样: AI 先生成了一个权限检查函数,后来被要求加数据格式化逻辑,改了函数体但没改函数名和注释。这种"假注释"比没注释更危险——没注释你会认真读代码,有注释你会下意识信任它,跳过代码。

怎么防: 好的代码不需要注释。与其修注释,不如改函数名——checkOrderPermission 改成 formatOrderForDisplay,删掉注释,可读性反而更高。


第四坑:万能 try-catch——出了 bug 永远定位不到

AI 特别喜欢给每个函数都包一层 try-catch:

async function createOrder(params) {
  try {
    const validated = validateParams(params);
    const order = await db.orders.create(validated);
    await notifyUser(order);
    await updateInventory(order);
    return order;
  } catch (error) {
    console.log('Error in createOrder:', error);
    return null;
  }
}

async function validateParams(params) {
  try {
    // ... 校验逻辑
  } catch (error) {
    console.log('Error in validateParams:', error);
    return null;
  }
}

每一层都默默吞掉错误,打个 console.log 就完事。

结果:线上出 bug,Sentry 里只有 "Error in createOrder: null"。是创建失败了?通知失败了?库存扣减失败了?你不知道。所有上下文都被 catch 吃掉了。

更要命的是 return null。调用方拿到 null 以为"没有订单",继续往下跑,导致后面一连串空指针。一个本该在源头暴露的错误,被层层掩盖,最后在距离原始问题八百里远的地方炸出来。

正确做法:错误冒泡,只在边界捕获。

async function createOrder(params) {
  const validated = validateParams(params); // 校验失败直接抛
  const order = await db.orders.create(validated); // 数据库错误直接抛

  // 只有不影响主流程的旁路操作才 try-catch
  try {
    await notifyUser(order);
  } catch (error) {
    logger.warn('通知发送失败,不影响订单创建', { orderId: order.id, error });
  }

  return order;
}

为什么会这样: AI 出于"安全感"给每个函数加 try-catch——它不想让任何错误导致程序崩溃。但过度防御恰恰让 debug 变成噩梦。错误处理不是越多越好,是在正确的位置处理。


第五坑:data、data2、data3——变量名丢失业务语义

这个问题随处可见:

async function handleSubmit() {
  const data = await fetchData();
  const data2 = processData(data);
  const data3 = data2.filter(item => item.status === 'active');
  const result = await submitData(data3);

  if (result.code === 0) {
    const data4 = result.data;
    setList(data4);
  }
}

读到 data3 的时候你已经忘了 data 是什么了。对比一下:

async function handleSubmit() {
  const allOrders = await fetchOrders();
  const formattedOrders = formatForDisplay(allOrders);
  const activeOrders = formattedOrders.filter(order => order.status === 'active');
  const submitResult = await submitOrders(activeOrders);

  if (submitResult.code === 0) {
    setOrderList(submitResult.data);
  }
}

一眼就知道每一步在做什么。

另一个变种更常见:

const res = await api.get('/orders');
const res2 = await api.get('/users');
const res3 = await api.get(`/orders/${res.data[0].id}/items`);

三个完全不同的接口,返回值都叫 res。改 bug 的时候搜 res,搜出来 47 个结果,祝你好运。

为什么会这样: AI 在生成整段代码时倾向于用通用名词。data 是最安全的命名——不会出错,但也不传达任何信息。它没有你的业务上下文,不知道 data 其实是"订单列表"。

怎么防: AI 生成代码后,第一件事搜 dataresresulttempitem,全部改成业务名词。


AI 代码验收 Checklist

以上 5 个坑归结起来,就是一句话:AI 的代码和实习生的代码一样,能跑但需要 review。

每次 AI 生成代码后,花 5 分钟过一遍:

# 检查项 怎么查
1 删减测试 删掉一半代码还能跑吗?能跑的就是过度设计
2 死代码检测 npx knip --include exports
3 注释一致性 每条注释和代码对一下,不确定就删注释
4 错误处理 catch,是不是只有 console.log?该冒泡还是该吞?
5 命名审查 data/res/result/temp,全改成业务名词

5 分钟能省你三天的 debug 时间。


你在 review AI 代码时踩到过什么坑?评论区聊聊。

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请 注册新账号