数据截至 (上游 commit 10374d6044f9)
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:1764),优雅绕开「向量分和 BM25 分不可比」的难题。
⑤ 对 LLM 输出的防御性校验。 LLM 返回的 id 一律做范围/重复/漏检检查(node_operations.py:560-619、edge_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:1805-1810、edge_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_communities先remove_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 写入成本」的权衡。