Code Review 修罗场:Vibe Coding 写出的“完美代码”,为何让我脊背发凉?

25 阅读12分钟

引言:周五下午的“惊喜”

上周五下午 4 点,按照惯例是团队的 Code Review 时间。 实习生小林兴奋地发起了 Merge Request(MR),备注写着:“完成多租户报表导出功能,支持 PDF/Excel,已自测通过。

我眉头一皱。这个功能涉及数据库分片读取、内存聚合计算、以及由 Puppeteer 生成 PDF,通常需要资深后端开发整整 3 天的排期。而小林,只用了 2 小时

“这也太快了吧?”我点开 Diff 界面,映入眼帘的是极其工整的代码: 注释详尽、变量命名优雅(甚至符合我最喜欢的 isProcessed 风格)、甚至连单元测试都写了。 小林得意地说:“我用了 Cursor,开着 Vibe Coding 模式,直接把需求告诉它,代码就‘流’出来了。那种心流感太强了,我感觉自己就是神!”

那一刻,我差点就信了。

直到我点开 ReportService.ts 的第 45 行,看到了一段处理大批量数据导出的逻辑。 我的心跳漏了一拍。那不是代码,那是埋在生产环境的一颗核弹

什么是 Vibe Coding?一场“去语法化”的狂欢

在批判之前,我们先得承认 Vibe Coding(氛围编程)的革命性。 它的核心在于 “意图优先” 。你不再需要纠结 Array.reduce 的语法细节,也不用手写繁琐的 Boilerplate。你只需要按下 Cmd+K,输入:“读取用户日志,按日期分组,计算活跃度”,代码瞬间生成。

这种模式极大地降低了认知负荷,让开发者进入一种“只关注逻辑,不关注实现”的 Flow State(心流状态) 。 对于原型开发(MVP)或独立开发者来说,这简直是上帝的礼物。

但问题在于,软件工程不是写作文。 作文写错一个字不影响阅读,代码写错一个状态,系统就会崩。

隐形债务:当 AI 开始“假装”懂架构

在小林的代码里,我发现了 Vibe Coding 最典型的三个“架构级”硬伤。这些硬伤在 Demo 阶段不可见,一旦上线遇上高并发,就是P0 级事故

1. 上下文的“失忆症” (Context Amnesia)

AI 是局部的神,却是全局的盲人。 在小林的代码中,AI 为了方便,直接在 Service 层手写了一个 Redis 连接:

// AI 生成的代码
const redis = new Redis({ host: 'localhost' ... }); 

它完全不知道我们的项目里早已封装了带有熔断机制连接池管理GlobalCacheManager。 结果:这行代码绕过了所有的监控和保护机制。一旦 Redis 抖动,这个“私接”的连接会直接拖死整个 Node 进程。

2. Happy Path 的“幸存者偏差”

Vibe Coding 生成的代码,默认世界是美好的:

  • 网络永远不会超时。
  • 数据库永远不会死锁。
  • 用户上传的文件永远是合法的 PDF。

小林的代码里全是 await,却少见 try-catch,更没有重试策略(Retry Policy)。AI 给了他一条通往成功的捷径,却没告诉他路两边全是悬崖。

3. “面条代码”的现代化变种

以前的面条代码是逻辑混乱,现在的 AI 面条代码是逻辑虽然清晰,但完全没有复用性。 为了实现“导出 Excel”,AI 生成了 500 行代码;为了实现“导出 PDF”,它又生成了 500 行极其相似但微小差异的代码。它不懂设计模式中的策略模式(Strategy Pattern) ,只是在机械地堆砌逻辑。

为了让你更直观地理解这种危害,我画了一张对比图。

图解:Vibe Coding 的“代码孤岛” vs 工程化架构

1.svg

左边是 Vibe Coding 容易产生的“烟囱式”架构,右边是我们需要的“分层式”架构。 看到了吗?Vibe Coding 让你赢在起跑线,但如果没有工程化约束,你会在半路摔得很惨。

那么,如何在享受 AI 极速编码的同时,又能保证代码的“含金量”? 这就需要我们将 AI 从“自由画师”变成“填色工”。

