# 多 Agent 叙事世界 · 设计与复刻 Playbook

> 来源:雾鸦岛(《雾鸦》无人生还式多 agent 凶案推理)从 0 到可玩的全过程沉淀。
> 目的:再做**别的世界游戏**时,照此快速复刻——架构、各层设计要点、素材/音效管线、以及**踩过的坑**。
> 技术栈:自研 `genesis` 仿真引擎(**无第三方 agent 框架**)+ `litellm`(模型网关→MiniMax/DeepSeek)+ `mem0`(长期记忆)+ WebAudio 前端。

---

## 0. 一句话架构

**客观世界状态(WorldState)是唯一真理;每个 agent 是一个 perceive→decide→act 的认知体(CognitiveMind),它感知世界状态、用 LLM 决策、再把动作落回世界状态;一个"软导演"只调环境、不下命令,让剧情成为聪明 agent 自然会演出来的东西。**

```
server/live2.py            ← 进程入口:建引擎 + HTTP + 静态前端
  └ runtime/engine.py      ← 事件循环引擎:调度、世界变更、出帧(唯一改世界的线程)
      ├ runtime/worldstate.py   ← 客观真理:位置/设施/物品/尸体/电力/天气/死亡记录
      ├ runtime/cognition.py    ← CognitiveMind:三层认知 + 结构化决策(调 LLM)
      ├ runtime/{actor,bus,scheduler,simclock,soma,attention,devices,weather,narrator,longterm,metering}.py
      └ manifest/{schema,loader,builder}.py  ← yaml 人设/地点 → 数据对象
  └ viz/web/mystery.{html,js} ← 前端:预渲染等角地图 + sprite 叠加 + 天气/音效/镜头
worlds/<world>/            ← 一个世界 = 一组 yaml + acts.json + viz.json + 存档
```

**数据→运行的实例化**:`worlds/<w>/characters/*.yaml`(声明式人设)→ 引擎启动时每份 `new` 一个 `CognitiveMind`(`engine.py` 构建循环),把人设压成 `persona_card` 塞进它的 prompt。

> 复刻新世界 = 换 `worlds/<新世界>/`(地点+角色+剧情+viz)+ 重生成素材/音效;**引擎几乎不用动**(向前兼容是设计红线)。

---

## 1. 世界设计(World)

**地点 = yaml**(`worlds/<w>/places/<id>.yaml`):
```yaml
kind: place
id: screening                 # 英文 id(代码用)
name: 放映室                  # 中文显示名(state/positions/viz 全用中文名,务必一致)
description: 拷贝架上那盘"未剪"被人抽出来摆在最上面…   # 进屋看到的氛围
geography: indoor             # indoor | 室外用 seaside/forest/plaza 等(见坑#3)
adjacent: [hall, prop_room]   # 邻接(用 id);双向连通由引擎建图
```
- **拓扑**:邻接表 → BFS 寻路(`worldstate.path`)。**门禁/特殊通道**:某地 `adjacent: []` 靠运行时 `state.connect()` 动态打通(如钥匙开锁的密室);某条边只对特定角色开放靠 `actor.blocked` 集(如凶手专属密道,寻路时其他人绕开)。
- **铁律:每个场景都要有"意义"**——要么有可搜的线索物品、要么有可动手的设施、要么是叙事/机关节点。空房间 = agent 没理由去 = 地图荒废。我们曾有 17/37 是死场景,补齐后 agent 才会分头探索。
- **连通性自检**:从出生地 BFS 应能到达所有"该到达"的地点(被钥匙门隔离的密室除外)。

---

## 2. 状态设计(State)——环境 & Agent

### 2.1 环境状态:WorldState 是唯一真理(`runtime/worldstate.py`)
持久存:`positions / facilities / items / bodies / dead / death_info / power_on / weather`。**任何动作改状态后,之后所有人的感知读到的都是同一份事实**(感知与事件流同源、不打架)。

