向量检索擅长"这段话和问题很像",不擅长"这三份文档里的实体其实是同一个人,且它们构成了因果链"。当问题需要跨文档串联、或者问的是"整体主题分布"时,切片检索会稳定地给出局部正确、全局错误的答案。GraphRAG 一类的做法是把语料先变成图,再在图上检索;GFM-RAG 则进一步把"在图上推理"这件事训练成一个可迁移的模型。
1. 朴素 RAG 的三处结构性盲区
| 盲区 | 具体表现 | 图谱方案的对应动作 |
|---|---|---|
| 多跳关系断裂 | 答案需要 A→B→C 三段各自在不同文档 | 建实体关系边,检索沿边扩展 |
| 全局聚合问题 | "这批反馈的主要诉求是什么"无法靠 top-k 段落回答 | 预先聚成社区并生成摘要 |
| 实体指代不一致 | "老王""王经理""Wang" 检索时互不相识 | 实体消歧与别名归并 |
| 时间/条件约束 | 只按语义相似度,忽略时间线 | 关系上带属性,检索时过滤 |
需要清醒的一点:图谱不是免费的。它用索引期的大量 LLM 调用换查询期的结构能力,适不适合取决于"问题的形态",不取决于"数据量"。
2. 索引管线:五步与各自的失败点
| 步骤 | 输入 | 输出 | 主要成本 | 典型失败 |
|---|---|---|---|---|
| 切块 | 原始文档 | 文本单元(chunk) | 低 | 切断了跨段实体关系 |
| 抽取 | chunk | 实体、关系、可选"主张" | LLM 调用,最贵 | 抽取粒度不一致,同实体多种写法 |
| 消歧归并 | 实体候选 | 唯一实体 ID | 中 | 过度合并,把两个人并成一个 |
| 社区检测 | 图 | 层次化社区 | 低(图算法) | 社区过大,摘要失去焦点 |
| 社区摘要 | 社区 + 成员描述 | 摘要文本 | LLM 调用,第二贵 | 摘要空洞、丢失关键实体名 |
抽取提示词决定了整张图的质量。实践里最有效的约束不是"抽得更多",而是"限定类型 + 强制给证据"。
EXTRACT_PROMPT = """从下面的文本中抽取实体与关系,只使用给定类型。
实体类型: [PERSON, ORG, PRODUCT, EVENT, METRIC]
关系类型: [WORKS_AT, OWNS, CAUSED, MENTIONED_WITH]
要求:
1. 每个实体必须给出原文中出现的原句作为 evidence。
2. 关系必须两端都是已抽出的实体。
3. 不确定时不要输出,宁缺勿滥。
输出 JSON: {"entities":[{"name","type","evidence"}],
"relations":[{"source","target","type","evidence"}]}
文本:
{chunk}
"""
强制 evidence 字段有两个直接收益:消歧时能按上下文判断是否同一实体;摘要时能引用原文而不是靠模型编。
3. 社区检测:为什么是 Leiden 而不是 Louvain
层次社区检测的作用是给"全局问题"提供可聚合的单位:把图分成多级社区,每级生成摘要,查询时按级别 map-reduce。
| 维度 | Louvain | Leiden |
|---|---|---|
| 社区连通性 | 可能产生内部不连通社区 | 保证连通 |
| 层次结构 | 有,但粒度不稳 | 明确的多级层次 |
| 收敛质量 | 好 | 通常更优(同规模下模块度更高) |
| 参数 | 分辨率 resolution |
分辨率 + 迭代次数 + 随机种子 |
import igraph as ig
import leidenalg
g = ig.Graph.from_networkx(nx_graph) # 需保证边权与节点属性已带上
partition = leidenalg.find_partition(
g,
leidenalg.RBConfigurationVertexPartition,
weights="weight",
resolution_parameter=1.0, # 越大社区越小
n_iterations=-1, # 迭代到收敛
seed=42,
)
for cid, members in enumerate(partition):
print(cid, len(members))
resolution_parameter 是唯一需要认真调的参数:调大→社区更细、摘要更多更贵;调小→社区更粗、摘要更泛。它直接决定索引成本,而不是"效果微调"。
4. 三种查询模式:别用一个模式回答所有问题
| 模式 | 检索范围 | 适合的问题 | 代价 |
|---|---|---|---|
| Local | 命中实体的邻域(1–2 跳)+ 关联原文块 | "某产品谁负责、出过什么问题" | 单次查询便宜 |
| Global | 社区摘要的 map-reduce | "整体主题分布、主要矛盾" | 需要遍历多个摘要,贵 |
| DRIFT | Local 为主,混入社区上下文 | 既需细节又需背景 | 中等,最像"人怎么查资料" |
// Local 检索:从命中实体向外扩一跳,带上原文块
MATCH (e:Entity {name: $name})-[r]-(n:Entity)
OPTIONAL MATCH (e)-[:MENTIONED_IN]->(c:Chunk)
RETURN e.name, type(r), n.name, collect(DISTINCT c.text)[0..5] AS evidence
LIMIT 50
注意 LIMIT 的位置:度数高的实体(如"公司""用户")一跳就能扩出上千邻居,不限制会让上下文窗口瞬间爆掉。生产实现里通常还要按边权或提及次数排序后截断,而不是靠数据库返回顺序。
5. GFM-RAG:把"图上推理"训练成模型
GraphRAG 的检索逻辑是规则 + LLM 摘要:先找实体,再扩邻域,再让模型读摘要。GFM-RAG 换了思路——用一个图神经网络直接在实体图上做相关性传播,把"哪些子图与问题相关"学成一个可迁移的打分函数。
| 维度 | GraphRAG(摘要路线) | GFM-RAG(图基础模型路线) |
|---|---|---|
| 索引期 | 需为每份语料生成社区摘要(LLM 密集) | 构图后主要由 GNN 编码,摘要非必需 |
| 查询期 | 规则扩邻域 + LLM 阅读 | GNN 前向传播,输出子图相关性 |
| 迁移性 | 换语料要重跑全部索引 | 论文报告可跨未见图零样本/少样本迁移 |
| 多跳能力 | 受限于显式扩跳数与截断 | 由传播层数与注意力决定 |
| 可解释性 | 高(摘要可读) | 中(要看节点权重) |
机制上有两层:先按查询与实体的初步相似度选出候选子图(否则全图前向不可行),再用 GNN 在候选子图上做若干轮消息传递,最后按节点/边得分裁剪出用于生成的知识。论文给出的跨数据集数值请按原文核对,这里只谈结构。
# 推理骨架:候选子图 -> GNN 传播 -> 子图裁剪 -> 交给生成模型
def retrieve(question, graph, encoder, gnn, top_k=8):
q = encoder.encode(question)
seeds = graph.search_entities(q, limit=64) # 粗筛,避免全图前向
sub = graph.subgraph(seeds, hops=2) # 局部子图
scores = gnn.propagate(sub, q, rounds=3) # 相关性在边上传播
keep = sub.prune(scores, ratio=0.3) # 丢掉低分节点与边
return keep.to_context(max_tokens=top_k * 512)
三个工程约束:种子数量决定召回上限;hops 决定多跳能力也决定噪声;prune ratio 决定上下文预算。三者一起动会互相掩盖问题,调参时要一次只动一个。
6. 成本与参数:先算账再动手
| 项目 | 量级来源 | 说明 |
|---|---|---|
| 抽取调用数 | ≈ chunk 数 | 每块一次 LLM 调用,是索引成本主项 |
| 摘要调用数 | ≈ 社区数(多级累加) | 层次越细,调用越多 |
| 图存储 | 实体数 + 关系数 | 图数据库或带索引的关系表均可 |
| 查询延迟 | 扩跳、GNN 层数、生成长度 | 扩跳是最容易失控的一项 |
| 增量更新 | 新文档涉及的社区 | 全量重建代价高,需支持局部重算 |
# 索引配置的关键项(各家实现字段名不同,按所用版本核对)
chunk:
size: 1200
overlap: 120
extract:
model: EXTRACT_MODEL
max_entities_per_chunk: 40
community:
algorithm: leiden
resolution: 1.0
levels: 2
summary:
model: SUMMARY_MODEL
max_tokens: 600
resolution 与 levels 是成本旋钮而不是精度旋钮:levels: 2 意味着摘要调用量大致翻倍。第一版索引建议只做一级,确认查询收益后再加层级。
7. 排错:六种典型问题
| 现象 | 根因 | 处理 |
|---|---|---|
| 全局问题答得很空 | 社区摘要缺少实体名与数字 | 摘要提示词强制保留关键实体与量级 |
| 同一实体在多处重复 | 消歧缺失,别名未归并 | 加别名表 + 上下文相似度合并 |
| 多跳问题只答对一半 | 扩跳被 LIMIT 截断在中间节点 |
按边权排序后截断,或提高跳数并裁剪 |
| 检索到大量无关节点 | 高 degree 实体污染邻域 | 对超级节点做降权或排除 |
| 索引跑了一夜没完 | 抽取并发太低 / 调用重试风暴 | 提高并发、加缓存、按文档分片 |
| 新增文档后答案变差 | 只补了图没更新社区摘要 | 触发受影响社区的局部重建 |
第四条是图谱检索最常见的"看起来像模型退化、实际是图结构问题"的现象。处理方式不是换更大的模型,而是在构图阶段就给超级节点打标,检索时限制其贡献。
8. 边界:哪些场景不值得上图
| 场景 | 更合适的方案 | 理由 |
|---|---|---|
| 单文档问答、事实查找 | 切片 + 向量检索 | 关系结构用不上,索引成本白花 |
| 语料每天大量新增 | 向量库 + 元数据过滤 | 图谱增量成本远高于收益 |
| 需要精确引用原文 | 原文块直取 | 摘要会引入不可核对的内容 |
| 实时性要求秒级索引 | 流式向量化 | 抽取 + 社区检测无法做到秒级 |
| 只有几十份短文档 | 全文检索 | 结构复杂度超过数据本身 |
判断标准可以简化成一句话:问题里有没有"之间""整体""为什么"这类跨实体、跨文档的诉求。有,图结构值得;只问"是什么",向量检索通常更便宜也更准。
9. 自测
- 朴素 RAG 在哪两类问题上会稳定失败?为什么向量相似度救不了?
- 索引管线里哪两步是 LLM 成本主项,各自与什么数量成正比?
- 为什么选 Leiden 而不是 Louvain?
resolution_parameter调大意味着什么? - Global 与 Local 检索的输入范围差在哪?各自适合什么问题?
- GFM-RAG 为什么要先"粗筛候选子图"再做 GNN 传播?
- 超级节点(高 degree 实体)会给检索带来什么污染,通常怎么处理?