Vibe Coding 多人游戏(十六)—— Vibe Coding 经验和教训合集

经验 1:测试金字塔不能倒——尤其集成测试

     /\
    /E2E\          ← 手动触发,不自动(有时)
   /集成 \
  /测试   \        ← BDD + Cucumber,不 mock
 /______\
/ 单元测试\       ← BDD Features(.feature \+ steps),AI 自动写、自动跑
/________\

最常见的错误:AI 写集成测试时用 mock 替代实际代码。

我遇到过好几次这种情况:AI 写了一个集成测试,其中用 jest.mock('some-module', () => ({ ... })) 模拟了整个依赖。跑一下绿了。但部署到生产环境,bug 还在。

为什么?因为 mock 出来的依赖不包含 bug。

最典型的案例是前文提到的 Object.keys(Immutable.Map()) 永远返回 5 个内部属性的 bug。如果集成测试用 mock 替代了真正的 Immutable.Map,测试永远发现不了问题。因为 mock 返回的是一个普通对象 { size: 0 }Object.keys 返回 ['size'],长度大于 0——测试通过。但生产环境用的是真正的 Immutable.MapObject.keys 返回 5 个属性——条件永远为 true。

原则:集成测试不 mock。 真实启动服务、真实连接 WebSocket、真实发送命令。测试必须因 bug 真实失败才能锁定。

对于单元测试,mock 可以——但只 mock IO 层(网络、文件系统)。业务逻辑层必须走真实路径。


经验 2:Skill 化 > 文档化——文档没人看,Skill AI 看

传统知识管理是写 Wiki。写完后有谁会看?

文档化:/docs/deploy.md — 15 步部署指南
→ "我看过" "我看完记不住" "我懒得看"

Skill 化:

Skill 化:gts-deploy SKILL.md — AI 自动执行 15 步
→ AI 看懂了,直接照着跑完了

唯一有效的知识管理形式是"能自动执行的"。

我举个例子。GTS-Play 部署到腾讯云 SCF 有 15 个步骤:打包、zip、上传、设置环境变量、验证……如果写成 Word 文档,新人(包括 AI)看着 15 步,每一步都可能出岔子。但写成 Skill 后,AI 自动执行,0 错误——而且每一步都加了防护(比如 zip-chmod.ps1 修复 Windows 压缩不保留 Unix 执行权限的问题)。

Skill 和文档不一样的地方在于:

  • 文档是给人看的——人有抽象能力,可以说"然后上传到 SCF",人会知道怎么操作
  • Skill 是给 AI 读的——AI 没有抽象能力,必须写清楚"用 node scripts/deploy-scf.js room1 命令上传,连接超时设 30 秒,重试 3 次"

2026 年 6 月 20 日我建了第一批 Skill,到 7 月初已经迭代到第 16 个。每个 Skill 的迭代路径都一样:发现一个坑 → 记到决策记录 → 更新 Skill → 下次自动避开。记忆(决策记录)+ Skill(可执行流程)形成了一条双循环:记忆告诉 AI "你以前遇到过这个问题",Skill 告诉 AI "你应该怎么做"。


经验 3:一个 bug 一个集成测试——锁定它

每次发现新 bug,先写集成测试复现,再修。这是 TDD 的纪律之一。

但为什么很多人不这么做?因为手动写测试麻烦。 但在 Vibe Coding 模式下,写测试是 AI 做——所以这个纪律执行起来零成本。

好处有三:

1. 修完后确保不会再犯。

2026 年 6 月 28 日集中修复了一批多人联机 bug,都是跟"房间准备"流程相关的。我们为每个 bug 写了测试:handleReadyStateAfterToggle 在满员全部 ready 时启动倒计时、startAutoReady 超时触发后正确 toggleReady……这些测试在今天依然在跑,每次部署前 BDD 全量测试都会把它们过一遍。

2. 下次重构时,测试会告诉你"这个逻辑不能改"。

最近一次重构 MultiplayerManager.tslistenGameState 的条件判断时,就是靠现有的集成测试发现 regression:改完后原来通过的场景"GameState with host pushes snapshot"红掉了,才知道新代码破坏了 host 的行为。

3. 新成员加入时,测试给安全感。

如果我现在招一个人来做 GTS-Play,我会让他先跑一遍 BDD 测试。测试全绿意味着环境配置好了,代码可以正常编译运行。测试里的场景就是他了解系统全部流程的入口。

