AI 编程进入下半场:从 Vibe 到 Spec

0 阅读6分钟

最近在看“从 Vibe Coding 到 Spec Coding”这组资料时,我最有共鸣的一句话是:

Code is a lossy projection of intent.代码是意图的有损投影。

这句话的价值在于,它把很多团队正在经历的问题说透了:我们不是不会“写代码”,而是经常没有把“为什么这样写”变成可验证、可协作、可演进的规范。

本文默认你使用 Node.js 20 + TypeScript + Vitest + Claude Code,目标不是做一个 Demo,而是把 AI 辅助开发拉到生产级。

问题分析:Vibe Coding 为什么总在后期失速

Vibe Coding 的优势是快,尤其适合探索期。但它在生产阶段常见 4 个问题:

    1. 意图丢失:Prompt 只保留“怎么做”,很难沉淀“为什么这样做”。
    1. 结果漂移:多人协作时,每个人的 Prompt 风格不同,输出标准不一致。
    1. 验证滞后:先写代码再补测试,返工成本高。
    1. 经验不可复用:一次性对话很难迁移到下一个项目。

所以关键不是否定 Vibe,而是给它一个更稳定的“骨架”——Spec Coding

解决方案:Spec Coding 的三层规范模型

把规范当成 source of truth,代码当成实现产物。推荐用三层结构:

    1. 功能规范(What) 定义用户故事、验收标准、边界条件。
    1. 架构规范(How - 语言无关) 定义数据模型、API 契约、安全约束、性能目标。
    1. 实现规范(How - 语言相关) 定义技术栈版本、测试框架、编码约束、发布要求。

