GraphRAG 解析:知识图谱增强的检索增强生成

引言

传统 RAG 基于向量相似度检索文档块,存在三个结构性缺陷:

  1. 多跳推理弱:答案需要关联多个文档中的实体关系时,向量检索难以覆盖
  2. 全局视角缺失:只返回局部相关片段,无法回答「整个文档集的主要主题是什么」
  3. 实体消歧差:同名实体或指代关系容易混淆

GraphRAG(微软 2024 年提出)通过知识图谱 + 层级社区摘要解决这些问题,在全局性问题上显著优于传统 RAG。


1. GraphRAG 架构总览

原始文档
[1] 文本分块
[2] 实体 & 关系抽取(LLM)
[3] 知识图谱构建(实体节点 + 关系边)
[4] 社区检测(Leiden 算法)
[5] 层级社区摘要(LLM)
[6] 检索:局部检索(实体子图) + 全局检索(社区摘要)
[7] 生成

1.1 与传统 RAG 的关键差异

维度 传统 RAG GraphRAG
检索单元 文档块(文本片段) 实体、关系、社区摘要
索引结构 向量索引 图结构 + 向量索引
多跳推理 ❌ 依赖单次检索 ✅ 图遍历天然支持
全局问题 ❌ 只看局部片段 ✅ 社区摘要提供全局视角
构建成本 低(嵌入即可) 高(需 LLM 抽取实体)
查询延迟 低(向量检索) 中高(图检索+摘要)

2. 实体与关系抽取

2.1 LLM 驱动的信息抽取

GraphRAG 使用 LLM 从文本块中抽取实体和关系:

from openai import OpenAI
import json
import re

client = OpenAI()

def extract_entities_relations(
    text: str,
    entity_types: list[str] = None,
    model: str = "gpt-4o-mini",
) -> dict:
    """
    用 LLM 从文本中抽取实体和关系
    """
    entity_types = entity_types or ["person", "organization", "location", "event", "concept"]

    prompt = f"""你是一个信息抽取专家。请从以下文本中抽取实体和关系。

实体类型:{', '.join(entity_types)}

要求:
1. 抽取所有提到的实体,给出名称、类型和简短描述
2. 抽取实体之间的关系,给出关系类型和描述
3. 合并相同实体(消歧)

返回 JSON 格式:
{{
  "entities": [
    {{"name": "实体名", "type": "类型", "description": "描述"}}
  ],
  "relations": [
    {{"source": "实体A", "target": "实体B", "type": "关系类型", "description": "描述"}}
  ]
}}

文本:
{text}
"""
    resp = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
        response_format={"type": "json_object"},
        temperature=0.0,
    )

    result = json.loads(resp.choices[0].message.content)
    return result


# 批量处理文档块
def build_graph_from_chunks(chunks: list[str]) -> dict:
    """从多个文本块构建知识图谱"""
    all_entities = {}
    all_relations = []

    for i, chunk in enumerate(chunks):
        result = extract_entities_relations(chunk)

        # 合并实体(同名实体合并描述)
        for ent in result["entities"]:
            name = ent["name"].lower().strip()
            if name in all_entities:
                # 合并描述
                existing = all_entities[name]
                existing["description"] += f" | {ent['description']}"
                existing["source_chunks"].append(i)
            else:
                ent["source_chunks"] = [i]
                all_entities[name] = ent

        for rel in result["relations"]:
            rel["source_chunk"] = i
            all_relations.append(rel)

    return {
        "entities": list(all_entities.values()),
        "relations": all_relations,
    }

2.2 实体消歧

