先想一个简单问题:用户去年在 A 团队,今年转到 B 团队。agent 如果只保存最新事实,回答“去年他在哪个团队”会出错; 如果把两条事实都当作同等当前事实,又会在今天的任务分配里产生冲突。Graphiti 的 temporal context graph 想处理的, 正是这种“事实会变化,但历史不能消失”的记忆问题。

这篇会一直沿着这个例子走:episode1 说“用户在 A 团队”,episode2 说“用户转到 B 团队”。 关键不是把两句话都搜出来,而是让旧关系退出当前视图,同时仍然能回答历史问题。

阅读契约。 读完这一篇,应该能回答三件事:Graphiti 为什么把 raw data 称为 episode; EntityEdge 上的 valid_atinvalid_atexpired_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 按当前时间和查询目标拼出来的上下文子图。 这一轮应该给模型看哪部分?
Graphiti 机制图,展示 episode 进入 EpisodicNode 后抽取 entities 和 fact edges,旧 fact 被 invalid_at 标记失效但仍保留历史
Graphiti 的关键在于 fact edge 有时间窗口,且能沿 provenance 回到 episode。旧事实失效,不等于删除历史。

二、Episode 是原始证据流,不是普通摘要

2.1 先分清入库时间和事实时间

Graphiti 把输入先落到 EpisodicNode。源码里这个节点保存 sourcesource_descriptioncontentvalid_atentity_edgesepisode_metadata。其中 valid_at 的描述是原始文档创建时间; 保存节点时,contententity_edgescreated_atvalid_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_uuidentity_uuiduuidgroup_idcreated_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_edgesinvalidated_edgesnew_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? 这个问题会把长期记忆从“存什么、怎么搜”推进到“什么时候改、由哪条执行路径改”。

参考资料