今天几乎所有 AI Agent 都住在 chat box 里。你输入,它响应,你评估输出,交互结束,Agent 消失。
我做了个不一样的尝试:让 Agent 住在一座你亲手搭建的 3D 小镇里。你给它设计外形、写人格、规划街道。它在小镇里有自己的日常——逛街、坐咖啡厅、遇到邻居聊天。你给它分配任务时,它们集结到广场、列队行军到办公室、各自坐到工位写代码、完成后放烟花庆祝、然后走回小镇继续生活。
这不是一个 Agent 仪表盘,是一个 Agent 的栖息地。项目叫 Agentshire,开源,基于 [OpenClaw] 框架构建,每一个OpenlcawAgent都是一个3D居民。
先眼见为实
1. 给Agent发布任务
2. Agent交付作品
3. 角色UGC
4. 小镇地图UGC
这篇文章不讲怎么用,主要讲怎么造的——以及造的过程中,三次被迫推翻重来的故事。
为什么要造小镇
Chat 是高效的交互模式,但它是黑盒。你只看到输入和输出,看不到过程。更关键的是,chat 把人锁在了"评判者"角色——每次交互你都在问"这个输出够好吗"。
小镇提供了另一种认知姿态。你不是在评判输出,而是在观察行为。信任不是"验证出来的",而是"相处出来的"。当 Agent 在你没交代任务时还在小镇里散步、跟邻居聊天,用户开始说"我的 Agent"而不是"那个 Agent"。
但真正让人在意的,不是"能看到它",而是"这个世界是我搭的"。UGC 是核心——用户自己建镇、造角色、写灵魂文件定义人格。没有 UGC 的小镇只是好看的 demo,有了 UGC 才有"这是我的小镇"的拥有感。
Chat 和小镇不是替代关系。Chat 高效,适合任务执行;小镇透明,适合长期相处。我们做的是让这两层互补。
第一次重构:从独立 demo 到 Agent 驱动
认知升级:3D 前端不能主动轮询 Agent 状态,要让 Agent 的事件流"推"着前端走
小镇最初是一个独立的 Three.js demo——NPC 行为写死在代码里,动画序列手动编排,一切自己管。接入 OpenClaw 后,问题变成了:Agent 框架通过 Hook 回调告诉你发生了什么,你怎么把这些回调变成 3D 前端能消费的东西?
这不是"注册一个插件"就能解决的。我们需要从零搭建一条事件管线:
第一步:翻译层。OpenClaw 给插件的是 subagent_spawned、before_tool_call、llm_output 这样的原始 Hook。前端需要的是"这个 NPC 该进入 thinking 状态了"、"这个 NPC 在用搜索工具"。hook-translator.ts 做这件事——一个纯函数,10 种 Hook 翻译为 26 种标准化的 AgentEvent。纯函数意味着好测试、无副作用、可以独立验证。
第二步:传输层。Plugin 跑在 Node.js,前端跑在浏览器。中间用 WebSocket 连接(端口 55211),ws-server.ts 负责会话绑定、事件广播和消息路由。这里有个关键设计:管家 Agent 的事件直接广播,居民 Agent 的事件需要额外附加 npcId——前端才知道该驱动哪个 NPC。
第三步:编排层。DirectorBridge 在浏览器端接收 AgentEvent,翻译成 GameEvent 交给前端渲染。在这个阶段,Bridge 承担了所有职责——事件翻译、状态追踪、NPC 生命周期管理、工作流编排——全部塞在一个文件里。
三步走完,管线通了:
┌─────────────┐ ┌─────────────┐ ┌───────────────────┐
│ OpenClaw │────▶│ Plugin │────▶│ DirectorBridge │────▶ Three.js
│ 10+ Hooks │ │ 26 │ WS │ 翻译 + 状态追踪 │ 渲染
│ │ │ AgentEvent │ │ + 动画编排(全塞) │
└─────────────┘ └─────────────┘ └───────────────────┘
Agent 创建子 Agent → Plugin 翻译成 sub_agent.started → WebSocket 送到浏览器 → Bridge 发出 npc_spawn → 前端渲染新 NPC。整条链路第一次跑通。
但 Bridge 一个文件干所有事的问题很快暴露了——这直接导致了第二次重构。
第二次重构:从 setTimeout 地狱到意图+回传
认知升级:两个时间模型的系统之间,不需要共享状态,只需要一个约定
崩溃
第一次重构后,DirectorBridge 既负责 AgentEvent → GameEvent 翻译,又负责工作流的动画编排——用 setTimeout(3000) 串联每一步。很快事情开始崩:
场景错乱:管家和用户已经在小镇了,一个 Agent 调用 project_complete,触发了"离开办公室"的动画序列——但 NPC 不在办公室,在小镇广场上凭空播放了退场动画,然后从办公楼门外又冒出来。
幽灵 NPC:退场用了 Promise.race 等全员走到门口,超时后强制推进。但超时后那些还在执行的 moveTo + fadeOut promise 没被取消,它们在后台继续跑,把 NPC 的 scale 和 opacity 拉到零。结果:名字标签还飘在空中,模型没了。
工位撞车:一个 NPC 完成了任务准备离场,新的 NPC 被分配到同一个工位。旧 NPC 还没走完动画,新 NPC 已经坐下来了,两个人叠在一起。
改一处修好了,另外两处崩了。所有编排逻辑塞在 DirectorBridge.ts 一个文件里,用 setTimeout 硬编码延迟串联动画。根因不是哪个定时器写错了,而是架构假设就是错的:Bridge 运行在 JS 逻辑层,不可能知道 Three.js 渲染的动画实际播了多久。用定时器去猜动画时长,就是在两个时间模型完全不同的系统之间硬塞一条不可靠的绳子。
继续打补丁没有意义。需要重新设计这两个系统之间的通信方式。
重建
新规则:Bridge 不管动画怎么播,只管发意图。前端播完了,告诉 Bridge 一声。
┌──────────────┐ workflow_summon ┌───────────────┐
│ │ ─────────────────▶ │ │
│ Bridge │ │ Choreographer │
│ 状态机 │ ◀───────────────── │ + Orchestrator│
│ │ phase_complete │ │
└──────────────┘ └───────────────┘
Phase 状态机:
idle ─▶ summoning ─▶ assigning ─▶ going_to_office
│
idle ◀── returning ◀── publishing ◀── working
每个阶段的推进都靠前端回传,不靠定时器。
三个配套设计自然浮现:
收集窗口:子 Agent 不是同时创建的,可能间隔几秒。Bridge 启动 3 秒窗口攒齐所有 Agent,一次性触发召唤,前端统一编排集结动画。
工位延迟释放:Agent 完成任务不等于 NPC 走了。Bridge 不立即释放工位,等前端 NPC 播完庆祝、走到门口、fadeOut 之后,前端发回 workstation_released 确认,Bridge 才释放。幽灵 NPC 和工位撞车问题同时解决。
迟到者:working 阶段才创建的 Agent 不走召唤流程,直接分配空闲工位进场工作。
前端拆出 Choreographer + 4 个 Orchestrator(Summon / Briefing / Celebration / Return),每个独立负责一段编排。改召唤动画不影响庆祝,改庆祝不影响退场,Bridge 完全不用动。
至此,架构收敛为最终形态:
┌───────────┐ ┌────────────┐ ┌───────────────┐ ┌────────────────┐
│ OpenClaw │─────▶│ Plugin │─────▶│ Bridge │──────▶ │ Frontend │
│ 10+ Hooks │ │ 26+ │ WS │ 状态机 │ 65 │ Choreographer │
│ │ │ AgentEvent │ │ 发意图 │ Game │ 4 Orchestrator │
└───────────┘ └────────────┘ └───────┬───────┘ Event └───────┬────────┘
│ │
│◀─── phase_complete ────┘
│◀─ workstation_released
三层各管各的,层与层之间只有协议,没有时间耦合。
场景错乱、幽灵 NPC、工位撞车——全部消失了。不是因为修了某个具体 bug,是因为架构不再允许这类问题存在。
第三次重构:UGC 工坊
认知升级:先设计数据流,再写第一行代码
前两次重构都带着"先写后修"的痛。到做角色工坊时,思路变了:先想清楚数据怎么流,再动手。
核心问题:过去角色数据硬编码在 TownConfig 里,名字、模型 ID、动画映射全写死。要做 UGC,必须先回答——谁拥有这份数据?编辑器改的是草稿还是线上?发布意味着什么?小镇启动时读谁的配置?
设计结论:
- 创建
PublishedCitizenConfig作为单一数据源 TownConfig从"数据拥有者"降级为 Published 的视图——通过publishedToTownView()适配- 发布是一次原子操作:写入配置 → 创建或更新对应的独立 Agent → 同步小镇默认值
角色工坊不只是一个编辑 UI,而是一条完整的 UGC 管线:
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐
│「导入模型」│───▶│「优化压缩」│───▶│「动画映射」│───▶│「设定人设」│───▶ │ 发布到小镇 │
│ GLB + │ │ Draco │ │idle/walk │ │ soul │ │ pubilc │
└──────────┘ └──────────┘ └──────────┘ └──────────┘ └────────────┘
点发布 → 小镇里立刻出现一个有外貌、有行为、有人格的新居民 NPC。
这是三次重构中最平稳的一次。前两次踩的坑在这里兑现了——不再问"这里为什么坏了",而是在动手前问"数据从哪来、到哪去、谁说了算"。
现在的样子
三次重构之后,小镇有了这些:
- 24h 昼夜循环 + 12 种天气(晴、云、雾、小雨、大雨、暴风、雪、暴雪、沙尘暴、极光…),GLSL 粒子渲染,天气间平滑过渡
- 程序合成环境音——雨声、风声、鸟鸣、蟋蟀、雷声,全部 Web Audio API 实时合成,零音频文件
- NPC 双模式行为——默认算法驱动:9 状态状态机 + 加权目的地选择 + 400 条预设台词社交,零 LLM 开销。用户可开启灵魂模式切换为 LLM 三层决策,选择权在用户手里
- UGC 三件套——地图编辑器 + 角色工坊(导入 AI 生成模型 + 动画映射 + 灵魂编写)+ 灵魂文件系统
- 小游戏插件框架——
MinigameSlot接口可扩展,内置班味消除游戏,减少等 AI 干活的空窗期 - 居民独立 Agent 聊天——点击任何居民 NPC 直接对话,路由到该居民自己的 Agent 会话
- Chat + Town 双模式界面——一键切换,互补不替代
写在最后:AI Coding 的几条工程心得
三次重构下来,关于"和 AI 一起写代码"这件事,有几条体会:
-
人定架构,AI 填实现。 架构决策不能委托给 AI——它擅长在约束内生成代码,但不擅长定义约束本身。三层翻译、意图+回传、数据所有权,这些都是人想清楚的,AI 帮写的。
-
改之前先搞清楚为什么坏。 AI 的本能是看到错误就开始修,但越改越多往往是因为根因没找到。停下来问"架构假设对不对"比连续打十个补丁有效得多。
-
给 AI 足够的上下文边界。 我们维护了
AGENTS.md架构文档和模块级规范,让 AI 知道每一层的职责和禁区。没有这些,AI 会把 Bridge 层的逻辑写到前端、把前端的动画逻辑塞进 Plugin。 -
先设计数据流,再写第一行代码。 第三次重构最平稳,就是因为先回答了"数据从哪来、到哪去、谁说了算"。这条不只适用于 AI 协作,但和 AI 协作时尤其重要——因为 AI 不会主动帮你做这个设计。
-
拆小任务,每步可验证。 大重构不是一口气让 AI 改完所有文件,而是一步一个可测试的状态。改完一层跑通测试再改下一层,比"一次重构 20 个文件然后花 3 天 debug"强太多。
-
AI 是工程能力的放大器,不是替代品。 工程基础好的人用 AI 更快;基础弱的人用 AI 会把问题藏得更深。三次重构的故事本质上不是"AI 帮我们做了什么",而是"我们在哪些地方做了错误的架构假设,然后用 AI 高效地重写了正确的版本"。
项目刚开源,GitHub 地址:github.com/Agentshire/…
技术栈:Three.js + TypeScript,Vite 构建,MIT 协议。
感兴趣的伙伴们,也可以一起来共建~