GTS-Play 的 BDD 测试结构分为三层:

前端集成测试(9 个 features)

packages/frontend/test/features/integration/
├── camera-orbitcontrols.feature       ← 相机轨道控制
├── camera-position.feature            ← 相机位置同步
├── game-end-reset-integration.feature ← 游戏结束重置
├── gamestate-fix-integration.feature   ← GameState 修复
├── multiplayer-error-handling.feature ← 多人错误处理
├── obb-integration.feature            ← OBB 碰撞检测
├── reentry-hud.feature                ← HUD 场景重入
├── room-kick-ready.feature            ← 踢人和准备
└── victory-modal-display.feature      ← 胜利弹窗

服务端集成测试(18+ 个 features)

packages/room-service/test/features/    ← 13 个
├── game.feature / game-end-reset.feature / game-over.feature / game-reset.feature
├── game-state-perf.feature / game-timeout.feature
├── player-ready.feature / player-ready-flow.feature / player-ready-set.feature
├── player-defeated.feature / kick-player.feature / member-exit.feature
├── host-transfer.feature / clear-room.feature / zombie-timer.feature

packages/match-service/test/features/ ← 5 个
├── find-room.feature / empty-room-cleanup.feature
├── health-check.feature / room-admin.feature
└── room-list-fault-tolerance.feature

每个 feature 都不是凭空造的——每个都对应一个真实踩过的坑:ESM 模块加载问题、SCF 部署后 API 不可达、WebSocket 断连重连、僵尸房间清理、Host Transfer 竞态条件……测试列表就是系统的踩坑史。


经验 4:OpenCode 的"体验式反馈"优于"技术 spec"

给 AI 的 Brief 里,写"用状态同步"不如写具体的体验问题。

❌ Brief 说:实现状态同步

✅ Brief 说:当前问题是帧同步下 A 和 B 看到的对方位置不一致,
原因在浮点数精度各平台不同。目标是服务端下发 position,
客户端只做展示和插值

前者是"告诉 AI 做什么技术选型",后者是"告诉 AI 为什么要做这件事"。

差在哪?AI 理解了"为什么"之后,做出的技术选择远比预设的"技术 spec"好。

一个真实案例:我们原本规划的是"帧同步",按照传统的多人游戏做法。但当我在 Brief 里描述了具体的体验问题(玩家 A 移动后,玩家 B 看到的是 0.5 秒后的位置,而且有跳跃感),OpenCode 主动提出"为什么不考虑状态同步?帧同步对网络延迟太敏感了"。

如果我在 Brief 里直接写了"实现帧同步",OpenCode 就会直接去做帧同步,不会有这个建议。但实际上状态同步更适合我们的场景(小房间、高频位置更新、延迟敏感)。

这就是为什么我在所有 Skill 里都要求:Brief 必须包含"体验问题"段落,而不仅仅是技术 spec。 OpenCode 的编码能力强,但设计能力有限——它需要你告诉它"问题是什么",而不是"解决方案是什么"。


教训 1:不要太早抽象——AI 需要清晰的边界

遇见第一个循环 → 抽象成 forEach
遇见第二个循环 → 抽象成 reduce
遇见第三个循环 → 抽象成一个 5 层高阶函数
   → 然后 AI 看不懂了

这是真实发生过的。我在 packages/logic/src/util.ts 里写了一个 4 层柯里化的工具函数。AI 在生成新代码时调用了这个函数,但它搞混了参数顺序——不是 AI 笨,是人类也要花 30 秒才看得懂这个函数签名。

AI 协作环境下,过早抽象是最大的敌人。 保持简单直接,让 AI 能理解。

但不是所有抽象都要避免。有一条例外:隔离层(开闭原则)需要提前做。

为什么?因为你不能在 1000 行代码里加一个功能时不设边界。GTS-Play 多人联机的错误处理就是一个例子:多人和单机共用的 ErrorHandle.tsx,但因为多人需要额外的逻辑(重连按钮、清理 WS 连接),如果直接改原有组件,单机逻辑会被波及。所以我们建了独立的 MultiplayerErrorHandle.tsx,完全隔离——开闭原则不是为了代码优雅,是为了让 AI 改 A 时不会破坏 B。

我的规则很简单:功能性抽象不要做太深(AI 看不懂),结构性隔离必须做(AI 才不怕改)。


