跳到主要内容

记忆的形态:三类记忆 + MemCube 容器

30 秒导读: MemOS 想让"给大模型的记忆"变成一件可以打包、搬运、共享的东西。它的答案是 MemCube——一个盒子,里面并排放着四种不同形态的记忆:能读的明文、能直接喂给模型的 KV-Cache、固化进权重的 LoRA 参数、还有用户偏好。这一章只讲**"记忆长什么样、装在哪"**, 不讲怎么抽取、怎么检索、怎么调度(那是后面几章的事)。

本章是 MemOS 全景与阅读地图 之后的第一站,专讲数据模型。读完你应该能回答: 一条记忆在 MemOS 里到底是什么数据结构?为什么要分成好几种?它们怎么被一个统一的容器管起来?


1. 这是什么(零基础也能懂)

先说要解决的问题

大模型本身没有长期记忆。你这轮对话告诉它的事,下轮它可能就忘了;它学到的所有"常识" 都焊死在权重里,改不动也搬不走。想给它"记性",业界有好几条完全不同的路子:

  • 把过去的内容写成文字存起来,用的时候再检索、拼进上下文(最常见,像做笔记)。
  • 把过去内容预先算成模型的中间状态(KV-Cache),下次直接加载,省掉重新读一遍的开销。
  • 干脆把知识训练进一小块可插拔的权重(LoRA),让模型"天生就会"。

问题是:这三条路的数据形态天差地别——一个是 JSON 文本,一个是 GPU 上的张量,一个是权重文件。 没有一个统一的容器,它们就没法被同一套系统管理、保存、分享。

MemOS 的答案:MemCube

MemOS 把这个容器叫 MemCube(记忆立方)。一句话:

MemCube = 一个能 load / dump / 共享的盒子,里面并排放着几类不同形态的记忆。

它像一个"记忆的集装箱":你可以把整盒记忆从磁盘加载进来、原样导出到另一个目录、 甚至从远程仓库(如 HuggingFace)拉一个别人做好的记忆盒直接用。

一句话直觉/类比

用计算机存储层次来记最省事:

MemOS 里的记忆类比特点
明文记忆(textual)磁盘容量大、能读能查、访问要"读进来"
KV-Cache 激活记忆(activation)内存 / 缓存已经是模型能直接用的形态,快,但占显存、易失
LoRA 参数记忆(parametric)固化 / 烧录进芯片变成模型本身的一部分,不用检索,但改起来最贵

偏好记忆(preference)是明文记忆的一个特化分支,专门记"这个用户喜欢什么风格"。

用起来什么样

从一个目录把一整盒记忆加载进来,本质就一行:

# 示意,非源码:从磁盘目录还原一个 MemCube
cube = GeneralMemCube.init_from_dir("./my_cube")

cube.text_mem # 明文记忆(可检索的图谱/列表)
cube.act_mem # KV-Cache 激活记忆
cube.para_mem # LoRA 参数记忆
cube.pref_mem # 偏好记忆

cube.dump("./backup_cube") # 原样导出到另一个空目录

四个槽位(text_mem / act_mem / para_mem / pref_mem)是 MemCube 对外最重要的接口, 真实定义见 src/memos/mem_cube/general.py:186-240(四个 @property)。


2. 顶层全景(它大概怎么转)

一张图:盒子、槽位、实现

MemCube 是个空壳容器,四个槽位各自按配置挑一个具体实现填进去。怎么读这张图: 从上到下是"容器 → 四个槽位 → 每个槽位可选的实现类",最下面一行是它们各自的存储介质。

┌───────────────────────────────┐
│ GeneralMemCube │ ← 容器:load / dump / 共享
│ (mem_cube/general.py) │
└───────────────────────────────┘
┌───────────────┬───────────────┬───────────────┐
▼ ▼ ▼ ▼
text_mem act_mem para_mem pref_mem
(明文) (激活/KV) (参数/LoRA) (偏好)
│ │ │ │
▼ ▼ ▼ ▼
TreeTextMemory KVCacheMemory LoRAMemory Preference
NaiveText… VLLMKVCache… (占位/TODO) TextMemory
│ │ │ │
▼ ▼ ▼ ▼
JSON / 图谱DB DynamicCache 权重文件 JSON
(磁盘) (显存/pickle) (磁盘) (磁盘)

谁来决定每个槽填哪个实现? 靠配置 + 一个工厂函数 MemoryFactory.from_config (src/memos/memories/factory.py:34-42)。配置里写 backend: "tree_text",工厂就 new 一个 TreeTextMemory 塞进 text_mem

四个槽位各是什么

槽位装的记忆形态允许的 backend存储介质
text_mem明文记忆naive_text / general_text / tree_textJSON、图数据库
act_memKV-Cache 激活记忆kv_cache / vllm_kv_cacheDynamicCache(pickle)
para_memLoRA 参数记忆lora(当前为占位)权重文件
pref_mem偏好记忆pref_textJSON

