Skip to content

知识图谱 + LLM:GraphRAG 原理,如何用知识图谱增强 RAG 的回答质量,实体关系抽取

提出问题

传统 RAG 的检索倚赖向量相似度,本质上是"找最像的段落"。这在回答"Redis 的持久化方式有哪些"这类单点问题时完全够用,但碰到"A 公司收购了 B 公司,B 的 C 产品是 D 的替代品,D 最近被 E 收购了,这对 A 有什么影响"这种多跳推理问题,向量检索就抓瞎了——它只能找到包含 A 或 B 的片段,断不开 A→B→C→D→E 这条关系链。GraphRAG 正是为了解决这个痛点:把文档中的实体和关系显式建图,检索时走图谱路径,把多跳推理能力补上。

从朴素 RAG 到 GraphRAG:为什么向量检索搞不定多跳推理

先看一个真实场景。假设你给 LLM 喂了 100 篇公司财报和技术新闻,问:"英伟达收购了哪些公司,这些公司对英伟达的 CUDA 生态有什么影响?"

  • 朴素 RAG:将 query 转成向量,在 100 篇文档的向量库中做 top-5 相似度检索。结果大概率是 5 篇都包含"英伟达"的高频词匹配文档。检索到的段落之间没有关系链,LLM 只能从碎片信息里拼凑,很容易漏掉关键收购事件或把收购顺位搞错。

  • GraphRAG:先将文档中的实体(英伟达、Mellanox、Arm、CUDA、DPU)和关系(收购、适配、竞争)抽取为三元组,建一个知识图谱。检索时不仅做向量相似度,还从"英伟达"节点出发,沿"收购"边做 2 跳遍历,直接拿到 英伟达 —[收购]→ MellanoxMellanox —[产品]→ InfiniBand 这些结构化的关系链。LLM 拿到的是带路径的上下文,回答的准确率和可解释性明显提升。

微软 GraphRAG 论文(2024)在播客转录数据集上做了对比实验:朴素 RAG 的多跳问答准确率约为 42%,GraphRAG 提升到 68%。在私有数据集(如企业内部文档、法律合同)上差距更大,因为这类场景中实体关系比语义相似度更有信息量。

分析问题

GraphRAG 的核心流程:从文档到图谱,再回到检索

GraphRAG 的完整工作流分三步:

  1. 实体关系抽取(NER + RE):对文档做命名实体识别(Named Entity Recognition)和关系抽取(Relation Extraction),输出三元组 (Subject, Relation, Object)。例如从新闻"苹果公司收购了 Beats Electronics"抽取出 (苹果, 收购, Beats Electronics)

  2. 知识图谱构建:三元组存入图数据库(Neo4j / NebulaGraph),建立实体节点和关系边,支持用 Cypher 或 nGQL 做图遍历查询。

  3. 检索增强:用户提问时,同时做两路检索——向量检索找语义相似文档,图谱检索找相关实体及其多跳邻居。两路结果合并后送 LLM 生成答案。

python
# GraphRAG 检索的简化实现
def graphrag_retrieve(query, vector_store, graph_db, top_k=5):
    # 1. 向量检索:找出语义最相似的 k 篇文档
    vector_results = vector_store.similarity_search(query, k=top_k)
    
    # 2. 图谱检索:从 query 中提取实体,走图遍历
    entities = extract_entities(query)  # NER 抽取
    graph_results = []
    for entity in entities:
        # 2 跳邻居查询
        # 注意:Neo4j 中 MATCH 的模式匹配是 Cypher 的核心语法
        # r*1..2 表示 1 到 2 层关系,具体 n 和边的方向取决于图模式
        neighbors = graph_db.query(
            f"MATCH (e {{name: '{entity}'}})-[r*1..2]-(n) RETURN e, r, n"
        )
        graph_results.extend(neighbors)
    
    # 3. 去重合并
    all_contexts = deduplicate(vector_results + graph_results)
    return all_contexts

实体关系抽取的技术选型

实体关系抽取是 GraphRAG 的构建成本大头。三种常见方案对比:

方案精度成本(百万文档)场景
纯 LLM 抽取(GPT-4o / Claude Sonnet 4)约 $5000-8000小规模、精度优先
轻量 NER 模型(GLiNER / UniMER)约 $50-100大规模初筛
LLM 初筛 + 二次校验(推荐)约 $500-1000生产环境首选

