先想一个简单问题:用户去年在 A 团队,今年转到 B 团队。agent 如果只保存最新事实,回答“去年他在哪个团队”会出错; 如果把两条事实都当作同等当前事实,又会在今天的任务分配里产生冲突。Graphiti 的 temporal context graph 想处理的, 正是这种“事实会变化,但历史不能消失”的记忆问题。
这篇会一直沿着这个例子走:episode1 说“用户在 A 团队”,episode2 说“用户转到 B 团队”。 关键不是把两句话都搜出来,而是让旧关系退出当前视图,同时仍然能回答历史问题。
阅读契约。
读完这一篇,应该能回答三件事:Graphiti 为什么把 raw data 称为 episode;
EntityEdge 上的 valid_at、invalid_at 和 expired_at 分别在表达什么;
hybrid retrieval 为什么要同时看语义、关键词和图遍历。
先把这条“用户从 A 团队转到 B 团队”的事实走完一遍。读 Graphiti 可以从事实归属切入: 一条原文先落到 episode,再变成实体和关系,最后由时间字段决定当前视图。读源码时只要能复述下面这条链, 后面的字段和检索策略就不会散。
| 阶段 | 这条事实的形状 | 谁会在后面使用它 |
|---|---|---|
| 原始输入 | episode1 / episode2,带 source、content、valid_at。 | 用于追溯事实来源和事实时间。 |
| 图入库 | EpisodicNode、实体节点和 episode edge。 |
把原文流和图里的对象连起来。 |
| 关系解析 | EntityEdge 保存 fact、embedding、episodes、时间字段。 |
同时服务相似召回、来源追踪和时间判断。 |
| 新事实到来 | 旧边进入 invalidated_edges,新边进入 new_edges。 |
当前视图看 B 团队,历史问题仍能回到 A 团队。 |
证据边界。 本文只使用 getzep/graphiti 的公开 README 与源码。 README 把 Graphiti 描述为 Zep context infrastructure 的开源 temporal context graph engine; Zep 托管服务的规模、治理和低延迟能力按 README 公开定位介绍,不推断私有实现。
一、为什么记忆需要 temporal context graph
1.1 向量检索能找相似句子,但不擅长回答“当时是否成立”
Graphiti README 对自己的定位很明确:它用于构建和查询 AI agents 的 temporal context graphs, 这些图会追踪 facts 如何随时间变化,保留到 source data 的 provenance,并支持 prescribed 与 learned ontology。 README 还把它和传统 RAG 区分开:Graphiti 会持续整合用户交互、结构化与非结构化数据、外部信息, 支持 incremental updates、historical queries,并且不需要每次完整重算图。 这些说法集中在 README 的项目介绍。
如果只做向量检索,系统可以找到“用户在 A 团队”和“用户在 B 团队”两段相似事实,却很难自己判断哪条是历史、哪条是当前状态。 直接覆盖旧事实会让“去年在哪个团队”无从回答;两条都保留但不加时间窗口,又会让今天的任务分配产生冲突。 Graphiti 的图不是为了把向量库画得更漂亮,而是把事实、实体、来源和时间有效期放进同一套查询结构。
1.2 context graph 更像事实账本,而不是普通知识图谱截图
更具体地说,Graphiti 的 context graph 由四类东西组成: entities、facts / relationships、episodes 和 custom types。 README 的表格把 facts / relationships 定义为带 temporal validity windows 的 triplets; episodes 则是 ingested raw data,也是每条 derived fact 能追溯回去的 ground truth stream。 这就把“图”从装饰性的关系视图,变成了一个带时间和来源的事实账本。
| 组件 | 在 Graphiti 里的意义 | 记忆问题 |
|---|---|---|
| Episode | 原始输入流,带 source、content、valid_at。 | 这条事实最早来自哪里? |
| Entity | 人、项目、文档、地点、概念等节点。 | 记忆里的对象是谁? |
| Fact edge | 实体之间的事实关系,带 fact text、embedding、来源 episode 和时间字段。 | 这段关系什么时候成立,现在还成立吗? |
| Current view | 按当前时间和查询目标拼出来的上下文子图。 | 这一轮应该给模型看哪部分? |
二、Episode 是原始证据流,不是普通摘要
2.1 先分清入库时间和事实时间
Graphiti 把输入先落到 EpisodicNode。源码里这个节点保存 source、
source_description、content、valid_at、entity_edges
和 episode_metadata。其中 valid_at 的描述是原始文档创建时间;
保存节点时,content、entity_edges、created_at、valid_at
都会作为 episode 数据写入图里。这可以直接看
EpisodicNode 定义。
class EpisodicNode(Node):
source: EpisodeType = Field(description="source type")
source_description: str = Field(description="description of the data source")
content: str = Field(description="raw episode data")
valid_at: datetime = Field(description="datetime of when the original document was created")
entity_edges: list[str] = Field(default_factory=list)
episode_args = {
"content": self.content,
"entity_edges": self.entity_edges,
"created_at": self.created_at,
"valid_at": self.valid_at,
"source": self.source.value,
}
这里有一个很容易被忽略的区别:created_at 是这条记录进入系统的时间,
valid_at 是原始事实对应的参考时间。用户今天导入一份去年的会议纪要,
系统写入时间是今天,但纪要里的事实可能属于去年。长期记忆要能回答历史问题,就不能只看入库时间。
用户今天导入去年的会议纪要:
created_at = 今天,系统何时看到这份资料
valid_at = 去年,资料里的事实从哪个时间点开始成立
回答“现在属于哪个团队”:
需要看当前有效的 fact edge
回答“去年属于哪个团队”:
需要按历史时间窗口回看旧 edge
2.2 Episode edge 让事实可以回到原始输入
episode 和 entity 之间通过 EpisodicEdge 相连。
EpisodicEdge.save()
写入的是 episode_uuid、entity_uuid、uuid、group_id 和
created_at,也就是“这段原始输入提到了哪个实体”。后面 fact edge 的来源追踪,就是建立在这些 episode 连接之上。
provenance 在长期记忆里不是附加信息,而是纠错入口。用户或系统后来发现某条事实不对时,需要知道它来自哪次对话、 哪个文档、哪个外部源。否则图里只剩下“节点之间有关系”,却没有办法解释这段关系为什么出现、什么时候出现、该不该撤回。
三、Fact edge 承担事实、来源和有效期
3.1 关系边同时保存事实文本、向量和时间窗口
真正表达长期记忆事实的,是 EntityEdge。源码字段非常直白:
name 表示关系名,fact 保存事实文本,fact_embedding 用于相似度检索,
episodes 保存引用这条关系的 episode ids。
时间字段则有三组:valid_at 表示 fact 何时开始为真,invalid_at 表示何时不再为真,
expired_at 表示这条边何时被系统 invalidated。
这些字段和保存逻辑在 EntityEdge
里能看到。
class EntityEdge(Edge):
name: str = Field(description="name of the edge, relation name")
fact: str = Field(description="fact representing the edge and nodes that it connects")
fact_embedding: list[float] | None = Field(default=None)
episodes: list[str] = Field(default=[])
expired_at: datetime | None = Field(default=None)
valid_at: datetime | None = Field(default=None)
invalid_at: datetime | None = Field(default=None)
reference_time: datetime | None = Field(default=None)
这几个字段解决的是不同问题。fact 负责给模型读,fact_embedding 负责召回相似事实,
episodes 负责追溯来源,valid_at / invalid_at 负责回答时间问题,
expired_at 则记录系统何时把旧边判为失效。Graphiti 不是在“文本记忆旁边放一张图”,而是让一条边同时成为检索对象、
事实对象和时间对象。
3.2 invalidation 不是删除,而是把当前视图和历史视图分开
这样设计以后,新信息到来时,系统不必把旧关系抹掉。Graphiti 的边解析流程会把结果拆成
resolved_edges、invalidated_edges 和 new_edges。
_extract_and_resolve_edges()
的 docstring 写得很清楚:invalidated_edges 是被新信息 invalidated 的旧边,
new_edges 是真正新增且不是重复的边。也就是说,Graphiti 的写入路径不是只做追加,也不是简单覆盖;
它会把“新事实让旧事实失效”显式记录下来。
resolved_edges, invalidated_edges, new_edges = await resolve_extracted_edges(
self.clients,
edges,
primary_episode,
nodes,
edge_types or {},
edge_type_map,
)
return resolved_edges, invalidated_edges, new_edges
形状示例:团队事实更新后
edgeA:
fact = "用户属于 A 团队"
episodes = ["episode1"]
valid_at = 2025-03-01
invalid_at = 2026-01-10
expired_at = 写入失效标记的系统时间
edgeB:
fact = "用户属于 B 团队"
episodes = ["episode2"]
valid_at = 2026-01-10
invalid_at = null
当前时间问题:未失效的 edgeB 进入当前答案候选
带时间条件的历史问题:沿 edgeA -> episode1 找回 A 团队事实
放回团队例子里,episode1 生成一条“用户 member_of A 团队”的 edge;episode2 到来后,
resolution 可以把旧 edge 放进 invalidated_edges,并产生“用户 member_of B 团队”的 new_edges。
当前任务看的是未失效的 B 团队关系;历史问题仍然能沿旧 edge 和 episode1 回到 A 团队事实。
valid_at 是事实声称开始成立的业务时间,invalid_at 是它被后续事实覆盖的业务时间,
expired_at 是系统写入失效标记的时间。三个字段分开,Graphiti 才能同时回答“现在是什么”和“当时是什么”。
这和 Mem0 v3 的 ADD-only 路线可以放在一起看。Mem0 v3 选择在 memory record 层面保留追加历史, 再把时间解释、排序和衰减放到检索与上下文组装阶段。Graphiti 则把时间窗口直接放进 fact edge, 让图结构本身携带“现在是否成立”和“曾经是否成立”的信息。两者都在避免把历史直接擦掉,只是一个偏 memory layer, 一个偏 temporal graph。
四、检索要组装当前子图
4.1 搜索对象不止一类,所以召回也要分路
既然事实存在图上,检索也不能只看一条文本相似度。Graphiti README 把 hybrid retrieval 描述为 semantic embeddings、
keyword / BM25 和 graph traversal 的组合;源码里的 search config 也对应这三类方法。
search_config.py
定义了 edge、node、episode、community 的搜索方法和 reranker:edge 与 node 都支持 cosine similarity、BM25 和 BFS,
reranker 可以用 RRF、node distance、episode mentions、MMR 或 cross encoder。
class EdgeSearchMethod(Enum):
cosine_similarity = "cosine_similarity"
bm25 = "bm25"
bfs = "breadth_first_search"
class EdgeReranker(Enum):
rrf = "reciprocal_rank_fusion"
node_distance = "node_distance"
episode_mentions = "episode_mentions"
mmr = "mmr"
cross_encoder = "cross_encoder"
这比“向量 + BM25”多了一层图语义:query 可能先命中某条 fact edge,也可能先命中某个 entity,再沿图找到相邻关系; 还可能需要 episode 来解释来源。不同搜索方法的结果最后要进入同一个上下文组装步骤,所以 Graphiti 的检索更像“先找候选子图”, 再决定哪些边、节点和 episode 该进入模型视图。
4.2 当前回答需要的是被时间和关系过滤后的子图
查询执行时,如果配置需要 cosine similarity 或 MMR,
search()
会先为 query 创建 embedding;同一套 search result 里可以返回 edges、nodes、episodes 和 communities。
这说明 Graphiti 的 retrieval 目标是一组有关系、有来源、有时间语义的对象,而不是只返回最像 query 的文本片段。
| 查询阶段 | Graphiti 在找什么 | 回到例子里的效果 |
|---|---|---|
| 语义 / BM25 | 和 query 相似的 fact edge、node、episode。 | “团队”“team A”“team B”这类说法都能进入候选。 |
| BFS / 图遍历 | 从命中实体扩展相邻关系。 | 先命中用户,也能走到团队关系。 |
| RRF / MMR / cross encoder | 合并和重排多路候选。 | 减少只靠一个分数导致的偏差。 |
| 时间过滤与上下文组装 | 按当前或历史时间点选择有效边和来源。 | 今天的问题看 B 团队;去年的问题能回看 A 团队。 |
这也解释了 Zep 和 Graphiti 的关系。README 说 Graphiti 是 Zep context infrastructure 的开源 temporal context graph engine; Zep 则负责规模化管理 context graphs,提供治理、低延迟 retrieval 和 production deployments 所需的上下文组装。 对读源码的人来说,可以把 Graphiti 看作可自托管、可改造的图记忆内核,把 Zep 看作围绕这套内核做成的生产上下文平台。
五、把 Graphiti 放回 Agent Memory 地图
5.1 Graphiti 的答案是:记忆由时间图拥有
Mem0、Letta 和 Graphiti 的差别,现在可以说得更具体。Mem0 的中心是 memory service:怎样从对话抽取、写入、检索和排序记忆。 Letta 的中心是 agent state:memory block、messages、tools、files 和 system prompt 怎样共同重建一个可持续工作的 agent。 Graphiti 的中心是 temporal graph:事实之间的关系、时间窗口和来源怎样成为可查询结构。
| 项目 | 谁拥有记忆 | 最核心的工程问题 |
|---|---|---|
| Mem0 | 外置 memory layer | 写入时怎样抽取与去重,读取时怎样排序、解释时间和控制噪声。 |
| Letta | stateful agent | 长期 block、消息窗口、工具和文件视图怎样进入模型可见状态。 |
| Graphiti | temporal context graph | 事实变化时,怎样保留历史、来源和当前可用子图。 |
所以下一篇转到 LangMem / LangGraph,会自然进入另一个问题:如果记忆不是单独服务,也不是 agent 自己的持久状态, 而是嵌在工作流运行路径里,那么写入和整理记忆应该发生在 agent 当前推理的 hot path,还是交给后台 manager? 这个问题会把长期记忆从“存什么、怎么搜”推进到“什么时候改、由哪条执行路径改”。
参考资料
- Graphiti README:temporal context graph 定位
- Graphiti README:context graph 的组成
- Graphiti README:Graphiti 与 Zep
- Graphiti README:Temporal Fact Management 与 Hybrid Retrieval
- Graphiti
EpisodicNode - Graphiti
EpisodicEdge - Graphiti
EntityEdge - Graphiti edge extraction and invalidation path
- Graphiti search config
- Graphiti search execution