Vibe Coding 实战指南:先规划再编码,用胶水思维告别 AI 幻觉代码

摘要: 拆解Vibe Coding三步法:先规划再编码划定边界,胶水编程复用成熟方案减少幻觉,元方法论让AI自我进化。以React+TS+Tailwind待办清单为实战案例,演示AI协作编程完整流程。


AI 写代码,为什么总在"幻觉"和"屎山"之间反复横跳?

用 AI 写代码的人,几乎都经历过两种极端。

第一种是幻觉代码。AI 生成的代码看起来像模像样,变量名规范、函数封装完整、甚至还有注释,但一跑就报错------调用的 API 不存在、props 字段名对不上、import 的路径是编的。就像一个面试时侃侃而谈但入职后一行代码都写不出来的候选人。

第二种是屎山代码。代码能跑,功能也实现了,但逻辑混乱、组件耦合、状态管理一塌糊涂。想加一个新功能,发现要改 5 个文件;想删一个旧逻辑,发现 3 个地方在依赖它。AI 只管"实现需求",不管"后续维护"。

这两种问题的根源,不在于 AI 不够聪明,而在于你给 AI 的指令太模糊了。一句"帮我写一个 React 待办清单页面,支持新增、删除任务",听起来清晰,实际上 AI 可以自由发挥的空间太大了:要不要本地存储?要不要拖拽排序?要不要设置优先级?要不要 API 远程同步?AI 面对这种开放式指令,大概率会自作主张加上一堆你没要求的功能,或者凭空捏造一些不存在的 API 调用。

Vibe Coding 的核心主张只有一个:把 AI 当成同事,而不是代码生成器。 你不会对新入职的同事说"帮我把这个系统做了",而是先让他熟悉技术栈、了解业务流程、确认需求边界,然后才动手。AI 编程也应该是这个流程。


第一步:先规划,再编码

任何 AI 编程任务的第一步,不是写 prompt,而是写规划文档

为什么必须先规划?

让 AI 直接写代码,相当于让一个刚入职的工程师在没有技术文档、没有需求评审、没有架构设计的情况下直接开工。他可能会写出功能正确的代码,但大概率会和现有系统的设计思路冲突,或者引入了你不需要的复杂度。

规划文档的作用是划定边界。它告诉 AI:这是我们用的技术栈,这是我们要做的功能,这是我们不做的功能,这是数据的结构,这是组件的拆分方式。在边界内,AI 可以自由发挥;跨出边界,AI 就被约束住了。

实战:Todo List 项目规划

以 React + TailwindCSS 待办清单项目为例,在一行代码都还没写的时候,先给出这样一份规划 prompt:

markdown 复制代码
遵守胶水编程思维:优先使用成熟方案,避免凭空造逻辑。
第一个阶段:只做规划,禁止输出任何代码。

1. 确认技术栈:
React 19 + TailwindCSS + useState

2. 梳理功能边界:
- 新增待办、删除待办、切换完成状态
- 不做本地持久化、筛选、拖拽功能

3. 拆分模块(乐高组件):
输入框组件、待办条目组件、列表容器组件

4. 定义数据流:
useState 存储的 task 数组,数据结构:
{ id, text, completed }

5. 输出这份完整规划,等待确认无误后,再分段实现代码。

这份规划的价值在于几件事:

划定功能边界。明确指出"不做本地持久化、筛选、拖拽功能",防止 AI 擅自加功能。很多屎山代码的成因就是 AI 在实现核心功能时,顺手加了一堆"看起来有用但你根本没要求"的附加功能,这些功能没有经过设计,代码质量差,耦合度高,后续想删都删不干净。

强制模块拆分。提前规定好组件拆分方式------输入框、待办条目、列表容器三个独立组件。AI 生成代码时就会天然按这个结构来组织,而不是把所有逻辑堆在一个几百行的组件里。好读好维护的前提,是结构在写代码之前就已经确定了。

预先规定数据结构{ id, text, completed } 三个字段写死在规划里,从根源上杜绝 AI 的字段幻觉。如果不规定,AI 可能会把 text 写成 title、把 completed 写成 done、把 id 写成 _id------同一个数据在不同组件里字段名不同,这就是后面维护噩梦的起点。

