知识图谱在RAG系统中的应用实践

背景与动机

传统 RAG(Retrieval-Augmented Generation)系统依赖向量检索从文档块中召回上下文,再交给大语言模型生成答案。这种架构在处理事实性问答时表现良好,但面对需要多跳推理、关系推断、全局摘要的场景,纯向量 RAG 的局限性暴露无遗。

知识图谱(Knowledge Graph, KG)的引入,为 RAG 系统带来了结构化的关系推理能力。本文聚焦工程实践,详解 GraphRAG 的架构设计、构建流程与生产调优。


纯向量 RAG 的核心痛点

痛点 典型表现 根因
多跳推理失败 "张三的老板的合作伙伴是谁"无法回答 向量检索只做语义相似度,不做关系遍历
全局摘要缺失 "这个领域的主要研究方向有哪些"回答片面 检索窗口有限,无法覆盖全局
实体消歧困难 "苹果的市值"在科技和水果间混淆 缺少实体类型约束
时序关系丢失 "A 公司收购 B 之前的营收"时序错乱 文本块打散后丢失时序线索
隐含关系挖掘 无法发现间接关联(如共同投资人) 向量空间不编码图结构

GraphRAG 系统架构

┌─────────────────────────────────────────────────────────┐
│                      用户查询层                          │
│  用户提问 ──→ 意图识别 ──→ 查询路由                      │
└─────────────┬───────────────────────┬───────────────────┘
              │                       │
     ┌────────▼────────┐    ┌────────▼────────┐
     │  向量检索通道     │    │  图谱检索通道    │
     │                  │    │                  │
     │ Embedding ──→    │    │ 实体识别 ──→     │
     │ ANN 检索 ──→     │    │ 子图遍历 ──→     │
     │ Top-K 文档块     │    │ 关系路径提取      │
     └────────┬────────┘    └────────┬────────┘
              │                       │
     ┌────────▼───────────────────────▼────────┐
     │            融合排序层(Fusion Ranker)    │
     │                                          │
     │  向量相关性得分 + 图谱置信度得分           │
     │  ──→ 加权融合 ──→ 上下文拼装              │
     └─────────────────┬──────────────────────┘
                       │
              ┌────────▼────────┐
              │   LLM 生成层    │
              │                  │
              │  Prompt 模板     │
              │  + 融合上下文    │
              │  ──→ 最终回答    │
              └─────────────────┘

核心模块说明

查询路由器(Query Router):根据查询意图决定走哪个通道或双通道并行。规则示例:

  • 包含实体关系词("谁是""属于""关联")→ 优先图谱通道
  • 包含描述性需求("解释""介绍""总结")→ 优先向量通道
  • 复杂查询 → 双通道并行

图谱检索通道:从查询中提取实体,在图谱中做子图遍历,返回相关三元组和路径。

融合排序层:将两个通道的结果做加权排序,避免信息冗余。


索引构建流程

第一步:文档预处理与分块

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,
    chunk_overlap=64,
    separators=["\n\n", "\n", "。", ";", " "]
)

chunks = splitter.split_documents(documents)

第二步:实体与关系抽取

from openai import OpenAI

client = OpenAI()

EXTRACTION_PROMPT = """
从以下文本中提取所有实体和关系。

输出格式(JSON):
{
  "entities": [
    {"name": "实体名", "type": "类型", "description": "简述"}
  ],
  "relations": [
    {"source": "源实体", "relation": "关系", "target": "目标实体"}
  ]
}

文本:
{text}
"""

def extract_triples(text: str) -> dict:
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": "你是知识抽取专家"},
            {"role": "user", "content": EXTRACTION_PROMPT.format(text=text)}
        ],
        response_format={"type": "json_object"},
        temperature=0.0
    )
    return json.loads(response.choices[0].message.content)

第三步:图谱入库

from neo4j import GraphDatabase

driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password"))

def ingest_triples(triples: dict):
    with driver.session() as session:
        for entity in triples["entities"]:
            session.run(
                """
                MERGE (e:Entity {name: $name})
                SET e.type = $type, e.description = $desc
                """,
                name=entity["name"],
                type=entity["type"],
                desc=entity["description"]
            )

        for rel in triples["relations"]:
            session.run(
                """
                MATCH (s:Entity {name: $source})
                MATCH (t:Entity {name: $target})
                MERGE (s)-[r:RELATES_TO {type: $rel_type}]->(t)
                """,
                source=rel["source"],
                target=rel["target"],
                rel_type=rel["relation"]
            )