教训 2:别让 AI 改 node_modules——但有个特例

追了一个月的 bug——不是 AI 改 node_modules 本身的问题,而是 AI 反复陷入 node_modules 的死循环。

故事是这样的:某个版本的 @rescript/runtime 和我们项目的类型定义不匹配。AI 每次编译报错,不看 tsconfig.json 的配置,而是直接去改 node_modules/@rescript/runtime/package.json。然后把 "type": "module" 改成 "type": "commonjs"——看起来好了,编译通过了。但下次 yarn install 后 node_modules 重置,又报错。AI 又去改 node_modules。循环了 3 次。

但这里有个更本质的问题:为什么 AI 需要改 node_modules?因为这是一个 monorepo

Monorepo 下,当某个 package 的依赖不匹配时,正确的流程不是修改 node_modules,而是改 根目录的 package.json,然后在项目根目录执行 yarn bootstrap(等同于 yarn install),让 Lerna 重新链接依赖。

比如遇到 peer dependency 版本冲突:

❌ AI 的做法:
  直接改 packages/room-service/node_modules/xxx/package.json 里的版本号
  → 下次 yarn install 重置,又报错
  → AI 又去改,无限循环

✅ 正确的做法:
改根目录 package.json 的 resolutions 或 devDependencies
→ 执行 yarn bootstrap 重新安装
→ 所有 package 的 node_modules 同步更新

AI 不理解 "monorepo 的依赖管理是全局的"——它看到的是单个 package.json,不知道改了根目录才管用。

原则:依赖问题改根 package.json → yarn bootstrap,不改 node_modules。


教训 3:E2E 前必须重启服务——状态残留的幽灵

多人游戏的状态会残留。你可能觉得这是常识,但我自己(和 AI)都踩了这个坑好几次。

场景:你修了一个 bug,AI 跑 E2E 测试。E2E 测试启动 room-service 和 match-service——但是是重启还是复用之前跑着的

如果不重启,旧的 tick loop 还在跑,旧的 WebSocket 连接还在,旧的内存状态还在。然后 AI 启动了另一个 instance,两个 instance 抢同一个端口——测试全乱套。

修复方案: 每次 E2E 前先杀旧服务进程(用精确的 netstat -ano | Select-String ":4003 .*LISTENING" 匹配),再启动新进程。启动后轮询端口确认就绪,再跑测试。

这个步骤也写了测试和 Skill,所以不会漏。唯一一次漏掉的原因是 AI 偷懒了——它觉得"服务已经在跑,应该没问题",结果测试崩了。

原则:部署和 E2E 之间必须有一个"清场"操作——杀旧进程 → 重启 → 端口确认 → 跑测试。


教训 4:Tool Loop 时间过长会炸上下文——子 session 是解药

这个坑踩了两次,两次都让 AI 丧失了一整段对话的所有上下文。

第一次是让 AI 跨包重构。它一口气读了 15 个文件,每个文件几十 KB,累计上下文超过 200k token 的极限。然后 AI 突然失忆——刚才读过的文件内容全忘了,开始在对话里回复"我还没读这个文件"。

第二次是 E2E 自动化场景。连续跑了 3 次 E2E,每次 5 分钟,每次跑完 AI 都往上下文里追加了大量日志输出。到第 4 次时上下文爆炸,AI 说"无法继续,因为 context overflow"。

解决方案:大任务拆成子 session。

在 OpenClaw 里,我们引入了一个规则:如果单个 AI 任务的 tool call 超过 30 轮,必须输出进度摘要,让 compaction 有机会压缩上下文。对于更大规模的任务(跨包重构、读取 10+ 个文件),要用 sessions_spawn 创建独立的子 session——每个子 session 有自己的上下文,爆了也只影响自己,不影响主 session。

规则现在写在 MEMORY.md 里:

MEMORY.md #29:单回合 >30 轮 tool call 时输出进度文本,让 compaction 能压缩。 MEMORY.md #30:跨文件重构/大规模读代码用 sessions_spawn 拆。

有了这两个规则后,再也没有 context overflow 的问题了。


经验 5:Goal 量化比模型选择更重要

问题:帮我修复支付系统的并发问题
→ AI 修了 15 轮还没完,因为每次修完都引入新 bug
→ 没有完成标准:通过单测就算修好了?性能不退步?安全无漏洞?