为什么纯 LLM 抽取会这么贵? 以 GPT-4o 为例,每百万输入 token 约 $5,输出 token 约 $15。一篇 2000 字的文档,抽取实体关系需要 2000+ 个输出 token,每篇成本约 $0.05-0.1。100 万篇就是 $5 万到 $10 万——一次建库就能烧掉一个初级工程师的年薪。

生产环境的标准做法是分层流水线:先用 GLiNER 做快速初筛,识别候选实体对;然后由 LLM 做去重和关系消歧——比如"阿里巴巴"和"Alibaba"是同一实体,"收购"和"acquire"是同一关系。这一步能省 60-80% 的 LLM 调用成本。GLiNER 的推理速度在单张 T4 GPU 上可达每秒 500 篇文档,比 LLM 快 3-4 个数量级。

python
# 分层抽取流水线:GLiNER 初筛 + LLM 精校
from gliner import GLiNER  # 或者使用 UniMER

def two_stage_extraction(text, llm_client):
    # 第一阶段:GLiNER 快速初筛
    gliner = GLiNER.from_pretrained("urchade/gliner_multi-v2.1")
    entities = gliner.predict_entities(text, labels=["person", "organization", "product", "event"])
    
    # 第二阶段:LLM 去重 + 关系消歧
    triplets = []
    for entity in entities:
        context = extract_surrounding_text(text, entity["start"], entity["end"])
        prompt = f"""从以下文本中提取实体关系三元组,只输出 JSON 数组:
文本:{context}
已知实体:{entity['text']}(类型:{entity['label']}
格式:[{{"subject": "...", "relation": "...", "object": "..."}}]"""
        response = llm_client.complete(prompt)
        triplets.extend(json.loads(response))
    
    return triplets

图谱检索的效率瓶颈与优化

图谱查询比向量检索慢 1-2 个数量级。实测数据:

  • 向量检索(FAISS IVF):10M 向量库,top-5 召回 ≈ 10-20ms
  • 图遍历(Neo4j BFS):1000 万节点,2 跳 BFS ≈ 200-500ms
  • 图遍历(NebulaGraph 2 跳):1000 万节点 ≈ 150-300ms

为什么这么慢?图遍历本质上是一个广度优先搜索,每跳查到的新节点可能是指数级增长的。如果一个节点的度数是 100,2 跳理论最大节点数就是 100² = 10000。实际生产中金融数据图谱的节点平均度数在 50-200 之间,2 跳查询可能遍历 5000-20000 个节点。

优化策略:先向量检索再图遍历

不是所有查询都需要全图遍历。可以用向量检索先定位候选文档,再在候选文档的子图上做图谱查询。这样将图遍历的规模从千万级降到万级,响应时间从 500ms 降到 50ms 以内。

python
# 先向量检索缩小范围,再图遍历
def efficient_graphrag_retrieve(query, vector_store, graph_db, top_k=3):
    # 1. 先向量检索找到最相关的 3 篇文档
    docs = vector_store.similarity_search(query, k=top_k)
    
    # 2. 从这些文档涉及的经济实体出发,做图遍历
    # 只遍历这些实体,而不是全图
    entities = extract_entities_from_docs(docs)
    subgraph = graph_db.query(
        f"MATCH (e)-[r*1..2]-(n) WHERE e.name IN {entities} RETURN e, r, n"
    )
    return subgraph

更激进的优化:社区检测

微软 GraphRAG 论文中提出了一个更激进的优化:社区检测(Leiden 算法)。核心思路是将密集连接的实体分组为社区,用 LLM 给每个社区生成摘要。全局性问题(如"公司整体的战略布局是什么")直接查社区摘要,不用深入图谱。

Leiden 算法是 Louvain 算法的改进版,通过三个阶段迭代优化模块度:

  1. 局部移动:每个节点尝试移动到邻居社区,看模块度是否提升
  2. 细化:检测社区内部是否可以进一步拆分
  3. 聚合:将同一社区的节点合并为一个新节点,重复上述过程
python
# 社区检测 + 社区摘要的查询优化
def community_aware_retrieve(query, communities, graph_db):
    # 先判断问题类型
    if is_global_query(query):
        # 全局问题:直接查社区摘要
        # 社区摘要由 LLM 预先生成,存储在 summaries 字典中
        return query_community_summaries(query, communities)
    else:
        # 局部问题:走图谱遍历
        entities = extract_entities(query)
        return graph_traversal(entities, graph_db, max_hops=2)

图谱的增量更新与实体对齐

生产环境中文档持续更新,不可能每次重建全量图谱。增量更新的核心挑战是实体对齐:新文档中提到的"Apple"和已有图谱中的"苹果公司"是同一实体吗?