第四步:社区检测与摘要生成

这是 Microsoft GraphRAG 的核心创新 -- 用 Leiden 算法将图划分为社区,对每个社区生成摘要,用于全局问答。

import networkx as nx
from graspologic.partition import leiden

G = nx.Graph()
# 从 Neo4j 导出节点和边到 NetworkX
for record in session.run("MATCH (a)-[r]->(b) RETURN a.name, b.name"):
    G.add_edge(record["a.name"], record["b.name"])

# Leiden 社区检测
communities = leiden(nx.to_numpy_array(G), resolution=1.0)

# 为每个社区生成摘要
for community_id, members in community_groups.items():
    subgraph_text = format_subgraph(members)
    summary = llm_summarize(subgraph_text)
    store_community_summary(community_id, summary)

查询执行流程

局部查询(Local Search)

适用场景:针对特定实体的事实性问答。

def local_search(query: str, top_k: int = 5) -> str:
    # 1. 实体提取
    entities = extract_entities_from_query(query)

    # 2. 图谱子图检索
    subgraph = []
    for entity in entities:
        result = session.run(
            """
            MATCH (e:Entity {name: $name})-[r]-(neighbor)
            RETURN e.name, type(r), neighbor.name, neighbor.description
            LIMIT 20
            """,
            name=entity
        )
        subgraph.extend(result.data())

    # 3. 向量检索补充
    vector_results = vector_store.similarity_search(query, k=top_k)

    # 4. 融合上下文
    context = format_graph_context(subgraph) + "\n\n" + format_vector_context(vector_results)

    # 5. LLM 生成
    return llm_generate(query, context)

全局查询(Global Search)

适用场景:需要跨领域、全局视角的摘要性问答。

def global_search(query: str) -> str:
    # 1. 检索相关社区摘要
    community_summaries = retrieve_relevant_communities(query)

    # 2. Map 阶段:对每个社区摘要单独回答
    partial_answers = []
    for summary in community_summaries:
        answer = llm_generate(
            query,
            context=summary,
            system="基于以下社区信息回答问题,给出 0-100 的相关性评分"
        )
        partial_answers.append(answer)

    # 3. Reduce 阶段:合并所有局部答案
    final_answer = llm_generate(
        query,
        context="\n".join(partial_answers),
        system="综合以下信息,给出全面、结构化的最终回答"
    )
    return final_answer

查询路由策略

from enum import Enum

class QueryType(Enum):
    LOCAL = "local"
    GLOBAL = "global"
    HYBRID = "hybrid"

def route_query(query: str) -> QueryType:
    """基于规则 + LLM 的查询路由"""

    # 规则层:关键词匹配
    global_keywords = ["总结", "概览", "所有", "主要", "趋势", "对比"]
    local_keywords = ["谁是", "什么时候", "在哪里", "属于", "关系"]

    if any(kw in query for kw in global_keywords):
        return QueryType.GLOBAL
    if any(kw in query for kw in local_keywords):
        return QueryType.LOCAL

    # LLM 层:语义判断
    classification = llm_classify(query)
    return QueryType(classification)

生产环境性能优化

缓存策略

缓存层 内容 TTL 命中率目标
L1 查询缓存 完整的查询-回答对 1h >30%
L2 子图缓存 热门实体的 N 跳子图 15min >60%
L3 社区缓存 社区摘要 24h >90%
L4 Embedding 缓存 查询向量 无过期 >95%

图谱索引优化

-- 实体名称全文索引
CREATE FULLTEXT INDEX entity_name_fulltext
FOR (n:Entity) ON EACH [n.name, n.description];

-- 复合属性索引
CREATE INDEX entity_type_name
FOR (n:Entity) ON (n.type, n.name);

-- 关系类型索引(Neo4j 5.x)
CREATE INDEX rel_type_index
FOR ()-[r:RELATES_TO]-() ON (r.type);

批量处理管道

import asyncio
from concurrent.futures import ThreadPoolExecutor

