跳到主要内容

Graphiti 深入 — 社区检测、巧妙之处、边界与对比

收尾章:补上社区检测这块独立机制,提炼可借鉴的设计,诚实列出它的边界,并和同类项目对比。

1. 社区检测(可选):标签传播 + 层级摘要

1.1 解决什么

实体多了之后,你想要「主题级」的概览,而非一个个孤立实体。社区(community)就是把紧密相连的实体聚成一簇,并生成一段摘要。入口 Graphiti.build_communities(graphiti.py:1489)。

1.2 标签传播算法

聚类用 label propagation(标签传播,一种社区发现算法),实现在 label_propagation(community_operations.py:93)。思路朴素:

1. 每个节点初始各自一个社区(标号=自己)
2. 反复:每个节点改投「邻居里加权最多的那个社区」
- 用边数(edge_count)加权
- 平局时投给标号更大的社区
3. 直到一轮没有任何节点改变 → 收敛

关键代码(community_operations.py:106-131):每个节点统计邻居的社区票数(按 edge_count 加权),取最高票;若最高票 > 1 用它,否则在「最高票社区」和「自己当前社区」里取较大标号(community_operations.py:117-121)。这是个无参数、确定收敛的轻量算法,符合 Graphiti「能不靠 LLM 就不靠」的基调。

1.3 层级式两两归并摘要

一簇的摘要不是把所有实体摘要一股脑塞给 LLM,而是两两归并(像锦标赛)直到剩一条(build_community,community_operations.py:174-200):

[s1 s2 s3 s4 s5]
│ 两两 summarize_pair(并行)

[s12 s34] + 落单的 s5

[s1234] + s5 → s12345

奇数时把落单的一个留到下一轮(community_operations.py:181-195)。这样每次 LLM 调用的输入都短、可并行,避免「上下文爆炸」。

2. 巧妙之处(可借鉴)

① 确定性优先、LLM 兜底——贯穿全项目。 实体去重的精确名/MinHash(dedup_helpers.py:220)、事实去重的逐字快路径(edge_operations.py:684-695)、社区的标签传播(community_operations.py:93)——都先用确定性算法解决大部分,LLM 只处理「确定性拿不准」的尾部。省钱、降延迟、提稳定性。

② 熵闸门防止模糊误判。 用香农熵判断名字是否「够独特到可以做模糊匹配」(dedup_helpers.py:79),短名直接交 LLM——一个很巧的「知道自己什么时候不该自信」的设计。

③ 过期而非删除 = 时间旅行。 双时间轴(edges.py:271-279)让「现在为真」和「历史为真」都可查,这是它做 agent memory 的根基,也是和静态 KG 的本质差异。

④ RRF 融合异构信号。 只看名次不看分数(search_utils.py:1780),优雅绕开「向量分和 BM25 分不可比」的难题。

⑤ 对 LLM 输出的防御性校验。 LLM 返回的 id 一律做范围/重复/漏检检查(node_operations.py:560-619edge_operations.py:736-744),保证流程「即使模型乱答也确定不崩」。

⑥ 一次批量落库。 节点/实体边/溯源边攒齐后一次 add_nodes_and_edges_bulk 写入(graphiti.py:726-733),减少往返。

3. 可插拔的图后端

Graphiti 把图数据库抽象成 GraphDriver,支持四种(GraphProvider,driver/driver.py:59-63):Neo4j(默认)、FalkorDB、Kuzu、Neptune。许多操作走一个 graph_operations_interface 的「控制反转」接口,具体后端可覆盖默认 Cypher 实现,覆盖不了就 NotImplementedError 回退到通用实现(例如 graphiti.py:398-404)。代价是查询里散落着 if driver.provider == GraphProvider.KUZU 之类的分支(如 search_utils.py:1821-1826edge_operations.py:880-897)。

4. 边界与局限(诚实)

  • 强依赖 LLM 抽取质量。 实体/关系/时间戳几乎全靠 LLM 抽。extract_edges 对 LLM 造出的不存在实体名只能丢弃(edge_operations.py:217-230)——抽错了下游就缺数据。
  • 写入需串行。 add_episode 文档明确要求每条 episode 顺序 await(graphiti.py:1056-1059),因为后一条去重依赖前一条已落库。高吞吐要靠 add_episode_bulk 或队列。
  • 去重是「召回受限」的。 实体去重只在 ≤15 个语义近邻里找(node_operations.py:64),向量没召回到的真重复会漏掉。
  • 每条事实多次 LLM 往返。 抽边、判重复/矛盾、抽时间戳、抽属性可能各是一次调用——成本随事实数线性增长。
  • 社区是全量重建。 build_communitiesremove_communities 再重建(graphiti.py:1503-1508),不是增量(虽有 update_community 增量路径用于单节点)。
  • 时序判定依赖 LLM 给对时间。 valid_at/invalid_at 多由 LLM 解析,时间抽错会导致失效判定错。

5. 横向对比

vs. GraphRAG(微软): GraphRAG 面向「静态文档批处理 + 社区摘要」,Graphiti 面向「增量、会变的 agent 上下文」。最大差异是 Graphiti 的时序模型:事实有有效期、可时间旅行;GraphRAG 没有(对比见仓库 README「Graphiti vs. GraphRAG」表)。

vs. 普通向量 RAG: 向量 RAG 检索「相似文本块」,无法表达「事实被推翻」;Graphiti 检索的是结构化的、带时效的「事实边」。

在 ai-frontier-reference / memory 货架里的取舍: Graphiti 把「记忆」建模成图 + 双时间轴,强在「事实演化与溯源」;代价是写入重(多次 LLM)、需串行。若兄弟项目走「向量+摘要」路线,则写入轻、但丢失了时序与关系结构。选型本质是「记忆的结构化程度 vs 写入成本」的权衡。

6. 全局代码地图

主题文件符号
顶层门面graphiti_core/graphiti.pyGraphiti
写入graphiti_core/graphiti.pyadd_episodeadd_episode_bulk
手工三元组写入graphiti_core/graphiti.pyadd_triplet
删除 episode(级联)graphiti_core/graphiti.pyremove_episode
实体去重graphiti_core/utils/maintenance/node_operations.pydedup_helpers.pyresolve_extracted_nodes_resolve_with_similarity
事实去重/时序graphiti_core/utils/maintenance/edge_operations.pyresolve_extracted_edgeresolve_edge_contradictions
社区聚类graphiti_core/utils/maintenance/community_operations.pylabel_propagationbuild_community
社区入口graphiti_core/graphiti.pybuild_communities
混合检索graphiti_core/search/search.pysearch_utils.pysearchrrfmaximal_marginal_relevance
数据模型graphiti_core/nodes.pyedges.pyEntityNodeEntityEdgeEpisodicNodeCommunityNode
图后端抽象graphiti_core/driver/driver.pyGraphDriverGraphProvider
时间工具graphiti_core/utils/datetime_utils.pyutc_nowensure_utc