知识图谱在AI-memory的“最佳实践”-zep底层存储架构Graphiti源码解读
Build Real-Time Knowledge Graphs for AI Agents

1. 引言
如标题所说的,Graphiti与其他知识图谱引擎的主要区别在于它具有处理动态信息更新的能力,通过时间抽取和边无效化过程来实现。
A key differentiating feature of Graphiti compared to other knowledge graph engines is its capacity to manage dynamicinformation updates through temporal extraction and edge invalidation processes
在构建边时,对于存在事实冲突的边会做失效处理,因此避免了冲突上下文的干扰。举个栗子,小明昨天说“我不喜欢吃西瓜”,今天又说“我喜欢吃西瓜”,这种在语义上存在冲突的句子在检索时往往会让大模型摸不着头脑,而Graphiti提出了双时间模型来解决这种冲突。
当然,该框架的实际优势远不仅限于此。Graphiti通过其先进的算法和数据结构设计,在保证高效实时更新的同时,还提供了对复杂查询的支持、灵活的数据模型以及强大的扩展能力,使其成为处理大规模知识图谱应用的理想选择。
接下来,我们将深入探讨Graphiti的内部机制,包括但不限于其实时处理架构、冲突解决策略以及如何实现高效的边管理。通过对源码的剖析,我们可以更全面地理解Graphiti是如何实现这些高级功能,并支持前沿应用场景的。
补充一点,Zep官方宣称其是“agent记忆中最先进的新艺术”,而其底层记忆存储便是Graphiti。
参考链接:https://blog.getzep.com/state-of-the-art-agent-memory/
2. 框架
2.1 解决了什么问题?(What&Why?)
官方给出的文档中,介绍了Graphiti使用的场景。
Graphiti is specifically designed to address the challenges of dynamic and frequently updated datasets, making it particularly suitable for applications requiring real-time interaction and precise historical queries.
我们知道,知识图谱在应用上的最大限制便是其昂贵的构建成本以及动态更新困难的问题。在构建成本上,没有看出Graphiti的优势,依然花token如流水...但在动态更新上,Graphiti能够很好的支持,特别是在存在语义冲突时如之前提到的栗子,Graphiti会为冲突边添加invalid_time来表示该边已经失效,在检索时也就不会再检索失效的边,这种做法很好的解决了上下文冲突。
Zep的官方文档也给出了Graphiti与GraphRAG的比较。

2.2 实现方式是什么(How?)
这里我把Graphiti的实现方式简单分为两部分,索引和检索。这也是大部分知识图谱构建的方式。
索引
索引是什么?我的理解是,实体和边的提取就是索引构建的过程,Graphiti支持自定义实体和边,我们可以根据自己的存储需求来设计,后续会详细介绍设计方法。在这里有一个Episode的概念,我们每次传入的文本会被当作一个Episode节点,其中记录了我们传入的完整文本内容。索引构建流程如下所示:

检索
在构建索引后,图谱保存在图数据库中Graphiti支持Neo4j和falkordb两种数据库。检索方式分为fact检索和node检索,当用户输入query后,会通过该query执行fact和node的全文和语义检索,也可以自己设定额外执行bfs检索。
3. 源码解读
Graphiti的核心功能集中在graphiti_core包下,包括索引、检索等模块其源码结构树如下:
graphiti_core
├── __init__.py
├── __pycache__
│ ├── __init__.cpython-313.pyc
│ ├── edges.cpython-313.pyc
│ ├── errors.cpython-313.pyc
│ ├── graph_queries.cpython-313.pyc
│ ├── graphiti.cpython-313.pyc
│ ├── graphiti_types.cpython-313.pyc
│ ├── helpers.cpython-313.pyc
│ └── nodes.cpython-313.pyc
├── cross_encoder
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-313.pyc
│ │ ├── client.cpython-313.pyc
│ │ └── openai_reranker_client.cpython-313.pyc
│ ├── bge_reranker_client.py
│ ├── client.py
│ └── openai_reranker_client.py
├── driver
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-313.pyc
│ │ ├── driver.cpython-313.pyc
│ │ └── neo4j_driver.cpython-313.pyc
│ ├── driver.py
│ ├── falkordb_driver.py
│ └── neo4j_driver.py
├── edges.py
├── embedder
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-313.pyc
│ │ ├── azure_openai.cpython-313.pyc
│ │ ├── client.cpython-313.pyc
│ │ └── openai.cpython-313.pyc
│ ├── azure_openai.py
│ ├── client.py
│ ├── gemini.py
│ ├── myEmbedder.py
│ ├── openai.py
│ └── voyage.py
├── errors.py
├── graph_queries.py
├── graphiti.py
├── graphiti_types.py
├── helpers.py
├── llm_client
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-313.pyc
│ │ ├── azure_openai_client.cpython-313.pyc
│ │ ├── client.cpython-313.pyc
│ │ ├── config.cpython-313.pyc
│ │ ├── errors.cpython-313.pyc
│ │ └── openai_client.cpython-313.pyc
│ ├── anthropic_client.py
│ ├── azure_openai_client.py
│ ├── client.py
│ ├── config.py
│ ├── errors.py
│ ├── gemini_client.py
│ ├── groq_client.py
│ ├── openai_client.py
│ ├── openai_generic_client.py
│ └── utils.py
├── models
│ ├── __init__.py
│ ├── __pycache__
│ │ └── __init__.cpython-313.pyc
│ ├── edges
│ │ ├── __init__.py
│ │ ├── __pycache__
│ │ │ ├── __init__.cpython-313.pyc
│ │ │ └── edge_db_queries.cpython-313.pyc
│ │ └── edge_db_queries.py
│ └── nodes
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-313.pyc
│ │ └── node_db_queries.cpython-313.pyc
│ └── node_db_queries.py
├── nodes.py
├── prompts
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-313.pyc
│ │ ├── dedupe_edges.cpython-313.pyc
│ │ ├── dedupe_nodes.cpython-313.pyc
│ │ ├── eval.cpython-313.pyc
│ │ ├── extract_edge_dates.cpython-313.pyc
│ │ ├── extract_edges.cpython-313.pyc
│ │ ├── extract_nodes.cpython-313.pyc
│ │ ├── invalidate_edges.cpython-313.pyc
│ │ ├── lib.cpython-313.pyc
│ │ ├── models.cpython-313.pyc
│ │ ├── prompt_helpers.cpython-313.pyc
│ │ ├── search_generate.cpython-313.pyc
│ │ └── summarize_nodes.cpython-313.pyc
│ ├── dedupe_edges.py
│ ├── dedupe_nodes.py
│ ├── eval.py
│ ├── extract_edge_dates.py
│ ├── extract_edges.py
│ ├── extract_nodes.py
│ ├── invalidate_edges.py
│ ├── lib.py
│ ├── models.py
│ ├── prompt_helpers.py
│ ├── search_generate.py
│ └── summarize_nodes.py
├── py.typed
├── search
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-313.pyc
│ │ ├── search.cpython-313.pyc
│ │ ├── search_config.cpython-313.pyc
│ │ ├── search_config_recipes.cpython-313.pyc
│ │ ├── search_filters.cpython-313.pyc
│ │ └── search_utils.cpython-313.pyc
│ ├── search.py
│ ├── search_config.py
│ ├── search_config_recipes.py
│ ├── search_filters.py
│ ├── search_helpers.py
│ └── search_utils.py
└── utils
├── __init__.py
├── __pycache__
│ ├── __init__.cpython-313.pyc
│ ├── bulk_utils.cpython-313.pyc
│ └── datetime_utils.cpython-313.pyc
├── bulk_utils.py
├── datetime_utils.py
├── maintenance
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-313.pyc
│ │ ├── community_operations.cpython-313.pyc
│ │ ├── edge_operations.cpython-313.pyc
│ │ ├── graph_data_operations.cpython-313.pyc
│ │ ├── node_operations.cpython-313.pyc
│ │ └── temporal_operations.cpython-313.pyc
│ ├── community_operations.py
│ ├── edge_operations.py
│ ├── graph_data_operations.py
│ ├── node_operations.py
│ ├── temporal_operations.py
│ └── utils.py
└── ontology_utils
├── __pycache__
│ └── entity_types_utils.cpython-313.pyc
└── entity_types_utils.py
3.1 Demo
graphiti提供了mcp_server我们一般也是将graphiti部署为mcp_serer并通过调用mcp_tool的方式来使用,可以通过运行graphiti_mcp_server.py部署mcp_server。在运行之前需要先配置下环境变量,可以通过.env文件进行配置也可以通过export命令配置,可以参考以下.env文件进行配置:
# Graphiti MCP Server Environment Configuration
NEO4J_URI=*********
NEO4J_USER=neo4j
NEO4J_PASSWORD=******
# OpenAI API Configuration
# Required for LLM operations
OPENAI_API_KEY=****可以配置阿里云百炼apikey,也可以使用OPENAI的apikey
MODEL_NAME=qwen-plus
# Optional: Only needed for non-standard OpenAI endpoints
OPENAI_BASE_URL=******根据选择的模型提供商进行配置
如果不是使用OPENAI的apikey会有一些坑,详细会写另一篇文档。
配置完成后就可以运行graphiti_mcp_server.py。
2025-07-17 19:10:29,729 - __main__ - INFO - Graphiti client initialized successfully
2025-07-17 19:10:29,730 - __main__ - INFO - Using OpenAI model: qwen-plus
2025-07-17 19:10:29,730 - __main__ - INFO - Using temperature: 0.0
2025-07-17 19:10:29,730 - __main__ - INFO - Using group_id: default
2025-07-17 19:10:29,730 - __main__ - INFO - Custom entity extraction: enabled
2025-07-17 19:10:29,730 - __main__ - INFO - Starting MCP server with transport: sse
2025-07-17 19:10:29,730 - __main__ - INFO - Running MCP server with SSE transport on 127.0.0.1:8000
INFO: Started server process [86089]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
运行完成后会提示当前服务运行在8000端口,可以将其配置到cursor当中,配置也比较简单可以参见后续文章~运行完成后你就拥有了一个graphiti mcp服务,可以让模型调用工具来添加和检索记忆,添加完知识后你就会拥有一张属于你自己的知识图谱。