async def parallel_extract(chunks: list, max_workers: int = 8):
    """并行三元组抽取"""
    executor = ThreadPoolExecutor(max_workers=max_workers)
    loop = asyncio.get_event_loop()

    tasks = [
        loop.run_in_executor(executor, extract_triples, chunk.page_content)
        for chunk in chunks
    ]

    results = await asyncio.gather(*tasks)
    return results

评估指标体系

检索质量评估

指标 计算方式 基线目标
命中率 (Hit Rate) 正确答案在 Top-K 中的比例 >85%
MRR (Mean Reciprocal Rank) 正确答案排名的倒数均值 >0.7
上下文相关性 LLM 评分(1-5 分) >4.0
多跳覆盖率 需多跳才能回答的问题准确率 >60%

端到端评估

def evaluate_graphrag(test_set: list) -> dict:
    metrics = {"hit_rate": 0, "faithfulness": 0, "relevancy": 0}

    for item in test_set:
        query = item["query"]
        ground_truth = item["answer"]

        # 执行 GraphRAG 查询
        answer, contexts = graphrag_query(query)

        # 命中率
        metrics["hit_rate"] += int(ground_truth in str(contexts))

        # 忠实度:答案是否由上下文支撑
        metrics["faithfulness"] += llm_judge_faithfulness(answer, contexts)

        # 相关性:答案是否切题
        metrics["relevancy"] += llm_judge_relevancy(answer, query)

    n = len(test_set)
    return {k: v / n for k, v in metrics.items()}

GraphRAG vs 纯向量 RAG 对比实验

评估维度 纯向量 RAG GraphRAG 提升幅度
单跳事实问答 88.2% 89.5% +1.5%
多跳关系推理 42.1% 73.6% +74.8%
全局摘要质量 3.2/5 4.3/5 +34.4%
实体消歧准确率 71.0% 91.2% +28.5%
首次回答延迟 1.2s 2.8s +133%
索引构建成本 5-10x

关键结论:GraphRAG 在关系推理和全局摘要场景优势明显,但引入了额外的索引成本和查询延迟。工程决策应基于具体业务场景的查询分布做取舍。


常见陷阱与最佳实践

陷阱一:过度依赖 LLM 抽取

LLM 抽取三元组的成本高、速度慢。建议混合策略:

  • 结构化数据(数据库、表格)→ 规则映射
  • 半结构化数据(JSON、XML)→ 模板抽取
  • 非结构化文本 → LLM 抽取(仅此场景)

陷阱二:社区粒度不当

社区太粗 → 摘要过于笼统;社区太细 → 需要合并过多摘要。

推荐做法:多层级社区(Leiden resolution 参数调整),查询时根据问题粒度选择合适层级。

陷阱三:图谱与向量索引更新不同步

文档更新后,向量索引和图谱需同步更新。建议使用事件驱动架构:

文档变更 ──→ 消息队列 ──→ 向量索引更新
                       ──→ 三元组抽取 ──→ 图谱更新
                       ──→ 社区重算 ──→ 摘要更新

最佳实践清单

  1. 查询路由先行:不是所有查询都需要图谱检索,避免无谓开销
  2. 增量构建:支持增量添加文档和三元组,避免全量重建
  3. 实体规范化:建立实体别名映射表,确保同一实体不会以不同名称重复入图
  4. 监控指标:持续监控缓存命中率、查询延迟 P99、图谱节点/边增长率
  5. A/B 测试:在生产环境用 A/B 测试验证 GraphRAG 是否真正优于纯向量 RAG

技术选型推荐

组件 推荐方案 备选
图数据库 Neo4j 5.x NebulaGraph / TigerGraph
向量数据库 Milvus / Qdrant Weaviate / Chroma
LLM 抽取 GPT-4o / Claude 本地模型 + LoRA
社区检测 Leiden (graspologic) Louvain
编排框架 LangGraph / LlamaIndex 自研 Pipeline

总结

GraphRAG 不是银弹,而是对纯向量 RAG 的针对性增强。核心价值在于:

  1. 关系推理能力:回答需要多跳遍历的复杂问题
  2. 全局视角:通过社区摘要覆盖大范围信息
  3. 结构化约束:用图结构消除歧义、保持一致性

工程落地的关键不是"要不要用 GraphRAG",而是"在什么场景、用多大力度引入图谱"。建议从高价值场景(如内部知识库、合规审查、客户关系分析)切入,验证 ROI 后逐步扩展。


Maurice | maurice_wen@proton.me