问题:确保测试全部通过,latency_p99 < 100ms,无新增 lint error
→ AI 跑了 3 轮就收敛了,因为明确了什么时候该停

一个血淋淋的教训:自然语言 Goal 让 AI 反复改同一个逻辑。

有段时间,我让 AI 修一个多人联机的同步 bug,说的是"修复玩家移动时位置不同步"。AI 修了一轮,单测过了,它说"修复完成"。但我玩游戏发现位置还是跳。于是我改成:

目标:玩家移动时位置误差 < 0.1 单位
完成标准:
- 集成测试 3 个场景全通过(直线、转弯、停止)
- 移动插值 60fps 不卡顿
- 无新增 tsc 编译警告

AI 只用了 2 轮就达到了。

原则:Brief 必须包含量化的完成标准,否则 AI 永远不知道什么时候该停。

经验 6:状态外置 > 对话历史

对话历史:AI 记得 "上次我修过了那个 bug"
→ 上下文溢出后忘了 → 重头开始修

仓库状态:MEMORY.md 里有 ADR 记录,锚点词能搜到
→ 新对话也能找到 → 复用上次的修复逻辑

"Agent 会忘,仓库不会忘"——这是经过多次惨痛教训后才真正内化的原则。

最开始的记忆都在对话上下文里。AI 说"我记得",我也信了。直到某次 context overflow 后,AI 完全忘了前 3 次修复都修了什么,重新开始分析,浪费时间。

现在所有关键信息都外置到仓库:

  • 决策记录 → 笔记/决策记录/
  • 项目知识 → MEMORY.md + memory/*
  • 每日日志 → 笔记/daily/
  • 技术方案 → 笔记/方案/
  • 项目文档 → 笔记/项目文档/

AI 每次启动新对话后,第一件事就是搜索锚点词,从仓库里读取记忆。而不是依赖"我记得上次我做了什么"。

原则:不要让 AI 在对话上下文里记东西——把它写进仓库。

教训 5:Maker/Checker 同模型的隐患

这是最新的一个发现。我们的 Maker 和 Checker 都是 DeepSeek Flash——同一个模型。

结果是什么?AI 修完 bug 说"测试全通过,完成"。我自己跑了一次游戏,发现 HUD 还是有问题。但 AI 的 check 过程里明明测了那个场景。

后来发现原因:Checker 和 Maker 用的是同一模型,Checker 默认信任 Maker 的实现,不会深挖。如果 Checker 看到测试签名、断言逻辑和 Maker 修的一致,就认为"没问题"——但两个可能同时忽略了同一个边界情况。

正确的做法是 maker 和 checker 用不同模型。 比如 Maker 用 DeepSeek Flash(快、便宜、够用),Checker 用更强的模型(比如 DeepSeek Pro 或 Claude)来真正审视 Maker 的输出。

这听起来奢侈,但其实只在关键节点用强模型做 check,总体 token 增量不超过 10%。相当于为一个项目多花 10% 的费用,买一个"独立审计"的保障。

原则:Maker 和 Checker 不要同一模型——独立审计才有价值。


全体协作总结

角色 做什么 别做什么
定义目标、做决策、审查结果 写模板代码、手动跑测试、搬运代码
OpenClaw 调度、写 Brief、检查结果、管理记忆 写代码、部署
OpenCode 写代码、跑测试、修 bug 改 node_modules、删文件、改运行时配置
Skill 告诉 AI 怎么做(可执行 SOP) 描述"为什么"(那是决策记录的事)
测试 锁定行为、防止 regression 模拟假逻辑、mock 真实路径

Vibe Coding 到这一步,已经不是一个"工具链"了,而是一个开发体系——有角色、有流程、有治理、有反馈环。

如果说有什么终极感悟,就是:

Vibe Coding 不是让 AI 替你写代码。Vibe Coding 是让你用系统化的方式指挥 AI 开发。 不是"说说而已",而是架构:Skill 定义流程,编码规则定义边界,测试定义质量,记忆定义知识的复用——四者缺一不可。


下期开始进入知识管理系列——从编码规则体系开始,看看为什么"怎么告诉 AI 该怎么做"本身就是一个值得体系的工程。

下一篇:Vibe Coding 多人游戏(十七)—— 三层编码规则体系 + agent-context.md

posted @ 2026-07-08 11:38  杨元超  阅读(7)  评论(0)    收藏  举报