规划不是一次性的

这份规划文档将伴随项目的整个开发周期。每次新增功能、修改逻辑、重构代码时,都把规划文档作为 prompt 的一部分传给 AI。AI 每次都能看到完整的技术栈、模块拆分、数据结构和功能边界,输出的一致性会远高于每次都从零开始描述需求。


第二步:胶水编程------能抄不写,能连不造

规划解决了"做什么"的问题,胶水编程解决"怎么做"的问题。

什么叫胶水编程?

胶水编程的核心原则只有八个字:能抄不写,能连不造。

"能抄不写"的意思是:如果 GitHub 上已经有成熟、经典、经过大规模验证的开源方案,就不要让 AI 从零手写。让 AI 去找那些已经被验证过千万次的组件,像拼乐高一样把它们组装起来。

"能连不造"的意思是:你只负责写衔接、调用、流转的粘合代码,把各个模块联通。胶水本身不创造零件,只负责把现成的零件粘合在一起。

为什么这个原则重要?因为 AI 最不擅长的就是"从零设计底层逻辑"。让它手写一套拖拽排序,它需要自己实现坐标监听、碰撞检测、排序算法、动画过渡------每一步都是幻觉的高发区。但让它引用 react-beautiful-dnd 这个成熟的拖拽库,只需要写几行配置代码把库和现有组件粘合在一起,AI 出错的概率断崖式下降。

边界在哪里?

胶水编程的边界很清晰:轮子别人造好,你只做胶水;胶水不生产零件,只联通零件。 你的代码里不应该出现"从零实现的排序算法"、"从零实现的状态管理"、"从零实现的动画系统"------这些都有成熟的社区方案。你的代码应该只有接口适配、数据流转、事件绑定这些"粘合逻辑"。

实战:拖拽排序的正反对比

假设你现在要给 Todo List 增加拖拽排序功能。两种方式来对比:

错误示范------从零造零件:

复制代码
帮我写 React 待办清单的拖拽排序功能。

这个 prompt 给了 AI 完全的自由度,它大概率会凭空手写一套拖拽逻辑:onMouseDown 记录起始位置、onMouseMove 计算偏移量、onMouseUp 触发排序算法。问题在于,手写拖拽的边界 case 非常多------快速拖拽时的节流、移动端触摸事件、拖拽过程中的视觉反馈、嵌套列表的拖拽穿透------每个边界 case 都是一个潜在的幻觉 bug。

正确示范------胶水编程:

markdown 复制代码
遵守胶水编程原则:绝不从零自研底层逻辑,优先选择社区长期验证的成熟开源组件。

当前需求:给待办列表增加拖拽排序。
1. 先调研:React 生态成熟的拖拽库,优先选用 react-beautiful-dnd(业内广泛使用)
2. 安装:pnpm i react-beautiful-dnd
3. 不要自己手写拖拽底层代码,只做粘合工作。
4. 输出内容顺序:安装依赖命令、把现有 TodoList 组件和 react-beautiful-dnd 进行衔接。
   只写模块之间适配、数据流转的粘合代码。

这个 prompt 明确告诉 AI:不要手写拖拽,去用现成的 react-beautiful-dnd,你只负责把现有组件和这个库对接起来。AI 的输出范围被严格限制在"粘合代码"的范畴内,底层拖拽逻辑由成熟的库负责,幻觉风险大幅降低。


第三步:元方法论------让 AI 自我进化

规划 + 胶水编程解决了"怎么让 AI 写出靠谱代码"的问题。但还有一个更底层的问题:怎么让 AI 写出越来越好的代码?

答案藏在元方法论里。这个概念来自 Harness 架构的思路:把 AI 的 prompt 本身也当成一个可以被优化的对象,用 AI 去优化 AI 的 prompt。

阿尔法提示词与欧米伽提示词

元方法论的核心机制是一个自反馈循环:

阿尔法提示词是当前使用的 prompt。它定义了 AI 应该怎么干活、输出什么格式、遵循什么规范。

欧米伽提示词是优化后的 prompt。它由 AI 根据阿尔法提示词的执行效果,自动生成改进版本。