我们已经看清了 Vibe Coding 最大的陷阱:用战术上的勤奋(快速出码),掩盖战略上的懒惰(架构缺失)。

现在,我们要反客为主。不是让 AI 替我们思考,而是我们思考完,让 AI 替我们干活。 这叫 Specs-Driven Development (SDD,规格驱动开发)


破局之道:把 AI 关进“规格”的笼子里

我在 Code Review 评论区留了一句话给小林:

“不要告诉 AI ‘帮我写个导出功能’,要告诉它 ‘基于现有的 BaseService,实现一个 ExportStrategy 接口,必须复用 GlobalRedis,且必须包含 Zod 校验。’”

这不仅是 Prompt 的区别,更是思维模式的降维打击。

核心技术点 A:Specs-Driven Development (规格驱动)

我们要把 AI 当作一个高级外包团队。在把任务交给外包前,你必须提供详细的技术规格书(Specs) 。 在 Cursor 或 Copilot 中,这意味着我们需要先建立一个 .cursorrules 或项目上下文文件,明确定义系统的“宪法”。

❌ 错误示范:Vibe Coding 的“直觉式写法”

这是小林原本提交的代码,典型的“脚本式”思维:

// 💀 危险代码:Vibe Coding 直出
import { createClient } from 'redis'; // ❌ 私自引入 Redis
import fs from 'fs';
​
export const exportReport = async (userId, type) => {
  // ❌ 没有任何参数校验,SQL 注入风险
  const data = await db.query(`SELECT * FROM logs WHERE user_id = ${userId}`); 
  
  if (type === 'pdf') {
     // ❌ 硬编码逻辑,无法扩展
     // ❌ 内存炸弹:一次性加载所有数据到内存生成 PDF
     const pdf = await generatePdf(data); 
     return pdf;
  }
  
  // ❌ 缺少错误处理,如果 Redis 挂了,请求直接 pending
  const client = createClient();
  await client.connect();
  await client.set(`report:${userId}`, 'done');
};

为什么会挂?

  1. 资源泄漏:每次请求都创建 Redis 连接且不关闭,连接数瞬间打满。
  2. OOM 风险data 可能是 10 万条数据,直接撑爆内存。
  3. 扩展性为零:如果要加 CSV 导出,又得加 if-else

✅ 正确示范:架构师视角的“规格驱动写法”

我要求小林按照以下 Specs 重构:

  1. 依赖注入:Redis 和 Logger 必须通过构造函数注入。
  2. 策略模式:PDF/Excel 逻辑分离。
  3. 流式处理:必须使用 Stream 避免 OOM。
  4. 防御性编程:输入参数必须经过 Zod 校验。

重构后的代码(AI 在我的 Specs 约束下生成):

// ✅ 生产级代码:架构清晰 + 类型安全 + 资源管理
import { z } from 'zod';
import { Pipeline } from 'stream';
import { Injectable } from '@nestjs/common';
import { RedisService } from '@/infra/redis'; // ✅ 复用基础设施// 1. 定义严格的输入规格 (Zod Schema)
const ExportSchema = z.object({
  userId: z.string().uuid(),
  type: z.enum(['pdf', 'excel', 'csv']),
  dateRange: z.object({ start: z.date(), end: z.date() })
});
​
// 2. 定义策略接口 (Strategy Pattern)
interface ExportStrategy {
  execute(stream: NodeJS.ReadableStream): Promise<string>;
}
​
@Injectable()
export class ReportService {
  constructor(
    private readonly redis: RedisService, // ✅ 依赖注入
    private readonly strategyFactory: StrategyFactory
  ) {}
​
  async exportReport(rawInput: unknown) {
    // 3. 防御性编程:运行时校验
    const input = ExportSchema.parse(rawInput);
    
    // 4. 状态管理:使用 Redis SETNX 防止并发重复提交
    const lockKey = `lock:export:${input.userId}`;
    const acquired = await this.redis.setnx(lockKey, 'processing', 300);
    if (!acquired) throw new Error('Task already running');
​
    try {
      // 5. 流式处理:获取数据库游标 (Cursor),而非一次性加载
      const dbStream = await this.db.logs.findMany({
        where: { userId: input.userId },
        stream: true // 关键:开启流模式
      });
​
      const strategy = this.strategyFactory.get(input.type);
      
      // 6. 核心逻辑:数据流像水一样经过管道,内存占用极低
      return await strategy.execute(dbStream);
      
    } catch (err) {
      this.logger.error('Export failed', err);
      throw err;
    } finally {
      // 7. 资源释放:确保锁被释放
      await this.redis.del(lockKey);
    }
  }
}