每个槽位允许哪些 backend,是在配置校验里硬约束的,见 src/memos/configs/mem_cube.py:63-105(四个 validate_* 校验器)。写错 backend 会直接报 ConfigurationError

主线走一遍(不进代码)

一个 MemCube 从"无"到"能用",大致是这条线:

  1. 读配置 → 每个槽位拿到一个 backend 名字。
  2. GeneralMemCube.__init__ 对四个槽位分别调 MemoryFactory.from_config,new 出实现类。
  3. 若 backend 是 "uninitialized",该槽位置为 None(允许只装部分记忆)。
  4. cube.load(dir) 让每个非空槽位各自从目录里读回自己那部分数据。
  5. 用的时候,上层通过 cube.text_mem / cube.act_mem … 拿到具体记忆对象去操作。

初始化时"按需跳过空槽位"这点很关键——见 general.py:28-48,四个槽位都写成 ... if config.xxx.backend != "uninitialized" else None


3. 核心原理:三/四类记忆形态

这一节把四种记忆逐个讲清楚:它是什么数据、装什么、怎么存。它们共享同一套 load/dump 接口(都继承 BaseMemory,src/memos/memories/base.py:4-19),但内部数据结构完全不同。

3.1 明文记忆(textual)——像磁盘上的笔记

要解决的小问题: 把对话/文档里的事实,存成人和机器都能读、能检索的文字条目。

核心数据结构 TextualMemoryItem 这是整个明文记忆的原子单位,结构极简——只有三个字段:

# 示意,非源码:一条明文记忆长这样
{
"id": "uuid", # 唯一 id
"memory": "用户住在杭州", # 记忆正文(一句话事实)
"metadata": { ... } # 一大坨元数据:来源、版本、类型、标签…
}

真实定义见 src/memos/memories/textual/item.py:299-315(TextualMemoryItem,字段 id / memory / metadata)。重点全在 metadata——正文就一句话,复杂度都在元数据里。

元数据分三层,越往下越"结构化"。 MemOS 用继承把元数据做成一个由浅入深的层次:

元数据类加了什么用途
TextualMemoryMetadatastatus / version / history / source / tags…通用记忆的基础字段
TreeNodeTextualMemoryMetadatamemory_type / sources / embedding / background图谱节点专用,支持分层检索
SearchedTreeNodeTextualMemoryMetadatarelativity(相似度分)检索返回时才有
PreferenceTextualMemoryMetadatapreference_type / dialog_id偏好记忆专用

对应源码:item.py:94(TextualMemoryMetadata)、item.py:175 (TreeNodeTextualMemoryMetadata)、item.py:276(SearchedTreeNodeTextualMemoryMetadata)、 item.py:284(PreferenceTextualMemoryMetadata)。

分层记忆类型(memory_type)。 图谱节点的元数据里有个关键字段 memory_type,把记忆按 生命周期分层,见 item.py:178-189:

memory_type白话
WorkingMemory工作记忆:当前活跃、随时可能被换掉的
LongTermMemory长期记忆:沉淀下来的事实
UserMemory用户记忆:关于某个用户的稳定画像
还有 OuterMemory / ToolSchemaMemory / PreferenceMemory外部/工具/偏好等特化类型

这三层各有容量上限(见 tree.py:81-87memory_size 默认 WorkingMemory: 20LongTermMemory: 1500UserMemory: 480),是后续"重构/淘汰"章节的伏笔。

溯源与版本——两个容易被忽略但很关键的子结构。

溯源(provenance)SourceMessage(item.py:16-46):每条记忆都能指回它是从哪来的—— 哪次对话(role / chat_time / message_id)、哪个文档(doc_path)、哪个文件。 它的注释直说目的是"memory provenance / traceability",让记忆可审计、可回滚、可去重。

版本/归档ArchivedTextualMemory(item.py:49-91):当一条记忆因冲突或重复被新内容更新时, 旧内容不会被直接扔掉,而是存成一个归档版本,挂在原节点的 history 字段里 (见 metadata.history,item.py:125-128)。update_type 会标明这次更新是 conflict / duplicate / extract 等哪种情况。

具体实现有好几个,从简到繁: NaiveTextMemory(最朴素)、GeneralTextMemoryTreeTextMemory(图谱式,最强)。其中 TreeTextMemory(src/memos/memories/textual/tree.py:39) 把记忆存进 Neo4j 图数据库,支持嵌入向量、BM25、重排、联网检索——这是后面第 4、5 章的主角。 它的 load/dump 就是把整张图 import/export 成 JSON(tree.py:439-476)。

3.2 KV-Cache 激活记忆(activation)——像内存里的缓存