循环的逻辑是:用阿尔法提示词驱动 AI 生成代码 → 根据生成结果的质量打分 → 判断阿尔法提示词中有哪些地方不够明确或不够有效 → 生成欧米伽提示词 → 用欧米伽提示词替代阿尔法提示词 → 进入下一轮循环。

这个循环的关键在于打分机制。每次 AI 生成代码后,你需要对结果进行质量评估:有没有幻觉?模块拆分是否合理?代码风格是否一致?有没有擅自加功能?把这些评估结果反馈给 AI,让它自己分析 prompt 中哪些约束不够强、哪些描述不够清晰,然后自动优化 prompt。

举个例子,假设你最初的规划 prompt 中只写了"拆分模块",但没有明确说"App 组件只负责状态管理,不包含 UI 逻辑"。AI 生成代码后发现 App.tsx 里混入了大量 UI 结构,你把这个问题反馈给 AI,AI 就会在欧米伽提示词中自动加上"App 组件严格遵循容器组件模式,只管理状态和事件处理,不包含任何 JSX 结构"。

重复这个循环,你的 prompt 会越来越精准,AI 生成的代码会越来越靠谱。这不是 AI 变聪明了,而是你的约束变强了。


完整项目实战:React + TypeScript + TailwindCSS 待办清单

理论讲完了,现在用前面的三步方法论,完整实现一个待办清单项目。

技术栈一览

技术 版本 用途
React 19.0.0 函数组件 + Hooks 状态管理
TypeScript 5.7.0 类型约束,消除字段幻觉
TailwindCSS 4.1.0 原子化 CSS,零手写样式
Vite 6.3.0 构建工具,极速 HMR
@vitejs/plugin-react 4.4.0 React JSX 编译支持
@tailwindcss/vite 4.1.0 TailwindCSS Vite 插件

项目启动链路非常简洁:index.htmlmain.tsxApp.tsx → 子组件。Vite 的 @tailwindcss/vite 插件让 TailwindCSS 的引入只需要一行 CSS import,不需要 tailwind.config.js 配置文件。

项目骨架

入口文件 index.html 是标准的 Vite 模板,唯一的差异是 lang="zh-CN" 和中文标题:

html 复制代码
<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>待办清单</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

main.tsx 是 React 的挂载入口,使用 createRoot API 渲染 App 组件,外层包裹 StrictMode 用于开发环境下的副作用检测:

tsx 复制代码
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

index.css 只有一行,TailwindCSS v4 的新用法------不再需要 @tailwind base/components/utilities 三件套,一个 @import "tailwindcss" 搞定:

css 复制代码
@import "tailwindcss";

Vite 配置同样简洁,两个插件一个负责 React JSX 编译,一个负责 TailwindCSS 样式处理:

typescript 复制代码
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [react(), tailwindcss()],
})

类型定义:从根源上消灭字段幻觉

在写任何组件之前,先定义 Task 接口。这一步是规划文档中"预先规定数据结构"的落地:

typescript 复制代码
export interface Task {
  id: number
  text: string
  completed: boolean
}

三个字段,不多不少。idnumber 类型,用 Date.now() 生成时间戳作为唯一标识;text 是待办事项的文本内容;completed 是布尔值标记是否完成。这个接口会被所有组件引用,任何一个组件试图用 title 代替 text、用 done 代替 completed,TypeScript 编译器直接报错------字段幻觉在编译阶段就被消灭了。

App:状态管理中心,不碰 UI

App.tsx 遵循容器组件模式:只负责状态管理和事件处理,不包含任何 UI 结构代码。UI 全部委托给子组件:

tsx 复制代码
import { useState } from 'react'
import type { Task } from './types'
import TodoInput from './components/TodoInput'
import TodoList from './components/TodoList'

