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

引言

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

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

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


1. GraphRAG 架构总览

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

1.1 与传统 RAG 的关键差异

维度传统 RAGGraphRAG
检索单元文档块(文本片段)实体、关系、社区摘要
索引结构向量索引图结构 + 向量索引
多跳推理❌ 依赖单次检索✅ 图遍历天然支持
全局问题❌ 只看局部片段✅ 社区摘要提供全局视角
构建成本低(嵌入即可)高(需 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 不同问题类型的表现

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

6.2 构建成本对比

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

6.3 查询延迟对比

查询类型传统 RAGGraphRAG LocalGraphRAG 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 的核心价值在于结构化知识表示全局视角

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

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


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

加入讨论

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

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