【Agent】MemOS 源码笔记---(2)---TreeTextMemory
【Agent】MemOS 源码笔记---(2)---TreeTextMemory
0x00 摘要
TreeTextMemory 提供了一个完整的记忆管理系统,能存储、组织、检索和维护各种类型的文本记忆、适用需要复杂记忆管理的AI系统。这是一个基于图的、树形明文记忆,支持以结构化方式组织、关联并检索记忆,同时保留丰富的上下文信息与良好的可解释性。我们可以通过这个TreeTextMemory 对象与庞大的知识库进行交互,为AI赋予专业的领域记忆。当前使用Neo4j作为后端,未来计划支持更多图数据库。
因为字数太多,因此把TreeTextMemory拆分为两部分,本篇介绍基本概念和如何管理,下一篇介绍如何搜索。
0x01 基本概念
1.1 特色
TreeTextMemory 的特色如下:
- 结构层次: 从原始文本或对话中提取结构化记忆,即像思维导图一样组织记忆——在图数据库中存储他们作为节点。
- 图风格的链接: 将记忆链接成层次结构和语义图,节点可以有父母、孩子和交叉链接,实现超越纯粹的层次结构-建立多跳推理链。
- 语义搜索+图扩展: 使用向量相似度+图遍历进行搜索,结合向量和图形的优点。
- 可解释性: 追踪记忆是如何连接、合并或随时间演变的。
TreeTextMemory 适合如下场景:
-
你需要带有可解释关系的层级基于图的明文记忆。
-
你想存储结构化知识并追踪连接关系。
-
适用于知识图谱、概念树和多跳推理。
1.2 定义
TreeTextMemory 的略图如下:
TreeTextMemory
- uses MemoryManager
- uses Neo4jGraphDB
- uses Embedder
- uses LLM
- uses Searcher
- uses GraphMemoryRetriever
- uses TaskGoalParser
- uses MemoryReasoner
- uses Reranker
其中关键组件为:
-
LLM:
- extractor_llm:用于提取记忆特征
- dispatcher_llm:用于任务分发和处理
-
Embedding模型:
- embedder:生成文本向量表示
-
图数据库:
- graph_store:存储记忆节点和关系,图链接有助于检索纯向量搜索可能遗漏的上下文。
-
Reranker:
- reranker:对检索结果进行重新排序
-
互联网检索器:
- internet_retriever:从互联网获取相关信息。
-
memory_manager:
- 负责协调和管理记忆的整个生命周期,包括添加、存储、维护和清理,是记忆系统的核心管理组件。
具体代码如下。
class TreeTextMemory(BaseTextMemory):
"""General textual memory implementation for storing and retrieving memories."""
def __init__(self, config: TreeTextMemoryConfig):
"""Initialize memory with the given configuration."""
time_start = time.time()
self.config: TreeTextMemoryConfig = config
self.extractor_llm: OpenAILLM | OllamaLLM | AzureLLM = LLMFactory.from_config(
config.extractor_llm
)
time_start_ex = time.time()
self.dispatcher_llm: OpenAILLM | OllamaLLM | AzureLLM = LLMFactory.from_config(
config.dispatcher_llm
)
time_start_em = time.time()
self.embedder: OllamaEmbedder = EmbedderFactory.from_config(config.embedder)
time_start_gs = time.time()
self.graph_store: Neo4jGraphDB = GraphStoreFactory.from_config(config.graph_db)
time_start_rr = time.time()
if config.reranker is None:
default_cfg = RerankerConfigFactory.model_validate(
{
"backend": "cosine_local",
"config": {
"level_weights": {"topic": 1.0, "concept": 1.0, "fact": 1.0},
"level_field": "background",
},
}
)
self.reranker = RerankerFactory.from_config(default_cfg)
else:
self.reranker = RerankerFactory.from_config(config.reranker)
self.is_reorganize = config.reorganize
time_start_mm = time.time()
self.memory_manager: MemoryManager = MemoryManager(
self.graph_store,
self.embedder,
self.extractor_llm,
memory_size=config.memory_size
or {
"WorkingMemory": 20,
"LongTermMemory": 1500,
"UserMemory": 480,
},
is_reorganize=self.is_reorganize,
)
time_start_ir = time.time()
# Create internet retriever if configured
self.internet_retriever = None
if config.internet_retriever is not None:
self.internet_retriever = InternetRetrieverFactory.from_config(
config.internet_retriever, self.embedder
)
1.3 记忆结构
每个节点在TreeTextMemory 是一个 TextualMemoryItem:
id: 唯一记忆ID(如果省略则自动生成)memory: 主要文本metadata: 包括层次结构信息、嵌入、标签、实体、源和状态
1.4 元数据字段
TreeNodeTextualMemoryMetadata 是元数据。需要使用有意义的标签和背景——它们有助于组织你的图进行多跳推理。
| 字段 | 类型 | 描述 |
|---|---|---|
memory_type |
"WorkingMemory", "LongTermMemory", "UserMemory" |
生命周期分类 |
status |
"activated", "archived", "deleted" |
节点状态 |
visibility |
"private", "public", "session" |
访问范围 |
sources |
list[str] |
来源列表 (例如: 文件, URLs) |
source |
"conversation", "retrieved", "web", "file" |
原始来源类型 |
confidence |
float (0-100) |
确定性得分 |
entities |
list[str] |
提及的实体或概念 |
tags |
list[str] |
主题标签 |
embedding |
list[float] |
基于向量嵌入的相似性搜索 |
created_at |
str |
创建时间戳(ISO 8601) |
updated_at |
str |
最近更新时间戳(ISO 8601) |
usage |
list[str] |
使用历史 |
background |
str |
附加上下文 |
1.5 API总结
TreeTextMemory的API如下。
初始化为:
TreeTextMemory(config: TreeTextMemoryConfig)
核心方法为:
| 方法 | 描述 |
|---|---|
add(memories) |
添加一个或多个记忆(项目或字典) |
replace_working_memory() |
更换所有的WorkingMemory节点 |
get_working_memory() |
得到所有的WorkingMemory节点 |
search(query, top_k) |
使用向量+图搜索检索top-k个记忆 |
get(memory_id) |
通过ID获取单个记忆 |
get_by_ids(ids) |
通过IDs获取多个记忆 |
get_all() |
将整个记忆图导出为字典 |
update(memory_id, new) |
通过ID更新记忆 |
delete(ids) |
通过IDs删除记忆 |
delete_all() |
删除所有的记忆和关系 |
dump(dir) |
在目录中将图序列化为JSON |
load(dir) |
从保存的JSON文件加载图 |
drop(keep_last_n) |
备份图和删除数据库,保留N个备份 |
当调用 dump(dir), 系统写到:
<dir>/<config.memory_filename>
这个文件包含一个JSON结构,有 nodes and edges. 它可以使用 load(dir)重新加载.
0x02 示例
下面是如何是如何使用 TreeTextMemory 的示例,当运行此示例时,工作流如下:
-
抽取: 使用LLM从原始文本中提取结构化记忆
-
嵌入: 为相似性搜索生成向量嵌入
-
存储和链接: 将具有关系的节点添加到图数据库(Neo4j)中
-
搜索: 通过向量相似度查询,然后通过图跳数展开结果
2.1 创建 TreeTextMemory 配置
TreeTextMemoryConfig 的定义为:
- 你的嵌入(创建向量),
- 你的图数据库后端(Neo4j),
- 记忆抽取器(基于LLM)(可选).
from memos.configs.memory import TreeTextMemoryConfig
config = TreeTextMemoryConfig.from_json_file("examples/data/config/tree_config.json")
2.2 初始化 TreeTextMemory
from memos.memories.textual.tree import TreeTextMemory
tree_memory = TreeTextMemory(config)
2.3 抽取结构化记忆
使用记忆抽取器将对话、文件或文档解析为多个TextualMemoryItem.
from memos.mem_reader.simple_struct import SimpleStructMemReader
reader = SimpleStructMemReader.from_json_file("examples/data/config/simple_struct_reader_config.json")
scene_data = [[
{"role": "user", "content": "Tell me about your childhood."},
{"role": "assistant", "content": "I loved playing in the garden with my dog."}
]]
memories = reader.get_memory(scene_data, type="chat", info={"user_id": "1234"})
for m_list in memories:
tree_memory.add(m_list)
2.4 搜索记忆
尝试向量搜索+图搜索:
results = tree_memory.search("Talk about the garden", top_k=5)
for i, node in enumerate(results):
print(f"{i}: {node.memory}")
2.5 从互联网检索记忆(可选)
你也可以从 Google / Bing / Bocha(博查) 等搜索引擎实时获取网页内容,并自动切分为记忆节点。MemOS 提供了统一接口。
以下示例演示如何检索“Alibaba 2024 ESG report”相关网页,并自动提取为结构化记忆。
# 创建embedder
embedder = EmbedderFactory.from_config(
EmbedderConfigFactory.model_validate({
"backend": "ollama",
"config": {"model_name_or_path": "nomic-embed-text:latest"},
})
)
# 配置检索器(以 BochaAI 为例)
retriever_config = InternetRetrieverConfigFactory.model_validate({
"backend": "bocha",
"config": {
"api_key": "sk-xxx", # 替换为你的 BochaAI API Key
"max_results": 5,
"reader": { # 自动分块的 Reader 配置
"backend": "simple_struct",
"config": ..., # 你的mem-reader config
},
}
})
# 实例化检索器
retriever = InternetRetrieverFactory.from_config(retriever_config, embedder)
# 执行网页检索
results = retriever.retrieve_from_internet("Alibaba 2024 ESG report")
# 添加到记忆图中
for m in results:
tree_memory.add(m)
你也可以直接在 TreeTextMemoryConfig 中配置 internet_retriever 字段,例如:
{
"internet_retriever": {
"backend": "bocha",
"config": {
"api_key": "sk-xxx",
"max_results": 5,
"reader": {
"backend": "simple_struct",
"config": ...
}
}
}
}
这样,在调用 tree_memory.search(query) 时,系统会自动调用互联网检索(如 BochaAI / Google / Bing)然后将结果与本地图中的节点一起排序返回,无需手动调用 retriever.retrieve_from_internet
2.6 替换工作记忆
用一个新的节点替换你当前的 WorkingMemory:
tree_memory.replace_working_memory(
[{
"memory": "User is discussing gardening tips.",
"metadata": {"memory_type": "WorkingMemory"}
}]
)
2.7 备份与恢复
支持树结构的持久化存储与随时重载:
tree_memory.dump("tmp/tree_memories")
tree_memory.load("tmp/tree_memories")
2.8 完整代码
该示例整合了上述所有步骤,提供一个端到端的完整流程。
from memos.configs.embedder import EmbedderConfigFactory
from memos.configs.memory import TreeTextMemoryConfig
from memos.configs.mem_reader import SimpleStructMemReaderConfig
from memos.embedders.factory import EmbedderFactory
from memos.mem_reader.simple_struct import SimpleStructMemReader
from memos.memories.textual.tree import TreeTextMemory
# 嵌入设置
embedder_config = EmbedderConfigFactory.model_validate({
"backend": "ollama",
"config": {"model_name_or_path": "nomic-embed-text:latest"}
})
embedder = EmbedderFactory.from_config(embedder_config)
# 创建TreeTextMemory
tree_config = TreeTextMemoryConfig.from_json_file("examples/data/config/tree_config.json")
my_tree_textual_memory = TreeTextMemory(tree_config)
my_tree_textual_memory.delete_all()
# 阅读器设置
reader_config = SimpleStructMemReaderConfig.from_json_file(
"examples/data/config/simple_struct_reader_config.json"
)
reader = SimpleStructMemReader(reader_config)
# 从对话中抽取
scene_data = [[
{
"role": "user",
"content": "Tell me about your childhood."
},
{
"role": "assistant",
"content": "I loved playing in the garden with my dog."
},
]]
memory = reader.get_memory(scene_data, type="chat", info={"user_id": "1234", "session_id": "2222"})
for m_list in memory:
my_tree_textual_memory.add(m_list)
# 搜索
results = my_tree_textual_memory.search(
"Talk about the user's childhood story?",
top_k=10
)
for i, r in enumerate(results):
print(f"{i}'th result: {r.memory}")
# 从文档添加[可选项]
doc_paths = ["./text1.txt", "./text2.txt"]
doc_memory = reader.get_memory(
doc_paths, "doc", info={
"user_id": "your_user_id",
"session_id": "your_session_id",
}
)
for m_list in doc_memory:
my_tree_textual_memory.add(m_list)
# 转储和丢弃[可选项]
my_tree_textual_memory.dump("tmp/my_tree_textual_memory")
my_tree_textual_memory.drop()
0x03 管理(MemoryManager)
MemoryManager 是 TreeTextMemory 类的一个核心组件,主要负责管理不同类型的记忆(如工作记忆、长期记忆和用户记忆)的添加、组织和维护。
3.1 定义
MemoryManager 的简要如下:
MemoryManager
- uses GraphStructureReorganizer
- uses NodeHandler
- uses Neo4jGraphDB
- uses LLM
- uses Embedder
- uses RelationAndReasoningDetector
- uses Neo4jGraphDB
- uses LLM
- uses Embedder
- uses Neo4jGraphDB
- uses Neo4jGraphDB
- uses Embedder
- uses LLM
MemoryManager 代码如下。
class MemoryManager:
def __init__(
self,
graph_store: Neo4jGraphDB,
embedder: OllamaEmbedder,
llm: OpenAILLM | OllamaLLM | AzureLLM,
memory_size: dict | None = None,
threshold: float | None = 0.80,
merged_threshold: float | None = 0.92,
is_reorganize: bool = False,
):
self.graph_store = graph_store
self.embedder = embedder
self.memory_size = memory_size
self.current_memory_size = {
"WorkingMemory": 0,
"LongTermMemory": 0,
"UserMemory": 0,
}
if not memory_size:
self.memory_size = {
"WorkingMemory": 20,
"LongTermMemory": 1500,
"UserMemory": 480,
}
self._threshold = threshold
self.is_reorganize = is_reorganize
self.reorganizer = GraphStructureReorganizer(
graph_store, llm, embedder, is_reorganize=is_reorganize
)
self._merged_threshold = merged_threshold
3.2 主要功能
以下是其主要功能:
-
记忆管理与组织:
- 添加记忆:通过 add() 方法并行处理不同类型的记忆添加。通过 MemoryManager 组件管理不同类型的记忆:
- 工作记忆(WorkingMemory)
- 长期记忆(LongTermMemory)
- 用户记忆(UserMemory)
- 每种记忆类型都有预设的容量限制,并支持动态调整。
- 替换工作记忆:通过 replace_working_memory() 方法更新工作记忆内容
- 内存大小管理:通过 get_current_memory_size() 获取和刷新各类记忆的当前数量
- 添加记忆:通过 add() 方法并行处理不同类型的记忆添加。通过 MemoryManager 组件管理不同类型的记忆:
-
记忆存储处理
- 工作记忆管理:所有记忆都会被添加到工作记忆中,通过 _add_memory_to_db() 方法处理
- 长期记忆和用户记忆管理:通过 _add_to_graph_memory() 方法处理特定类型的记忆存储
- FIFO 机制:自动维护各类记忆的最大容量,删除过期的记忆项
-
图结构维护
- 结构路径管理:通过 _ensure_structure_path() 确保记忆的结构化存储路径
- 边继承:通过 _inherit_edges() 方法在节点合并时迁移连接关系
-
与图数据库交互
- 依赖图数据库:通过 graph_store(如 Neo4j)进行实际的记忆存储和检索操作
- 定期清理:根据配置的容量限制自动清理过期的记忆项
-
与Reorganizer 集成
- 记忆重组织:与 GraphStructureReorganizer 集成,在添加记忆时触发图结构的重新组织
- 异步处理:通过消息队列机制异步处理记忆的重新组织任务
总的来说,MemoryManager 负责协调和管理记忆的整个生命周期,包括添加、存储、维护和清理,是记忆系统的核心管理组件。
3.3 GraphStructureReorganizer
GraphStructureReorganizer 是负责维护和优化 Memos 系统中记忆图结构的组件,本质上充当了一个记忆维护系统,随着新记忆的不断添加,它能够保持基于图的记忆结构有序、高效且在语义上连贯。
3.3.1 主要功能
以下是其主要功能:
-
消息队列管理
- 异步处理:使用优先级队列(PriorityQueue)来管理图结构操作,如添加、删除、合并和更新节点/边
- 在单独的线程中运行,避免阻塞主记忆操作
- 使用线程池并行处理聚类和关系检测任务
- 提供等待正在进行的重组任务完成的方法
- 优先级处理:不同操作有不同的优先级(add/remove > merge > end),确保关键操作优先处理
- 线程安全操作:运行一个专用线程(_run_message_consumer_loop)来处理排队的消息
- 异步处理:使用优先级队列(PriorityQueue)来管理图结构操作,如添加、删除、合并和更新节点/边
-
图结构(记忆结构)优化
- 周期性优化:运行一个独立线程(
_run_structure_organizer_loop),定期触发对 LongTermMemory 和 UserMemory 的结构优化,以保持效率和一致性 - 自动触发:当有新节点添加时可以自动触发优化(_reorganize_needed 标志)
- 周期性优化:运行一个独立线程(
-
记忆节点关系管理(冲突解决和记忆融合)
- 冲突检测:检测新添加节点与现有节点之间的关系,即与 RelationAndReasoningDetector 集成,识别记忆之间的关系(因果、条件、冲突等)
- 冲突解决:与 NodeHandler 协作,通过基于大语言模型的融合来解决相似记忆之间的冲突和冗余
- 记忆合并:处理重复或相似记忆的合并,归档或删除过时/不那么重要的记忆,以保持图的效率
-
结构增强
- 局部聚类:使用诸如 LOCAL_SUBCLUSTER_PROMPT 和 REORGANIZE_PROMPT 等提示词来识别和组织相关记忆到集群中
- 层次化组织:创建摘要节点并在记忆之间建立层次关系(在摘要节点与其组成节点之间建立父子关系(PARENT 边)),以创建多层次的组织结构(维护类似树的结构),以提高记忆检索效率
- 边管理:在重组过程中维护节点间的连接关系
3.3.2 与其他组件集成
- 图数据库接口:GraphStructureReorganizer直接与 Neo4jGraphDB 交互执行图操作
- 大语言模型集成:GraphStructureReorganizer使用语言模型来理解关系并生成摘要
- 嵌入支持:GraphStructureReorganizer利用 OllamaEmbedder 进行语义相似度计算
总的来说,GraphStructureReorganizer 通过持续优化结构、解决冲突和在记忆节点间建立有意义的关系来维护记忆图的质量和组织性。
3.3.3 如何调用
我们接下来看看 GraphStructureReorganizer 如何调用。
- 初始化时候启动周期性优化任务
当TreeTextMemory初始化时,如果配置了重组织功能(is_reorganize=True),会启动一个周期性的结构优化线程。
class GraphStructureReorganizer:
def __init__(
self, graph_store: Neo4jGraphDB, llm: BaseLLM, embedder: OllamaEmbedder, is_reorganize: bool
):
self.queue = PriorityQueue() # Min-heap
self.graph_store = graph_store
self.llm = llm
self.embedder = embedder
self.relation_detector = RelationAndReasoningDetector(
self.graph_store, self.llm, self.embedder
)
self.resolver = NodeHandler(graph_store=graph_store, llm=llm, embedder=embedder)
self.is_reorganize = is_reorganize
self._reorganize_needed = True
if self.is_reorganize:
# ____ 1. For queue message driven thread ___________
self.thread = threading.Thread(target=self._run_message_consumer_loop)
self.thread.start()
# ____ 2. For periodic structure optimization _______
self._stop_scheduler = False
self._is_optimizing = {"LongTermMemory": False, "UserMemory": False}
self.structure_optimizer_thread = threading.Thread(
target=self._run_structure_organizer_loop
)
self.structure_optimizer_thread.start()
每100秒自动触发一次优化。
def _run_structure_organizer_loop(self):
"""
Use schedule library to periodically trigger structure optimization.
This runs until the stop flag is set.
"""
import schedule
schedule.every(100).seconds.do(self.optimize_structure, scope="LongTermMemory")
schedule.every(100).seconds.do(self.optimize_structure, scope="UserMemory")
while not getattr(self, "_stop_scheduler", False):
if any(self._is_optimizing.values()):
time.sleep(1)
continue
if self._reorganize_needed:
self.optimize_structure(scope="LongTermMemory")
self.optimize_structure(scope="UserMemory")
self._reorganize_needed = False
time.sleep(30)
- 添加新节点时触发优化
当通过MemoryManager 添加新记忆节点时,会向 GraphStructureReorganizer 的消息队列添加一条"add" 消息。
def _add_to_graph_memory(self, memory: TextualMemoryItem, memory_type: str):
"""
Generalized method to add memory to a graph-based memory type (e.g., LongTermMemory, UserMemory).
Parameters:
- memory: memory item to insert
- memory_type: "LongTermMemory" | "UserMemory"
- similarity_threshold: deduplication threshold
- topic_summary_prefix: summary node id prefix if applicable
- enable_summary_link: whether to auto-link to a summary node
"""
node_id = str(uuid.uuid4())
# Step 2: Add new node to graph
self.graph_store.add_node(
node_id, memory.memory, memory.metadata.model_dump(exclude_none=True)
)
self.reorganizer.add_message(
QueueMessage(
op="add",
after_node=[node_id],
)
)
return node_id
当消息处理器处理”add“操作完成后,会设置_reorganize_needed=True,在下一次检查时触发优化。
def handle_add(self, message: QueueMessage):
logger.debug(f"Handling add operation: {str(message)[:500]}")
added_node = message.after_node[0]
detected_relationships = self.resolver.detect(
added_node, scope=added_node.metadata.memory_type
)
if detected_relationships:
for added_node, existing_node, relation in detected_relationships:
self.resolver.resolve(added_node, existing_node, relation)
self._reorganize_needed = True
在 _run_structure_organizer_loop 中检查到该标识时触发优化。
- 也可以直接调用 optimize_structure()方法
总结,这种设计确保了记忆图结构能够定期优化,同时在有新内容添加时也可以及时调整结构。
3.3.4 LLM 的使用
GraphStructureReorganizer 不仅使用了 LLM,而且在其核心功能中深度依赖 LLM 来实现:
- 节点间语义关系的理解和建立
- 冲突记忆的检测与融合
- 新的知识推理和聚合
- 图结构的智能优化
这些功能使记忆图谱不仅是一个简单的数据存储结构,还能自主地维护语义一致性和结构性
直接依赖
GraphStructureReorganizer 在初始化时接收一个 LLM 实例作为参数:
def __init__(
self, graph_store: Neo4jGraphDB, llm: BaseLLM, embedder: OllamaEmbedder, is_reorganize: bool
):
self.llm = llm
关键组件中的 LLM 使用
在优化图结构时,LLM 用于:
- 生成聚类摘要(通过 _summarize_cluster 方法)
- 推断节点间的关系
- 创建新的推理节点
GraphStructureReorganizer 创建了一个 RelationAndReasoningDetector 实例,该检测器大量使用 LLM:
- 关系检测:使用 LLM 判断节点间的因果、条件、冲突等关系
- 事实推断:基于现有关系推断新的事实节点
- 聚合概念生成:将相似节点聚合成更高层次的概念节点
例如在 _detect_pairwise_causal_condition_relations 方法中:
prompt = PAIRWISE_RELATION_PROMPT.format(
node1=node.memory,
node2=candidate.memory
)
response_text = self._call_llm(prompt) # 调用 LLM 进行判断
GraphStructureReorganizer 也创建了一个 NodeHandler 实例用于处理节点冲突和冗余,即当添加新节点时,使用 LLM:
- 冲突检测:检测新节点与现有节点的关系。使用 LLM 判断两个记忆是否相互矛盾
- 冲突解决:解决潜在的冲突或冗余。当发现冲突或冗余时,使用 LLM 融合记忆内容
result = self.llm.generate(prompt).strip() # 检测关系
3.4 RelationAndReasoningDetector
RelationAndReasoningDetector 是一个用于检测和推理记忆节点之间关系的组件,它在记忆图结构优化过程中发挥关键作用。它的主要功能包括:
- 节点关系检测。RelationAndReasoningDetector 负责检测记忆节点之间的各种语义关系,通过使用大语言模型(LLM)和嵌入向量来判断节点间的关系类型。
- 推理新节点生成。基于已检测到的关系推断新的事实节点,创建推理类型的节点以丰富记忆图谱的内容
- 图结构优化支持。为 GraphStructureReorganizer 提供关系和推理信息,帮助构建更完整的语义网络结构。
3.4.1 主要方法
process_node 方法。统一处理节点的关系检测和推理流程,调用其他专门方法完成具体任务。
- 关系检测。检测成对节点之间的因果、条件、冲突,相关等关系,使用 _detect_pairwise_causal_condition_relations 方法实现。
- 推理节点生成。基于因果/条件关系推断新的事实节点,调用 _infer_fact_nodes_from_relations 方法生成聚合概念节点(将相似节点聚合成更高层次的概念)
- 时序关联检测。检测节点间的时间顺序关系,通过 _detect_sequence_links 方法实现。
- 聚合节点检测。识别可以被聚合的概念组,使用 _detect_aggregate_node_for_group 方法
Relation Detection Methods。专门用于检测不同类型节点关系的方法集合
- _detect_pairwise_causal_condition_relations检测两个节点之间的因果和条件关系,使用 LLM 判断关系类型:CAUSE、CONDITION、RELATE、CONFLICT 或 NONE。
- _detect_sequence_links根据时间戳检测节点间的时序关系,创建 FOLLOWS 类型的边连接相关节点。
- _detect_aggregate_node_for_group识别具有相似标签的节点组并生成聚合概念节点,使用 LLM 判断是否需要创建聚合节点。
Inference Methods。用于从现有关系中推断新节点的方法
- _infer_fact_nodes_from_relations:从因果和条件关系中推断新的事实节点,生成带有推理标记的新节点并连接到源节点。
3.4.2 工作流程
- 初始化阶段。接收 graph_store、llm 和 embedder 作为依赖组件,准备处理关系检测和推理所需的基础工具
- 处理阶段。通过 process_node 方法处理单个节点,查找邻近节点并进行关系分析,生成推理节点和关系连接
- 结果输出。返回包含新关系、推理节点、序列链接和聚合节点的结果集,为图结构重组提供数据支持
数据流如下:
Background Task → GraphStructureReorganizer
↓
RelationAndReasoningDetector.process_node()
↓
Neo4jGraphDB [Get Neighbors/Nodes/Edges]
↓
RelationAndReasoningDetector [LLM Analysis]
↓
Neo4jGraphDB [Add Inferred Nodes/Relations]
3.4.3 promt
RelationAndReasoningDetector 引入了以下 prompt。
from memos.templates.tree_reorganize_prompts import (
AGGREGATE_PROMPT,
INFER_FACT_PROMPT,
PAIRWISE_RELATION_PROMPT,
)
举例如下:
PAIRWISE_RELATION_PROMPT = """
You are a reasoning assistant.
Given two memory units:
- Node 1: "{node1}"
- Node 2: "{node2}"
Your task:
- Determine their relationship ONLY if it reveals NEW usable reasoning or retrieval knowledge that is NOT already explicit in either unit.
- Focus on whether combining them adds new temporal, causal, conditional, or conflict information.
Valid options:
- CAUSE: One clearly leads to the other.
- CONDITION: One happens only if the other condition holds.
- RELATE: They are semantically related by shared people, time, place, or event, but neither causes the other.
- CONFLICT: They logically contradict each other.
- NONE: No clear useful connection.
Example:
- Node 1: "The marketing campaign ended in June."
- Node 2: "Product sales dropped in July."
Answer: CAUSE
Another Example:
- Node 1: "The conference was postponed to August due to the venue being unavailable."
- Node 2: "The venue was booked for a wedding in August."
Answer: CONFLICT
Always respond with ONE word, no matter what language is for the input nodes: [CAUSE | CONDITION | RELATE | CONFLICT | NONE]
"""
INFER_FACT_PROMPT = """
You are an inference expert.
Source Memory: "{source}"
Target Memory: "{target}"
They are connected by a {relation_type} relation.
Derive ONE new factual statement that clearly combines them in a way that is NOT a trivial restatement.
Requirements:
- Include relevant time, place, people, and event details if available.
- If the inference is a logical guess, explicitly use phrases like "It can be inferred that...".
Example:
Source: "John missed the team meeting on Monday."
Target: "Important project deadlines were discussed in that meeting."
Relation: CAUSE
Inference: "It can be inferred that John may not know the new project deadlines."
If there is NO new useful fact that combines them, reply exactly: "None"
"""
AGGREGATE_PROMPT = """
You are a concept summarization assistant.
Below is a list of memory items:
{joined}
Your task:
- Identify if they can be meaningfully grouped under a new, higher-level concept that clarifies their shared time, place, people, or event context.
- Do NOT aggregate if the overlap is trivial or obvious from each unit alone.
- If the summary involves any plausible interpretation, explicitly note it (e.g., "This suggests...").
Example:
Input Memories:
- "Mary organized the 2023 sustainability summit in Berlin."
- "Mary presented a keynote on renewable energy at the same summit."
Language rules:
- The `key`, `value`, `tags`, `background` fields must match the language of the input.
Good Aggregate:
{
"key": "Mary's Sustainability Summit Role",
"value": "Mary organized and spoke at the 2023 sustainability summit in Berlin, highlighting renewable energy initiatives.",
"tags": ["Mary", "summit", "Berlin", "2023"],
"background": "Combined from multiple memories about Mary's activities at the summit."
}
If you find NO useful higher-level concept, reply exactly: "None".
"""
这三组提示词是面向 MemOS 记忆系统中关系推理、事实推导、概念聚合的专业化 prompt 设计,三个prompt的特点如下:
-
PAIRWISE_RELATION_PROMPT将 AI 定义为reasoning assistant(推理助手),聚焦两个记忆单元的关系判定,匹配 “逻辑关系分析” 的任务属性;- 仅要求判断两个节点的关系类型,且明确限定 “仅当关系能揭示新知识时才判定,否则为 NONE”;
- 关系判定中,将有效关系拆分为 CAUSE(因果)、CONDITION(条件)、RELATE(关联)、CONFLICT(冲突)、NONE(无) 五类,并对每类的判定依据(如因果是 “一方明确导致另一方”)做了清晰说明;
- 关系判定强制要求仅输出一个单词,无论输入语言为何,避免 AI 产生冗余解释;
- 关系判定是记忆关联的基础环节,为两个独立记忆单元建立语义连接,要求 “揭示新的推理 / 检索知识”。
-
INFER_FACT_PROMPT设定为inference expert(推理专家),侧重基于已知关系推导新事实,突出 “深度语义挖掘” 的专业性;- 仅要求基于关系推导一个新事实,禁止简单复述原记忆内容;
- 事实推导是记忆挖掘的深化环节,基于关联关系生成新的知识节点,要求 “生成非平凡的新事实”。
- 事实推导要求 “结合时间、地点、人物、事件细节”,若为推测需使用 “It can be inferred that...” 等固定表述,确保推导结果的可验证性。
- 事实推导若无可推导内容,需严格返回 “None”,聚合 prompt 同理,统一了无效场景的输出格式。
-
AGGREGATE_PROMPT定位为concept summarization assistant(概念总结助手),针对多记忆单元的高阶聚合,贴合 “抽象归纳” 的核心需求。- 概念聚合是记忆结构化的高阶环节,将零散记忆归纳为抽象概念,要求 “提炼高阶共享概念”,适配 MemOS 中 “节点 - 关系 - 概念” 的层级化记忆管理架构。
- 仅要求识别多记忆单元的高阶共享概念,若重叠为常识则直接返回 “None”。
3.5 NodeHandler
NodeHandler 是 Memos 系统中负责处理记忆节点冲突检测与解决的核心组件。
3.5.1 主要功能
NodeHandler 的主要功能包括:
-
冲突检测(Conflict Detection)
- 使用嵌入向量相似度搜索潜在的冲突记忆节点
- 利用 LLM 判断两个记忆节点之间的关系(矛盾、冗余或独立)
- 基于预设阈值(EMBEDDING_THRESHOLD)进行初步筛选
-
冲突解决(Conflict Resolution)
- 对检测到的冲突进行融合处理
- 使用 LLM 生成融合后的记忆内容
- 处理元数据合并和图结构更新
-
图结构维护
- 更新合并后的记忆节点到图数据库
- 维护原有节点间的关系连接
- 将旧节点标记为已归档状态
3.5.2 调用关系与依赖关系
直接依赖组件如下:
- GraphStore(Neo4jGraphDB):
- 搜索相似节点:search_by_embedding
- 获取节点信息:get_nodes
- 更新图结构:add_node, delete_node, add_edge, update_node, edge_exists
- LLM(BaseLLM)
- 判断节点关系:MEMORY_RELATION_DETECTOR_PROMPT
- 用于解决冲突:MEMORY_RELATION_RESOLVER_PROMPT
- Embedder(BaseEmbedder)
- 用于生成新记忆的嵌入向量
被其他组件调用
- GraphStructureReorganizer:在 handle_add 方法中调用 NodeHandler.detect 和 NodeHandler.resolve,用于处理新增节点时的冲突检测与解决。
# 在 GraphStructureReorganizer.handle_add 中的调用示例
def handle_add(self, message: QueueMessage):
added_node = message.after_node[0]
detected_relationships = self.resolver.detect(
added_node, scope=added_node.metadata.memory_type
)
if detected_relationships:
for added_node, existing_node, relation in detected_relationships:
self.resolver.resolve(added_node, existing_node, relation)
self._reorganize_needed = True
数据模型依赖
- TextualMemoryItem:处理的主要对象类型
- TreeNodeTextualMemoryMetadata:节点元数据结构
- Templates:使用 MEMORY_RELATION_DETECTOR_PROMPT 和 MEMORY_RELATION_RESOLVER_PROMPT 模板
3.5.3 流程图
NodeHandler 的流程图如下:
User Input → MemoryManager.add()
↓
NodeHandler.detect() → [Embedding Search]
↓
NodeHandler.resolve() → [LLM Conflict Resolution]
↓
Neo4jGraphDB [Add/Update/Delete Nodes & Edges]
具体如下:
-
检测阶段:detect() → search_by_embedding() → get_nodes() → LLM 判断关系 → 返回冲突列表
-
解决阶段:resolve() → LLM 生成融合记忆 → 解析响应 → _merge_metadata() → _resolve_in_graph() → 更新图数据库结构
-
硬更新处理:当无法通过 LLM 解决冲突时,采用时间戳比较方式进行硬更新:_hard_update() → 比较 updated_at → 删除较旧节点
总的来说,NodeHandler 是 Memos 记忆管理系统中保证记忆一致性和避免冲突的关键组件,它协调了图数据库、大语言模型和嵌入模型来完成记忆节点的智能化管理。
3.6 NodeHandler vs RelationAndReasoningDetector
为何要拆分成两个类,而不合二为一?
3.6.1 设计思路
RelationAndReasoningDetector(关系与推理检测器)和 NodeHandler(节点处理器)的拆分设计,本质是遵循 “单一职责原则” 与 “关注点分离” 的架构设计思想 —— 二者虽共同服务于 “记忆的结构化管理与智能推理”,但核心职责、输入输出、复用场景完全不同,合并会导致代码耦合度飙升、可维护性下降、功能扩展受限。
MemOS 的核心架构是 “分层设计”(感知层→推理层→存储层):
- 感知层:处理输入数据(如文本解析);
- 推理层:负责智能分析(如关系识别、推理);
- 存储层:负责数据持久化(如节点、关系存储)。
RelationAndReasoningDetector 属于 “推理层”,属于主动发起推理阶段,在添加新节点后主动寻找关联,NodeHandler 属于 “存储层”,属于被动响应阶段,在发现冲突时进行处理。
MemOS 中两个类的拆分,本质是 “将‘智能决策(推理)’与‘数据执行(存储)’分离”—— 前者负责 “想清楚要做什么”(从文本中识别节点和关系),后者负责 “把事做好”(将识别出的节点和关系落地到存储)。这种设计不仅符合软件工程的核心原则,更适配 MemOS 作为 “Agent 记忆系统” 的核心需求:既要支持灵活的推理逻辑迭代,又要保证存储操作的稳定可靠。合并二者相当于打破了分层架构,导致架构边界模糊,后续无法对某一层进行独立的性能优化或功能扩展(如给推理层加缓存、给存储层加容灾)。
3.6.2 定位边界
要理解 “为何不能合并”,首先要厘清二者的定位边界,这是拆分的根本依据:
| 维度 | RelationAndReasoningDetector(关系与推理检测器) | NodeHandler(节点处理器) |
|---|---|---|
| 核心职责 | 负责 “语义层面的智能分析”:从非结构化数据(如文本、对话)中识别实体关系、触发推理逻辑、挖掘隐含信息。 | 负责 “数据层面的结构化操作”:管理知识图谱的节点生命周期(创建、更新、删除、查询),处理节点的存储与格式适配。 |
| 输入 / 输出 | 输入:非结构化数据(文本片段、用户指令)、现有知识图谱元数据;输出:关系三元组(实体 A - 关系 - 实体 B)、推理结论、待处理的节点关联需求。 | 输入:结构化数据(节点 ID、属性键值对、关联关系标识);输出:节点操作结果(创建成功 / 更新后的节点数据 / 查询结果)。 |
| 核心能力依赖 | 依赖大模型(LLM)的语义理解与推理能力,需处理模糊、歧义的非结构化信息(如 “小明喜欢喝拿铁”→ 识别 “小明 - 喜欢 - 拿铁” 三元组)。 | 依赖数据存储层(如 Neo4j、向量数据库)的 CRUD 接口,需保证操作的原子性、一致性(如创建节点时校验唯一标识,避免重复)。 |
| 变化频率 | 较高:推理规则、关系类型可能随场景迭代(如从 “通用对话” 扩展到 “专业领域”,需新增领域特定关系识别逻辑)。 | 较低:节点的存储格式、操作规范(如 ID 生成规则、属性校验逻辑)一旦确定,很少频繁变更。 |
| 复用场景 | 可跨模块复用:除了给 NodeHandler 提供关联数据,还可给 RAG 模块提供推理结论、给 Agent 决策模块提供关系依据。 | 聚焦记忆存储层:主要服务于知识图谱的节点管理,复用场景集中在 “节点数据的读写操作”。 |
3.6.3 详细思路
解耦 “智能推理” 与 “数据操作”
MemOS 的核心价值是 “让 Agent 拥有可推理的记忆”,而这一目标需要 “先分析(推理)、后操作(存储)” 的流程:
- 第一步:
RelationAndReasoningDetector把 “模糊的自然语言” 翻译成 “结构化的关系与节点需求”(如 “小红买了新手机”→ 识别出 “小红”“新手机” 两个实体,“购买” 关系,生成 “创建两个节点 + 建立关联” 的需求); - 第二步:
NodeHandler接收这个 “明确需求”,执行具体的存储操作(创建小红节点、创建新手机节点、在二者间添加 “购买” 关系边)。
若合并为一个类,会导致 “推理逻辑” 与 “存储逻辑” 交织在一起:比如在 “创建节点” 的代码中,既要写 “如何校验节点 ID 唯一性”,又要写 “如何从文本中识别节点实体”—— 当需要修改推理规则(如新增 “合作” 关系识别)时,可能误改存储相关代码;当需要更换数据库(如从 Neo4j 换成 ArangoDB)时,又要动推理相关的逻辑,维护成本指数级上升。
适配不同的 “扩展与优化方向
两个类的优化目标完全不同,拆分后可独立迭代,”—— 避免 “牵一发而动全身”。
- 对
RelationAndReasoningDetector:优化方向是 “提升推理准确率”(如引入少样本学习、领域微调、思维链提示)、“支持更多关系类型”(如从二元关系扩展到多元关系)、“降低推理延迟”(如缓存高频推理规则); - 对
NodeHandler:优化方向是 “提升存储性能”(如批量操作、索引优化)、“增强数据一致性”(如事务支持、冲突解决)、“适配多存储引擎”(如同时支持图数据库、向量数据库)。
例如:当需要给 MemOS 新增 “时序关系推理” 功能时,只需修改 RelationAndReasoningDetector,新增时序关系识别逻辑(如 “2024 年小明入职→ 小明 - 2024 年入职 - 公司”),NodeHandler 完全无需改动;反之,当需要将节点存储从 “单节点数据库” 扩展到 “分布式存储” 时,只需优化 NodeHandler 的操作接口,推理逻辑不受任何影响。
提升代码复用性与测试效率
- 复用性:
RelationAndReasoningDetector的推理能力可被 MemOS 多个模块复用(如 Agent 的决策模块、RAG 的检索增强模块),而NodeHandler的节点操作能力可被知识图谱的多个子功能复用(如节点更新、节点查询、节点删除)。若合并,这些复用场景会被迫引入无关逻辑(如调用推理功能时,还要加载存储相关的依赖); - 测试效率:拆分后可独立编写单元测试 —— 测试
RelationAndReasoningDetector时,只需模拟输入文本,验证输出的关系三元组是否正确,无需依赖真实数据库;测试NodeHandler时,只需模拟结构化的节点数据,验证 CRUD 操作是否符合预期,无需启动大模型。若合并,测试用例需同时覆盖 “推理逻辑” 和 “存储逻辑”,场景组合爆炸(如 “推理正确但存储失败”“推理错误但存储成功”),测试难度和维护成本陡增。
0x04 图例
4.1 组件关系图

4.2 调用流程图
调用流程图如下。

详细组件交互说明
- MemoryManager 与 GraphStructureReorganizer:
- MemoryManager 在添加新记忆时通过 GraphStructureReorganizer.add_message() 发送消息。
- GraphStructureReorganizer 作为后台线程处理这些消息
-
GraphStructureReorganizer 与 NodeHandler
- 在处理 "add" 操作时,调用 NodeHandler.detect() 检测冲突
- 对于检测到的冲突,调用 NodeHandler.resolve() 解决冲突
-
GraphStructureReorganizer 与 RelationAndReasoningDetector
- 在结构优化过程中,调用 RelationAndReasoningDetector.process_node() 处理节点关系检测并创建因果关系、条件关系、推理节点等
-
所有组件与 Neo4jGraphDB。所有组件都直接与 Neo4jGraphDB 交互进行图操作:
- 节点操作: add_node(), delete_node(), update_node(), get_node()
- 边操作: add_edge(), delete_edge(), edge_exists(), get_edges()
- 查询操作:search_by_embedding(), get_neighbors_by_tag()
这个设计实现了分层处理:MemoryManager 负责基本的记忆管理,GraphStructureReorganizer 负责后台结构优化,而 NodeHandler 和 RelationAndReasoningDetector 则专门处理特定的图结构维护任务。
0xFF 参考
https://arxiv.org/pdf/2507.03724
浙公网安备 33010602011771号