要解决的小问题: 如果每轮都把同一段长长的用户背景重新喂给模型、重新算一遍注意力,太浪费。 能不能把"算好的中间状态"存下来,下次直接加载?

思路: Transformer 推理时,每层都会为已读的 token 算出 Key/Value 张量,缓存在 KV-Cache 里。 MemOS 的做法是——提前把一段文本"读"一遍,把产生的 KV-Cache 存下来当记忆。下次要用时, 直接把这个 cache 拼到模型的注意力里,模型就"仿佛已经读过"这段背景,省掉了重算。

核心动作 extract:文本 → KV-Cache。src/memos/memories/activation/kv.py:32-52 (KVCacheMemory.extract):它调 self.llm.build_kv_cache(text)一次前向,把文本变成 一个 DynamicCache,再包成 KVCacheItem

# 示意,非源码:extract 的核心两步
kv_cache = llm.build_kv_cache(text) # 一次前向,得到 DynamicCache
item = KVCacheItem(memory=kv_cache, # 把 cache 当"记忆正文"装进 item
metadata={"source_text": text})

build_kv_cache 真身在 src/memos/llms/hf.py:354-393:把文本套上 chat 模板、tokenize、 跑一次前向,返回 DynamicCache

数据结构 KVCacheItem 和明文 item 结构类似(id / memory / metadata),但 memory 字段 装的不是字符串,而是一个 DynamicCache 对象——见 src/memos/memories/activation/item.py:32-43。 它继承自 ActivationMemoryItem(item.py:12-15)。因为 DynamicCache 是任意 torch 对象, 所以特意开了 arbitrary_types_allowed=True

合并多个 cache。 一个用户可能有多段背景各自缓存,用的时候要拼成一个。 get_cache_concat_caches(kv.py:63-81kv.py:200-255)会逐层把多个 cache 的 Key/Value 张量 torch.cat 起来,合成一个大 cache 喂给模型。

存储方式: 因为是张量对象,dump/loadpickle 落盘(kv.py:139-198), 不是人可读的 JSON。这也印证了"内存/缓存"的类比——它是给机器直接用的,不是给人读的。

vLLM 变体的巧思: VLLMKVCacheItem(item.py:46-56)里 memory 反而存的是字符串, 因为 vLLM 在服务端自己管 KV-Cache,客户端只需保存"用来预热的 prompt"即可。

3.3 LoRA 参数记忆(parametric)——像烧进芯片的知识

要解决的小问题: 有些知识不该每次检索,而应该"内化"成模型能力。做法是训练一小块 LoRA(低秩适配)权重——插上就改变模型行为,拔掉就恢复。

当前状态:占位。 诚实地说:这一支在本 commit 里还是占位符,没有真正实现src/memos/memories/parametric/lora.py:1-6 顶部明确写着 "This file currently serves as a placeholder … Please do not use this as a functional module yet"。 LoRAMemory.dump 目前只往文件里写一个字节串 b"Placeholder"(lora.py:31-41),load 是空的。

尽管如此,它在架构上占了 para_mem 这个槽位,backend: "lora" 是配置允许的合法值 (configs/mem_cube.py:89)。这是 MemOS 为"参数化记忆"预留的位置,数据模型上三类记忆的 三足鼎立是完整的,只是这一足的实现留待未来。

3.4 偏好记忆(preference)——明文记忆的特化分支

要解决的小问题: "用户喜欢简洁回答""偏好中文"这类偏好,和普通事实不太一样, 值得单独一类来管。

它本质仍是明文记忆。 pref_mem 槽位在容器里的类型标注就是 BaseTextMemory (general.py:44-48base.py:22),backend 只能是 pref_text(configs/mem_cube.py:100), 实现为 PreferenceTextMemory / SimplePreferenceTextMemory (src/memos/memories/textual/preference.pysimple_preference.py:16)。

特化在元数据上。 用的是 PreferenceTextualMemoryMetadata(item.py:284-296), 多了 preference_type(explicit_preference 显式 / implicit_preference 隐式)、 dialog_idoriginal_textpreference 等字段,专门刻画"从哪段对话里读出了什么偏好"。


4. 工厂:配置怎么变成实现

四种记忆的所有具体实现,最终都由同一个工厂backend 字符串挑选。 见 src/memos/memories/factory.py:19-42,核心是一张映射表 + 一个查表函数:

# 示意,非源码:工厂就是一张 backend → 类 的查表
backend_to_class = {
"naive_text": NaiveTextMemory,
"general_text": GeneralTextMemory,
"tree_text": TreeTextMemory,
"kv_cache": KVCacheMemory,
"vllm_kv_cache": VLLMKVCacheMemory,
"lora": LoRAMemory,
"pref_text": PreferenceTextMemory,
# …
}
cls = backend_to_class[config.backend] # 查表
return cls(config.config) # new 出来