- **Item(物品=可拾取的世界状态)**:`{id,name,place,holder,destroyed,hidden,hint}`。
  - `hidden=True` 要 `search` 才现形;`hint` = **这东西干嘛用的**(感知/搜出/拿到时一并告知——见坑#8)。
- **Facility(设施=有状态机的可动手物)**:`{id,name,place,state,desc{state→文案},hint,verbs}`。
  - `verbs` 是**声明式可供性**:`{动词:{feedback,event,scope,salience,set_state,reveal_item}}`,引擎 `_do_use` 末尾通用派发(`verb in arg` 匹配)。新增一个"可互动物"不用改引擎,只加一条 facility。
  - 例:总电闸 `砸→停电(set power_off, 全局广播)/修→复电`;灯塔储油罐 `加油(集齐3桶)→点亮→求救→渡船提前`。
- **道具=世界状态,用道具=感知/修改世界状态**——这是统一心智模型,别把"道具"做成纯文本 flavor。

### 2.2 Agent 状态(`CognitiveMind`)
- `persona_card`(由 manifest 压成,见 §3)、`goals`(可改的任务清单)、`relations{name:{impression,trust(-1..1)}}`、`soma`(饥饿/疲劳/恐惧)、`memory`(mem0 长期 + 本地近期)、`intent`。
- **soma(生理需求)**驱动 L0 反射(饿了→吃),给 agent"活人"的底噪。

---

## 3. Agent 设计(核心)

### 3.1 三层目标(治"任务机器人")
```
① 终极目的 drive   —— 静态、写死在 manifest、永置 prompt 最顶(永不变的北极星)
② 阶段聚焦 stage_directive —— 导演按"幕"注入(导演层↔认知层的唯一接口)
③ 当前任务清单 goals —— agent 自有、可改;降格成"我目前打算怎么走的假设"
```
**关键洞察**:别把"做什么(任务清单)"供成中心、把"为什么(drive)"降成脚注——否则 agent 退化成打勾机器。**drive 置顶、任务降格、每拍先"盘算"(手段-目的推理)**。

### 3.2 人设 manifest 字段(`worlds/<w>/characters/<id>.yaml`)
| 字段 | 作用 |
|---|---|
| `drive` | 终极目的+赌注,一句话,**prompt 顶端锚点** |
| `secret` | 隐藏秘密(只进自己 prompt) |
| `role` | 隐藏身份(如"凶手"→触发专属逻辑,`is_killer = "凶手" in role`) |
| `knows` | **可立即行动的情报/机关线索**(把对的信息给对的人→更有行动力;别人未必知道) |
| `relationships` | 初始关系网(影响信任/说服) |
| mbti/occupation/backstory/start_location | 基础人设 |

> 新增字段必须**同步加进 `manifest/schema.py` 的 dataclass**,否则严格 loader 报"未知字段"(坑#5)。

### 3.3 三层认知(`cognition.py decide()`)
- **L0 反射**:soma 候选(饿/累)。
- **L1 对话**(`DIALOGUE_DOC`):交谈中只产出 thought/speech/gesture/leave。
- **L2 行动**(`ACTION_DOC`):产出结构化 JSON `Decision`(scheme/thought/intent/speech/gesture/action{kind,arg}/goal_update/goals_replan)。
- **结构化输出**:prompt 强制只输出 JSON → 解析成 `Decision` dataclass → 引擎执行。务必加"**务必简短**"约束(输出越短世界转得越快)。

### 3.4 记忆与防幻觉
- **事实锚 vs 传闻锚**:亲历/亲见(自己的反馈/动作/环境)入"事实";别人说的话入"传闻"(说法≠事实,须自行掂量)——掐死"全信别人的话"。
- **地图常识 + 在岛名册**喂进 prompt,**掐死幻觉实体**(不存在的地点/人)。坑#6:人设文本里别写不存在的地点(如曾经的"剪辑室"),会诱发 agent 去找幽灵地点。
- **世界回执校验**:凶手把"杀了X"标 done 必须有真实死亡背书,否则"脑中预演"会污染台账、谎言自我固化(实测踩过)。

### 3.5 关系/信任动力学
- `relations{impression,trust}`,由 `reflect()`(反思节拍)+ 关键事件(如投票)更新。
- **信任影响信息共享与说服**:越信任越容易被说动(诱饵/假任务),越提防越容易识破。
- 坑#9:`relations` 只在 LLM 脑的 `CognitiveMind` 上有;`StubMind` 没有 → 信任类逻辑 stub 测不出,要 llm 实跑或手动塞 `relations={}`。

---

## 4. 导演 / 节奏设计(治"主线推不动")

**哲学:软导演——只调环境、不下命令、不写结果。** 幕是潜在目标态,导演通过注入世界状态/改环境,让"剧情往前"成为聪明 agent 自然会做的事。**认知是地基,软导演是上层,顺序不能反**(软推只对会随机应变的聪明 agent 有用)。

- **戏剧温度 0..1**:由多信号实时算(死亡数 + 是否独处逼近 + 距上次大事件 + 停电 + 临近终局…),驱动**全员同步升温**(prompt 情绪基线、对话预算收紧)和**音乐自适应**(见 §7)。
- **幕状态机 + 双轨推进**:幕的退出 = 钟点到点 **OR** 关键事件发生(死人/指认/停电),谁先到谁触发(钟点兜底不空转,事件允许涌现改写节奏)。
- **软杠杆表**(都只调环境):升温=暴雨加剧/灯闪/异响;散人群=停电/某处异响把扎堆的人引开(我们的 `_disperse_herd`:某地≥4人扎堆→在别处制造异响引开,顺带把人引向有线索的空场景);给反派机会=目标与反派凑独处+掩护+机会提示。
- **兜底**:连续软推无效(如反派迟迟不动手)才升一档更露骨的环境手,且尽量仍是环境而非脚本。地基越好越少触发。

---

## 5. 剧情设计(Plot)

- **把剧情绑到地点**:`worlds/<w>/acts.json` = `phases`(全员处境帧+张力档)+ `kill_acts`(反派的"幕菜单":每幕 target/取景地/方法/掩护/诱饵)+ `dossier`(反派对每人的静态情报:软肋+私欲+物证位置+诱饵话术)。
- **每幕取景地 ≈ 该幕目标的物证地点附近**(地图与剧情共埋一半),让"重合"自然发生。
- **结局机器(胜负条件)要齐全且可达**:反派死(被指认处决/被杀)/反派赢(仇人尽清)/幸存者逃生(熬到救援)/反派逍遥(救援到时仍未被识破)。
- **真实压过剧本**(踩过的大坑#7):验尸/线索一律以**客观死亡记录**(`death_info`)为准,不被剧本台词污染——反派在 A 地用 B 法杀人,验尸就报 A/B,不能因为"剧本安排他该死在 C"就报 C。"对得上剧本仪式"反而成为指向反派的破案信号。

---

## 6. 道具 / 游戏机制设计

- **可供性(affordance)统一走 facility.verbs**(声明式,见 §2.1)——加机关不改引擎。
- **消费链要闭环且可感知**:灯油(船屋/储藏室/温室)→搬去灯塔→加满→点亮→求救→渡船提前。每一环的 `hint` 都说清"下一步去哪、做什么、什么后果"。
- **物品用途必须进 agent 的 prompt**:① 进屋 `describe` 带 hint;② **inventory(身上带什么+用途)每拍注入**(坑#8:agent 以前根本不知道自己拿了什么、能干嘛——"拿了灯油不知干嘛"就是这么来的);③ 搜出/拿到时反馈带 hint。
- **反派专属机制**(给信息差优势,同时要"装正常"避免被指认):
  - `lure`:把人支去僻静处,**并种下持久"假任务"**(假念头留在目标心里,日后仍勾着他去,可识破)。
  - 专属快速通道(密道):`actor.blocked` 让只有反派能走,既快又隐秘=不在场证明;但侦探能在入口搜到"有暗道"的痕迹(公平)。
  - 信息差:反派 prompt 实时列出各人"此刻在哪、惦记什么(部分目标)"用于精准下饵/嫁祸——同时强提醒"行事务必像个无害的普通人"。
  - **反派落刀纪律**(踩过的大坑#10):别做成"逮谁落单就杀谁、在显眼处动手"。我们用 R3 窗口**只在『与本幕目标独处 且 地点僻静(非门厅/走廊枢纽)』才开杀**;否则忍住/改 lure 引去取景地。这条规则散落在三处(engine `_killer_stage` + cognition `KILLER_DOC` + 角色 yaml role),**改一处不够,三处合力**。
- **言行一致**(坑#11):agent 会"嘴上答应跟人走、转头干自己的"。治法:`ACTION_DOC`/`DIALOGUE_DOC` 加"言行一致"硬规则 + L2 注入"你刚说过『…』"锚(把 agent 自己最近的原话摆到决策前)。

---

## 7. 音效 / BGM 设计

- **WebAudio 三层声场**:室外天气(走低通→切室内自动变闷)/ 室内底噪 / 配乐(score)。用 `AudioBuffer + BufferSource`(不受浏览器 autoplay 拦截,首次点击 `AudioContext.resume()` 后常驻循环、只调 gain)。
- **自适应分层配乐**:平静→紧张→压迫随**戏剧温度**平滑交叉淡入,高温叠加张力层(心跳/滴答)——永不显得静止。
- **主题动机(leitmotif)**:做一段"主角/亡者"的动机,在关键节点(灯塔/密室/凶案/破晓/终局)浮现,期间主配乐 ducking 让位——音乐与剧情咬合。
- **诊断式叙事音**:凶案=惨叫、钟声、汽笛等 sting,带合成混响(空间感)、声像(雷声从闪电方向来)、变速(远近)。
- **总线打磨**:轻压缩(成片感)+ 高通(去隆隆)+ 合成卷积混响(衰减白噪声 IR,**幅度务必压到 ~0.35**,否则糊成一片)。
- **生成管线**:meowa `music-run --audio-generate`(Lyria,**约 111s/曲**)。**ElevenLabs Music API 需付费套餐,免费 key 被 402 拦**——用 meowa 顶上。坑#12:meowa CLI 用 prompt 当目录名,**prompt 太长会"文件名过长"报错→prompt 压到 ~150 字内**。
- **"诊断式播放位置"**:像留声机老歌这种,要从**指定位置起播**(不是后台自由循环到哪算哪),按场景把"从第几秒起/音量/淡入淡出"做成可调参数。

---

## 8. 素材创建管线(meowa / Lyria / PixelLab)

- **等角场景图(预渲染整图路线)**:`gemini-generate-content`,`aspectRatio 4:3 imageSize 2K/4K`,哥特暴雨夜 + 精细像素氛围。**统一画风的关键:把已定稿的第一张图当 `--image-file` 风格参考**传入,后续 10 张调色/雨感高度一致。内景用"剖面娃娃屋"(矮墙俯角,房间内部全可见),prompt 强命令"NO text/labels"+"2:1 dimetric 3D cutaway, NOT flat top-down"。
- **4:3 立体上限 ~6 房**;16:9 能塞更多但会压成俯视(除非 4K+强调纵深)→ **超 6 房就拆楼层**,别硬塞。
- **角色八向 sprite**:`pixel-gen-run --template-name pixel_char_1 --job-name <c>`(出参考图)→ `character-multi-view-run --mode pixel --output-size 128`(八向,透明底)。坑#13:`pixel_char_1` 强制忽略 prompt 里的衣服颜色;gemini 出的角色喂 multiview 会裁头脚。**区分度靠渲染层"签名色"**(脚下光环+名牌色条)而非衣服色。
- **预渲染等角图接入渲染层**:每地点一个锚点 `region [x,y]` + `world_w/h`;`w2s` 线性缩放;`ringSpot` 自带等角压扁(sin*0.6);按 y 深度排序;sprite 底部中心锚定。**换图只需:换 map 路径 + 重标锚点**,引擎契约不变(向前兼容)。
- **锚点校准法**:叠坐标网格到 4K 图直读"站位点"(等角里站位在物体右下方地板);单图看不清就裁象限高清看。

---

## 9. 前端 / 可视化(`viz/web/mystery.js`)

- **预渲染等角地图 + sprite 叠加**;`devicePixelRatio` 设 canvas 内部分辨率(治模糊)+ 字号下限(治文字糊)。
- **导演镜头**:按 `focus_place` 自动切到最热场景(凶案/对质/线索),带迟滞防抽搐;用户可点地图/角色抢镜头。
- **程序化步态**:抱臂半身像做不了真腿部行走帧 → 用 bob+lean+squash 程序化步态(零素材,任何缩放都读得出"在走")。
- **天气**:雨要**连续下落**(按时间平滑位移),不能每帧瞬移到新随机点(那会糊成"一堆点点"——坑#15);多层视差+阵风斜移+落地溅点;闪电做"滚动"频闪+横向扫光;阴天罩压顶云层冷调。
- **沉浸 UI**:常驻栏(如场景切换)改悬停下拉,平时隐藏。

---

## 10. 踩过的坑(合集,务必记住)

1. **改 viz.json/世界数据必重启服务**——`live2` 启动时一次性把数据缓存进内存。
2. **改前端 JS 默认不生效**:`mystery.js` 通过 `<script src>` 引入,普通刷新吃缓存的旧 JS。**已加自动版本号**(HTML 注入 `?v=Date.now()`),否则必须硬刷新。改图靠 `?v=` 防缓存。
3. **geography**:室内必须精确 `"indoor"`(`is_indoor` 据此判定天气遮蔽);室外用 seaside/forest/plaza 等(`!= indoor` 即按室外处理)。别新增莫名其妙的值。
4. **stub 脑 = 零对话**:看 agent 思考/对话必须 `GENESIS_BRAIN=llm`;stub 只走动,用于零成本验证渲染/移动。
5. **新 manifest 字段必须加进 `schema.py` 的 dataclass**(严格 loader 拒绝未知字段)。
6. **人设/剧情文本里别引用不存在的地点/人**(幻觉源)。改了地点名要全链路对齐(yaml/worldstate/viz/acts/dossier/角色文本)。
7. **真实压过剧本**:验尸/线索以客观状态为准,别被剧本台词污染。
8. **agent 不知道自己拿了什么**:inventory + 物品用途 hint 必须进 prompt。
9. **`relations`/信任只在 llm 脑上**:stub 测不出信任类逻辑。
10. **反派会"逮谁杀谁、显眼处动手"**:落刀纪律要 R3 门控(本幕目标+僻静地),且 engine+cognition+yaml 三处合力。
11. **言行不一**:加"言行一致"规则 + "你刚说过"锚。
12. **meowa 生成 prompt 太长→文件名过长报错**:prompt 压短(~150 字内)。**ElevenLabs Music 需付费套餐**(免费 key 不可用)。
13. **pixel_char_1 忽略颜色 / gemini 角色喂 multiview 裁头脚**:区分度靠渲染层签名色。
14. **音乐别后台自由循环**:要从指定位置按场景起播,做成可调参数。
15. **雨用连续位移、别每帧瞬移**(否则像噪点);天气是全局的,但渲染在每张图上都生效。
16. **没有版本控制(本仓库无 git)**:删代码/重构前先 `tar` 备份。删除前确认目标不被活跃路径(从 `live2` 的 import 闭包)引用——注意 `from X import Y` 式的延迟/间接引用会让静态闭包有假阳性(如 `config` 看似不可达实则被用)。

---

## 11. 复刻新世界 · 快速清单

1. **建世界目录** `worlds/<新世界>/`:
   - `places/*.yaml`(地点+邻接+geography),保证连通、每处有意义。
   - `characters/*.yaml`(drive/secret/role/knows/relationships;新字段先加进 schema)。
   - `acts.json`(幕/反派菜单/情报档,或你这类游戏对应的"剧情↔地点"绑定)。
   - `viz.json`(maps 列表 + 每地点锚点 region + 总览 overview)。
2. **种世界状态**:在 `worldstate.py` 的 seed 函数里加该世界的 items(带 hint)+ facilities(带 verbs/hint),让"每个场景有可感知可动手的东西"。
3. **生成素材**:等角场景图(统一画风参考)+ 八向角色 sprite,落盘到 `assets/`,viz.json 指过去。
4. **配音效**:meowa 生成主题/分层/动机/sting,接入 WebAudio(戏剧温度驱动)。
5. **跑起来**:`GENESIS_WORLD=<新世界> GENESIS_BRAIN=llm python -m genesis.server.live2`,硬刷新前端。
6. **自底向上迭代验证**:先 stub 验渲染/移动/锚点 → 再 llm 验思考/对话/剧情 → 观察"不合理"与"精彩",逐项调 prompt/数据,**改一层验一层**。

---

## 12. 设计哲学(元经验)

- **客观状态是唯一真理**;道具/场景都是可感知、可修改的世界状态。
- **涌现 > 脚本**:软导演只调环境;真正推剧情的是聪明 agent。
- **drive 置顶、任务降格**;每拍先盘算"为了终极目的此刻该做什么"。
- **每个场景都要有意义**;每件道具都要说清用途;**把对的信息给对的人**。
- **真实压过剧本**;**言行要一致**;给反派信息差优势但要求它"装正常"。
- **认知是地基,导演是上层**;**向前兼容是红线**(换世界不该动引擎)。
- **改一层、验一层**;无 VCS 先备份。