def entity_resolution(entities: list[dict]) -> list[dict]:
    """
    实体消歧:合并指代同一实体的不同名称
    """
    # 用嵌入相似度 + LLM 判断
    import numpy as np
    from sentence_transformers import SentenceTransformer

    model = SentenceTransformer("BAAI/bge-large-zh-v1.5")

    # 计算实体名+描述的嵌入
    texts = [f"{e['name']} {e['description']}" for e in entities]
    embeddings = model.encode(texts)

    # 找相似实体对
    from sklearn.metrics.pairwise import cosine_similarity
    sim_matrix = cosine_similarity(embeddings)

    merged = set()
    clusters = []

    for i in range(len(entities)):
        if i in merged:
            continue
        cluster = [i]
        for j in range(i + 1, len(entities)):
            if j in merged:
                continue
            if sim_matrix[i][j] > 0.85:  # 高相似度阈值
                cluster.append(j)
                merged.add(j)
        merged.add(i)
        clusters.append(cluster)

    # 合并同簇实体
    resolved = []
    for cluster in clusters:
        primary = entities[cluster[0]]
        if len(cluster) > 1:
            aliases = [entities[c]["name"] for c in cluster[1:]]
            primary["aliases"] = aliases
        resolved.append(primary)

    return resolved

3. 知识图谱存储

3.1 Neo4j 图数据库

from neo4j import GraphDatabase

class KnowledgeGraph:
    def __init__(self, uri: str, user: str, password: str):
        self.driver = GraphDatabase.driver(uri, auth=(user, password))

    def close(self):
        self.driver.close()

    def create_entity(self, name: str, entity_type: str, description: str):
        with self.driver.session() as session:
            session.execute_write(
                self._create_entity_tx, name, entity_type, description
            )

    @staticmethod
    def _create_entity_tx(tx, name, entity_type, description):
        query = """
        MERGE (e:Entity {name: $name})
        SET e.type = $type, e.description = $description
        """
        tx.run(query, name=name, type=entity_type, description=description)

    def create_relation(self, source: str, target: str, rel_type: str, description: str):
        with self.driver.session() as session:
            session.execute_write(
                self._create_relation_tx, source, target, rel_type, description
            )

    @staticmethod
    def _create_relation_tx(tx, source, target, rel_type, description):
        query = """
        MATCH (s:Entity {name: $source})
        MATCH (t:Entity {name: $target})
        MERGE (s)-[r:RELATION {type: $type}]->(t)
        SET r.description = $description
        """
        tx.run(query, source=source, target=target, type=rel_type, description=description)

    def build_from_graph_data(self, graph_data: dict):
        """批量导入图谱数据"""
        with self.driver.session() as session:
            # 创建实体
            for ent in graph_data["entities"]:
                session.execute_write(
                    self._create_entity_tx,
                    ent["name"], ent["type"], ent["description"]
                )
            # 创建关系
            for rel in graph_data["relations"]:
                session.execute_write(
                    self._create_relation_tx,
                    rel["source"], rel["target"],
                    rel["type"], rel.get("description", "")
                )

    def get_entity_subgraph(self, entity_name: str, hops: int = 2) -> dict:
        """获取实体的 N 跳子图"""
        with self.driver.session() as session:
            result = session.run("""
                MATCH path = (e:Entity {name: $name})-[*1..$hops]-(related)
                RETURN path
            """, name=entity_name, hops=hops)

            nodes, edges = set(), []
            for record in result:
                path = record["path"]
                for node in path.nodes:
                    nodes.add(node["name"])
                for rel in path.relationships:
                    edges.append({
                        "source": rel.start_node["name"],
                        "target": rel.end_node["name"],
                        "type": rel["type"],
                    })
            return {"nodes": list(nodes), "edges": edges}

3.2 NetworkX 内存图(轻量替代)

import networkx as nx

class InMemoryGraph:
    """基于 NetworkX 的内存知识图谱"""
    def __init__(self):
        self.graph = nx.MultiDiGraph()

    def add_entity(self, name: str, **attrs):
        self.graph.add_node(name, **attrs)

    def add_relation(self, source: str, target: str, rel_type: str, **attrs):
        self.graph.add_edge(source, target, type=rel_type, **attrs)

    def get_subgraph(self, entity: str, hops: int = 2) -> nx.Graph:
        """获取 N 跳子图"""
        if entity not in self.graph:
            return nx.Graph()
        nodes = nx.single_source_shortest_path_length(self.graph, entity, cutoff=hops)
        subgraph = self.graph.subgraph(nodes.keys()).copy()
        return subgraph

    def get_all_entities(self) -> list[str]:
        return list(self.graph.nodes())

    def get_stats(self) -> dict:
        return {
            "nodes": self.graph.number_of_nodes(),
            "edges": self.graph.number_of_edges(),
        }