真实 backend_to_classfactory.py:22-32,from_configfactory.py:34-42 (非法 backend 抛 ValueError)。

注意一个不对称: 工厂表里其实还有 simple_tree_text / simple_pref_text 等实现, 但 GeneralMemCube 的配置校验只放行其中一部分(比如 text_mem 只允许 naive_text/general_text/tree_text,见 configs/mem_cube.py:67)。也就是说—— 工厂能造的 ≠ MemCube 槽位允许装的,后者更严格。


5. 巧妙之处(可借鉴的技术)

"容器 + 槽位 + 工厂"三段式,让异构记忆统一可管。 明文是 JSON、激活是张量、参数是权重, 三者毫无共性;MemOS 用一个 BaseMemoryload/dump 抽象(base.py:4-19)把它们收敛到同一接口, 再用工厂按配置装配。加一种新记忆形态,只要实现 load/dump 并在工厂表登记即可。

槽位可空(uninitialized)= 按需付费。 不是每个应用都要 KV-Cache 或 LoRA。 general.py:28-48 让任何槽位都能置 None,load/dump 时自动跳过(general.py:74-88)。 你可以只用明文记忆,零额外开销。

记忆不"就地覆盖",而是"版本化归档"。 冲突/重复更新时旧内容进 history (ArchivedTextualMemory,item.py:49-91),配合 SourceMessage 溯源,让记忆可审计、可回滚—— 这为后面"冲突消解与重构"打了地基。

init_from_remote_repo:记忆可以像模型一样被分享。 general.py:164-184 支持从 HuggingFace 这类远程仓库直接拉一个做好的 MemCube。记忆盒变成可分发的 artifact,这是 "MemCube 作为一等公民"理念的直接体现。


6. 边界与局限

  • LoRA 参数记忆尚未实现。 para_mem 目前是占位符,dump 只写 b"Placeholder" (lora.py:1-6lora.py:31-41)。三类记忆里,只有明文和激活是真能用的。
  • KV-Cache 依赖具体推理栈且体积大。 extract 要真跑一次前向(kv.py:44),需要 torch、 显存;落盘用 pickle(kv.py:183-198),不跨版本、不可读,且加载有反序列化信任问题 (代码里 add_safe_globals + 宽泛 except 兜底,kv.py:155kv.py:179)。
  • 槽位类型是固定四格。 容器写死 text/act/para/pref 四个槽(base.py:19-22), 想加第五类记忆得改容器本身,不是纯配置能扩展的。
  • 本章只讲"装什么",不讲"怎么装进去"。 记忆如何从原始对话被抽取TextualMemoryItem、 如何检索、如何调度刷新——都在后续章节,本章刻意不展开。

7. 横向对比与延伸阅读

同类系统的取舍:多数"记忆库"只做明文 + 向量检索这一种形态;MemOS 的差异化在于把 激活(KV-Cache)与参数(LoRA)也纳入同一容器,并强调记忆的可打包、可分享、可版本化


8. 代码地图(导航索引)

主题文件路径关键符号
记忆容器(四槽位)src/memos/mem_cube/general.pyGeneralMemCubetext_mem/act_mem/para_mem/pref_mem property、loaddumpinit_from_dirinit_from_remote_repo
容器抽象基类src/memos/mem_cube/base.pyBaseMemCube
槽位 backend 校验src/memos/configs/mem_cube.pyGeneralMemCubeConfigvalidate_text_mem/validate_act_mem/validate_para_mem/validate_pref_mem
记忆工厂(查表装配)src/memos/memories/factory.pyMemoryFactorybackend_to_classfrom_config
记忆统一接口src/memos/memories/base.pyBaseMemory.loadBaseMemory.dump
明文记忆项 & 元数据src/memos/memories/textual/item.pyTextualMemoryItemTextualMemoryMetadataTreeNodeTextualMemoryMetadataSourceMessageArchivedTextualMemoryPreferenceTextualMemoryMetadata
明文记忆实现(图谱)src/memos/memories/textual/tree.pyTreeTextMemoryloaddumpmemory_size
明文记忆抽象src/memos/memories/textual/base.pyBaseTextMemory
KV-Cache 激活记忆src/memos/memories/activation/kv.pyKVCacheMemory.extractget_cache_concat_cachesfrom_textual_memory
激活记忆项src/memos/memories/activation/item.pyKVCacheItemActivationMemoryItemVLLMKVCacheItem
激活记忆抽象src/memos/memories/activation/base.pyBaseActMemory.extract
构建 KV-Cachesrc/memos/llms/hf.pybuild_kv_cache
LoRA 参数记忆(占位)src/memos/memories/parametric/lora.pyLoRAMemory(placeholder)
参数记忆抽象src/memos/memories/parametric/base.pyBaseParaMemory
偏好记忆src/memos/memories/textual/simple_preference.pySimplePreferenceTextMemory