犀牛鸟 2026 研究笔记 estelledc.github.io

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 月生效,但可能早已失效。

这看起来是细微的区别,但在时序推理中至关重要。比如:

没有双时间轴,这些时序查询都不可能准确回答。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_atinvalid_atcreated_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 应用。生产部署至少需要:

对比 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 有以上局限,但它也有合理的一面:

这本质上是一个经典的工程权衡:锁定换来简洁。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 层增加了时间维度。MMD 注入也可以利用时间信息:只注入当前有效的事实到动态区,历史事实不占用宝贵的上下文预算。

9.2 两阶段检索(中等优先级)

DB-Agent-Memory 目前的检索是 BM25 + 向量融合的单步检索。借鉴 Graphiti 的两阶段架构,在候选集较大时增加一轮 LLM 精排,可以提升检索精度。

实现路径:

需要注意延迟预算——LLM 精排可能增加 200-500ms 延迟。可以在检索超时时降级为单步检索。

9.3 冲突检测的时序化(低优先级,但学术价值高)

当前 DB-Agent-Memory 的冲突检测是批量去重——LLM 判断两条事实是否矛盾,然后选保留哪条。借鉴 Graphiti 的时序边思路,可以把”冲突”概念化为”旧事实被新事实取代”,保留完整的历史演变。

实现路径:

这个改进的学术价值在于:它把 DB-Agent-Memory 的四层金字塔和 Graphiti 的时序图谱统一了起来——金字塔的 L1 层变成了一个带时序的事实图谱。


10. 本章小结

回顾本章的核心要点:

下一章我们看 cognee——另一个使用图数据库的项目,但走的是完全不同的”本体论驱动”路线。


导读目录:README.md 上一章:Ch10: Letta/MemGPT——虚拟内存与 Agent 自管理 下一章:Ch12: cognee——本体论驱动的认知管线