4. 社区检测与层级摘要

4.1 Leiden 社区检测

import community as community_louvain
import networkx as nx
from typing import List, Dict, Tuple

def detect_communities(
    graph: nx.Graph,
    resolution: float = 1.0,
    min_community_size: int = 3,
) -> Dict[str, int]:
    """
    使用 Louvain/Leiden 算法检测社区
    返回: {entity_name: community_id}
    """
    # 转为无向图
    undirected = graph.to_undirected()

    # Louvain 社区检测
    partition = community_louvain.best_partition(
        undirected,
        resolution=resolution,
        random_state=42,
    )

    # 过滤小社区
    community_sizes = {}
    for node, comm in partition.items():
        community_sizes[comm] = community_sizes.get(comm, 0) + 1

    valid_communities = {
        comm for comm, size in community_sizes.items()
        if size >= min_community_size
    }

    return {
        node: comm for node, comm in partition.items()
        if comm in valid_communities
    }


def build_hierarchy(
    graph: nx.Graph,
    partition: Dict[str, int],
    max_levels: int = 3,
) -> List[Dict]:
    """
    构建层级社区结构
    Level 0: 所有实体
    Level 1: 一级社区
    Level 2: 社区中的子社区
    """
    hierarchy = []

    # Level 0: 全局
    hierarchy.append({
        "level": 0,
        "community_id": 0,
        "entities": list(graph.nodes()),
        "size": graph.number_of_nodes(),
    })

    # Level 1+: 社区划分
    current_level = 1
    current_partition = partition

    while current_level <= max_levels:
        communities = {}
        for node, comm_id in current_partition.items():
            if comm_id not in communities:
                communities[comm_id] = []
            communities[comm_id].append(node)

        if len(communities) <= 1:
            break

        for comm_id, entities in communities.items():
            hierarchy.append({
                "level": current_level,
                "community_id": comm_id,
                "entities": entities,
                "size": len(entities),
            })

            # 在社区内继续细分
            if len(entities) > 10:
                subgraph = graph.subgraph(entities)
                sub_partition = community_louvain.best_partition(
                    subgraph.to_undirected(),
                    resolution=1.5,  # 更高分辨率 = 更小社区
                )
                # 更新 partition 供下一层使用
                for node in entities:
                    current_partition[node] = f"{comm_id}_{sub_partition[node]}"

        current_level += 1

    return hierarchy

4.2 社区摘要生成

def generate_community_summaries(
    graph: nx.Graph,
    hierarchy: List[Dict],
    llm_client,
) -> List[Dict]:
    """
    为每个社区生成摘要
    """
    for community in hierarchy:
        if community["level"] == 0:
            # 全局摘要
            community["summary"] = generate_global_summary(graph, llm_client)
            continue

        entities = community["entities"]
        # 收集社区内的实体描述和关系
        entity_descs = []
        for ent in entities:
            node_data = graph.nodes[ent]
            entity_descs.append(f"- {ent} ({node_data.get('type', 'unknown')}): {node_data.get('description', '')}")

        # 收集社区内部关系
        internal_edges = []
        for u, v, data in graph.subgraph(entities).edges(data=True):
            internal_edges.append(f"- {u} --[{data['type']}]--> {v}: {data.get('description', '')}")

        # LLM 生成摘要
        prompt = f"""请为以下知识图谱社区生成结构化摘要。

社区实体:
{chr(10).join(entity_descs[:50])}

社区内部关系:
{chr(10).join(internal_edges[:50])}

请输出:
1. 社区主题(一句话)
2. 关键实体列表(5-10个)
3. 主要关系模式(2-3条)
4. 社区整体描述(3-5句话)
"""
        resp = llm_client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}],
        )
        community["summary"] = resp.choices[0].message.content

    return hierarchy

5. 检索策略

5.1 局部检索:实体子图

