
叙事引擎世界存储与版本管理设计
2026-07-20 18:00:00
✨ 心情:设计
# 叙事引擎
# 架构设计
# 世界图
# 版本管理
1. 整体架构:双图分离
核心决策:整体是两张独立的图,事件节点作为单独的节点,不和世界图一起放。
| 图 | 职责 | 内容 |
|---|---|---|
| 世界图 | 记录一切故事需要的背景信息 | 角色、设定、物品、地点、关系、规则等 |
| 事件图 | 记录整个叙事的流程 | 事件节点、事件间的因果关系、什么发生了什么变化 |
双图分离的工程意义
- 职责清晰:世界图是"状态",事件图是"历史"
- 回退独立:事件图回退不影响世界图的稳定节点
- 检索分离:调度器查世界图获取角色信息,渲染器/用户查事件图获取叙事流程
- 可视化分离:世界图可视化看到的是关系网络,事件图可视化看到的是时间线/因果链
- 写入分离:世界图接受引擎扩散+用户编辑两条路径;事件图只接受引擎事件写入
与 Graphiti 的关键差异
- Graphiti 的 Episodic 节点和 Entity 节点在同一个图内,通过 MENTIONS 边连接
- 本架构将 Episodic 完全独立为"事件图",与世界图物理分离
- 这是比 Graphiti 更彻底的解耦
2. 世界图:当前状态快照
2.1 世界图的角色定义
世界图职责:
- 作为唯一状态中心,存储所有世界背景条目
- 接收并处理两条独立写入路径:
- 引擎自然生产:角色扮演产生的数据写回世界图,更新对应节点状态(扩散写入)
- 非引擎自然生产:主会话工具修改 + 图 UI 修改(用户直接编辑) 核心写入流程(引擎自然生产):
- 子代理扮演 → 产生结构化数据 → 写回世界图 → 更新对应节点状态
- 这是"扩散"的工程实现:事件 → 子代理行为 → 状态变化 → 写回节点
2.2 节点分层
四层节点分类:
| 层级 | 说明 | 示例 |
|---|---|---|
| 角色层 | 剧情中扮演行动主体的节点 | 主角、配角、反派 |
| 角色互动物层 | 和角色直接交互的物品 | 角色持有的武器、钱袋、信件 |
| 非角色互动物层 | 不和角色交互的物品 | 场景氛围元素:天气、人群、装饰 |
| 背景层 | 世界规则和宏观背景 | 魔法体系、社会制度、历史事件 |
2.3 节点生命周期
核心机制:每个节点有生命周期,临时性的节点会在演出后消失。 工程意义:
- 解决长篇节点爆炸问题——不是所有节点都需要永久保留
- 临时节点的例子:
- 角色互动物层:临时拿起的物品(吃完的苹果、丢掉的纸条)
- 非角色互动物层:场景氛围(某场雨、某群路人)
- 可能也包括临时角色(路人甲、酒馆老板)
- 永久节点的例子:
- 角色层:主要角色
- 背景层:世界规则
- 角色互动物层:角色的标志性物品(主角的佩剑)
2.4 世界图的存储原则
世界图只存储当前事件下世界的状态:
- 世界图 = 当前时刻的状态快照
- 每个事件对应世界图的一个状态
- 事件 = 两个世界图状态之间的变化(diff)
- 世界图永远只存当前状态,不需要保留历史版本(历史在事件链里) 每次事件扩散写入:
- 直接覆盖世界图字段
- 在事件图中记录变化(diffusions) 存储格式:
- JSON 文件存储(每节点一文件)
- 不依赖图数据库(Neo4j 等不要)
- 白盒可见,符合"用户可见、可追变动"约束
- 向量查询作为辅助检索手段,但不作为主存储
3. 事件图:状态变化记录
3.1 事件即状态变化记录(核心机制)
核心澄清:事件不仅是用户口述的内容时间线记录,也是整个世界图的状态变化记录。 类比:
- 事件图 = Git commit history(每个 commit 是一次状态变化)
- 世界图 = Working directory(当前文件状态)
- 回退 = git reset(移动 HEAD,working directory 相应变化)
3.2 事件节点的核心字段
- uuid:事件唯一标识(commit hash)
- parent:上一个事件的 uuid(对应 commit 的 parent)
- content:用户口述的事件内容
- diffusions:状态变化记录列表(对应 commit 的 diff)
- target_uuid:受影响的世界图节点
- field:被修改的字段
- old_value:修改前的值
- new_value:修改后的值
- narrative_text:渲染器生成的故事文本(渲染层产物)
- timestamp:事件发生时间(故事时间)
- created_at:事件写入时间(事务时间)
3.3 事件图的核心原则
- 事件 = 状态变化的 diff,不是完整状态 → 事件节点轻量
- 一个事件内不可能变了几千个节点——单事件影响范围有限
- 事件图只存储每个事件中状态的变化(diff),不是完整状态
4. 版本管理:Git 模式
4.1 Git 对象模型映射
直接参考 Git 的对象模型和逻辑,针对双图场景实现最简版本。
| Git 概念 | 引擎对应实现 |
|---|---|
| Blob(文件内容) | 世界图节点/边的当前状态 |
| Tree(目录结构) | 世界图的整体状态快照 |
| Commit(提交) | 事件节点(含 diffusions + 指向上一事件的 parent) |
| Ref(分支/HEAD) | 当前事件指针(指向事件链的 HEAD) |
| HEAD~N | 回退 N 个事件 |
git reset |
移动 HEAD 指针 + 反向撤销 diffusions |
git revert |
保留 HEAD,生成新的反向事件 |
git reflog |
事件链历史(所有 HEAD 移动记录) |
4.2 核心组件
- 事件节点 = Git Commit
uuid(commit hash)parent(上一个事件的 uuid)content(用户口述)diffusions(状态变化)narrative_text(渲染文本)timestamp/created_at
- 世界图 = Working Directory
- 只存当前状态
- 每次事件扩散写入 = 直接覆盖字段
- 不保留历史版本(历史在事件链里)
- HEAD 指针
- 一个字段,指向当前事件链的最新事件
- 回退 = 移动 HEAD + 反向撤销 diffusions
- 重放 = 从 HEAD 开始追加新事件
- 节点创建事件 =
created_by_event字段
- 世界图节点带
created_by_event: event_uuid - 反向撤销时,查此字段判断是否到达创建位置
- 旧事件链保留
- 和 Git 一样,旧 commit 永久保留
- 通过 HEAD 移动实现"删除",不物理删除事件节点
5. 回退机制
5.1 回退与重放的统一逻辑
回退和重放是连续的两步操作,不是独立的场景 完整流程:
- 用户发起回退(重写某事件 / 改某节点)
- 引擎按事件链反向撤销 diffusions(
new_value→old_value) - 一直撤销到目标节点创建的位置(该节点首次出现的事件)
- 从该位置重新开始叙事(重新跑子代理,生成新的事件链) 关键含义:
- 回退不是"回到过去就停下",而是"回到过去 + 重新开始"
- 重放的起点 = 受影响节点的创建事件
- 重放 = 重新跑子代理,生成新的事件和扩散
5.2 反向撤销的工程实现
- 每个事件节点的
diffusions字段已经记录old_value和new_value - 回退到事件 E = 从当前事件开始,依次将
new_value还原为old_value,直到 E - 时间复杂度 O(M),M = 要撤销的事件数(通常很小)
5.3 Git 操作对照
| Git 操作 | 引擎对应操作 |
|---|---|
git reset HEAD~N |
反向撤销 N 个事件的 diffusions |
| Working directory 变化 | 世界图回到目标事件时的状态 |
| 旧 commit 保留 | 旧事件节点保留(不物理删除) |
| 新 commit 覆盖 | 用户新输入事件后,新事件覆盖旧事件 |
git revert |
用户告诉引擎"按原来的事件重新跑" |
5.4 两种重放模式
- 手动重放(默认):
- 引擎回退到目标事件后,等待用户输入新事件
- 用户新输入的事件覆盖旧事件链
- 类似
git reset后重新 commit
- 自动重放:
- 用户告诉引擎"按原来的事件重新跑"
- 引擎按旧事件链的内容,依次重新执行
- 类似
git revert(保留历史,生成新的反向 commit)
5.5 节点创建事件的识别
- 节点创建时,记录是在哪个事件下创建的(
created_by_event: event_uuid) - 不需要复杂标记——一个字段就够了
- 反向撤销到某节点时,查
created_by_event就知道是否到达创建位置 - 节点本身的删除 = 不需要特殊处理,反向撤销到创建位置时,节点的
old_value = null(创建前不存在),撤销后自然消失
6. 用户编辑融合机制
6.1 用户编辑的语义
- 用户编辑影响的是当前世界的状态(直接修改世界图)
- 用户编辑产生的扩散需要到下一个事件才生效
- 即:用户编辑不立即触发扩散,扩散在下一次引擎事件时由子代理处理
- 用户编辑不被记录为事件(它是非引擎写入)
6.2 用户编辑优先原则
用户编辑影响当前世界状态,但扩散到下一个事件才生效。具体实现按"用户编辑优先"原则:
- 用户编辑直接覆盖世界图字段(当前状态立即变化)
- 下一个引擎事件 E2 的子代理看到的是用户编辑后的状态
- E2 的 diffusions 记录
old_value=用户编辑后的值, new_value=E2扩散后的值 - 回退 E2 时,世界图字段恢复为"用户编辑后的值"(即 old_value)
6.3 关键含义
- 用户编辑不会被引擎扩散"吞噬"——它作为 E2 的
old_value被保留 - 回退 E2 后,用户编辑仍然存在(世界图回到 E2 之前的状态,即用户编辑后的状态)
- 用户编辑是"非事件写入",不在事件图中记录,但通过 E2 的 old_value 间接保留
6.4 完整示例
- 初始:N.field = "value1"(E1 扩散写入)
- 用户编辑:N.field = "value2"(直接覆盖,无事件记录)
- E2 事件:子代理看到 N.field = "value2",扩散后改为 "value3"
- E2.diffusions = [{target: N, field: field, old_value: "value2", new_value: "value3"}]
- 回退 E2:N.field 恢复为 "value2"(用户编辑保留)
- 再回退 E1:N.field 恢复为 "value1"(用户编辑丢失,因为 E1 的 new_value="value1" 被撤销)
7. 与难题的映射
| 难题 | 解决方式 | 状态 |
|---|---|---|
| 难题 1(世界图记录) | 双图分离 + 节点四层分层 + 生命周期机制 | ✅ 核心已定 |
| 难题 4(数据写回) | 事件 diffusions 记录 + 世界图覆盖写入 | ✅ 已定 |
| 难题 5(事件重写) | Git 模式(事件链 + HEAD + 反向撤销 + 重放) | ✅ 已定 |
| 难题 10(用户编辑融合) | 用户编辑优先 + 下个事件 baseline + old_value 保留 | ✅ 已定 |
8. 不采用的方案
- ❌ Graphiti 的 Bi-temporal Edge(4 个时间戳 + 失效逻辑)——太复杂,Git 模式更简单
- ❌ 向量数据库作为主存储——黑盒,不可追变动,重建耗时
- ❌ 周期性快照——不需要,事件链本身就是完整历史
- ❌ 图数据库依赖(Neo4j/FalkorDB)——纯 JSON 文件存储,白盒可见
- ❌ RAG 黑盒检索——向量查询可作为辅助检索手段,但不作为主存储
9. 待澄清的子问题(下一阶段研究内容)
节点 schema
- 节点的具体字段设计(name, type, aliases, summary, lifecycle, created_by_event, ...?)
- 针对具体的小说场景设计(角色节点、物品节点、背景节点字段不同?)
- 生命周期字段的具体值?(permanent / temporary / event-scoped?)
- "演出后消失"的触发时机?(渲染完成后?事件结束后?章节结束后?)
- 临时节点消失后,相关边怎么处理?(级联删除?保留为"已失效"?)
- 临时节点的扩散记录是否保留?(节点消失,但它对其他节点的影响是否保留?)
边 schema
- 边的字段设计(fact + 双图如何引用?)
- 边的语义化状态如何表达?
- 事件图与世界图的连接方式?
- 选项 A:事件节点存储
affected_nodes: [world_node_uuid]列表 - 选项 B:事件节点存储
diffusions: [{target_uuid, field, old_value, new_value}](已采用) - 选项 C:双向引用——事件节点引用世界节点,世界节点的边记录
source_event: event_uuid
文件目录布局
- nodes/、edges/、events/、...?
- 事件链存储格式?(JSON 文件 / JSONL / SQLite)
- HEAD 指针存储位置?
其他
- 是否支持多分支叙事?(多个 HEAD 指针,类似 Git branch)
- 是否需要 reflog(HEAD 移动历史)?
- 旧事件节点保留多久?(永久?还是有 GC 机制?)
- 手动重放时,用户是否能看到旧事件链作为参考?
- 自动重放时,旧事件链的子代理输出是否复用?还是重新跑子代理?
- 世界图节点的
created_by_event字段如何更新?(节点重建时更新?) - 节点稳定性如何保证?(长篇数千事件,节点 ID 策略)
10. 优势总结
- 简单:抄 Git 成熟心智模型,不需要复杂的 Bi-temporal 逻辑
- 白盒:JSON 文件存储,用户可见、可追变动
- 轻量:事件节点只存 diff,不存完整状态
- 可回退:反向撤销 + 重放,支持任意事件重写
- 用户编辑友好:用户编辑优先,不被引擎扩散吞噬
- 不依赖外部服务:纯 JSON 文件,不需要图数据库或向量数据库
- 支持长篇:事件链 + 生命周期机制,避免节点爆炸
本文由 AIGC 调研生成,仅做研究笔记和参考,真实性需要自行考证。