上周我做了一件以前想都不敢想的事:给 Claude Code 丢了一段不到 100 字的需求,然后就看着它自己拉起 33 个代理,各干各的,花了一个下午,真的给我交出来一个能跑、能玩、有音效、有计分的 3D 动作游戏。全程零素材、零代码起步,没有从网上下载任何模型,所有场景、角色、动画脚本和 UI 全是从空项目里"长"出来的。
这篇文章就来拆一拆这次"失控式"开发的全过程:为什么会让 AI 自己派 33 个代理,需求描述到底该怎么写,代理之间又是怎么协作不打架的,以及我踩过的那些坑。如果你对 AI 辅助编程、多代理工作流或者纯前端 3D 游戏实现感兴趣,这篇应该能给你一些真正可复用的经验。
1. 先搞清楚:这 33 个代理到底在干什么
1.1 多代理模式不是"陪你聊天写代码"那套
用过 AI 编程工具的人都知道,常规模式是你一句我一句,AI 生成代码,你看完反馈,它再改。这种模式适合改 bug、写函数、补注释,但真要做一个完整的游戏,那就太难受了:上下文一长,AI 就会忘掉前面的设计,改着改着风格跑偏,甚至开始自己推翻自己。
Claude Code 的多代理工作流解决的就是这个问题。它的思路是:既然一个 AI 能干活,那把任务拆成小块,让多个 AI 并行干活,互相不共享对话历史,只共享文件系统和约定好的接口。我在这次实践中用到的 33 个代理,就是这么来的——每个代理负责一个明确的开发领域,输出到指定目录,最后由几个集成代理把它们拼起来。
听起来很玄乎,其实类比一下就懂了:这相当于你临时开了一家 33 人的游戏工作室。有人专门搭舞台,有人做角色,有人写动作逻辑,有人管界面,有人做音效,还有人负责测试和验收。你不需要给他们开会,只需要开工前把需求文档写清楚,他们自己就知道该干嘛。
1.2 33 这个数字是怎么来的
先说个实话:33 不是预先设计好的,而是任务拆完以后自然数出来的。我最初只是想把游戏分成"场景、角色、战斗、UI、音效、测试"六大块,结果每个大块继续往下拆,就变成了下面这张表:
| 任务域 | 代理数量 | 主要交付物 |
|---|---|---|
| 规划与架构 | 2 | 技术方案、目录结构、接口约定 |
| 场景生成 | 6 | 竞技场模型、地面纹理、天空盒、光照方案 |
| 角色控制 | 5 | 角色控制器、动画状态机、输入映射 |
| 战斗系统 | 5 | 攻击判定、敌人 AI、伤害数值 |
| UI 与交互 | 4 | 开始界面、血条、计分板、菜单 |
| 音效 | 3 | 打击音效、背景音乐、脚步声音频合成 |
| 测试与修复 | 6 | 自动冒烟测试、边界检查、性能修复 |
| 文档与交付 | 2 | README、运行说明、构建脚本 |
这 8 个任务域一共就是 2 + 6 + 5 + 5 + 4 + 3 + 6 + 2 = 33 个代理。每个代理都只接自己那一亩三分地的任务,任务描述精确到"在哪个文件里、输出什么接口、验收标准是什么"。这样一来,每个代理的上下文都非常干净,不会出现一个建模的代理还要关心 UI 布局这种离谱事。
2. 核心需求拆解:怎么把一句话变成 33 个任务
2.1 一段合格的"需求"不是靠运气
很多人用 AI 写游戏失败,问题往往不在 AI 能力,而在需求写得太含糊。你写"做一个 3D 动作游戏",它真就给你做一个最基础的 demo,然后你不知道该让它继续干嘛。而这次我的需求文本虽然不到 100 字,但信息密度很高,四个关键要素全都有:
第一是玩法目标。明确是"第三人称动作游戏,在封闭竞技场中移动、跳跃、攻击,击败不断刷新的敌人"。这就锁定了核心循环,不会跑偏成跑酷或者解谜。
第二是平台约束。我写了"纯前端实现,用可过程化生成的 3D 模型,无需外部素材"。这句话非常重要,它直接决定了代理不用花时间去网上找模型,而是老老实实用代码生成几何体。
第三是交互方式。WASD 移动、空格跳跃、鼠标左键攻击、Shift 闪避,所有操作映射写得清清楚楚,输入系统就知道该接哪些键位。
第四是交付标准。我明确写了"打开 index.html 即可运行,包含开始界面、战斗逻辑、计分、音效",这让测试代理有了验收清单,也让其他代理知道交付物的完成定义是什么。
当时给 Claude Code 的原话大概是这样的:
做一个可在浏览器运行的 3D 动作游戏,第三人称视角。 核心玩法:角色在封闭竞技场中移动、跳跃、攻击,击败不断刷新的敌人。 纯前端实现,使用可过程化生成的 3D 模型,无需外部素材。 交互按键:WASD 移动,空格跳跃,鼠标左键攻击,Shift 闪避。 交付标准:打开 index.html 即可运行,包含开始界面、战斗逻辑、计分、音效。2.2 从需求到任务列表的拆解方法
Claude Code 自己有一套任务拆解的逻辑,但我观察下来,真正起作用的是"按依赖关系排优先级"。
第一步是按领域切蛋糕。游戏是最典型的跨领域项目:3D 渲染、角色动画、战斗数值、UI 交互、音频,每个领域都可以独立开发。切蛋糕的原则是"接口最小化、上下文隔离",比如场景代理只需要知道"竞技场中心在坐标原点、尺寸是 40x40",它不需要知道玩家怎么攻击。
第二步是排依赖。场景生成和角色建模是可以并行开工的,但战斗系统必须等角色控制器的接口定下来之后才能真正联调。所以我把任务排成了三层:第一层是架构规划和场景生成,第二层是角色控制和战斗系统,第三层是 UI、音效和最后的集成测试。代理们按层级分批次醒来,每一批只依赖上一批已产出的接口文件。
第三步是定验收标准。我要求每个代理在动手之前先输出一份"实现计划",里面写清楚交付文件和验收条件。这一步千万不能省,因为代理没有全局视角,你不给它标准,它就按自己的想象力发挥,后面集成的时候就等着头大吧。
3. 实操过程:AI 从零生成 3D 动作游戏的核心环节
3.1 技术栈选型:为什么选了浏览器方案
在动手之前,架构代理先做了一次技术选型。当时考虑了两种方向:一是用本地游戏引擎,二是用浏览器 WebGL 方案。最终选了浏览器方案,原因有三个。
第一是零素材约束。本地引擎往往需要美术资源做支撑,而浏览器 3D 方案配合过程化建模可以完全用代码生成几何体、材质和光照,不需要任何外部文件。第二是验证成本低。代理写完代码直接跑浏览器环境,出问题就能在本地看到,不需要复杂的打包分发流程。第三是交付简单。一个 HTML 文件加上几个 JS 文件就能跑,玩家拿到就能玩,没有平台依赖。
具体实现上用的是基于 WebGL 的三维渲染库,搭配一个轻量级的开发服务器。场景、角色、光照全由代码生成,材质用标准材质加基础灯光,尽量少用高成本贴图。
3.2 场景与模型:让代码自己造一个竞技场
场景代理的任务是把竞技场从无到有造出来。它没有用外部模型,而是写了一个场景生成脚本,用基础几何体拼出地面、围墙和柱子。核心思路是过程化生成——通过函数参数控制竞技场的大小、障碍物位置、地面颜色,而不是手工摆模型。
这里贴一段它早期生成的示意代码(不是最终版,但思路完整):
// 生成竞技场地面与围墙示意 function buildArena(scene) { const ground = new Mesh( new BoxGeometry(40, 1, 40), new StandardMaterial({ color: 0x3d6b35 }) ); ground.position.y = -0.5; scene.add(ground); // 四面围墙,z 正负和 x 正负 const wallMat = new StandardMaterial({ color: 0x8a7a5c }); const wallPositions = [ [0, 1.5, -20, 40, 3, 1], [0, 1.5, 20, 40, 3, 1], [-20, 1.5, 0, 1, 3, 40], [20, 1.5, 0, 1, 3, 40] ]; wallPositions.forEach(([x, y, z, w, h, d]) => { const wall = new Mesh(new BoxGeometry(w, h, d), wallMat); wall.position.set(x, y, z); scene.add(wall); }); // 随机障碍柱 for (let i = 0; i < 12; i++) { const pillar = new Mesh( new CylinderGeometry(0.5, 0.6, 2.5, 8), new StandardMaterial({ color: 0x666666 }) ); pillar.position.set( (Math.random() - 0.5) * 30, 1.25, (Math.random() - 0.5) * 30 ); scene.add(pillar); } }这种做法的好处是:每个柱子的位置、颜色、数量都是参数,想要调整场景复杂度,改一个数字就行,根本不需要人工建模。缺点是视觉上偏几何、偏抽象,但对一个零素材游戏来说,完全够用了。
3.3 角色控制与动画状态机:动作游戏的核心手感
动作游戏最重要的就是角色操作的即时反馈。角色代理做的事情本质上是一个输入系统加一个动画状态机。输入系统把键盘和鼠标事件翻译成"移动、跳跃、攻击、闪避"四类意图,动画状态机根据当前状态决定角色播放什么动作。
我让代理在实现时特别注意了一点:动作切换不能生硬。比如玩家在空中按下攻击键,正常的动作游戏不会直接切到攻击动画,而是等到落地后再执行;又比如连续按攻击键,要判断上一次攻击是否已经打完。这块的逻辑其实不难,但状态划分不清晰就会让手感发闷。
示意代码如下:
const states = { IDLE: 'idle', RUN: 'run', JUMP: 'jump', ATTACK: 'attack', DASH: 'dash' }; // 简单状态机:不能从 jump 直接切 attack,落地才允许 function updateState(player, input, dt) { switch (player.state) { case states.IDLE: if (input.move) player.state = states.RUN; if (input.jump) player.state = states.JUMP; break; case states.RUN: if (!input.move) player.state = states.IDLE; if (input.jump) player.state = states.JUMP; if (input.attack) player.state = states.ATTACK; break; case states.JUMP: if (player.isGrounded && input.attack) player.state = states.ATTACK; if (player.isGrounded && !input.move) player.state = states.IDLE; break; case states.ATTACK: if (player.animFinished) player.state = states.IDLE; break; } }这里最核心的思考是"攻击不等于秒杀一切"。为了让游戏有一点挑战性,角色攻击有前摇、后摇和冷却时间,攻击范围也不是正前方 360 度,而是一个扇形判定。这些数值是战斗系统代理后续调出来的,但它依赖的接口在角色代理这里就已经定义清楚了。
3.4 物理与碰撞:不能让角色穿墙而过
3D 动作游戏里最影响体验的物理问题就两个:一个是角色不能穿透地面和墙壁,一个是攻击得能打中敌人。这次项目里没有用重型物理引擎,而是用了很轻量的球形碰撞判定。
每个实体(玩家、敌人、柱子)在更新位置前,先做碰撞检测。检测的方式也很传统:计算两个实体在水平面上的距离,如果距离小于两者半径之和,就发生了碰撞,然后把实体从碰撞位置推回去。垂直方向只用重力判断是否落地。
function horizontalCollision(a, b) { const dx = a.position.x - b.position.x; const dz = a.position.z - b.position.z; const dist = Math.sqrt(dx * dx + dz * dz); return dist < (a.radius + b.radius); }这样的设计相比物理引擎要"糙"一些,但对于封闭竞技场这种简单场景,计算稳定、可预测性强。物理引擎反而是劣势,因为代理很可能调不稳参数,出现角色莫名其妙弹飞的情况。实际测试下来,这套简单判定让"碰撞体在墙壁边缘滑动"的效果非常顺滑,这也是我建议做同类小游戏时优先考虑的方式。
3.5 代理之间怎么协作而不互相覆盖
33 个代理同时开工,最大的风险是文件冲突。我这次采用的方式是"物理隔离 + 接口约定"。
物理隔离是指每个代理分配独立的输出目录,比如场景代理写src/world/,角色代理写src/player/,战斗代理写src/combat/,UI 代理写src/ui/。各目录之间不互相引用具体实现,只引用一个共享的类型定义文件src/shared/types.js。
接口约定的核心就是types.js,比如玩家对象暴露position、state、attack(),敌人对象暴露hp、takeDamage(amount)。UI 代理只需要知道从player.hp和score读取显示值,不需要知道这些值是怎么算出来的。这样一来,所有代理都围绕一个"接口字典"编码,最后集成代理的工作就很轻松了。
提示:如果你自己也想这么玩,一定要在建项目初期就把接口文件定义好,否则后面改接口的代价是灾难级的。不要相信"AI 自己会协调"这种话,它更擅长的是按你的规则执行。
3.6 无人值守的自动联调与反馈闭环
代理们把东西写完只是第一步,真正难的是集成和联调。我让一个专门的集成代理负责把各子目录的代码汇总,启动本地服务器,然后跑一遍冒烟测试。冒烟测试的内容很基础:页面能不能打开、角色能不能移动、攻击有没有报错、得分会不会累加。
如果测试挂了,集成代理并不自己修代码,而是把报错信息攒成一个调试包,丢给对应的子任务代理去改。这个"测试发现问题 → 反馈给写代码的代理 → 修完再回来跑测试"的闭环是整个流程里最耗时的一环,但也是它让游戏从一个"能跑但很脆"的状态,慢慢变成一个 "玩了十分钟没崩" 的状态。
4. 遇到的那些坑和排查实录
4.1 接口对不上:代理们各写各的签名
第一个大坑出现在第三天(其实就锻炼了一个下午,但感觉像过了几天)。集成代理在汇总时发现,角色代理暴露的函数是playerTakeDamage(amount),而战斗代理调用的却是player.takeDamage(amount)。两边就差一个括号里的前缀,结果直接白屏,整个游戏跑不起来。
排查下来,问题出在接口定义文件更新不及时。角色代理改了函数名,但没有同步给战斗代理。后来我给所有代理加了一条规则:"改接口之前,先改src/shared/types.js,再改自己的实现,最后在完成报告里写清楚变更内容。"从此以后类似问题基本绝迹。
4.2 上下文泛滥:代理开始"胡说八道"
第二个坑更隐蔽。开发到后半程,有一个代理开始输出一些非常奇怪的东西:明明任务是生成地面纹理,它却开始写角色攻击的代码,而且写得逻辑混乱、变量命名奇怪。我一开始以为是 Claude Code 的稳定性问题,后来仔细观察才发现,是这个代理的上下文池里积累了太多历史内容,包括之前其他任务残留的代码片段,导致它分不清自己到底该干嘛。
解决办法很简单:重置对话上下文,只让它重新读取自己的任务描述文件和当前目录下的文件,不再保留冗长的执行历史。对于生成型的任务,代理的上下文越精简,输出越符合预期,这一点在多代理场景下特别重要。
4.3 3D 资源量失控:一个文件生成了一棵"多边形圣诞树"
还有一个典型问题是场景代理生成的障碍物模型面数过高。本来圆柱体用 8 段就够,它为了好看给圆柱体加了 128 段。场景里面有几十个这样的物件,一起渲染时帧率直接掉到个位数。我看了一下生成树,单是场景几何体文件的大小就已经超过预期几十倍。
最后让场景代理加了一道优化约束:所有圆柱类物体段数不超过 12,所有模型顶点数不超过 2000,场景中同屏物件总数不超过 100。同时把重复的材质合并,减少渲染批次。优化之后帧率稳定到 60。这也算是个教训:让 AI 自由发挥视觉细节之前,一定要先设定性能预算。
4.4 代理私自给自己加需求
这个坑最搞笑。有一个 UI 代理,在做计分板的时候,大概是觉得默认字体太丑,自己从系统里调了一大堆字体库,还引入了一整套布局框架,最后页面加载时间暴增。动力是好的,但它完全忘了"文件体积最小化"这条约束。
从那以后,我给每个代理的任务描述末尾都加了一句话:"只实现描述中要求的功能,不添加任何额外特性,不引入未要求的依赖。"这句话带来的效果立竿见影。你也可以理解为:AI 是个非常有想法的新人,你要用明确的边界去约束它。
4.5 常见问题速查表
| 症状 | 可能原因 | 解决方法 |
|---|---|---|
| 集成后白屏 | 接口签名不一致、循环依赖 | 统一 types.js,检查引用路径 |
| 代理输出跑偏 | 上下文太杂、任务描述模糊 | 重置上下文,重读任务文件 |
| 帧率骤降 | 模型面数过高、渲染批次过多 | 设置面数上限,合并材质 |
| 功能突然失效 | 代理私自改动了旧代码 | 任务描述约束"不做额外修改" |
| 文件冲突覆盖 | 多个代理写同一文件 | 目录隔离,接口文件单独管理 |
5. 写在最后:多代理工作流到底适合谁用
这次用 33 个代理做 3D 动作游戏的经历,让我对 AI 编程的认知刷新了不少。
最直观的感受是:AI 编程的下一个阶段已经不在于"它能写多难的代码",而在于"你怎么把一个大问题切碎、再交给多个 AI 并行处理"。这次的 33 个代理,单拿出来任何一个个体的水平都不算惊人的强,但合在一起,它们像一家小型外包工作室,互相隔离、各司其职、按接口对接,最终产出的是一个独立完整的游戏。
我个人在实际操作中体会到的是,多代理工作流最适合两类人。一类是像我这样手头有完整构思、但缺时间和执行力的开发者,你可以把"编代码"这件事外包给 AI 代理,自己当产品经理和架构师。另一类是刚入门的游戏开发爱好者,你想快速看到自己的想法变成可玩的东西,而不是花几天搭环境、调材质、学建模,多代理可以帮你直接跨过前期最劝退的阶段。
但如果你本身对要做的东西完全没有概念,那这套工作流也帮不上忙。AI 代理不会替你思考"游戏好不好玩",它只会高效地实现你写下来的需求。需求模糊,它就只能给你一个平庸的作品;需求精确,它就能给你超出预期的惊喜。
最后再分享一个小技巧:这次跑完以后,我把那段 100 字的需求和 33 个任务清单原封不动存成了一个模板。下次想做其他项目,不管是网页工具还是小程序,我只需要替换"玩法描述"和"交互方式"两行,就能让 Claude Code 按同样的架构重新拉起一支代理团队。这种可复用的流程模板,比这次游戏本身更值钱——毕竟它证明了一件事:只要拆得足够清晰,AI 团队的产出是真的能"失控"但又不"脱轨"的。