Ch11: Graphiti——时序知识图谱与双时间轴
Part 3: 七大开源项目精读 前置章节:Ch10: Letta/MemGPT——虚拟内存与 Agent 自管理 后续章节:Ch12: cognee——本体论驱动的认知管线
1. 一句话定位
Zep 团队开源的时序知识图谱引擎,核心创新是给每条边加上 valid_at / invalid_at 双时间轴,让记忆能自动区分”正在成立的事实”和”已经过时的事实”——是七个项目中唯一在时序维度做到深度的方案。
2. 设计哲学——城市地图类比
想象你生活在一座不断变化的城市里。你的手机地图 app 会定期更新——新开了一家咖啡店,老街被改成了步行街,地铁 8 号线通车了。传统地图的做法是直接覆盖旧版:新版一发,旧版消失。你再也看不到”这家咖啡店原来是个书店”的信息。
Graphiti 的做法不同:它像一本带修订记录的城市志。新信息不是覆盖旧信息,而是在旧信息旁边标注”已过时”,然后写下新信息。旧地图不扔掉,只是标记为”截至某年某月有效”。
为什么旧信息值得保留?因为”曾经住在北京”本身是有价值的历史。如果 Agent 只记住”现在住在上海”,它就丢失了”用户在北京生活过”这个可能影响推荐和对话的上下文。信息的”死亡”和它的”诞生”一样,都是值得记录的事件。
这就是 Graphiti 的核心洞察:记忆不是静态快照,而是随时间演化的流。
传统记忆系统(mem0、Letta 的 core memory)像一块白板——新信息直接擦掉旧信息。Graphiti 像一本日记——旧的记录不擦,而是在旁边标注”已更新为…“。白板只保留”现在”,日记保留”时间的轨迹”。
这个类比有一条边界需要说明:城市志的修订是人工标注的,Graphiti 的时序标记是系统自动生成的。当新事实和旧事实冲突时,Graphiti 通过 LLM 推理自动检测冲突,然后给旧边打上 invalid_at 时间戳,同时创建新边。开发者不需要手动管理”过时”——系统帮你做。
3. 双时间轴的核心创新
Graphiti 最重要的技术贡献是双时间轴模型。理解它,就理解了 Graphiti 和所有其他记忆系统的根本区别。
3.1 valid_at——事实生效时间
valid_at 记录的是事实在现实中何时成立,不是”这条记录何时被写入数据库”。这是一个关键区分。
举个例子:用户在对话中说”我去年 3 月搬到了北京”。这条事实的 valid_at 应该是去年 3 月(事实生效的时间),而不是对话发生的今天(记录入库的时间)。
如果把 valid_at 等同于入库时间,会发生什么?Agent 会认为”用户去年 3 月到今天一直住在北京”——因为从”去年 3 月”到”今天”,这条边都是”生效的”。但实际上用户可能早就搬走了,只是今天才告诉 Agent。
3.2 invalid_at——事实失效时间
当用户说”我搬到上海了”,Graphiti 不是删除”住在北京”这条边,而是给它打上 invalid_at = 当前时间。同时创建一条新边:”住在上海”,valid_at = 当前时间。
现在图谱里有两条边:
| 事实 | valid_at | invalid_at | 状态 |
|---|---|---|---|
| 住在北京 | 去年3月 | 今天 | 已过时 |
| 住在上海 | 今天 | NULL | 当前有效 |
检索”用户现在住哪”时,条件是 invalid_at IS NULL,只返回”住在上海”。检索”用户住过哪些城市”时,不加 invalid_at 过滤,两条都返回。
这种设计比 mem0 的”覆盖旧事实”优雅得多。mem0 在检测到冲突时让 LLM 二选一——选错了,历史信息永久丢失。Graphiti 从不删除,只做标记——即使标记错了,也可以撤销(把 invalid_at 改回 NULL),历史信息完好无损。
3.3 处理”回忆性描述”——双时间轴的真正威力
双时间轴最精妙的应用是处理”回忆性描述”——用户在当前对话中提到了过去发生的事。
场景:6 月的对话中,用户说”我去年 3 月搬到了北京,今年 1 月又搬到了上海”。
在 Graphiti 的模型中,这两条信息会产生如下记录:
| 事实 | valid_at | invalid_at | created_at |
|---|---|---|---|
| 住在北京 | 去年3月 | 今年1月 | 6月(今天) |
| 住在上海 | 今年1月 | NULL | 6月(今天) |
注意 created_at 都是 6 月(今天,即信息入库的时间),但 valid_at 分别是去年 3 月和今年 1 月(事实在现实中成立的时间)。
如果只有一个时间维度(created_at),系统无法区分”用户是去年 3 月说的’住在北京’“还是”用户今天回忆说去年 3 月住在北京”。前者意味着从去年 3 月到今天,这条边一直在生效;后者意味着这条边从去年 3 月生效,但可能早已失效。
这看起来是细微的区别,但在时序推理中至关重要。比如:
- 查询”去年 6 月用户住在哪”——正确答案应该是北京,不是上海
- 查询”用户上个月住哪”——正确答案应该是上海
- 查询”用户住过哪些城市”——北京和上海都应该出现
没有双时间轴,这些时序查询都不可能准确回答。mem0 完全没有时间概念(add() 不记录时间戳),Letta 的 core memory 是扁平文本块(没有结构化时间),DB-Agent-Memory 的四层金字塔有创建时间但没有”事实生效时间”——当用户说”去年开始偏好 TypeScript”时,DB-Agent-Memory 只知道这条偏好在今天被记录,不知道它从去年就开始生效。
3.4 双时间轴的工程代价
双时间轴不是免费的午餐。它引入了两类工程复杂度。
第一类是检索复杂度。每条查询都需要考虑时间过滤。查”当前状态”加 invalid_at IS NULL,查”某时刻的状态”加 valid_at <= 目标时间 AND (invalid_at IS NULL OR invalid_at > 目标时间)。这比 mem0 的简单向量搜索复杂得多,查询性能也受影响——Neo4j 的时间范围查询比等值查询慢。
第二类是图谱膨胀。信息只增不减意味着边的数量会持续增长。1000 轮对话后,一个用户的图谱可能有数千条边,其中大部分已经是”已过时”的。虽然检索时可以通过 invalid_at IS NULL 过滤掉过时边,但 Neo4j 仍然需要扫描这些边来判断它们是否过时。在极端场景下(用户信息频繁变化),图谱膨胀会成为性能瓶颈。
4. 检索架构——两阶段混合检索
Graphiti 的检索不是简单的向量 cosine 相似度排序。它采用两阶段架构:向量检索粗筛 + Cross-Encoder 精排,同时利用 Neo4j 的向量索引和 Cypher 图遍历能力。
4.1 第一阶段:向量检索 + 图遍历粗筛
第一阶段的目标是从海量候选中快速筛选出一个较小的候选集(比如 top-20),为后续的精排提供输入。
Graphiti 利用了 Neo4j 的两个能力:
向量索引:Neo4j 从 5.11 版本开始内建向量索引。Graphiti 把每条边和节点的文本描述做 embedding 后存入 Neo4j 的向量索引。查询时,先用向量相似度搜索找到语义相关的候选。这和 mem0 用 Qdrant/Chroma 做向量搜索没有本质区别,只是向量索引被内建在了图数据库中。
Cypher 图遍历:这是 Graphiti 独有的能力。向量检索只看”语义相似度”——查询”小明的同事喜欢什么”,向量检索可能返回所有和”喜欢”语义相关的边,但不知道”小明”和”同事”之间的结构关系。Cypher 图遍历从”小明”节点出发,沿着”同事”关系找到目标节点,再获取它们的”喜欢”关系。
两种检索的结果取交集或并集——具体策略取决于查询类型。对于关系型查询(”谁是谁的什么”),图遍历权重更高;对于语义型查询(”和这个概念相关的有什么”),向量检索权重更高。
查询:"小明的同事喜欢什么?"
阶段1 粗筛:
向量检索 → 找到与"喜欢"语义相关的边(top-50)
图遍历 → 从"小明"出发,经"同事"关系到达的节点(无限制)
合并 → 取交集/加权融合 → 候选集(top-20)
4.2 第二阶段:Cross-Encoder 精排
第二阶段用 Cross-Encoder 模型对候选集重新排序。
Cross-Encoder 和 Bi-Encoder 的区别:
Bi-Encoder(双塔模型)是向量检索的基础——查询和候选分别编码为向量,计算向量之间的相似度。优点是速度快(候选向量可以预计算),缺点是查询和候选在编码时互不感知,无法捕捉细粒度的语义交互。
Cross-Encoder(交叉编码器)把查询和候选拼在一起输入模型——模型同时看到查询和候选的文本,能捕捉”查询中的哪个词和候选中的哪个词最相关”这种细粒度交互。精度显著高于 Bi-Encoder,但速度也慢一个数量级——每个候选都需要单独推理,不能预计算。
这就是两阶段架构的逻辑:第一阶段用快速的 Bi-Encoder 把候选从数万缩小到数十,第二阶段用精确但慢的 Cross-Encoder 从数十中选出最好的几个。
阶段2 精排:
对 top-20 候选逐一 Cross-Encoder 推理
按精排分数重新排序 → top-5 最终结果
4.3 时序过滤在检索中的位置
时序过滤(invalid_at IS NULL 或时间范围过滤)可以在不同阶段执行:
- 在粗筛阶段过滤:减少候选数量,提升后续精排速度,但如果时间标注有误,有效事实可能在粗筛阶段就被误过滤
- 在精排之后过滤:保证不遗漏,但可能让大量过时事实进入精排阶段,增加推理开销
Graphiti 的实际策略是在粗筛阶段就做时间过滤——这和它的”只返回当前有效事实”的设计哲学一致。但这意味着如果一个事实被错误地标记为过时,它在检索中完全不可见,没有纠正的机会。
4.4 检索延迟分析
Graphiti 的完整检索路径是:Neo4j 向量搜索 + Cypher 图遍历 → 结果合并 → Cross-Encoder 精排 → 时序过滤 → 最终结果。
延迟分布大致如下:
| 步骤 | 典型延迟 | 备注 |
|---|---|---|
| Neo4j 向量搜索 | 20-50ms | 取决于索引大小和硬件 |
| Cypher 图遍历 | 30-100ms | 取决于图的深度和宽度 |
| 结果合并 | 5-10ms | 内存操作 |
| Cross-Encoder 精排 | 100-300ms | 对 top-20 逐一推理 |
| 时序过滤 | 5-10ms | 内存操作 |
| 总计 | 160-470ms | 简单图谱 |
大规模图谱(万级节点、数万条边)上图遍历本身可能消耗数百毫秒,加上 Cross-Encoder 推理,总延迟可能超过 1 秒。对比 DB-Agent-Memory 的 sqlite-vec 本地检索(通常 <50ms),Graphiti 的延迟高了一个数量级。
Graphiti 代码中没有明确的超时保护——它假设用户愿意为更准确的结果等待。这对交互式 Agent 来说可能不可接受——用户期望的是毫秒级响应,而不是等待 1 秒让 Agent “回忆”。
5. 写入架构——从对话到时序图谱
Graphiti 的写入流程(add_episode 方法)也是一个多步管线:
5.1 实体提取
LLM 从对话文本中识别实体(人、地点、组织等)和它们之间的关系。这一步和 cognee 的 Extract 阶段类似——用 LLM 做 NER(命名实体识别)和关系提取。
提取出的实体被映射到图谱中的节点。如果同名实体已经存在(通过实体解析判断是否为同一实体),则复用现有节点;否则创建新节点。
5.2 时序边创建
每条关系被表示为一条带时间戳的边。LLM 判断关系的生效时间(valid_at):如果用户说”我去年 3 月搬到北京”,valid_at 被设为去年 3 月;如果用户只说”我住在北京”,valid_at 被设为当前时间。
5.3 冲突检测与边失效
当新边和已有边冲突时(”住在北京” vs “搬到上海”),Graphiti 不是删除旧边,而是给旧边设置 invalid_at。这个冲突检测由 LLM 完成——LLM 判断新旧信息是否矛盾,如果矛盾,旧边失效。
这个机制的可靠性取决于 LLM 的判断质量。”住在北京”和”搬到上海”显然矛盾。但”喜欢猫”和”养了一条狗”矛盾吗?”用 Mac”和”用 Windows”呢?这些边界情况需要深层推理,LLM 的判断不一定总是正确的。
5.4 向量嵌入
每条边和节点的文本描述被嵌入为向量,存入 Neo4j 的向量索引。嵌入使用 OpenAI 的 embedding 模型(默认 text-embedding-3-small)。
6. 代码结构精读
Graphiti 的代码量在七个项目中最小之一(约 200 文件),组织得很精炼:
graphiti/
graphiti_core/
graphiti.py # 核心入口类,add_episode / search 等主方法
nodes.py # 实体节点定义(EntityNode)
edges.py # 时序边定义(EntityEdge),包含 valid_at / invalid_at
search/
search.py # 混合检索入口(向量 + 图遍历 + cross-encoder)
search_utils.py # 检索工具函数(结果合并、分数归一化)
llm_client/
openai_client.py # LLM 调用封装(实体提取、关系识别、冲突检测)
embedder/
openai_embedder.py # 向量嵌入封装
cross_encoder/
cross_encoder_client.py # Cross-Encoder 重排器
utils/
maintenance/
temporal_operations.py # 时序边失效/更新操作
tests/
examples/
值得注意的几个关键文件:
edges.py——这是 Graphiti 最核心的文件。EntityEdge 类定义了时序边的数据结构,包含 valid_at、invalid_at、created_at 三个时间字段,以及边的源/目标节点引用、关系类型、向量嵌入等。所有时序逻辑都围绕这个数据结构展开。
search.py——检索入口。它编排了完整的两阶段检索流程:先做向量搜索和图遍历的粗筛,再做 Cross-Encoder 精排。搜索策略(SEARCH_STRATEGY)可以配置——选择偏向向量搜索还是偏向图遍历。
temporal_operations.py——时序维护逻辑。当检测到冲突时,这个模块负责更新 invalid_at 时间戳、创建新的有效边。它还负责”时序一致性检查”——确保同一条边的 valid_at 早于 invalid_at,确保同一个节点的同一类关系不会同时有多条有效边。
cross_encoder_client.py——Cross-Encoder 封装。默认使用 cross-encoder/ms-marco-MiniLM-L-6-v2 模型,也可以替换为其他模型。推理是同步的,每个候选串行处理——这是延迟的主要来源。
7. Neo4j 强依赖——最大的局限
Graphiti 对 Neo4j 的强依赖是它最大的工程局限。这个依赖体现在三个层面:
7.1 部署层面
Neo4j 是一个重量级的 Java 应用。生产部署至少需要:
- JVM 运行时(推荐 JDK 17+)
- 4GB+ 内存(社区版推荐 8GB+)
- 独立服务进程(需要网络端口开放)
- 数据目录的磁盘空间(随图谱增长)
对比 DB-Agent-Memory 的零依赖(npm install 就能跑,数据就是一个 .db 文件),Graphiti 的部署门槛高了一个数量级。对于个人开发者或小型团队,部署 Neo4j 本身就是一个负担。社区版不支持集群,生产环境需要企业版(付费)。
7.2 查询层面
Graphiti 的核心检索逻辑用 Cypher 查询语言编写。Cypher 是 Neo4j 的专有查询语言——虽然其他图数据库(如 Memgraph、FalkorDB)也部分支持 Cypher,但 Neo4j 的 Cypher 有很多独有扩展(如向量索引的 db.index.vector.queryNodes 过程、全文索引的 db.index.fulltext.queryNodes 过程)。这些扩展在其他图数据库中不可用,导致 Graphiti 的查询深度绑定了 Neo4j。
社区有 PR 尝试加 FalkorDB 和 Kuzu 支持,但核心代码深度依赖 Neo4j 的特有功能,移植工作量巨大。
7.3 性能层面
Neo4j 是一个网络服务——每次查询都需要一次网络往返(即使是本机部署)。网络延迟加上 Neo4j 的查询执行时间,单次检索的延迟通常在 100-500ms 范围内。对比 DB-Agent-Memory 的 sqlite-vec 本地检索(通常 <50ms),延迟高了一个数量级。
在大规模图谱上,Neo4j 的图遍历性能会下降——特别是涉及多跳遍历和聚合操作时。Neo4j 社区版没有查询并行执行能力,所有查询串行处理,在高并发场景下延迟会更严重。
7.4 依赖的合理性
尽管强依赖 Neo4j 有以上局限,但它也有合理的一面:
- Neo4j 是目前最成熟的图数据库,生态和社区支持远超其他图数据库
- Neo4j 内建的向量索引和全文索引避免了额外的向量服务依赖(Graphiti 不需要 Qdrant/Chroma)
- Cypher 是表达能力最强的图查询语言之一,Graphiti 的复杂检索逻辑用 Cypher 实现比用其他查询语言更清晰
这本质上是一个经典的工程权衡:锁定换来简洁。Graphiti 选择绑定 Neo4j,代码量得以控制在约 200 文件;如果像 cognee 那样支持多后端,代码量可能翻倍还不止。
8. 和 DB-Agent-Memory 的对比
Graphiti 和 DB-Agent-Memory 代表了两种截然不同的记忆系统设计哲学。把它们放在一起对比,能更清楚地看到各自的优势和局限。
8.1 图结构 vs 扁平分层
| 维度 | Graphiti | DB-Agent-Memory |
|---|---|---|
| 存储模型 | 知识图谱(节点 + 边) | 四层金字塔(L0-L3) |
| 信息组织 | 实体间的关系网络 | 按提炼层次分层的扁平结构 |
| 关系推理 | 原生支持(图遍历) | 不支持(无图结构) |
| 多跳查询 | 天然擅长 | 无法直接做 |
| 数据结构复杂度 | 高(图需要维护实体解析、边失效等) | 低(每层独立存储) |
Graphiti 的图结构在多跳推理上天然占优——”小明的同事的公司在哪个城市”这类查询,图遍历一步到位。DB-Agent-Memory 没有图结构,这类查询只能依赖 LLM 从 L1 事实中拼凑,准确率和效率都不如图遍历。
但图结构的维护成本也更高——实体解析(判断两个”小明”是不是同一个人)和边失效管理(检测冲突、更新 invalid_at)都是复杂的工程问题。DB-Agent-Memory 的扁平结构简单直接——一条事实就是一个独立的文本条目,不需要维护它和其他事实的关系。
8.2 时序能力 vs 无时序
这是两者最本质的区别。Graphiti 有双时间轴,DB-Agent-Memory 没有显式的时间处理机制。
当用户信息变化时(搬家、换工作),DB-Agent-Memory 的 L3 画像需要系统通过 LLM 批量去重来更新——但更新是覆盖式的,旧信息直接被替换,没有历史记录。你只能看到”用户住在上海”,看不到”用户曾经住在北京”。
从竞赛贡献的角度,这是 DB-Agent-Memory 最值得借鉴的设计点。在 L1 事实层加上 valid_at / expired_at 字段,让金字塔有时间维度,是一个有明确价值的改进方向。
8.3 重依赖 vs 零依赖
| 维度 | Graphiti | DB-Agent-Memory |
|---|---|---|
| 外部依赖 | Neo4j(必须)+ LLM API + Embedding API | SQLite + sqlite-vec(仅此) |
| 部署复杂度 | 高(需要独立服务) | 低(npm install 即可) |
| 检索延迟 | 100-500ms | <50ms |
| 关系推理能力 | 强 | 无 |
| 贡献者门槛 | 高(需要搭 Neo4j) | 低(即装即跑) |
Graphiti 用重依赖换来了图结构和时序能力;DB-Agent-Memory 用零依赖换来了部署简单和低延迟。两者不是”谁更好”的关系,而是”什么场景用什么”。
如果你的 Agent 需要复杂的关系推理(比如法律助手需要推理”原告和被告的合同关系”、医疗助手需要推理”药物之间的相互作用”),Graphiti 的图结构是刚需。如果你的 Agent 主要是个人助手(记住偏好、记录事件、提供个性化回复),DB-Agent-Memory 的扁平结构就够用,零依赖反而是优势。
8.4 对比总结
| 对比维度 | Graphiti | DB-Agent-Memory |
|---|---|---|
| 核心创新 | 双时间轴 + 知识图谱 | 四层金字塔 + MMD 注入 |
| 时序能力 | 有(valid_at / invalid_at) | 无 |
| 关系推理 | 原生支持 | 不支持 |
| 检索精度 | 高(两阶段 + 精排) | 中(BM25 + 向量融合) |
| 检索延迟 | 100-500ms | <50ms |
| 部署难度 | 高 | 低 |
| 代码量 | ~200 文件 | ~90 文件 |
| 适合场景 | 关系推理 + 时序演化 | 个人助手 + 轻量记忆 |
9. 竞赛启发——DB-Agent-Memory 可以借鉴什么
从 Graphiti 的设计中,DB-Agent-Memory 可以借鉴三个方向:
9.1 时序维度(最高优先级)
DB-Agent-Memory 的 L1 事实层目前没有时间戳。加上 valid_at(事实生效时间)和 expired_at(事实失效时间)字段,就能处理信息随时间变化的问题。
实现路径:
- L1 层的每条原子事实增加
valid_at和expired_at字段 - L0 到 L1 的提取过程中,LLM 判断事实的生效时间(处理回忆性描述)
- L1 层的去重扫描中,检测到冲突时给旧事实打上
expired_at而非删除 - 检索时增加时间过滤选项:只查当前有效事实 / 查历史事实 / 查某时刻快照
这个改进和现有的四层金字塔结构完全兼容——不需要改变金字塔的层级设计,只是在 L1 层增加了时间维度。MMD 注入也可以利用时间信息:只注入当前有效的事实到动态区,历史事实不占用宝贵的上下文预算。
9.2 两阶段检索(中等优先级)
DB-Agent-Memory 目前的检索是 BM25 + 向量融合的单步检索。借鉴 Graphiti 的两阶段架构,在候选集较大时增加一轮 LLM 精排,可以提升检索精度。
实现路径:
- 第一阶段:BM25 + sqlite-vec 融合检索,取 top-20
- 第二阶段:用 LLM 对 top-20 做精排,选出 top-5 注入上下文
- 时序过滤:精排前过滤掉已过时的事实
需要注意延迟预算——LLM 精排可能增加 200-500ms 延迟。可以在检索超时时降级为单步检索。
9.3 冲突检测的时序化(低优先级,但学术价值高)
当前 DB-Agent-Memory 的冲突检测是批量去重——LLM 判断两条事实是否矛盾,然后选保留哪条。借鉴 Graphiti 的时序边思路,可以把”冲突”概念化为”旧事实被新事实取代”,保留完整的历史演变。
实现路径:
- 不再删除冲突的旧事实,而是标记为”已被替代”
- 记录替代关系:新事实 ID → 旧事实 ID
- L3 画像只引用当前有效事实,但可以通过替代关系链追溯到历史
这个改进的学术价值在于:它把 DB-Agent-Memory 的四层金字塔和 Graphiti 的时序图谱统一了起来——金字塔的 L1 层变成了一个带时序的事实图谱。
10. 本章小结
回顾本章的核心要点:
- Graphiti 是 Zep 团队的时序知识图谱引擎,核心创新是给每条边加上
valid_at/invalid_at双时间轴 - 设计哲学是”城市志类比”——旧信息不删除,而是标注”已过时”,保留完整的时间轨迹
- 双时间轴的关键区分:
valid_at是事实生效时间(不是入库时间),invalid_at是事实失效时间(不是删除);处理”回忆性描述”时created_at和valid_at可能相差数月 - 检索架构是两阶段:向量检索 + 图遍历粗筛,Cross-Encoder 精排;同时利用 Neo4j 向量索引和 Cypher 图遍历
- 最大局限是 Neo4j 强依赖——部署重、延迟可能数百ms、深度绑定 Cypher 特有扩展
- 和 DB-Agent-Memory 对比:图结构 vs 扁平分层、时序能力 vs 无时序、重依赖 vs 零依赖
- 竞赛启发:最高优先级是为 L1 事实层增加时序维度(
valid_at/expired_at),这是其他六个项目都没做但 DB-Agent-Memory 可以率先实现的改进
下一章我们看 cognee——另一个使用图数据库的项目,但走的是完全不同的”本体论驱动”路线。
导读目录:README.md 上一章:Ch10: Letta/MemGPT——虚拟内存与 Agent 自管理 下一章:Ch12: cognee——本体论驱动的认知管线