这段代码的含金量在于:

  • 内存恒定:无论数据量多大,内存占用始终维持在 Buffer 大小。
  • 并发安全:分布式锁防止了用户狂点按钮导致服务器雪崩。
  • 可测试:所有依赖都可以 Mock,单元测试极易编写。

图解:AI 辅助开发的正确工作流 (The Review-Refine Loop)

为了让团队理解这套新的工作流,我画了下面这张流程图。这不仅仅是写代码的流程,更是人机协作的协议。

2.svg

在这个闭环中,人类的职责从“写代码”变成了“定义规格”和“验收质量”。 这才是 Vibe Coding 时代,高级工程师的核心竞争力。


破局之道:把 AI 关进“规格”的笼子里

面对小林的那堆“面条代码”,我没有让他直接改,而是让他把 Cursor 关掉,先在白板上画出设计规格(Specs)

如果你想用好 AI,必须通过 “Specs-Driven Development(规格驱动开发)” 来反向约束它。 简单来说,就是 “人类写接口(Interface),AI 写实现(Implementation)”

核心技术点 B:控制反转(IoC)与防御性编程

在重构中,我们引入了两个关键的工程概念,这是 AI 往往会忽略的:

  1. 依赖注入(Dependency Injection) :不再让 AI 自己 new Redis(),而是强制它使用我们可以 mock、可以监控的全局单例。
  2. 防御性校验(Defensive Validation) :引入 Zod 库,强制 AI 对所有输入输出进行运行时校验,防止“脏数据”污染系统。

代码实战:Vibe 写法 vs 架构师修正版

❌ Vibe 写法(反面教材)

这是小林最初生成的代码,典型的“脚本式”思维,耦合度极高。

// 💀 死亡代码:Vibe Coding 产物
// 问题 1: 硬编码数据库连接,无法复用,无法测试
const db = require('knex')({ client: 'mysql', connection: '...' });
​
export async function exportUserReport(userId) {
  // 问题 2: 直接拼接 SQL,虽然 Knex 有防注入,但逻辑极度脆弱
  const users = await db('users').where('id', userId);
  
  // 问题 3: 内存炸弹!如果 logs 有 100万条,这里直接 OOM
  const logs = await db('logs').where('user_id', userId);
  
  // 问题 4: 同步计算,阻塞 Event Loop
  const report = logs.map(log => calculate(log)); 
  
  return report;
}

✅ 架构师修正版(Production-Ready)

这是我指导小林利用 Claude Code/Cursor 重构后的代码。我们先定义了 interface,然后要求 AI 填充实现。

// ✅ 生产级代码:规格驱动 + 依赖注入 + 流式处理
import { z } from 'zod';
import { Injectable, Inject } from '@nestjs/common';
import { pipeline } from 'stream/promises';
import { Transform } from 'stream';
​
// 1. 定义严谨的输入规格 (Schema)
const ReportQuerySchema = z.object({
  userId: z.string().uuid(),
  startDate: z.date(),
  endDate: z.date(),
});
​
@Injectable()
export class ReportService {
  // 2. 依赖注入:DB 连接池由框架管理,AI 无法私自创建
  constructor(
    @Inject('DATABASE_POOL') private readonly db: Knex,
    @Inject('LOGGER') private readonly logger: Logger
  ) {}
​
  async generateReportStream(query: z.infer<typeof ReportQuerySchema>, res: Response) {
    // 3. 防御性校验
    const cleanQuery = ReportQuerySchema.parse(query);
​
    try {
      // 4. 流式查询:使用 Async Generator 处理海量数据,内存占用恒定
      const logStream = this.db('logs')
        .where('user_id', cleanQuery.userId)
        .stream(); // Knex Stream Interface
​
      // 5. 管道处理:数据像水流一样经过,不会积压
      await pipeline(
        logStream,
        this.createTransformStream(), // 业务逻辑封装在 Transform 中
        res // 直接流向 HTTP Response
      );
    } catch (error) {
      this.logger.error('Export Failed', { error, userId: cleanQuery.userId });
      throw new InternalServerErrorException('Report generation failed');
    }
  }
​
  // AI 只需要负责在这个小盒子里写业务逻辑,这就安全多了
  private createTransformStream() {
    return new Transform({
      objectMode: true,
      transform(chunk, encoding, callback) {
        // ... 具体的计算逻辑
        const result = JSON.stringify(chunk) + '\n';
        callback(null, result);
      }
    });
  }
}