export default function App() {
  const [tasks, setTasks] = useState<Task[]>([])

  const handleAdd = (text: string) => {
    const newTask: Task = {
      id: Date.now(),
      text,
      completed: false,
    }
    setTasks([...tasks, newTask])
  }

  const handleToggle = (id: number) => {
    setTasks(tasks.map(t => t.id === id ? { ...t, completed: !t.completed } : t))
  }

  const handleDelete = (id: number) => {
    setTasks(tasks.filter(t => t.id !== id))
  }

  return (
    <div className="min-h-screen bg-gray-100 flex items-start justify-center pt-20">
      <div className="bg-white rounded-xl shadow-md p-6 w-full max-w-md mx-4">
        <h1 className="text-2xl font-bold text-center text-gray-800 mb-6">待办清单</h1>
        <TodoInput onAdd={handleAdd} />
        <TodoList tasks={tasks} onToggle={handleToggle} onDelete={handleDelete} />
      </div>
    </div>
  )
}

三个状态操作函数的设计是典型的不可变更新模式

  • handleAdd:用 [...tasks, newTask] 展开旧数组并追加新任务,不直接修改原数组
  • handleToggle:用 map 遍历,找到匹配的 id 后展开并反转 completed 字段,其余项原样返回
  • handleDelete:用 filter 过滤掉匹配 id 的项,返回新数组

三个函数都通过 props 向下传递,App 组件本身不持有任何 UI 逻辑。TailwindCSS 的原子类直接写在 JSX 的 className 中,min-h-screen 保证最小全屏高度、flex items-start justify-center pt-20 实现水平居中并距离顶部 20 单位间距、bg-white rounded-xl shadow-md 提供白色卡片圆角和阴影。

TodoInput:隔离输入状态的独立组件

TodoInput 维护自己的输入框状态,只通过 onAdd 回调向父组件提交最终的文本内容:

tsx 复制代码
import { useState } from 'react'

interface TodoInputProps {
  onAdd: (text: string) => void
}

export default function TodoInput({ onAdd }: TodoInputProps) {
  const [text, setText] = useState('')

  const handleSubmit = () => {
    const trimmed = text.trim()
    if (!trimmed) return
    onAdd(trimmed)
    setText('')
  }

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter') {
      handleSubmit()
    }
  }

  return (
    <div className="flex gap-2 mb-4">
      <input
        type="text"
        value={text}
        onChange={e => setText(e.target.value)}
        onKeyDown={handleKeyDown}
        placeholder="输入新的待办事项..."
        className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent text-gray-700 placeholder-gray-400"
      />
      <button
        onClick={handleSubmit}
        className="px-5 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors font-medium whitespace-nowrap"
      >
        添加
      </button>
    </div>
  )
}

输入框的键盘事件处理是一个细节:onKeyDown 监听 Enter 键触发提交,配合 onChange 的受控输入模式,用户可以用键盘完成输入+提交的完整流程,不需要鼠标点击按钮。handleSubmit 中的 trim() 和空值检查是防御性编程,防止提交空白待办。

TodoItem:单一职责的条目组件

TodoItem 只负责一条待办事项的渲染和交互,不关心列表逻辑:

tsx 复制代码
import type { Task } from '../types'

interface TodoItemProps {
  task: Task
  onToggle: (id: number) => void
  onDelete: (id: number) => void
}

export default function TodoItem({ task, onToggle, onDelete }: TodoItemProps) {
  return (
    <div className="flex items-center gap-2 py-2 border-b border-gray-100 last:border-b-0">
      <input
        type="checkbox"
        checked={task.completed}
        onChange={() => onToggle(task.id)}
        className="w-5 h-5 rounded border-gray-300 text-blue-500 focus:ring-blue-400 cursor-pointer"
      />
      <span className={`flex-1 text-gray-800 ${task.completed ? 'line-through text-gray-400' : ''}`}>
        {task.text}
      </span>
      <button
        onClick={() => onDelete(task.id)}
        className="px-2 py-1 text-red-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors text-sm"
      >
        删除
      </button>
    </div>
  )
}

完成的待办通过 line-through text-gray-400 添加删除线并变灰,这是 TailwindCSS 的条件样式写法------模板字符串中根据 task.completed 动态拼接类名。last:border-b-0 是 TailwindCSS 的伪类选择器,移除最后一项的底部边框,避免多余的分割线。

TodoList:空状态与列表渲染

TodoList 负责遍历任务数组和空状态处理:

