前阵子朋友问我,Vibe Coding 到底靠不靠谱,能不能拿来写点真东西。我不太喜欢空口解释,所以直接用了周末两天,用 React 做了一个最经典的练手项目:飞机大战游戏。做完之后我的感受很直接——Vibe Coding 不是玄学,也不是"AI 全部代劳、我躺在旁边等结果",它更像一场需要你全程主导节奏的协作。这篇文章不写那些概念性的介绍,只把这两天里我是怎么跟 AI 协作、怎么把游戏从零搭出来、在哪几个地方差点被 AI 带沟里,以及最终沉淀下来的边界感讲清楚。如果你也想试试 Vibe Coding,或者想在 React 里做一个小游戏,这份复盘应该能帮你少踩几个我踩过的坑。
1. Vibe Coding 为什么能用来做游戏:一次完整的起步对话
1.1 我理解的Vibe Coding:不是让AI全代劳,而是"描述-生成-验证-修正"的循环
Vibe Coding 这个词最近在圈子里传得很热,很多人把它理解成"随便说句话就能得到整个项目",在我看来这是一种误读。它真正的变化是:你不再花大量时间在语法和 API 细节上,而是把注意力集中在"你到底想要什么"以及"AI 给的东西对不对"这两件事上。换句话说,编程的重心从"写"变成了"审"。
我自己的实践循环是这样的:把游戏需求拆成小块 → 用自然语言把每一块的期望讲清楚 → AI 生成代码 → 我运行、观察、检查边界 → 发现问题再让它修正。这个循环看起来跟传统开发很像,唯一的区别是"敲键盘"这个体力活大部分被 AI 接走了,我的角色变成了产品经理加测试加架构师。做飞机大战这种小项目特别适合验证这套流程,因为范围小、反馈快,改一行代码马上能感受到效果,AI 写得好不好也一眼就能看出来。
打个比方:以前自己写代码像一个人下厨,洗菜切菜配菜炒菜都是自己干;Vibe Coding 更像你带了一个执行力很强的帮厨,你负责定菜单、把控火候、尝味道。但帮厨不会主动思考"这道菜是不是太咸了",它只是照你说的做。所以你对"菜"的理解越清楚,最后出来的东西才越靠谱。
1.2 为什么选飞机大战这个项目练手
选飞机大战不是随手一拍。这个游戏麻雀虽小,五脏俱全,几乎覆盖了前端游戏会遇到的所有典型问题:Canvas 渲染、游戏循环、键盘输入、碰撞检测、状态切换、计分和生命值管理,甚至还有音效和动画。拿它来试 Vibe Coding,正好可以看看 AI 在这些不同类型的代码上分别表现如何。
另外,它的范围非常可控。对我来说,一个周末完成绰绰有余,不会因为项目太大而失去对全局的掌控感。如果你现在让我用 Vibe Coding 做一个电商后台,我大概率要花很多时间跟 AI 对齐业务模型,那不是体验编程方式的好场景。但游戏不一样,它的逻辑是通用的、自包含的,即使 AI 某段代码写得稀烂,修复成本也不高。对想入门 Vibe Coding 的朋友,我真心建议从小游戏或者工具型页面开始练手,别一上来就挑战复杂业务系统。
顺便说一句,这个小项目对准备前端面试也很有用。组件划分、生命周期函数、性能优化这些面试高频题,在飞机大战里全都能落地,你可以一边玩一边把知识点串起来,比死记硬背强得多。
1.3 第一段提示词怎么写:把模糊想法变成可执行方案
我打开 AI 编程助手后,第一句话并没有写"帮我做个飞机大战",而是直接把技术栈、功能和约束一次说清楚:
请帮我用 React + TypeScript + Canvas 实现一个飞机大战游戏。 要求:玩家用键盘控制飞机移动并射击;敌机从顶部随机生成并下落; 有计分系统和生命值系统;游戏需要开始画面、进行中、结束画面。 先不要写完整代码,先给我技术方案和文件结构。最后一句"先不要写完整代码"是关键。让 AI 先输出方案,而不是直接生成一坨代码,能有效避免它一次性铺一大堆不可控的东西。AI 给我的方案很清晰:组件层拆出 App、GameCanvas、HUD、StartScreen、GameOverScreen;高频变化的游戏运行数据放进 ref,低频的界面状态放进 useState;工具函数单独放在 game 目录下。说实话,这个结构已经是一个合格前端工程师的水平了。
我当时确认了方案可行之后,才让它动笔写代码。这也是我这次实践里学到的一个重要原则:Vibe Coding 的上限,取决于你把需求讲得多清楚。你给 AI 的信息越结构化,它还给你的代码就越接近你想要的东西。
1.4 环境准备:一行命令搭起React项目框架
技术栈我选了 Vite + React + TypeScript,原因只有一个:快。Vite 的启动速度比老牌的 Create React App 快一个量级,而且模板自带 TypeScript,省得后面补类型。创建项目的命令我放在这:
npm create vite@latest plane-battle -- --template react-ts cd plane-battle npm install npm run dev跑起来之后,我先把项目里自带的示例代码全部清掉,替换成一个空的 App 组件和一个全屏 Canvas 组件。这么做是给 AI 一个干净的工作区,省得它读到你不需要的代码后产生混乱。
这里多说一句:npm create vite在较新版本里可能会提示交互式选择框架,如果你遇到这种情况,也可以直接手动选择 React + TypeScript 选项,不影响后续操作。Node 版本建议 18 以上,太老的版本跑 Vite 会报兼容性问题。
2. 游戏架构是怎么被"聊"出来的:Canvas与React组件边界划分
2.1 先让AI做架构师:一场关于组件与状态的分工讨论
环境准备好后,我继续跟 AI 聊架构。我问它:"游戏循环、玩家位置、子弹数组这些高频变化的数据应该放哪?为什么?"这个问题是我故意抛的,因为我想看它是否理解 React 的性能特性,而不是机械地写代码。
AI 的回答很到位:高频变化的数据不要直接放进 React state,否则setState一秒执行几十次,组件会反复重渲染,Canvas 上画的内容也跟着抖动;正确做法是用useRef保存这些运行期数据,只在必要时通过回调通知 React 更新 UI。当时看到这个回答,我心里其实挺满意的。它主动提到了很多人做 React 游戏时最容易踩的坑。
我后来复盘发现,AI 能给出这个方案,是因为我前面把约束讲得足够具体。如果你只是说"做一个飞机大战",它可能真的会把玩家坐标写进 state,跑起来卡成幻灯片。这又一次说明了:你的约束越清晰,AI 的设计越合理。在 Vibe Coding 里,提问本身就是一种架构能力。
2.2 Canvas负责帧级渲染,React负责界面与状态:这条边界的意义
架构确定后,我们的分工很明确:React 管"什么时候进入游戏、游戏结束后显示什么、分数和生命值怎么展示"这些偏界面的事情;Canvas 管"每一帧画什么飞机、什么子弹、什么敌机"这些偏渲染的事情。两者之间通过回调通信,而不是互相直接读写数据。
文件结构也跟着这个思路定了下来:
src/ App.tsx // 游戏阶段状态机:menu / playing / gameover components/ GameCanvas.tsx // Canvas渲染 + 游戏循环 HUD.tsx // 分数/生命值展示 StartScreen.tsx // 开始画面 GameOverScreen.tsx // 结束画面 game/ engine.ts // 游戏循环、update/render entities.ts // 玩家、敌机、子弹的类型与创建函数 collision.ts // 碰撞检测纯函数 constants.ts // 画布宽高、速度、生成间隔等配置这个结构里的关键点是"渲染流程"的归属。画布渲染流程(从清屏、绘制、到下一帧的持续循环)完全放在 GameCanvas 内部,不经过 React 的虚拟 DOM;React 只在状态切换的瞬间介入。这个边界划清楚后,后续所有模块的提示词都能聚焦到各自的文件里,上下文不会越扯越长,也为我后面要讲到的"AI 遗忘问题"提前打好了预防针。
2.3 游戏循环的接入点:requestAnimationFrame与状态机的耦合
游戏循环是飞机大战的心脏,它长这样:调用requestAnimationFrame(loop),每次 loop 里计算时间差dt,然后根据当前游戏阶段决定是更新逻辑还是只渲染画面。这里我贴一段简化后的示意代码,它代表了我们最终的实现方式:
useEffect(() => { let rafId = 0; let lastTime = performance.now(); const game = createGame(); const loop = (timestamp: number) => { const dt = (timestamp - lastTime) / 1000; lastTime = timestamp; if (phaseRef.current === 'playing') { game.update(dt); } game.render(ctx); rafId = requestAnimationFrame(loop); }; rafId = requestAnimationFrame(loop); return () => cancelAnimationFrame(rafId); }, []);注意这段代码里有两个细节。第一,phase不是通过useState直接读的,而是存在phaseRef里。为什么?因为在事件回调或 rAF 回调里读取 state 容易拿到旧值,用 ref 可以随时拿到最新状态。第二,effect 的依赖数组是空的,这保证了游戏循环只启动一次,不会被 React 的生命周期反复触发。这个细节我当时没太在意,结果后来就在这里翻了个大车,第 4 章我会详细讲。
3. 三大核心模块逐个攻破:从控制到碰撞的实战记录
3.1 玩家控制:按键集合与移动计算的AI实现
架构搭好后,我开始让 AI 逐个模块写代码。第一个模块是玩家控制。我的提示词是:
请实现玩家飞机的键盘控制:方向键和WASD都能移动,支持长按持续移动, 空格键射击,射击要有间隔。移动速度用常量配置。AI 第一次给出的版本很朴素:在keydown事件里直接修改玩家坐标。这个写法有一个问题:键盘的keydown事件本身会按系统频率重复触发,长按方向键时飞机移动速度会忽快忽慢,而且同时按两个方向键时,位置更新会乱掉。
我让 AI 改成"按键集合"方案:把所有被按下的键记录到一个Set里,每帧在update函数里统一读取这些按键状态,再计算位移。这个方案的妙处在于,无论键盘事件触发多少次,最终移动只取决于"当前按住了哪些键",逻辑干净且稳定。简化后的核心代码是这样的:
const keys = new Set<string>(); window.addEventListener('keydown', e => keys.add(e.code)); window.addEventListener('keyup', e => keys.delete(e.code)); function updatePlayer(player: Player, dt: number) { let dx = 0; let dy = 0; if (keys.has('ArrowLeft') || keys.has('KeyA')) dx -= 1; if (keys.has('ArrowRight') || keys.has('KeyD')) dx += 1; if (keys.has('ArrowUp') || keys.has('KeyW')) dy -= 1; if (keys.has('ArrowDown') || keys.has('KeyS')) dy += 1; player.x += dx * player.speed * dt; player.y += dy * player.speed * dt; player.x = clamp(player.x, 0, WIDTH - player.width); }代码里还加了clamp把飞机限制在画布范围内,这是游戏手感的第一个保障。后来我还让 AI 给移动加了一点点惯性,也就是目标位置和当前位置之间做一个插值,让飞机不显得那么"硬"。这种手感层面的优化,AI 能很快理解并实现,但前提是你得主动提出来。你不说,它默认给你的就是最简单的匀速直线运动。
3.2 敌机生成与碰撞检测:AI最容易写歪的部分
第二个模块是敌机生成和碰撞检测。提示词我这样写:
敌机从顶部随机生成,向下移动,速度随机;子弹与敌机、敌机与玩家发生矩形碰撞检测; 碰撞后如果是子弹击中敌机则加分,如果是敌机撞击玩家则扣命。AI 很快给出了矩形碰撞检测函数:
function rectHit(a: Rect, b: Rect): boolean { return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y; }这个公式本身是标准的矩形相交判断,没有毛病。但问题往往不出在公式上,而出在坐标系的约定上。rectHit里的a.x到底是飞机的左上角还是中心点?AI 在上下文里很容易前后不一致。这个坑我在第 4 章会详细讲,这里先按下不表。
除了碰撞检测,敌机生成还有一个隐藏细节:生成间隔要随着游戏进程逐渐缩短,难度曲线才会合理。AI 默认给的版本是固定间隔生成,我要求它改成"每过 20 秒,生成间隔缩短 10%,并设一个下限",这样游戏越玩越刺激,但又不会难到无解。难点设计这种东西,AI 不会替你想,你给出规则它才能执行。
3.3 计分、生命值与游戏状态切换:先想清楚再让AI写
第三个模块是计分、生命值和游戏状态。我给的提示词是:
击落一架敌机加10分;玩家被敌机撞击或者敌机到达底部就扣一条命; 生命值为0时游戏结束,显示GameOver画面与最终得分;重新开始必须重置所有运行状态。这里我没有让 AI 直接写,而是先让它给出了状态机的设计。游戏阶段我定义了四个:menu、playing、paused、gameover。在 React 层,我用useState保存当前阶段;在游戏引擎层,所有的实体数据都在引擎对象里。两个方向之间的同步,我坚持"只从引擎向 React 单向同步":引擎修改分数,调用onScoreChange回调,React 层拿到新分数后更新 UI;反过来,React 不会把分数写回引擎。这个单向数据流帮我避免了很多奇怪的双向同步 Bug。
AI 在这个模块里表现得中规中矩,但有一个点它容易漏:游戏结束的时候,要清掉画布上的残留内容,否则最后一帧画面会一直挂在屏幕上。我让我加了一个"半透明黑色遮罩 + 居中显示 Game Over"的处理,玩家操控也要及时禁用,不然键盘事件还会继续移动那架已经不存在的飞机。
总的来说,三大模块做完之后,游戏已经可以完整跑起来了。但紧接着,我就在联调和体验阶段连续踩了好几个坑,接下来这部分是这篇文章的重头戏。
4. 踩坑实录:AI写游戏代码最容易翻车的四个地方
4.1 上下文越写越长,AI开始"遗忘"自己定过的坐标约定
做到碰撞检测联调的时候,我遇到了第一个大坑。当时项目已经写了挺多代码,AI 的上下文里塞满了各种模块的对话记录。我让它往引擎里新增一种"分裂敌机",它新生成的代码里,敌机坐标用的是"中心点 +x/y表示中心位置",但整个项目此前的约定是"左上角 +width/height"。两边混在一起,结果就是所有碰撞都偏移了半个机身。
我当时玩了几局,总感觉有些子弹明明离敌机还有一段距离,敌机却炸了;有些子弹明明穿过了敌机,却没反应。第一反应是碰撞公式写错了,但公式一眼扫过去没问题。后来我在碰撞函数里加了打印日志,把参与碰撞的两个矩形坐标输出,才发现差异刚好等于width / 2和height / 2。再翻 AI 生成的代码,确认是坐标锚点不统一。
这个经历让我明白了:Vibe Coding 的长对话有上下文窗口的天花板,AI 不会像人一样牢牢记住自己最早定下的约定。我的解决办法有两个。第一,项目里建立了一个CONVENTIONS.md文件,显式写清楚"实体坐标锚点统一为左上角;坐标单位是像素;dt单位是秒"这些约定,每次开新对话前都把它贴给 AI。第二,把坐标相关的计算收敛到一个文件里,减少跨文件传递时发生歧义的可能。
这是我在整个项目里收获最大的一条经验:AI 写代码,你得给它一本"项目宪法",否则它写着写着就会跑偏。
4.2 useEffect生命周期陷阱:暂停功能把游戏循环跑出了双份
第二个坑来自 React 本身的特性。我给游戏加了暂停功能,实现方式是点击按钮切换phase到paused,再点一下切回playing。刚开始一切正常,但我很快发现一个诡异的现象:每点击一次"暂停再继续",飞机和子弹的速度就会翻倍。玩了几轮之后,子弹快得跟机关枪扫射一样,完全没法正常游戏。
我的排查过程是这样的:先怀疑是dt计算出了问题,毕竟速度翻倍最直接的解释就是dt被算成原来的两倍。但检查代码,dt = (timestamp - lastTime) / 1000这行没问题。然后我在 loop 函数里打印requestAnimationFrame返回的 id,结果发现每次暂停恢复后,会出现两个不同的 id 交替打印。这说明内存里同时跑着两个游戏循环。
根因很快定位:我当时把游戏循环放在了useEffect里,依赖数组里有phase。每次切换暂停/继续,phase变化,effect 重新执行,新的 rAF 循环开始;而旧循环没有被正确取消,于是两个循环同时在跑,所有速度全部翻倍。
这里我想多说几句 React 生命周期函数。很多从 Class 组件时代过来的人,可能还记得componentDidMount、componentDidUpdate、componentWillUnmount这三件套。useEffect这个 Hook 把这三件事合并成了一件事,依赖数组决定它要不要重跑。问题在于,游戏循环本质上是一个持续运行的进程,不是一次性的副作用,把它绑在会变化的依赖上,就是给自己挖坑。修复方式就是我第 2 章里写的那样:循环放在空依赖数组里启动,phase通过ref读取,让循环本身不被 React 渲染周期打断。
> 提示:如果你在 React 里做游戏或者动画,一定要记住:requestAnimationFrame 循环应该只启动一次,不要在依赖数组里放任何会变化的变量。4.3 碰撞检测的坐标系错位:一次靠打印日志定位的Bug
第 4.1 节里提到的坐标锚点问题,其实不是一个单独的"坑",而是一整类问题。在实际打游戏的时候,玩家飞机的坐标锚点是左上角,而 AI 新生成的敌机坐标锚点是中心点,肉眼看起来飞机轮廓是重叠的,但rectHit判断出来的碰撞区域整体偏移了半个机身。
我当时是靠打印日志锁定的。在碰撞检测函数入口加了一行console.log('bullet:', bullet, 'enemy:', enemy, 'hit:', rectHit(bullet, enemy)),然后故意朝敌机边缘射击。日志里bullet.x和enemy.x的差值,刚好等于enemy.width / 2。这一刻我立刻明白了问题不在碰撞公式,在数据本身。
修复的方式也很直接:统一让 AI 把所有实体的x、y理解为左上角坐标。同时,我把碰撞检测函数拆成了纯函数,单独放进了collision.ts,并用 Vitest 给它写了两组基础测试:一组验证两个矩形相交应该返回 true,另一组验证相离应该返回 false。从那以后,每次 AI 修改引擎代码,我都能跑一次测试来兜底,它如果又把坐标锚点改回去了,测试会立刻亮红灯。
import { describe, it, expect } from 'vitest'; import { rectHit } from '../game/collision'; describe('rectHit', () => { it('两个矩形相交时返回true', () => { const a = { x: 0, y: 0, width: 10, height: 10 }; const b = { x: 5, y: 5, width: 10, height: 10 }; expect(rectHit(a, b)).toBe(true); }); it('两个矩形相离时返回false', () => { const a = { x: 0, y: 0, width: 10, height: 10 }; const b = { x: 20, y: 20, width: 10, height: 10 }; expect(rectHit(a, b)).toBe(false); }); });这段代码不是什么高深的东西,但它代表了我对 Vibe Coding 的一个核心态度:AI 生成代码很爽,但它的最大风险是"表面正确"。这种几何相关的函数,用单元测试把边界锁死,是人应该做的事。
4.4 性能衰减:子弹数组无限增长,用对象池解决
第三个坑出现在游戏玩到三分钟之后。帧率肉眼可见地往下掉,飞机移动开始发飘,子弹也不跟手了。我打开浏览器 Performance 面板,发现每一帧的脚本执行时间越来越长,主要消耗在数组遍历上。
原因很简单:子弹和敌机对象只创建、不回收。AI 生成的代码里,子弹飞出屏幕后只是不再渲染,但对象还留在数组里;敌机被击毁后也只是标记为"不绘制",并没有从数组里移除。游戏越玩,数组越长,每帧要做的碰撞检测次数就越多,性能自然雪崩。
修复方案有两个。第一是"边界剔除":子弹飞出屏幕或者超出某个寿命阈值,直接从数组里删除;敌机飞到底部也一样处理。第二是"对象池":提前预分配一组子弹对象,子弹被发射时从池子里取一个,回收后放回池子,避免频繁创建销毁对象造成 GC 压力。两个方案配合起来,数组长度被控制在稳定范围内,帧率恢复到了流畅水平。
这里也想提醒一句:AI 能写出优化的代码,但"什么时候该优化"这件事,它不会主动告诉你。性能瓶颈的定位需要人来做,这恰恰是 Vibe Coding 替代不了的部分。我当时如果直接把 AI 的第一版代码拿去长时间玩,根本不会意识到数组在膨胀,因为短时间内的体验是完全正常的。这种问题,只有带着性能意识去实测才能发现。
5. Vibe Coding 的边界在哪里:AI负责写,我负责懂
5.1 好的提问是Vibe Coding的一半工程能力
做这个项目的过程中,我越来越感觉到,AI 的能力边界是在快速扩大的,但使用者的"提问能力"成了新的瓶颈。同样是让 AI 写一个碰撞检测模块,问"帮我写碰撞检测"和问"请实现矩形碰撞检测,坐标锚点统一为左上角,返回布尔值,并给出一组单元测试"得到的结果质量完全不同。
我总结了自己在这次项目里用得比较顺手的提问技巧,分享给各位:
- 指明技术栈和运行环境,比如 React 版本、TypeScript、Canvas。
- 先要方案,再要代码。让 AI 在动手前想清楚结构。
- 一个任务开一轮对话,不把两个不相关的需求混在一起问。
- 对关键函数,要求 AI 写明参数含义和假设条件。
- 边界条件主动说清楚,比如坐标锚点、
dt的单位、碰撞检测的矩形含义。
这套方法本质上没有多玄妙,它就是把"需求拆解"做扎实了。Vibe Coding 并没有降低需求分析的门槛,它只是把"打字写代码"这个环节变成了"打字讲需求"。
顺便提一嘴,最近看到的 Agent 框架里经常提到"规划-执行-反思"的循环,我觉得 Vibe Coding 的个人实践其实也是这个套路。我先规划架构,AI 执行生成,我再通过测试和实际运行来反思纠错,一轮一轮循环下去。理解了这套底层逻辑,你对 AI 在项目里扮演的角色会清醒很多。
5.2 code review还是得自己来,AI不会主动承认"我不确定"
我在这两天里遇到过好几次"代码能跑但逻辑明显不对"的情况。最典型的一次是 AI 直接引用了一张我还没下载的飞机贴图路径,ctx.drawImage跑起来直接报错,但它自己不觉得有问题。还有一次,它把玩家速度初始值写成了 0,导致飞机按方向键毫无反应,这段代码没有任何语法错误,编译也通过,只有真正上手玩才知道不对劲。
这些经历让我形成了一个习惯:AI 写完的每个模块,我必须自己通读一遍,重点检查它有没有引用不存在的资源、有没有把初始化逻辑放在错误的位置、有没有把常量硬编码到业务逻辑里。我更愿意把 AI 看作一个"执行力超强的初级工程师",而不是"靠谱的资深专家"。初级工程师写完代码需要 review,AI 写代码也一样。
> 提示:AI 不会主动告诉你"这里我不确定"或者"这个资源不存在"。它在信息不足时,倾向于编一个看起来合理的值继续往下走。所有不被你确认过的东西,都要默认它可能是错的。5.3 哪类代码适合留给AI,哪类必须人肉把关
经过这次实践,我给 AI 和人的分工划了一条比较清晰的线。适合让 AI 去做的:模板化 UI 组件、工具函数、状态机骨架、Canvas 绘制基础代码、常见算法实现。这些代码模式固定、标准明确,AI 生成的效率极高,而且质量稳定。
必须人来把关的:跟游戏手感、体验细节、性能边界相关的逻辑;关键状态机的转移顺序;任何涉及"你打算怎么发展这个代码"的架构决策。举例来说,"碰撞后敌机消失"可以让 AI 写,但"碰撞后要不要加一个 0.1 秒的爆炸动画来增强手感"这个问题就得人拍板。再比如,"暂停时要隐藏 Canvas 画面"这件事,AI 不会主动想到,因为它没有玩过你这个游戏。
我做了一张简单的分工对照表,方便你参考:
| 任务类型 | 适合AI | 必须人把关 |
|---|---|---|
| 组件模板和样式骨架 | 是 | 否 |
| 矩形碰撞检测 | 是,但需测试锁定 | 坐标系约定 |
| 游戏状态机框架 | 是 | 状态转移的合理性 |
| 移动手感与惯性参数 | 否 | 是,需要实际游玩调整 |
| 性能优化方案 | 否,AI给思路 | 是,瓶颈定位靠人 |
| 资源引用与加载 | 否 | 是,AI会编造路径 |
判断标准其实很朴素:凡是"跑起来能看见对错"的,AI 能帮你做;凡是"要玩过才知道好不好"的,必须自己上手。这次飞机大战项目里,所有 AI 觉得"差不多了"的地方,在我实际玩过之后都还有优化的空间,这一点体会特别深。
项目收尾那天,我又多玩了好几局,反复调整了敌机的生成频率和子弹的射速,终于把它调到"有点难但还能打"的状态。经历了这次完整的 Vibe Coding 实践,我现在每天写代码的流程已经变了:AI 负责把每个模块的骨架搭出来,我负责把每个模块的细节和手感调到满意为止。我也不再强求 AI 一句话写出完美代码,而是养成了一个习惯——每轮对话结束前,我会让它总结一下"刚才我用了哪些坐标约定、状态命名和常量配置",再把这些摘要贴进项目里的约定文件。这样即使 AI 的上下文会话关闭,下一个新会话还能快速接上。这个做法不是什么高大上的技巧,但如果你也打算用 Vibe Coding 做一个完整的小项目,它大概率能帮你省下好几个小时的调 bug 时间。