摘要: 拆解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.html → main.tsx → App.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
}
三个字段,不多不少。id 用 number 类型,用 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 等任何技术栈。工具会变,但"先规划、再胶水、后进化"的协作模式不会变。