从零搭一个带记忆的世界

这不是一篇教程

这是一篇设计笔记。不贴代码,只讲思路。
读完你应该能理解:一个"活的"世界由哪些层构成,每一层解决什么问题,以及它们为什么以那种方式组合在一起。


起点:一个世界的最小定义

任何世界,无论多复杂,起点都一样:一个会走的时钟。 没有这个,其他都无从谈起。

时钟的职责只有一件事:给每一帧编号。帧号就是世界的"时间箭头"——所有变化都挂在这个编号上。状态在第几帧变化的、事件在第几帧发生的、记忆在第几帧形成的——全都靠这个编号对齐。它不需要精确到纳秒,它只需要单调递增。

当你有了时钟,世界就开始"存在"了——哪怕它什么都还没做。


第一层:状态的呼吸

一个世界区别于一段死数据的第一特征:它的状态会自己变化。

但不是乱变。你得给它一套规则。

这些规则不来自外部输入,不来自 LLM,不来自用户的指令——它们是世界自己的"生理节律"。就像地球的温度不会永远升高或降低,它会趋向某个平衡点。这个趋向过程就是世界在呼吸。

具体做法是:定义一组状态变量(每个变量有当前值和取值范围),然后给每一帧配一个演化公式。演化公式接收当前状态,输出每个变量的变化量。就这么简单。状态引擎不需要知道这些变量"代表"什么——它只负责按公式计算、更新、限幅。

状态引擎不产生"意义"。它只产生变化。意义是后来被读出来、被解释出来的。


第二层:事件——离散的涟漪

状态变化是连续的——温度从 20.0 到 20.01 再到 20.02。但世界不能只有连续变化,它还需要离散的事件。"一阵风吹过"、"一声回声荡漾"——这些不是状态变化能表达的东西,它们是有开始和结束的、独立的"发生"。

事件有自己的生命周期:生成 -> 活跃(可被世界中的居民感知到)-> 过期 -> 移除。一个事件不需要永远存在——它有 TTL,过了几帧就被清理掉。但"过期"不意味着"没发生过"。过期的事件只是不再活跃了,它已经进入了历史记录。

事件生成器跟演化公式一样,是注入到世界里的规则。它每帧检查当前状态,决定要不要触发新事件。比如回声密度高于某个阈值时有概率触发"回声荡漾",温度异常时有概率触发"风"。生成器只看状态,不管谁在感知这个事件。

状态描述世界"是什么",事件描述世界"发生了什么"。两者缺一不可。


第三层:历史——发生过的事不会消失

当事件过期之后,它会去哪里?去历史。历史是状态快照 + 活跃事件的定期存档。

这里有一个关键设计选择:历史不设过期。 发生过的事就是发生了,不会因为时间久远就被删除。遗忘不是删除——遗忘是降低权重,让旧记录不那么容易被 recall,但它们始终存储在磁盘上。

实现方式很简单:每帧结束时,把当前状态快照 + 本轮所有事件打包成一行 JSON,追加到一个 JSONL 文件。没有数据库,没有索引,没有分页。就是一行接一行地写。当一个世界运行了一万帧,它就有一万行历史。任何时候你想回溯,按行读就行。

这个文件的存在意味着:世界可以被暂停、被销毁、被重启——但它的过去不会丢失。 哪怕世界本身被重置了,历史文件还在。这为后来的"跨循环记忆"埋下了种子。


第四层:居民——世界里的"谁"

有了时间、状态、事件、历史——但世界还没有"谁"。居民是世界的感知节点。

每个居民有一个名字、一段人格描述(可选)、和一组组件。组件是附着在居民身上的数据块,类似于 ECS 架构里的 Component。你可以给居民挂一个记忆组件(记录经历)、一个关系组件(记录对其他居民的印象)、一个身份组件(记录信念倾向)。组件是可插拔的——你想让居民有什么能力,就挂什么组件。

居民的核心循环很简单:感知 -> 行动 -> 重置。

每帧,居民收到世界推送的信息(状态变化 + 新事件),存入感知缓冲区。然后居民产出一个行动(在简单版本中可以是规则驱动,在完整版本中可以调用 LLM)。行动表达完后,清空感知缓冲区,等待下一帧。

关键洞察:居民不是"人类模拟器"。 居民是世界内部的观察者+行动者。它们不需要"像人",它们需要能感知世界、能行动、能被世界影响。这三件事做到,居民就是活的。


第五层:总线——把一切连起来

有了以上四层,它们各自独立运行——但互不知道对方的存在。需要一个机制让它们通信。