tsx 复制代码
import type { Task } from '../types'
import TodoItem from './TodoItem'

interface TodoListProps {
  tasks: Task[]
  onToggle: (id: number) => void
  onDelete: (id: number) => void
}

export default function TodoList({ tasks, onToggle, onDelete }: TodoListProps) {
  if (tasks.length === 0) {
    return (
      <p className="text-center text-gray-400 py-8">
        暂无待办事项,去添加一个吧
      </p>
    )
  }

  return (
    <div>
      {tasks.map(task => (
        <TodoItem
          key={task.id}
          task={task}
          onToggle={onToggle}
          onDelete={onDelete}
        />
      ))}
    </div>
  )
}

空状态处理是前端开发中容易被忽略的细节。当 tasks 数组为空时,直接渲染 map 会得到一片空白,用户不知道是没加载还是真的没数据。tasks.length === 0 的早期返回显式渲染了一条引导文案,告诉用户当前状态并引导下一步操作。

数据流全景

整个项目的数据流是单向的,清晰可追踪:

scss 复制代码
App (useState<Task[]>)
  ├── TodoInput
  │     └── onAdd(text) → App.handleAdd → setTasks
  └── TodoList
        └── tasks.map → TodoItem
              ├── onToggle(id) → App.handleToggle → setTasks
              └── onDelete(id) → App.handleDelete → setTasks

所有状态变更都发生在 App 组件中,子组件只负责触发回调,不直接修改状态。这种状态提升模式保证了数据流的可预测性------任何时候出问题,只需要检查 App 中的三个状态操作函数,而不需要在多个组件中排查。


总结

Vibe Coding 不是一套新的编程语言或框架,而是一套与 AI 协作的思维模型

第一步"先规划再编码"解决的是边界问题 ------在 AI 动手之前,用规划文档划定技术栈、功能范围、模块拆分和数据结构,防止 AI 擅自加功能或捏造字段。第二步"胶水编程"解决的是可靠性问题 ------不信任 AI 的"从零设计"能力,只让它写粘合代码,底层逻辑交给成熟的开源方案。第三步"元方法论"解决的是进化问题------通过打分反馈让 AI 自己优化 prompt,形成持续改进的闭环。

这三步本质上在回答同一个问题:如何把 AI 从一个"偶尔惊艳但经常不靠谱"的代码生成器,变成一个"稳定、可预测、持续进化"的编程伙伴? 答案不是让 AI 更聪明,而是让你对 AI 的约束更精准。

把 AI 当成同事,而不是代码生成器。给它看技术文档,跟它确认需求边界,约束它的发挥空间,然后定期给它反馈帮它改进。这套方法论适用于 Cursor、Codex、Claude Code 等任何 AI 编程工具,也适用于 React、Vue、Python 等任何技术栈。工具会变,但"先规划、再胶水、后进化"的协作模式不会变。

相关推荐
Darling噜啦啦8 小时前
Vibe Coding 工程化指南:9 步前置流程告别 AI 屎山代码
vibecoding
臼犀13 小时前
大语言模型响应延迟对软件工程师尿液浓缩程度的影响 —— 一项基于水杯见底速度的观察性研究
程序员·ai编程·vibecoding
爱丶不疚15 小时前
Code Review「问意图」这件事,在 AI 时代还重要吗?
ai编程·vibecoding
To_OC1 天前
跟 AI 写代码越写越乱?我靠这套「Vibe Coding」思路彻底治好了幻觉屎山
人工智能·agent·vibecoding
Oo9201 天前
Vibe Coding 工程化:别让 AI 把你的项目写成屎山
vibecoding
小月土星2 天前
Vibe Coding 破局之道:用工程化流程驾驭 AI,告别代码"屎山"
vibecoding
不好听6132 天前
Vibe Coding :在 AI 写代码之前,先把规矩立好
vibecoding
柒和远方3 天前
V053: 从 Git 回退到 AI 工程治理:Vibe Coding 的 Harness 工作流与质量阀门
git·vibecoding
用户84913717547164 天前
想做护眼工具却脑子一片空白?我用 OpenSpec 把模糊想法聊成了 v0.1
github·vibecoding