def local_search(
    query: str,
    graph: nx.Graph,
    embed_model,
    vector_index,           # 实体描述的向量索引
    community_summaries: list,
    top_k_entities: int = 5,
    hops: int = 2,
) -> dict:
    """
    局部检索:找到相关实体 → 扩展子图 → 返回结构化上下文
    """
    # Step 1: 向量检索找到相关实体
    entity_scores = vector_index.search(query, top_k=top_k_entities)
    seed_entities = [s["entity"] for s in entity_scores]

    # Step 2: 扩展子图
    subgraph_nodes = set()
    subgraph_edges = []
    for entity in seed_entities:
        if entity in graph:
            sub = nx.single_source_shortest_path_length(graph, entity, cutoff=hops)
            for node in sub:
                subgraph_nodes.add(node)
            for u, v, data in graph.subgraph(sub.keys()).edges(data=True):
                subgraph_edges.append({"source": u, "target": v, **data})

    # Step 3: 构建上下文
    entity_context = []
    for node in subgraph_nodes:
        node_data = graph.nodes[node]
        entity_context.append(f"- {node} ({node_data.get('type', '')}): {node_data.get('description', '')}")

    relation_context = [f"- {e['source']} --[{e['type']}]--> {e['target']}" for e in subgraph_edges]

    return {
        "entities": list(subgraph_nodes),
        "entity_descriptions": entity_context,
        "relations": relation_context,
        "subgraph_size": len(subgraph_nodes),
    }

5.2 全局检索:社区摘要

def global_search(
    query: str,
    community_summaries: list[dict],
    embed_model,
    llm_client,
    top_k_communities: int = 5,
) -> dict:
    """
    全局检索:找到相关社区摘要 → 聚合答案
    适合回答「整个文档集的主要主题」等全局性问题
    """
    # Step 1: 检索相关社区
    summaries = [c["summary"] for c in community_summaries if c["level"] >= 1]
    summary_embeddings = embed_model.encode(summaries)
    query_emb = embed_model.encode([query])

    from sklearn.metrics.pairwise import cosine_similarity
    scores = cosine_similarity(query_emb, summary_embeddings)[0]
    top_indices = scores.argsort()[-top_k_communities:][::-1]

    # Step 2: 每个社区生成局部答案
    partial_answers = []
    for idx in top_indices:
        comm = community_summaries[idx + 1]  # 跳过 level 0
        prompt = f"""基于以下社区摘要回答问题。如果信息不足以回答,请说明。

社区摘要:
{comm['summary']}

问题:{query}

答案:"""
        resp = llm_client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}],
        )
        partial_answers.append({
            "community_id": comm["community_id"],
            "answer": resp.choices[0].message.content,
            "score": scores[idx],
        })

    # Step 3: 聚合答案
    combined = "\n\n".join([
        f"[社区 {a['community_id']}]: {a['answer']}"
        for a in partial_answers
    ])
    final_prompt = f"""以下是多个社区对同一问题的回答,请综合生成最终答案。

问题:{query}

各社区回答:
{combined}

最终答案:"""
    final_resp = llm_client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": final_prompt}],
    )

    return {
        "answer": final_resp.choices[0].message.content,
        "partial_answers": partial_answers,
        "communities_used": len(partial_answers),
    }

6. GraphRAG vs 传统 RAG 效果对比

6.1 不同问题类型的表现

问题类型 传统 RAG GraphRAG (Local) GraphRAG (Global)
具体事实查询 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
多跳推理 ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
全局主题概括 ⭐⭐ ⭐⭐⭐⭐⭐
实体关系查询 ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
时序推理 ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐
计算类问题 ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐

6.2 构建成本对比

维度 传统 RAG GraphRAG
文档分块 ✅ 快 ✅ 快
实体抽取 ❌ 不需要 ✅ 需要(LLM 调用)
关系抽取 ❌ 不需要 ✅ 需要(LLM 调用)
社区检测 ❌ 不需要 ✅ 需要(Leiden)
社区摘要 ❌ 不需要 ✅ 需要(LLM 调用)
总 LLM 调用 0(仅嵌入) O(n × chunks)
索引时间 分钟级 小时级
存储成本 向量索引 图数据库 + 向量索引

6.3 查询延迟对比