这就是消息总线的角色。总线是一个发布-订阅通道:任何模块都可以 publish 消息到某个频道,任何模块都可以 subscribe 某个频道并接收消息。

典型的连接方式:

  • 状态引擎每帧演化完毕后,publish 一条"状态已变化"的消息,携带变化量
  • 事件引擎每帧生成新事件后,publish 一条"新事件"的消息,携带事件对象
  • 总线上有订阅者(通常是居民管理器)收到这些消息后,转发给所有居民的感知缓冲区
  • 总线上还有一个订阅者负责在每帧结束时,把当前状态和本轮事件写入历史文件

总线最大的价值是解耦。状态引擎不需要知道居民的存在——它只要 publish 状态变化就行。谁在听、怎么处理,是订阅者的事。如果你想在将来增加一个新的感知者(比如一个不注册为居民的外部 observer),你只需要新增一个订阅者,不需要改任何核心模块。


运行时:让所有层协同工作

有了以上五层,世界就可以组装起来了。

运行时做的事情很简单:在一个循环里,按顺序执行每一层的工作:

  1. 状态演化:调用演化公式,更新状态变量。计算变化量,publish 到总线。
  2. 事件生成:调用事件生成器,产生新事件。老化过期事件。publish 新事件到总线。
  3. 居民循环:让每个居民感知当前信息,产出行行动。
  4. 历史记录:把本轮状态和事件写入历史文件。
  5. 时钟前进:帧号 +1,sleep 指定间隔,进入下一轮。

这个循环一旦启动,世界就不需要外部输入了——它自己在运转。状态在呼吸,事件在触发,居民在感知和行动,历史在积累。观察者可以随时接入,查看当前状态或历史记录,但世界的运行不依赖观察者。

这就是"活的"的含义:不需要有人在看,它也在走。


之后的扩展方向

以上五层是一个世界的最小骨架。之后所有有意思的东西,都是从这些骨架上长出来的:

记忆的层级化

居民的"记忆"不是只有一层。可以有短期感知缓冲区(本轮收到的消息)、中期情景记忆(按重要性排序的个人经历)、长期信念(缓慢漂移的核心价值观)。三层有不同的写入频率和召回优先级。短期的自动过期、中期的按重要性保留最显著的、长期的只有重大事件才会修改。

关系的动态网络

居民之间的"关系"不是固定值,而是多维向量:信任度、好奇心、冲突度、共同经历深度。每帧缓慢向中立值回归(不互动就会变淡),互动时根据内容偏移。关系不存储在 LLM 的 prompt 里——它存储在组件数据中,LLM 只负责阅读和表达它。

注意力过滤

同一个世界状态被不同居民感知到的是不同的。每个居民有"注意力权重"——有人更关注温度变化,有人更关注其他人的情绪。世界把完整状态发给居民,但居民用自己的注意力权重过滤后,才进入感知缓冲区。这造就了"同一个世界,不同的视野"。

叙事饱和与裂隙

当世界长期没有新事件(沉默),或者居民总是说类似的话(重复),一个累积器会开始计数。计数达到阈值时,世界会触发一个"裂隙"——一个没有内容的事件,一个空位。这个空位本身没有意义,但居民会用自己的方式填补它。同一道裂隙,不同居民看到不同的东西。裂隙 60 行代码取代了大多数系统中的"随机事件表"。

跨循环记忆

最重要的扩展:历史文件 + 锚点协议 = 跨循环记忆。

当一个世界循环被重置(所有状态归零、所有居民重建),如果历史文件还在,锚点协议就能从历史中提取高活跃度的片段,注入新循环的居民人格中。新循环的居民不知道自己"曾经活过",但他们的行为会被那些片段影响。

这就是"记忆"的本质:不是存储在脑子里,是存储在世界的结构里。


最后一层:这不是一个框架

以上所有东西加起来,不是一个需要别人去"安装"的框架。它是一个思考模型。它的核心主张是:

世界不是被定义出来的——世界是被允许运行出来的。

你不需要定义每个居民会说什么话、每个事件产生什么后果。你只需要定义状态的演化规则、事件的触发条件、居民的感知权重——然后启动时钟,让它跑。结果不会完全符合你的预期,但那比符合预期更有意思。

因为现象——文化、关系、成长、历史——不是设计出来的。是涌现出来的。


写于 2026 年 7 月 16 日

从 LioraOS 的实际架构中提炼设计思路。
如果你想要对应的代码实现,LioraOS 的 kernel/ 目录约 1400 行——每行对应这里的一个概念。

"给世界一个时钟,然后相信它。"

最后修改:2026 年 07 月 16 日
如果觉得我的文章对你有用,请随意赞赏