Narrative Engine 静态分析:模块设计与工作原理

对象:Sagesheep/NarrativeEngine-P(MIT)· 方法:纯静态分析

一、总体架构

Narrative Engine 是一个单体桌面/本地 Web 应用:人类玩家通过聊天界面与"AI 主持人(AI DM)"互动,AI 负责叙事、判定与世界推进。技术栈为 React 19 + Zustand 5 + Vite 8(前端,src/)+ Node.js Express 5 ESM(后端,server/),另有一个平台无关的纯逻辑包 packages/engine@narrative/engine),以及一个运行时插件系统(mods/public/bundled-mods/)。版本 2.0.0package.json:4)。

┌─────────────────────────────────────────────────────────────────────┐
│ 浏览器 (Electron / http://localhost:5173)                            │
│  ChatArea/Composer → useAppStore (Zustand 6 slices)                  │
│  turnOrchestrator.runTurn → turnStages(7阶段)                         │
│     ├ contextGatherer (并发召回: planner/semantic/recommender/lore)   │
│     ├ payloadBuilder  (stable/world/volatile/history 拼装)            │
│     └ llmService.sendMessage (流式, 工具调用, DSML 回退)              │
│          │  Vite proxy /api → :3001  (或 /api/llm/proxy 兜底)        │
└──────────┼──────────────────────────────────────────────────────────┘
           ▼
┌─────────────────────────────────────────────────────────────────────┐
│ Node 后端  server.js (127.0.0.1:3001, CORS 白名单)                   │
│  vault(KeyVault AES-256-GCM) · 16+ routers (/api/*)                  │
│  services/archiveService (appendScene 同步写盘)                      │
│  lib/vectorStore (better-sqlite3 + sqlite-vec, 3 张 vec0 表)         │
│  lib/embedder (@huggingface/transformers · mxbai-embed-large-v1 q8)   │
│  lib/nlp (NPC 名/见证人/重要性 6-pass 启发式)                        │
└──────────┬──────────────────────────────────────────────────────────┘
           ▼
┌─────────────────────────────────────────────────────────────────────┐
│ 持久化                                                              │
│  data/settings.json · data/embeddings.db (向量)                      │
│  data/campaigns/<id>.{json,state,lore,npcs,locations,archive.md,     │
│     archive.index.json,chapters,timeline,entities,facts,overworld,   │
│     divergence,relationship-memory.*}.json                           │
│  data/backups/ · data/.embeddings_cache                              │
└─────────────────────────────────────────────────────────────────────┘

数据流方向:前端 Zustand store 是运行时状态源,后端以"每战役多 JSON 文件"为持久层,向量/语义记忆放在 SQLite 侧;LLM 调用全部由前端发起(llmService.ts),经后端 /api/llm/proxy 兜底转发以绕开 CORS(server.js:102src/services/llm/llmFetch.ts)。

二、核心工作流

链路 A:玩家输入 → 上下文装配 → LLM 调用 → 状态更新

  1. 掷骰/注入(Stage 1) src/services/turn/turnStages.ts:92 resolveEngineRolls:预掷引擎骰池,解析玩家 armed 的 dice/loot/one-shot,把结果作为"事实断言"拼进 ctx.finalInput:112)。
  2. 挂用户气泡(Stage 2) turnStages.ts:168 addUserTurnMessage:同步加入聊天列表,保证重异步前可见。
  3. 上下文装配(Stage 3) src/services/turn/contextGatherer.ts:228 gatherContext:6 路并发——planner(LLM)、semantic-candidates(服务端向量检索)、archive-recall(混合召回)、recommender(LLM 选上下文)、lore-rules(IDF+RRF)、dynamic-elevation/slotted-RAG,用 Promise.race 做超时兜底(:324-330)。召回入口 memory.recall 角色在 :155
  4. 导演阶段(Stage 5) turnStages.ts:245 runDirectorStage:确定性 watchdog 案卷 +(pro/max 档)一次阻塞式 Director Brief LLM 调用,为下一回合写"作家简报"。
  5. 组装 payload(Stage 6) turnStages.ts:434 buildTurnPayloadsrc/services/payload/payloadBuilder.ts:116 buildPayload:5 块拼装(stable/world/volatile/history + final-user 贡献注册表),带 Anthropic cache_control 标注。
  6. 生成(Stage 7) turnStages.ts:546 runGenerationStage → 递归 executeTurn:590):sendMessage 流式输出,工具调用循环(每回合上限 MAX_TOOL_CALLS_PER_TURN=5:48/:603),三级重试(重试→去工具重试→放弃,:842-887),产出 SwipeVariant 并 stamp pendingCommit
  7. 提交与状态更新 src/services/turn/pendingCommit.ts + postTurnPipeline.ts:91 runPostTurnPipelinerunArchiveTrack:175)把整回合写入 .archive.mdapi.archive.append:213)并刷新 index/timeline/chapters;随后跑 post-commit 轨迹(NPC 更新、事件抽取、章节自动封印、重要性评分、道具/角色扫描),Arc 引擎 tick 与 NPC Agency tick 也在提交后触发。

链路 B:记忆召回(混合检索)

src/services/archive-memory/recall.ts:24 retrieveArchiveMemory:对 archive 索引做 IDF 加权关键词排序 + 服务端向量排序,经 RRF 融合:110),再把"分歧寄存器"(divergence)场景强制前置(:117-131),最终按共识上限 dynamicMax 截断;:149 fetchArchiveScenes 按 token 预算取回逐字场景文本。深搜路径 deepArchiveSearch.ts:279 是"章节扫描→场景扫描→分区摘要"的两轮 LLM 管线。

关键函数/文件索引(按职责)

职责 函数 位置
回合主循环 runTurn src/services/turn/turnOrchestrator.ts:155
回合阶段编排 resolveEngineRolls/gatherTurnContext/runDirectorStage/runGenerationStage src/services/turn/turnStages.ts:92/188/245/546
上下文装配 gatherContext(6 路并发) src/services/turn/contextGatherer.ts:228
Prompt 拼装 buildPayload src/services/payload/payloadBuilder.ts:116
流式调用 sendMessage src/services/llm/llmService.ts:22
协议适配 getApiFormat/buildChatBody src/utils/llmApiHelper.ts:54/269
回合提交 runPostTurnPipeline/runArchiveTrack src/services/turn/postTurnPipeline.ts:91/175
混合召回 retrieveArchiveMemory/fetchArchiveScenes src/services/archive-memory/recall.ts:24/149
深搜 deepArchiveScan src/services/archive-memory/deepArchiveSearch.ts:279
摘要压缩 shouldCondense/getCondenseBudgetRatio src/services/archive-memory/condenser.ts:14/6
章节封印 runCombinedSeal src/services/turn/postTurnPipeline.ts:373
NPC 代理 tick runAgencyTick src/services/npc/agency/agencyEngine.ts:53
向量存储 initDb/createSearchFn/mmrSelect server/lib/vectorStore.js:121/225/52
本地嵌入 embedText/embedBatch server/lib/embedder.js:118/129
场景归档 appendScene server/services/archiveService.js:84
原子写盘 writeJson/getNextSceneNumber server/lib/fileStore.js:91/162

三、模块逐一分析

1. LLM 提供商抽象层(重点)

2. 记忆系统(重点:摘要 + 向量)

3. NPC 状态与代理(重点)

4. 世界模拟

5. 持久化与存档格式(重点)

6. 插件/Mod 系统

mods/(用户安装)与 public/bundled-mods/(随应用发布,如 enemies)通过 server/lib/modLoader.js 加载;turn 阶段暴露 turn.start/turn.generated 等核心事件与 prompt interceptor/fact publisher 钩子(turnOrchestrator.ts:187turnStages.ts:365/415)。

7. 数据模型(类型层)

src/types/ 12 个文件:GameContextgamecontext.ts:156,约 70 字段,含 playerCharacter: NPCEntry)、ArchiveIndexEntry/ArchiveScene/ArchiveChapterarchive.ts)、TimelineEventarchive.ts:166,带 SUPERSEDE 规则 :160)、DivergenceEntry/Registerdivergence.ts,含 knownBy 知识边界 :22)、SemanticFactarchive.ts:119)。

8. 骰子与自动判定

四、GM/玩家端区分

不区分 GM/玩家双端。 这是单用户应用:人类是唯一玩家,AI 扮演主持人(README:10 "Your AI Dungeon Master")。后端日志自称 "GM-Cockpit API"(server.js:149),实为"玩家操控 AI GM 的座舱"。玩家对 GM 的控制通过 OOC 通道实现:Ask GMsrc/services/ooc/oocService.ts)、Absolute Command(一次性 OOC 强指令,不进历史,turnStages.ts:267)。无多用户/联网 GM 客户端——grep 无 multiplayer/GM client/player client 命中。

五、与 Z.R.I.C 功能对照表

Z.R.I.C 能力 是否支持 实现位置(文件:行)
剧本/世界观解析 src/services/lore/loreChunker.tsloreRetriever.ts(IDF+RRF)
推演分支 ✅(部分) Swipe 多结局变体(swipeGeneration.ts)、World Arc(arc/)、叙事事件引擎
自动判定 engine/engineRolls.tsengine/diceTier.tsroll_dice 工具
世界状态 Divergence Register(divergence.ts)、Timeline(archive.ts:166)、GameContext.canonState
NPC 情绪/记忆 ✅(强) PersonalityHex 漂移、relationshipMemory.tsrelationMeterpressurerepressionPressure
地图 mapEngine/worldOrchestrator.ts(Perlin+Voronoi)、OverworldCanvas.tsx
触发器 NPC behavioralTriggerscharacter.ts:135)、引擎 DC 衰减、压力阈值
时间线 TimelineEvent + timelineResolver.ts(supersede)
记忆系统 ✅(强) 混合召回 + 向量 + 摘要 + 深搜 + 章节封印(见三.2)
本地模型 Ollama(llmService.ts:117)、本地 ONNX 嵌入(embedder.js
许可 ✅ MIT README.md:369package.json

六、设计评价

优点

  1. 记忆是真正的分层体系:无损逐字归档(archiveService.js:95)+ 章节摘要 + 向量检索 + 混合 RRF 融合(recall.ts:110)+ 两轮深搜 + 动态摘要压缩(condenser.ts),是同类项目中最完整的记忆实现。
  2. 提供商抽象干净且实用getApiFormat 单点判定 + buildChatBody 按格式分派 + DSML 回退解析(llmService.ts:192),用一套 OpenAIMessage 统一了 OpenAI/Ollama/Claude/Gemini/DeepSeek,且对 reasoning/thinking 预算做了逐厂商映射(llmApiHelper.ts:14-28)。
  3. 提交可靠性设计严谨:同步写盘 + 场景编号跨文件取最大值(fileStore.js:162)+ 丢响应后按 userSnippet 前缀对账防重复归档(postTurnPipeline.ts:36-56)+ 崩溃后 persistTurnState 立即落盘(turnStages.ts:787),明显为"长线多会话不丢档"下过功夫。
  4. NPC 代理可解释且分层:六轴人格 + 三层目标 + 心跳骰 + 碰撞解缠,全部确定性骰子驱动、可按 AI tier 关闭,避免无脑烧 LLM(agencyEngine.ts:80 标注 "+0 LLM")。

缺陷

  1. 复杂度失控、演进未同步文档ARCHITECTURE.md 描述的 runTurn()(509 行、contextGatherer 14 变量、payloadBuilder 5 块)已被重构为 turnStages.ts 8 阶段 + TurnContext 数据总线 + 贡献注册表 + mod 钩子,但文档与代码注释大量保留"阶段 3.2/Phase 5.2/WO-P1-03"等工作单痕迹(turnStages.ts:166-179payloadBuilder.ts:90-113),新读者极难对齐。
  2. "事实来源单一化"被打散:世界状态同时散落在 Divergence Register、Timeline、GameContext(~70 字段)、NPC 字段、relationship-memory 文件五处(fileStore.js:235),存在多套并存/需迁移的旧字段(@deprecateddiceConfiginventory、legacy 字符串 characterProfilegamecontext.ts:56/163/512),一致性维护成本高。
  3. 前端直连 LLM、后端仅代理llmService.ts 在浏览器内发起所有模型调用,/api/llm/proxy 只是 CORS 兜底(llmFetch.ts),意味着"任意 OpenAI 兼容端点"的密钥、请求都从前端发出,虽用 settingsCrypto.ts 加密,但架构上缺少统一服务端 LLM 网关。
  4. 关键状态为内存态、依赖前端存续:核心游戏循环 runTurn 在浏览器跑,pendingCommit/swipe 快照存于内存(pendingCommit.ts PendingTurnSnapshot 单例),依赖 persistTurnState 补写磁盘;一旦前端崩溃于两次写之间仍有丢档窗口。

七、结论

它是什么:一个功能极其丰富、以"AI 当主持人、玩家单人游玩"为形态的本地自托管 TTRPG 引擎,其核心竞争力是把"长线战役记忆"做成了分层可检索的工程系统(无损归档 + 向量 + 摘要 + 深搜 + 章节封印),并在 NPC 代理、世界弧、骰子公平性、知识边界(knownBy/见证人)等维度远超一般"套壳聊天"项目。

它不是什么:它不是多人 GM/玩家分离的桌面跑团工具(无联网、无双端、无实时多人),也不是"剧本解析→确定性分支树"的叙事引擎(分支靠 LLM + 骰子概率涌现,而非静态剧本图),更不是轻量可读的示例项目——它是一台由大量工作单(WO-*)驱动的、仍在高速演进中的重型单机引擎。