2025 年主流做法:用 Embedding 相似度做候选召回,再用 LLM 做消歧确认。

python
def entity_alignment(new_entity, existing_entities, llm_client, threshold=0.85):
    # 1. Embedding 召回候选
    new_emb = embed(new_entity.name)
    candidates = []
    for ent in existing_entities:
        sim = cosine_similarity(new_emb, ent.embedding)
        if sim > threshold:
            candidates.append(ent)
    
    if not candidates:
        return None  # 新实体,直接插入
    
    # 2. LLM 消歧确认
    # 这里用结构化 prompt 而不是简单问"是/否",
    # 避免 LLM 的默认肯定倾向
    prompt = f"""判断以下两个实体是否指向同一真实世界对象:
实体 A:{new_entity.name}(上下文:{new_entity.context[:100]}
实体 B:{candidates[0].name}(上下文:{candidates[0].context[:100]}
请输出「是」或「否」,并给出理由。"""
    result = llm_client.complete(prompt)
    return candidates[0] if result.startswith("是") else None

实体对齐的坑:阈值设得越低(比如 0.7),召回率越高但误报也高,LLM 消歧的调用量暴增。阈值设得高(0.95),漏对齐导致实体重复。推荐 0.85 作为起始值,用 1000 条标注数据做 ROC 曲线找到最优切分点。

存储选型:Neo4j vs NebulaGraph vs 纯内存图

维度Neo4jNebulaGraph纯内存(NetworkX/igraph)
查询语言CyphernGQL(类 SQL)Python API
分布式企业版原生分布式单机
百万级节点查询2 跳 ≈ 200ms2 跳 ≈ 150ms2 跳 ≈ 30ms
持久化本地多副本
运维成本
适用规模百万-千万亿级百万以下

选型建议:如果你做的是企业知识库(百万级文档),Neo4j 就够了。如果要做全网级别的知识图谱(亿级节点),上 NebulaGraph。纯内存方案适合实验和原型验证,千万别在生产环境用——一旦进程重启,全量重建需要几个小时。

生产环境踩坑实录

坑 1:LLM 抽取的三元组质量不可控

实测发现 GPT-4o 抽取实体关系时,20% 的抽取结果包含无关实体或者关系方向搞反。比如"张三向李四借款 100 万"被抽成 (张三, 借款, 100 万) 而不是 (张三, 借款人, 李四)。解决方法是约束输出格式:用 JSON Schema + Pydantic 做结构化输出校验,不满足 Schema 的三元组直接丢弃。

坑 2:Cyper 注入风险

python
# 危险做法:直接拼接实体名到 Cypher 查询
neighbors = graph_db.query(
    f"MATCH (e {{name: '{entity_from_user}'}})-[r*1..2]-(n) RETURN e, r, n"
)
# 如果 entity_from_user = "a'})-[*1..2]-(n)-[:DROP]->(m) RETURN m"
# 就变成了注入攻击

解决办法:用参数化查询,或者实体名做严格校验(只允许字母数字和中文)。

坑 3:图遍历的 N+1 问题

有些实现是每找到一个实体就发一次图查询,100 个实体发 100 次查询,每次都建立 TCP 连接。应该用批处理:一次查询列出所有实体的邻居。

总结

GraphRAG 的核心价值不在于复杂,而在于用图结构补上了向量检索的多跳推理短板。落地时三个关键点需要把握好:

  • 成本控制:分层抽取(轻量 NER 初筛 + LLM 二次校验),别让 LLM 直接扫全文。实测可省 60-80% 的 LLM 调用成本
  • 效率取舍:社区摘要兜底全局问题,图谱遍历只处理局部问题,避免全图 BFS。Leiden 社区检测 + 预先摘要,把全局查询从 500ms 降到 20ms
  • 增量维护:实体对齐用 Embedding 召回 + LLM 消歧,别动不动全量重建。阈值 0.85 起步,用 ROC 曲线调优

面试话术示例:问 GraphRAG 原理时,先讲清楚"解决什么问题"(多跳推理),再讲"三层结构"(抽取→建图→检索),最后给出"生产落地的三个坑"(成本、效率、增量更新)。能讲出 Leiden 社区检测和实体对齐的细节,就是 P7+ 的水平。

参考:GraphRAG 论文(微软 2024,arXiv:2404.16130)、Neo4j 官方文档、GLiNER 模型(urchade/gliner_multi-v2.1)、GraphRAG GitHub 仓库(microsoft/graphrag)

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。