在详细介绍源码前,想先讲一下图谱中的实体和关系在代码中的定义,以及每个字段的含义。
EntitiNode
class EntityNode(Node):
name_embedding: list[float] | None = Field(default=None, description='embedding of the name')
summary: str = Field(description='regional summary of surrounding edges', default_factory=str)
attributes: dict[str, Any] = Field(
default={}, description='Additional attributes of the node. Dependent on node labels'
)
实体节点包括三个字段,其中name_embedding主要用在语义相似度检索中,summary会简要介绍该实体作为补充上下文。attributes是实体的各种属性,在自定义实体时不同实体类型可以定义不同的属性。
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, description='embedding of the fact')
episodes: list[str] = Field(
default=[],
description='list of episode ids that reference these entity edges',
)
expired_at: datetime | None = Field(
default=None, description='datetime of when the node was invalidated'
)
valid_at: datetime | None = Field(
default=None, description='datetime of when the fact became true'
)
invalid_at: datetime | None = Field(
default=None, description='datetime of when the fact stopped being true'
)
attributes: dict[str, Any] = Field(
default={}, description='Additional attributes of the edge. Dependent on edge name'
)
关系的主要属性为fact,在检索时根据fact和fact_embedding执行关键字和语义检索,这里有三个与时间相关的属性expired_at、valid_at和invalid_at分别表示了关系的过期时间、生效时间和失效时间,这也是Graphiti的创新点所在。当关系产生时会设置其valid_at属性,当从之后的文本中提取出冲突的事实时会设置关系的invalid_at表示关系失效以避免冲突。
这里我想先通过一个具体的业务场景来引出我们所做的优化方案,这样可以让技术实现更有上下文和落地价值。
目前我们探索播客场景的应用,该场景下可以根据用户输入的文本内容,自动生成一篇播客。用户可以自由选择喜欢的音色、感兴趣的主题内容,从而实现个性化的播客定制。此外,会有多个官方频道,每个频道下设有不同类型的播客栏目。这些播客内容同样由 AI 自动生成,并每日更新,形成连载形式的内容输出。
然而,在生成下一期播客内容时,我们希望它能够参考之前的内容,保持观点的一致性、事件发展的连贯性以及话题之间的关联性。这就带来了一个挑战:如何有效地对历史播客内容进行建模和记忆。
目前常见的做法是使用基于向量的记忆机制,即将历史内容进行分段、编码为向量后存储。虽然这种方式实现简单、扩展性强,但在我们的场景中暴露出一些明显的问题,例如:
- 主播观点前后冲突;
- 事件发展的时序关系不清晰;
- 不同话题之间缺乏关联性,难以形成整体认知。
为了解决这些问题,我们引入了知识图谱(Knowledge Graph)作为全局记忆机制,构建一个结构化的知识网络。通过知识图谱,我们不仅能够记录事件的发展过程,实现时序感知,还可以在不同话题之间建立关联关系,使得在生成内容时具备更强的上下文理解和逻辑连贯能力。
接下来,我们将围绕这一场景,结合源码来具体讲解我们是如何实现这一优化方案的。
3.2 实体设计
针对播客这一特定场景,我们对 Graphiti 原有的实体类型体系进行了定制化扩展和优化。在原始设计中,Graphiti 通常使用统一的 Entity 类型来表示各类信息,但在播客生成这一复杂语义场景中,单一实体类型难以准确表达内容之间的丰富关系。
因此,我们引入了六种结构化实体类型:Podcast(播客)、Channel(频道)、Person(人物)、Event(事件)、Viewpoint(观点)和 Topic(话题)。每种实体类型都定义了与其语义相符的属性。例如:
Podcast包含title、publish_time等属性,表示播客的主题与发布时间;Person定义了occupation(职业)和bio(个人简介)等属性,用于描述主播或嘉宾信息;- Event 记录事件的起止时间、涉及人物及关键节点;
Viewpoint用于表达主播或嘉宾对某一事件的看法,并与Event和Person建立关联;Topic表示播客讨论的核心话题;Channel则用于组织播客栏目,定义了频道名称、类型和描述等信息。
通过这种结构化的实体建模方式,我们能够更精准地捕捉播客内容中的语义关系,提升知识图谱对上下文的理解能力。同时,这种可扩展的实体体系也具备良好的通用性,开发者可以根据不同业务场景灵活定义实体类型,实现真正的场景化定制。相比原始的单一 Entity 类型,该方案在信息表达能力和推理能力上都有显著提升。
3.3 关系设计
在关系建模方面,我们也根据播客场景的特点,扩展了一系列自定义的关系类型,以更准确地表达知识图谱中的语义关联。新增的关系类型包括:
BELONGS_TO:表示“播客属于某个频道”,例如“播客A → BELONGS_TO → 频道9”;PLAY:表示“频道播放某播客”,例如“频道9 → PLAY → 播客A”;HAS_SPEAKER:表示“播客包含某个主播”,例如“播客A → HAS_SPEAKER → 小茶”;MENTIONS:表示“播客提及了某个实体”,例如“播客A → MENTIONS → 跨境支付”或“播客A → MENTIONS → 话题A”;EXPRESSES:表示“播客表达了某个观点”,例如“播客A → EXPRESSES → 观点B”;DISCUSSES:表示“观点讨论了某个话题”,例如“观点B → DISCUSSES → 区块链”。
为了实现关系类型的灵活扩展,我们新增了一个 customed_config 配置文件,用于快速定义和注册自定义实体与关系类型。在定义好关系后,还需建立一个关系映射字典,用于指定每种关系的源节点类型(`、head_type)与目标节点类型(tail_type),以确保图谱构建过程中的类型一致性。
该映射字典可按照如下格式进行定义:
custom_edge_type_map = {
("Podcast", "Channel"): ["Belongs_to"],
("Channel", "Podcast"): ["Play"],
("Podcast", "Person"): ["Has_speaker"],
("Person", "Podcast"): ["Hosts"],
("Podcast", "Event"): ["Mentions"],
("Podcast", "Topic"): ["Mentions"],
("Podcast", "Viewpoint"): ["Expresses"],
("Entity", "Entity"): ["Expresses", "Belongs_to", "Has_speaker", "Mentions"],
}
3.4 索引
索引的建立从你添加的第一段文本开始,首先会检索之前加入的n个Episode节点作为参考上下文为模型提取实体和关系补充信息,同时创建新的EpisodicNode节点保存当前传入的文本内容。
retrieve_episodes
previous_episodes = (
await self.retrieve_episodes(
reference_time,
last_n=RELEVANT_SCHEMA_LIMIT,
group_ids=[group_id],
source=source,
)
)
extract_nodes
在获取之前n个episode后,会利用大模型从当前文本中提取实体节点,这里有三种文本类型可以选择包括text、message和json,如果使用mcp工具调用可以传入对应参数来选择文本类型。模型提取出的实体会保存在extracted_entities列表中。
之后会根据规则过滤掉空值的实体,并将其转为EntityNode类型(Graphiti中规定的实体类型,所以实体都使用这个类来表示,其中定义了许多实体处理函数如向量化等),实体和关系底层通过Pydantic做格式限制,当模型响应的实体格式不符合要求时会报错(报错最多,最大的坑)。这里也可以参考下下一篇文章“避坑指南”,零报错不是梦🎉 。
resoleve_extracted_nodes
提取实体节点后需要与之前的实体节点进行去重,利用search_node方法检索相似实体节点,这里根据node.name来判断实体相似性。方法内会同时执行全文本、语义相似性和bfs检索(需配置),在检索到相似节点后利用大模型进行实体去重。这里为了进行bfs检索,会分别利用全文本和语义检索得分最高的实体节点作为中心节点进行bfs。
search_results: list[list[EntityNode]] = list(
await semaphore_gather(
*[
node_fulltext_search(driver, query, search_filter, group_ids, 2 * limit),
node_similarity_search(
driver, query_vectors, search_filter, group_ids, 2 * limit, config.sim_min_score
),
node_bfs_search(
driver, bfs_origin_node_uuids, search_filter, config.bfs_max_depth, 2 * limit
),
]
)
)
实体去重时大模型会根据提供的上下文判断当前提取的实体节点是否与已有的实体节点重复,如果存在重复则会整合重复的实体。
extract_edges
提取完实体后,接下来就是提取实体之间的关系了,与实体提取类似首先会构建上下文,包括提取到的实体以及前n个Episode节点,然后传入模型根据提示词提取对应的事实三元组提取完成后最终会以EntityEdge类型返回。
resolve_extracted_edges
该方法会对提取到的关系进行语义去重、类型推断和冲突处理,首先会通过语义相似性检索与当前提取边相关的事实以及可能存在冲突的事实,然后会根据关系的源节点类型和目标节点类型映射到相应的关系类型,最后利用大模型对存在冲突的关系进行整合处理。
"""
检索相关的边和可能冲突的边
"""
search_results: tuple[list[list[EntityEdge]], list[list[EntityEdge]]] = await semaphore_gather(
get_relevant_edges(driver, extracted_edges, SearchFilters()),
get_edge_invalidation_candidates(driver, extracted_edges, SearchFilters(), 0.2),
)
related_edges_lists, edge_invalidation_candidates = search_results
"""
映射到对应关系类型
"""
edge_types_lst: list[dict[str, BaseModel]] = []
for extracted_edge in extracted_edges:
source_node_labels = uuid_entity_map[extracted_edge.source_node_uuid].labels + ['Entity']
target_node_labels = uuid_entity_map[extracted_edge.target_node_uuid].labels + ['Entity']
label_tuples = [
(source_label, target_label)
for source_label in source_node_labels
for target_label in target_node_labels
]
extracted_edge_types = {}
for label_tuple in label_tuples:
type_names = edge_type_map.get(label_tuple, [])
for type_name in type_names:
type_model = edge_types.get(type_name)
if type_model is None:
continue
extracted_edge_types[type_name] = type_model
edge_types_lst.append(extracted_edge_types)
"""
冲突处理、去重和属性提取
"""
results: list[tuple[EntityEdge, list[EntityEdge]]] = list(
await semaphore_gather(
*[
resolve_extracted_edge(
llm_client,
extracted_edge,
related_edges,
existing_edges,
episode,
extracted_edge_types,
)
for extracted_edge, related_edges, existing_edges, extracted_edge_types in zip(
extracted_edges,
related_edges_lists,
edge_invalidation_candidates,
edge_types_lst,
strict=True,
)
]
)
)
extract_attributes_from_nodes
在提取完实体和关系并完成相关处理后,会为实体节点提取其属性。Graphiti支持自定义实体类型,每种实体类型的属性不尽相同,提取实体属性时会从输入的文本中根据不同实体类型提取对应的属性。这一步也是通过大模型来实现的,可以说图谱构建效果在一定程度上取决于提示词。
async def extract_attributes_from_node(
llm_client: LLMClient,
node: EntityNode,
episode: EpisodicNode | None = None,
previous_episodes: list[EpisodicNode] | None = None,
entity_type: BaseModel | None = None,
) -> EntityNode:
node_context: dict[str, Any] = {
'name': node.name,
'summary': node.summary,
'entity_types': node.labels,
'attributes': node.attributes,
}
attributes_definitions: dict[str, Any] = {
'summary': (
str,
Field(
description='Summary containing the important information about the entity. Under 250 words',
),
)
}
if entity_type is not None:
for field_name, field_info in entity_type.model_fields.items():
attributes_definitions[field_name] = (
field_info.annotation,
Field(description=field_info.description),
)
unique_model_name = f'EntityAttributes_{uuid4().hex}'
entity_attributes_model = pydantic.create_model(unique_model_name, **attributes_definitions)
summary_context: dict[str, Any] = {
'node': node_context,
'episode_content': episode.content if episode is not None else '',
'previous_episodes': [ep.content for ep in previous_episodes]
if previous_episodes is not None
else [],
}
llm_response = await llm_client.generate_response(
prompt_library.extract_nodes.extract_attributes(summary_context),
response_model=entity_attributes_model,
model_size=ModelSize.small,
)
node.summary = llm_response.get('summary', node.summary)
node_attributes = {key: value for key, value in llm_response.items()}
with suppress(KeyError):
del node_attributes['summary']
node.attributes.update(node_attributes)
return node
到这里知识图谱的构建基本完成,后续就是将提取到的实体和关系添加到图数据库中。
这里还有一个社区的概念,第一次见到这个概念是在GraphRAG中,通过Leiden聚类算法来获取图谱中的社区。这里简要介绍一下社区,实际是一种聚类的方式,将具有相同主题或语义的实体和关系聚类为一个社区,并对社区中的所有实体和关系总结摘要,利用总结得到的摘要生成社区的name,在后续检索时会根据社区name的embedding进行检索得到对应的社区。社区中整合了其中的实体和事实中所有的内容,能够看到整个社区子图的全貌,使图谱能够从更全面的去回答用户问题。
3.5 检索
Graphiti的检索分为node检索和edge检索两种方式,接下来分别讲解这两种方式的具体实现细节。
search_memory_nodes
Graphiti中检索有检索方法关键词检索、语义相似度检索和bfs检索,相对一般的向量存储方式新增了基于图的bfs检索。
async def node_search(
driver: GraphDriver,
cross_encoder: CrossEncoderClient,
query: str,
query_vectors: list[list[float]],
group_ids: list[str] | None,
config: NodeSearchConfig | None,
search_filter: SearchFilters,
center_node_uuid: str | None = None,
bfs_origin_node_uuids: list[str] | None = None,
limit=DEFAULT_SEARCH_LIMIT,
reranker_min_score: float = 0,
) -> list[EntityNode]:
if config is None:
return []
search_results: list[list[EntityNode]] = list(
await semaphore_gather(
*[
node_fulltext_search(driver, query, search_filter, group_ids, 2 * limit),
node_similarity_search(
driver, query_vectors, search_filter, group_ids, 2 * limit, config.sim_min_score
),
node_bfs_search(
driver, bfs_origin_node_uuids, search_filter, config.bfs_max_depth, 2 * limit
),
]
)
)
前两种检索方法都是利用neo4j中自带的检索方法实现,利用大模型根据检索实体的类型生成对应的cypher语句进行检索。bfs检索会分别从关键词检索和语义相似度检索中选择得分最高的一个实体节点作为中心节点,并从该节点出发在指定深度范围内检索实体节点。
search_memory_facts
和实体检索类似,关系的检索也有三种检索方法关键词、语义和bfs。关键词检索会匹配关系的源实体节点、关系类型和目标实体节点,返回检索到的所有关系。语义检索会利用用户query的embedding与现有关系进行语义检索,返回Topk关系。bfs检索会从起始节点出发,沿着某种类型关系,展开n层深度。
3.6 Prompt
实体提取prompt:
def extract_text(context: dict[str, Any]) -> list[Message]:
sys_prompt = """You are an AI assistant that extracts entity nodes from text.
Your primary task is to extract and classify the speaker and other significant entities mentioned in the provided text.Please extract entities from the user's input and return them in the following JSON format:
{
"extracted_entities": [
{
"name": "entity name",
"entity_type_id": integer
}
]
}
The `entity_type_id` must correspond to a predefined category ID in your system.
Do not include any extra text or explanation—only the JSON output."""
user_prompt = f"""
<TEXT>
{context['episode_content']}
</TEXT>
<ENTITY TYPES>
{context['entity_types']}
</ENTITY TYPES>
Given the above text, extract entities from the TEXT that are explicitly or implicitly mentioned and return pydantic support JSON format.
For each entity extracted, also determine its entity type based on the provided ENTITY TYPES and their descriptions.
Indicate the classified entity type by providing its entity_type_id.
For each extracted entity, use the following naming rules based on its type:
- **Podcast**: Name should be the podcast's **title/theme**
- **Event**: Name should be the event's **name**
- **Person**: Name should be the person's **full name**
- **Viewpoint**: Name should be a **summarizing the viewpoint**
- **Channel**: Name should be the channel's **name**
{context['custom_prompt']}
Guidelines:
1. Extract significant entities, concepts, or actors mentioned in the conversation.
2. Avoid creating nodes for relationships or actions.
3. Avoid creating nodes for temporal information like dates, times or years (these will be added to edges later).
4. Be as explicit as possible in your node names, using full names and avoiding abbreviations.
"""
return [
Message(role='system', content=sys_prompt),
Message(role='user', content=user_prompt),
]
关系提取prompt:
def edge(context: dict[str, Any]) -> list[Message]:
return [
Message(
role='system',
content='You are an expert fact extractor that extracts fact triples from text and return pydantic support JSON format. '
'1. Extracted fact triples should also be extracted with relevant date information.'
'2. Treat the CURRENT TIME as the time the CURRENT MESSAGE was sent. All temporal information should be extracted relative to this time.'
'3. Extract relation_type strictly from the given FACT TYPES.'
"""Please extract relationships from the user's input and return them in the following JSON format:
{
"edges": [
{
"relation_type": "RELATION",
"source_entity_id": 1,
"target_entity_id": 2,
"fact": "Relationship description.",
"valid_at": "YYYY-MM-DDTHH:MM:SSZ",
"invalid_at": "YYYY-MM-DDTHH:MM:SSZ"
}
]
}
All fields are required except for 'valid_at' and 'invalid_at', which can be null.
Dates must follow ISO 8601 format. Do not include any extra text—only the JSON output.""",
),
Message(
role='user',
content=f"""
<PREVIOUS_MESSAGES>
{json.dumps([ep for ep in context['previous_episodes']], indent=2)}
</PREVIOUS_MESSAGES>
<CURRENT_MESSAGE>
{context['episode_content']}
</CURRENT_MESSAGE>
<ENTITIES>
{context['nodes']}
</ENTITIES>
<REFERENCE_TIME>
{context['reference_time']} # ISO 8601 (UTC); used to resolve relative time mentions
</REFERENCE_TIME>
<FACT TYPES>
{context['edge_types']}
</FACT TYPES>
# TASK
Extract all factual relationships between the given ENTITIES based on the CURRENT MESSAGE.
Only extract facts that:
- involve two DISTINCT ENTITIES from the ENTITIES list,
- are clearly stated or unambiguously implied in the CURRENT MESSAGE,
and can be represented as edges in a knowledge graph.
- The FACT TYPES provide a list of the most important types of facts, make sure to extract facts of these types
- The FACT TYPES are not an exhaustive list, extract all facts from the message even if they do not fit into one
of the FACT TYPES
You may use information from the PREVIOUS MESSAGES only to disambiguate references or support continuity.
{context['custom_prompt']}
# EXTRACTION RULES
1. Only emit facts where both the subject and object match IDs in ENTITIES.
2. Each fact must involve two **distinct** entities.
3. Use a SCREAMING_SNAKE_CASE string as the `relation_type` (e.g., FOUNDED, WORKS_AT).
4. Do not emit duplicate or semantically redundant facts.
5. The `fact_text` should quote or closely paraphrase the original source sentence(s).
6. Use `REFERENCE_TIME` to resolve vague or relative temporal expressions (e.g., "last week").
7. Do **not** hallucinate or infer temporal bounds from unrelated events.
# DATETIME RULES
- Use ISO 8601 with “Z” suffix (UTC) (e.g., 2025-04-30T00:00:00Z).
- If the fact is ongoing (present tense), set `valid_at` to REFERENCE_TIME.
- If a change/termination is expressed, set `invalid_at` to the relevant timestamp.
- Leave both fields `null` if no explicit or resolvable time is stated.
- If only a date is mentioned (no time), assume 00:00:00.
- If only a year is mentioned, use January 1st at 00:00:00.
""",
),
]
实体去重prompt:
def nodes(context: dict[str, Any]) -> list[Message]:
return [
Message(
role='system',
content='You are a helpful assistant that determines whether or not ENTITIES extracted from a conversation are duplicates'
'of existing entities.'"""Respond with a JSON object that has a key "entity_resolutions",
which is a list of objects with fields: id (int), name (str), duplicate_idx (int).""",
),
Message(
role='user',
content=f"""
<PREVIOUS MESSAGES>
{json.dumps([ep for ep in context['previous_episodes']], indent=2)}
</PREVIOUS MESSAGES>
<CURRENT MESSAGE>
{context['episode_content']}
</CURRENT MESSAGE>
Each of the following ENTITIES were extracted from the CURRENT MESSAGE.
Each entity in ENTITIES is represented as a JSON object with the following structure:
{{
id: integer id of the entity,
name: "name of the entity",
entity_type: "ontological classification of the entity",
entity_type_description: "Description of what the entity type represents",
duplication_candidates: [
{{
idx: integer index of the candidate entity,
name: "name of the candidate entity",
entity_type: "ontological classification of the candidate entity",
...<additional attributes>
}}
]
}}
<ENTITIES>
{json.dumps(context['extracted_nodes'], indent=2)}
</ENTITIES>
<EXISTING ENTITIES>
{json.dumps(context['existing_nodes'], indent=2)}
</EXISTING ENTITIES>
For each of the above ENTITIES, determine if the entity is a duplicate of any of the EXISTING ENTITIES.
Entities should only be considered duplicates if they refer to the *same real-world object or concept*.
Do NOT mark entities as duplicates if:
- They are related but distinct.
- They have similar names or purposes but refer to separate instances or concepts.
Task:
Your response will be a list called entity_resolutions which contains one entry for each entity.
For each entity, return the id of the entity as id, the name of the entity as name, and the duplicate_idx
as an integer.
- If an entity is a duplicate of one of the EXISTING ENTITIES, return the idx of the candidate it is a
duplicate of.
- If an entity is not a duplicate of one of the EXISTING ENTITIES, return the -1 as the duplicate_idx
""",
),
]
关系去重prompt:
def resolve_edge(context: dict[str, Any]) -> list[Message]:
return [
Message(
role='system',
content='You are a helpful assistant that de-duplicates facts from fact lists and determines which existing '
'facts are contradicted by the new fact.',
),
Message(
role='user',
content=f"""
<NEW FACT>
{context['new_edge']}
</NEW FACT>
<EXISTING FACTS>
{context['existing_edges']}
</EXISTING FACTS>
<FACT INVALIDATION CANDIDATES>
{context['edge_invalidation_candidates']}
</FACT INVALIDATION CANDIDATES>
<FACT TYPES>
{context['edge_types']}
</FACT TYPES>
Task:
If the NEW FACT represents the same factual information as any fact in EXISTING FACTS, return the duplicate_fact_id of the duplicate fact.
If the NEW FACT is not a duplicate of any of the EXISTING FACTS, return -1.
Given the predefined FACT TYPES, determine if the NEW FACT should be classified as one of these types.
Return the fact type as fact_type or DEFAULT if NEW FACT is not one of the FACT TYPES.
Based on the provided FACT INVALIDATION CANDIDATES and NEW FACT, determine which existing facts the new fact contradicts.
Return a list containing all idx's of the facts that are contradicted by the NEW FACT.
If there are no contradicted facts, return an empty list.
If there are no duplicates, set duplicate_fact_id to -1. If there are no contradictions, return an empty list for contradicted_facts. The fact_type must be one of the provided types or 'DEFAULT'. Do not include any extra text—only the JSON output.
Guidelines:
1. The facts do not need to be completely identical to be duplicates, they just need to express the same information.
""",
),
]
实体属性提取prompt:
def extract_attributes(context: dict[str, Any]) -> list[Message]:
return [
Message(
role='system',
content='You are a helpful assistant that extracts entity properties from the provided text and return JSON.',
),
Message(
role='user',
content=f"""
<MESSAGES>
{json.dumps(context['previous_episodes'], indent=2)}
{json.dumps(context['episode_content'], indent=2)}
</MESSAGES>
Given the above MESSAGES and the following ENTITY, update any of its attributes based on the information provided
in MESSAGES. Use the provided attribute descriptions to better understand how each attribute should be determined.
Guidelines:
1. Do not hallucinate entity property values if they cannot be found in the current context.
2. Only use the provided MESSAGES and ENTITY to set attribute values.
3. The summary attribute represents a summary of the ENTITY, and should be updated with new information about the Entity from the MESSAGES.
Summaries must be no longer than 250 words.
4. The returned fields of different ENTITY types are as follows:
- If the entity type is Podcast, the returned attributes include
'''
{{
"id": "",
"title": "",
"description": "",
"summary": ""
}}
'''
- if entity_type is Event, the returned attributes include id, event_name, description;、summary
'''
{{
"id": "",
"event_name": "",
"description": "",
"summary": ""
}}
'''
- if entity_type is Channel, the returned attributes include id, channel_name, description、summary
'''
{{
"id": "",
"channel_name": "",
"description": "",
"summary": ""
}}
'''
- if entity_type is Person, the returned fields include id, occupation, bio, summary
'''
{{
"id": "",
"occupation": "",
"bio": "",
"summary": ""
}}
'''
- if entity_type is Viewpoint, the returned fields include id, text, sentiment, topic, summary
'''
{{
"id": "",
"text": "",
"sentiment": "",
"summary": ""
}}
'''
'''
- if entity_type is Topic, the returned fields include id, text, summary
'''
{{
"id": "",
"text": "",
"summary": ""
}}
<ENTITY>
{context['node']}
</ENTITY>
""",
),
]
4. 总结与思考
自定义能力:灵活构建定制化知识图谱
Graphiti 提供了较强的自定义能力,允许用户根据实际需求定义实体类型(Entity Types)和关系类型(Relation Types)。这种机制使得构建的图谱更具针对性和可扩展性,有助于提升后续检索的准确性和相关性。
然而,在使用过程中需要注意的是,Graphiti 的实体与关系提取依赖于大模型(LLM),而默认提示词(prompt)中预设的是内置的实体和关系类型。一旦用户自定义了新的类型,必须同步修改提示词内容,以确保模型能够正确识别并输出所需的结构化结果。否则,可能会导致提取结果不匹配或格式错误。
此外,Graphiti 使用 Pydantic 对实体和关系的输出格式进行严格校验,虽然提升了数据的一致性和可解析性,但也对提示词的设计提出了更高的要求。因此,在实际应用中,建议在提示词中明确限定输出结构,并与 Pydantic 模型保持一致,以避免因格式不符导致的建图失败或重试。
大模型驱动:便利与代价并存
Graphiti 在多个核心环节(如实体提取、实体去重、属性抽取等)中广泛依赖大模型,这种设计带来了极大的灵活性和智能化能力,使得图谱构建过程更加自动化。然而,这也带来了两个显著的问题:
-
建图成本高:由于每一步都依赖大模型推理,构建图谱的整体耗时和资源开销较大,尤其在数据量较大的场景下更为明显。
-
依赖模型输出格式:Graphiti 通过 Pydantic 对模型输出进行结构化约束,提高了数据的规范性,但同时也增加了提示词设计的复杂度。如果模型返回格式不匹配,会导致解析失败,进而影响建图流程。
因此,在使用 Graphiti 时,需要在提示词设计、模型选择和成本控制之间做好权衡。
时间感知能力:理论强大,实践尚需深入
Graphiti 官方强调其“双时间模型”以及时间感知能力,这被认为是其区别于其他图谱系统的重要特性之一。理论上,这一机制能够支持精确的时间上下文建模和历史状态查询。
然而,在实际使用过程中,这一能力的体现尚不够直观,功能边界和使用方式也有待进一步探索。目前来看,时间感知更多体现在边(Edge)上的 invalid_time 标记机制,用于标识边的有效性。但对于更复杂的时间推理(如历史状态回溯、时序路径查询等),Graphiti 的文档和接口支持仍显不足。
因此,建议在后续的实践中持续关注 Graphiti 在时间建模方面的高级用法,并结合源码进一步挖掘其潜力。
在与多位图谱领域从业者的交流中,大家普遍认同一个观点:知识图谱的质量和实用性,很大程度上取决于实体和关系的设计。一个设计良好的图谱结构能够显著提升信息检索的准确性、推理的逻辑性和系统的可扩展性。
在工业界和学术界的实践中,通常会引入领域专家参与图谱的本体设计,协助定义合适的实体类型、关系类型及其语义约束。这种做法在对精度要求极高的场景下尤为重要。例如,在生物学、医学或金融等领域,实体和关系的定义不仅需要准确,还需要符合专业术语体系和逻辑结构。
以生物学为例,如果希望图谱能够精确地表示物种之间的分类关系,模型虽然具备一定的分类知识,但在没有明确约束的情况下,可能只会提取到“科”或“目”的层级,而不会深入到更精细的“属”或“种”。而这些细节对于实际应用来说可能是至关重要的。
因此,在构建图谱时,如果对实体和关系的提取精度有较高要求,通常建议:
- 由领域专家参与设计图谱本体结构,定义清晰的实体类别和关系语义;
- 通过高质量提示词引导大模型输出符合预期的结构化结果;
- 结合规则或后处理机制,对模型输出进行校验和修正,以确保最终图谱的准确性。
虽然大模型在通用知识抽取方面表现优异,但在特定领域的深度和准确性上,仍难以替代专家的经验判断,明确的本体设计可以有效提升图谱质量。

浙公网安备 33010602011771号