优化原理剖析:

  • 内存安全:从 await db() 改为 .stream(),内存占用从 O(N) 降为 O(1) 。无论导出 1 万条还是 100 万条,内存占用都稳定在几 MB。
  • 可测试性:因为用了 @Inject,写单元测试时我们可以轻松注入一个 mock 的 DB,而不需要真的连数据库。
  • 健壮性:Zod 保证了 userId 绝对是 UUID,如果是恶意注入的字符串,第一行就会报错拦截。

架构工作流:Review-Refine Loop

为了固化这种开发模式,我设计了一套新的工作流。在 Vibe Coding 的基础上,增加了“人工介入点”。

我们来看这套 “AI 辅助开发的正确姿势”

3.svg

在这个流程中,Step 1 (Define) 是绝对不能交给 AI 的。那是灵魂,是骨架。AI 负责的 Step 2 (Generate) 只是肌肉。 如果你连骨架都让 AI 画,生出来的必然是怪物。


数据验证:这就是工程的力量

为了验证重构的效果,我们模拟了 500 并发导出 的场景(这是小林原版代码直接崩溃的阈值)。 压测工具使用 k6,监控使用 Grafana

📊 压测成绩单

指标Vibe Coding (原版)Specs-Driven (重构版)结果解读
P99 响应延迟超时 (> 30s)1.2s (首字节时间)用户不再以为系统挂了,而是立刻看到下载开始
内存峰值2.8GB (直接 OOM 重启)120MB (平稳直线)流式处理让内存与数据量彻底解耦
数据库连接数500+ (瞬间打满)50 (连接池控制)保护了脆弱的数据库,防止级联雪崩
错误率100% (后半段请求全挂)0%系统稳如磐石

看着监控大屏上那条平滑的绿色曲线,小林沉默了很久。 他说:“哥,我以前以为代码跑通了就是写完了。现在才知道,跑通只是开始,让它跑得稳、跑得久,才是本事。

架构师心法:你是司机,AI 只是引擎

Vibe Coding 是不可逆转的趋势。它极大地释放了创造力,让非专业人士也能构建产品。 但作为专业开发者,我们不能沉溺于这种“浅层的快感”。

在 AI 时代,我总结了三条新的生存法则:

  1. 从“写代码”转向“审代码” 以前你的价值是写出复杂的算法,现在 AI 也能写。你的价值变成了:一眼看出 AI 代码里的安全漏洞、性能瓶颈和架构坏味道。 这需要更深厚的内功。
  2. 不仅要会 Prompt,更要会 Specs 不要试图用自然语言去描述复杂的架构逻辑,那是缘木求鱼。用 InterfaceSchemaTest Case 去约束 AI。代码本身就是最精准的 Prompt。
  3. 保持敬畏,守住底线 无论 AI 多强大,生产环境的敬畏之心不能丢。所有的 I/O 必须有超时,所有的输入必须有校验,所有的依赖必须可控。这是工程的底线,也是 AI 无法替代的人类智慧。

结语

那天 Code Review 结束后,我没有责怪小林,而是把那本经典的《重构:改善既有代码的设计》放在了他的桌上。 我说:“AI 可以帮你把这书里的代码敲出来,但什么时候该用哪一种重构手法,得靠你这颗脑袋。”

Vibe Coding 不是洪洪水猛兽,它是一匹烈马。 缰绳抓在手里,它能带你一日千里; 缰绳一旦松开,悬崖就在前方。

希望这篇文章,能帮你握紧手中的缰绳。


关注公众号【爱三味】

wx