在 Claude Code 中,对应关系通常是:

  • CLAUDE.md:项目级总规范
  • .claude/rules/:分层规则
  • specs/*.md:功能级规范
  • /plan:把规范拆成可执行任务

实战示例:用规范驱动通知系统(可运行)

下面给一套最小可运行样例,体现“先规范,后实现,再验证”。

步骤 1:先写 specs/notification.md
# 用户通知系统规范## 功能需求1. 支持站内通知、邮件通知、推送通知2. 通知类型:系统公告、订单状态、促销活动、安全提醒3. 支持已读/未读、批量已读4. 用户可按渠道与类型配置偏好## 数据模型- notifications: id, userId, type, channel, title, content, isRead, createdAt- preferences: userId, type, channel, enabled## 验收标准- 未读数在标记已读后实时正确- 批量已读后用户未读数为 0- 偏好变更后立即生效## 非功能性要求- 所有输入参数必须校验- 错误要有明确错误码- 关键行为可测试
步骤 2:实现 src/notification-service.ts
export type NotificationType = 'system' | 'order' | 'promo' | 'security'export type Channel = 'in_app' | 'email' | 'push'export interface Notification {  id: string  userId: string  type: NotificationType  channel: Channel  title: string  content: string  isRead: boolean  createdAt: Date}interface PreferenceKey {  userId: string  type: NotificationType  channel: Channel}export class NotificationService {  private notifications: Notification[] = []  private preferences = new Map()  constructor(seed: Notification[] = []) {    this.notifications = [...seed]  }  // 统一生成偏好键,避免拼接逻辑分散  private prefKey(key: PreferenceKey): string {    return `${key.userId}:${key.type}:${key.channel}`  }  // 更新通知偏好,默认 true 表示开启  updatePreference(input: PreferenceKey, enabled: boolean): void {    this.preferences.set(this.prefKey(input), enabled)  }  // 判断某类通知渠道是否开启  isEnabled(input: PreferenceKey): boolean {    const value = this.preferences.get(this.prefKey(input))    return value ?? true  }  // 获取用户通知,支持按已读状态过滤  list(userId: string, isRead?: boolean): Notification[] {    return this.notifications.filter((n) => {      if (n.userId !== userId) return false      if (typeof isRead === 'boolean' && n.isRead !== isRead) return false      return true    })  }  // 标记单条已读,不存在时抛错,方便上层返回 404  markAsRead(userId: string, notificationId: string): void {    const target = this.notifications.find(      (n) => n.userId === userId && n.id === notificationId    )    if (!target) throw new Error('NOTIFICATION_NOT_FOUND')    target.isRead = true  }  // 批量标记已读  markAllAsRead(userId: string): number {    let affected = 0    for (const n of this.notifications) {      if (n.userId === userId && !n.isRead) {        n.isRead = true        affected += 1      }    }    return affected  }  // 未读数用于前端角标展示  unreadCount(userId: string): number {    return this.notifications.filter((n) => n.userId === userId && !n.isRead).length  }}
步骤 3:验证 tests/notification-service.test.ts
import { describe, expect, it } from 'vitest'import { NotificationService } from '../src/notification-service'const seed = [  {    id: 'n1',    userId: 'u1',    type: 'system',    channel: 'in_app',    title: '系统维护',    content: '今晚 22:00 维护',    isRead: false,    createdAt: new Date('2026-04-09T10:00:00Z')  },  {    id: 'n2',    userId: 'u1',    type: 'promo',    channel: 'email',    title: '限时活动',    content: '全场 8 折',    isRead: false,    createdAt: new Date('2026-04-09T11:00:00Z')  }] as constdescribe('NotificationService', () => {  it('markAsRead 后未读数应减少', () => {    const service = new NotificationService([...seed])    expect(service.unreadCount('u1')).toBe(2)    service.markAsRead('u1', 'n1')    expect(service.unreadCount('u1')).toBe(1)  })  it('markAllAsRead 后未读应为 0', () => {    const service = new NotificationService([...seed])    const affected = service.markAllAsRead('u1')    expect(affected).toBe(2)    expect(service.unreadCount('u1')).toBe(0)  })  it('偏好关闭后 isEnabled 应返回 false', () => {    const service = new NotificationService([...seed])    service.updatePreference(      { userId: 'u1', type: 'promo', channel: 'email' },      false    )    expect(      service.isEnabled({ userId: 'u1', type: 'promo', channel: 'email' })    ).toBe(false)  })})

运行方式:

npm i -D typescript vitest tsxnpx vitest run

在 Claude Code 中的推荐指令

@specs/notification.md请按规范实现通知系统,严格分 4 步执行:1) 先输出 plan;2) 按任务逐步实现;3) 每步都运行测试;4) 完成后做一次 code review 并列出风险项。

这条指令的重点是:把“生成代码”变成“执行规范 + 验证规范”。

最佳实践与常见陷阱

最佳实践:

    1. 规范与代码同仓库同版本管理,PR 同时审查。
    1. 每个规范都带验收标准和非功能性要求。
    1. 规范变更必须触发自动化测试,防止实现漂移。

常见陷阱:

    1. 规范只写 happy path,漏掉错误处理与边界条件。
    1. 把“实现细节”误写成“需求事实”,导致规范过早僵化。
    1. 只让 AI 生成,不做验证,把草稿当成成品。

渐进迁移:从 Vibe 到 Spec 的可执行路径

    1. 第 1 阶段(探索) 用 Vibe Coding 在 30 分钟内验证想法是否成立。
    1. 第 2 阶段(沉淀) 把有效结论抽成 specs/*.md,补齐验收与约束。
    1. 第 3 阶段(重建) 基于规范重做生产实现,并强制测试与审查。

这个路径的核心是:速度来自 Vibe,质量来自 Spec。

总结

Spec Coding 不是替代 Vibe Coding,而是把 AI 编程从“会写”推进到“可交付”。 当你开始认真维护 CLAUDE.mdrulesspecs,你已经在做工程化的 AI 开发。

一句话收尾: 规范不是文档附属品,规范本身就是代码。

如果你也在把 AI 从“会写”推进到“可交付”,欢迎留言说说你团队现在最卡的是规范、测试,还是协作流程。

参考资料

  • • 从 Vibe Coding 到 Spec Coding:AI 编程的进化之路

2026.04.09 17:26 沪 · 赵巷KFC

📌 声明:本文由 AI 辅助完成