查询类型 传统 RAG GraphRAG Local GraphRAG Global
单次查询 ~200ms ~500ms ~2-5s
检索步骤 1次向量检索 向量检索 + 图遍历 多社区检索 + 聚合

7. 混合检索:GraphRAG + 传统 RAG

def hybrid_graph_rag(
    query: str,
    vector_index,       # 传统向量索引
    graph: nx.Graph,    # 知识图谱
    community_summaries: list,
    embed_model,
    llm_client,
    top_k_docs: int = 5,
    top_k_entities: int = 5,
    use_global: bool = False,
) -> str:
    """
    混合检索:同时使用向量检索和图谱检索
    """
    # 1. 传统 RAG 检索
    doc_results = vector_index.search(query, top_k=top_k_docs)
    doc_context = "\n\n".join([r["content"] for r in doc_results])

    # 2. GraphRAG 检索
    if use_global:
        graph_result = global_search(query, community_summaries, embed_model, llm_client)
        graph_context = graph_result["answer"]
    else:
        graph_result = local_search(query, graph, embed_model, vector_index)
        graph_context = "\n".join(graph_result["entity_descriptions"] + graph_result["relations"])

    # 3. 融合上下文
    prompt = f"""基于以下信息回答问题。

文档检索结果:
{doc_context}

知识图谱信息:
{graph_context}

问题:{query}

回答:"""
    resp = llm_client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": prompt}],
    )
    return resp.choices[0].message.content

8. 实战:使用微软 GraphRAG 库

# 安装
pip install graphrag

# 初始化项目
graphrag init --root ./my-graphrag

# 配置 settings.yaml
# settings.yaml
encoding_model: cl100k_base
llm:
  model: gpt-4o-mini
  api_key: ${OPENAI_API_KEY}
chunks:
  size: 1200
  overlap: 100
entity_extraction:
  max_gleanings: 1
community_reports:
  max_length: 2000
# 构建索引(从文档到知识图谱)
graphrag index --root ./my-graphrag

# 查询
graphrag query --root ./my-graphrag \
  --method local \
  --query "谁是张三的合作伙伴?"

graphrag query --root ./my-graphrag \
  --method global \
  --query "这些文档的主要主题是什么?"

9. 适用场景与局限

9.1 适合 GraphRAG 的场景

场景 为什么适合
企业知识库(大量实体关系) 实体关联是核心需求
法律文档分析 案例引用、法条关联
学术论文集 作者、机构、引用关系
医疗病历 症状、诊断、用药关系
情报分析 人物、组织、事件网络

9.2 不适合的场景

场景 为什么不适合
简单 FAQ 问答 向量检索足够,图谱开销浪费
代码库 代码结构不是实体关系型
实时新闻 构建成本高,实时性不足
短文档(<10篇) 图谱太小,社区检测无意义

10. 优化建议

10.1 降低构建成本

# 1. 使用小模型做实体抽取
extract_model = "gpt-4o-mini"  # 而非 gpt-4o

# 2. 增大分块大小,减少 LLM 调用
chunk_size = 1200  # 而非 512

# 3. 并行处理
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=10) as pool:
    results = list(pool.map(extract_entities_relations, chunks))

# 4. 缓存抽取结果,增量更新

10.2 降低查询延迟

# 1. 预计算常见查询的子图
# 2. 社区摘要预嵌入
# 3. 使用更快的 LLM 做社区摘要(gpt-4o-mini)
# 4. Global 查询用 map-reduce 并行

总结

GraphRAG 的核心价值在于结构化知识表示全局视角

能力 传统 RAG GraphRAG
局部事实检索 ✅ 强 ✅ 强
多跳推理 ❌ 弱 ✅ 强
全局主题概括 ❌ 弱 ✅ 强
实体关系查询 ❌ 弱 ✅ 强
构建成本
查询延迟 中高

最佳实践:不要用 GraphRAG 替代传统 RAG,而是互补使用。简单问题走向量检索,复杂问题走图谱检索,全局问题走社区摘要。


相关阅读:高级 RAG 模式、RAG 分块策略、RAG 重排序指南

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。