GraphRAG(Chunk 级遍历版)—— 带图的检索,还是假的图谱?
GraphRAG(Chunk 级遍历版)—— 带图的检索,还是假的图谱?
对应 notebook是https://github.com/NirDiamant/RAG_Techniques里面的:all_rag_techniques/graph_rag.ipynb。跟微软 GraphRAG、Milvus 版完全是三套思路。这篇拆它的架构、和普通 RAG 的区别、以及为什么它”挂着 GraphRAG 的名,干的还是检索的活”。
1. 一句话理解
节点 = 文本 chunk,边 = chunk 之间的内容相似度。查询时先向量检索找起点,再顺着图用类似 Dijkstra 的方式扩散,每走一步问 LLM”答案够了吗”,够了就停。 不像前二者主要依赖提取实体和关系
2. 架构总览
DocumentProcessor ──分块+embedding──▶ FAISS 向量库
KnowledgeGraph ──建节点+概念+边──▶ NetworkX 图
QueryEngine ──检索+Dijkstra遍历──▶ 答案
Visualizer ──画图+高亮路径──▶ 图
3. 建图(一次性)
3.1 节点 = chunk,不是实体
每个文本 chunk 是一个节点。概念只是节点属性,不单独建实体节点。所以这张图表达的不是”世界知识”,而是”哪些段落之间有关联”。
3.2 概念抽取,双通道
- spaCy NER:只留
PERSON / ORG / GPE / WORK_OF_ART四类 - LLM 结构化输出:抽通用概念(
ConceptsPydantic 模型) - 合并后 lemmatize(词形还原),用于共享概念匹配
每块要调一次 LLM。30 块 = 30 次调用,这是建图成本之一。
3.3 O(n²) 两两建边
for node1 in range(num_nodes):
for node2 in range(node1 + 1, num_nodes):
sim = similarity_matrix[node1][node2]
if sim > edges_threshold: # 0.8
shared = concepts[node1] & concepts[node2]
weight = 0.7 * sim + 0.3 * (len(shared) / min(len(c1), len(c2)))
graph.add_edge(node1, node2, weight=weight, ...)
边权 = 相似度 + 共享概念比例,两个信号加权:
\[w = \alpha \cdot sim + \beta \cdot \frac{|shared|}{\min(|c_1|, |c_2|)}\]30 节点 = 435 对,很快。但 O(n²) 是硬伤——一万块就是五千万对,直接崩。
Insight:这里的”图”本质是检索的扩散机制,不是实体知识库。向量检索只给你直接相似的 chunk;图遍历让你从起点顺着强边跳到”不直接像但强关联”的 chunk——解决答案散落在多块、单块相似度都不够的问题。
4. 查询 = 向量找入口,图做扩散
4.1 两段式,各用各的数据
| 相似度 | 谁对谁 | 何时算 | 用在哪 |
|---|---|---|---|
| 边权 | chunk ↔ chunk | 建图时,一次 | 遍历时读取 |
| 起点分数 | query ↔ chunk | 查询时,一次 | 找起点 top-5 |
4.2 流程
query embedding → 点积全库 → top-5 chunk 入最小堆(priority = 1/sim)
循环:
堆.pop() → 取累计距离最小的节点
content 加入 context
LLM judge:context 能完整回答 query 吗?
能 → 输出答案,结束
不能 → 对每个邻居:
new_dist = dist + 1/edge_weight ← 读预存边权,不算向量
邻居入堆
堆空还没完整答案 → 全部 context 硬生成兜底
4.3 为什么是 Dijkstra,不是单调栈
- 用
heapq最小堆:堆顶永远是累计距离最小(优先级最高)的节点,push/pop 都 O(log n)。Dijkstra 需要反复”取当前最优 + 更新邻居”,堆正好支持。 - 单调栈是另一个数据结构(保持栈内单调,用于”找下一个更大/更小元素”),这里无关。
1/edge_weight:边越粗,增量越小,累计距离越小,越早被弹出。遍历完全不碰 embedding,只读建图时存好的数字 + 堆操作。
5. 和普通 RAG 的唯一实质区别
普通 RAG:检索 top-k → 拼 context → 一次生成,结束。中间没有决策。
这个版本:top-k 只是起点,真正的检索发生在图上——每跳扩展 + 每跳一次 LLM 判断”够不够”。LLM 不只是最后生成答案,遍历中每一步都在当”答案完整性裁判”。
代价也在这:每跳烧一次 LLM 调用,token 和美元成本高;judge 误判”够”→ 答案残缺,误判”不够”→ 白白多爬。
6. 和微软 GraphRAG / Milvus 版对比
| 维度 | 本 notebook | 微软 GraphRAG | Milvus 版 |
|---|---|---|---|
| 节点 | 文本 chunk | 实体 | 实体 / 关系(向量库) |
| 边 | embedding 相似度 + 概念重叠 | 实体关系 | 邻接矩阵 |
| 社区 / 摘要 | 无 | 有(社区检测 + 摘要) | 无 |
| 查询 | 向量起点 + Dijkstra 遍历 | 社区定位 + 全局搜索 | 双路召回 + 矩阵多跳 |
| 多跳成本 | 每跳 1 次 LLM judge | 便宜(预建摘要) | 矩阵乘法现算 |
一句话:微软版是真图谱(图里存可查询的知识),这个版本是伪图谱(图里只存相似度缓存)。
7. 优势与争议
优势
- 比纯向量捞得全:图遍历让起点顺着强边跳出相似度天花板。
- LLM judge 让遍历有目标、能早停:每跳判断”够不够”,不盲目爬全网。
- 可解释、可展示:遍历路径可画出,能看到答案从哪几块、按什么顺序拼出来。
- 实现简单:networkx + 堆 + 两次 LLM 调用,几十行。
争议
- 名不副实:节点是 chunk 不是实体,边是”像不像”不是关系,图不编码世界知识。
- O(n²) 建边:规模天花板低,文档一大就崩。
- 查询烧 token:每跳一次 judge,且 judge 本身会误判。
- 起点强依赖向量检索:top-5 起点全错,图再爬也爬不回对的——图只能扩围,不能纠错。
- 边权建立在两个噪声信号上:概念抽错 → 共享概念错 → 边权错 → 遍历歪。
8. 关于”黑盒”
- 普通 RAG:top-k 依据是”相似度最高”,数字层面完全可审计。但相似度高的都对的世界里它够用;一旦正确答案相似度不高(答案散落需拼合),数字依据就解释不了为什么该选它。
- 本版本:能展示”走了哪些节点、每跳 judge 判了什么”——过程透明。
- 微软版:能解释”为什么捞这段——因为它属于’碳排放’社区”——语义透明。
三档透明度递进:数字依据 → 过程透明 → 语义透明。
9. 设计评价:为什么说它”设计得一般”
- 图是死的:边权建图时固化,文档变更全图重建,无增量。
- O(n²) 建边:好设计该用近似最近邻 / 概念倒排压缩候选。
- LLM judge 是贵而不稳的单点:正确性押在一个频繁调用、又可能误判的组件上。
- 起点错则全错,无纠错机制。
- 两个检索没有协同:向量 top-5 和遍历是两条独立线路,没有反馈环。
- 图的语义信息量几乎为零,只是”预存相似度”。
核心缺陷:没想清楚图到底要贡献什么。 好设计先回答”图提供向量检索给不了的东西是什么”,再谈实现。这里图的贡献(预存相似度 + 跳转),向量检索用更便宜的方式也能近似做到。图没有提供不可替代的语义增量,所以它只是加了张缓存表的 RAG。
总结
- 本质:向量找入口 + 图做扩散 + LLM judge 定停止,是”向量检索”到”真图谱”之间最便宜的升级档。
- 适用:中小规模文档、答案依赖多段落拼合、需要展示检索过程的场景。
- 局限:规模、语义、成本三头都压不过微软版;作为教学 demo 及格,作为生产 GraphRAG 不合格。
Enjoy Reading This Article?
Here are some more articles you might like to read next:
- Google Gemini updates: Flash 1.5, Gemma 2 and Project Astra
- Displaying External Posts on Your al-folio Blog
- Agent 评测体系与评测集构建——美团《评测漫谈》+《评测白皮书 01》笔记
- 多模态 LLM 用户智能体做推荐系统离线 A/B 测试
- 自我改进 Agent 统一拆解:θ / Σ 双路线
- CS146S 学习笔记(Week 4-8):从智能体管理者到多栈 AI 构建
- CS146S 学习笔记:从 Prompt 技术全景到 AI IDE 设计文档规范
- 二分查找双模板 + searchInsert 逐行拆解:从模板到边界
- Agent Memory 全景:30 个记忆技术的模块化拆解
- LightRAG 深度解析:简单快速的图增强 RAG