课程0基础Agent开发课 / RAG与向量数据库 / GraphRAG-基于知识图谱的RAG增强
— 25 min read

GraphRAG-基于知识图谱的RAG增强

> **[进阶选读]** 本篇适合已完成必读五篇(01-05)、希望解决**多跳推理**问题(如"A 公司的创始人和 B 公司的 CEO 有什么共同背景")的读者。如果你的 RAG 系统主要用于单文档事实查询,可暂时跳过。

GraphRAG:基于知识图谱的 RAG 增强

[进阶选读] 本篇适合已完成必读五篇(01-05)、希望解决多跳推理问题(如"A 公司的创始人和 B 公司的 CEO 有什么共同背景")的读者。如果你的 RAG 系统主要用于单文档事实查询,可暂时跳过。

1.1 标准 RAG 的根本局限:碎片化的向量空间

GraphRAG

文档

分块

实体关系抽取

知识图谱

向量化

向量数据库

双路检索
向量 + 图谱

融合上下文

LLM 生成

回答
保留关系信息

传统 RAG

文档

分块

向量化

向量数据库

向量检索

Top-K 文本块

LLM 生成

回答
关系信息丢失

GraphRAG 通过知识图谱+向量双路检索,解决传统 RAG 丢失实体关系的问题

标准 RAG 有一个优雅的特性:它不依赖任何预先定义的知识结构,把任何文档都切成 chunk、向量化、存起来,查询时语义相似的 chunk 就能被检索出来。

但这个优雅背后隐藏着一个致命的架构问题:向量化抛弃了实体之间的关系信息

当文档被切成 chunk 的那一刻,跨 chunk 的实体关系就断了。chunk A 说"张伟创立了公司 X",chunk B 说"公司 X 和公司 Y 是竞争对手",chunk C 说"公司 Y 的投资方是机构 Z"。这三个 chunk 存进向量数据库后,变成了三个独立的浮点数向量——关系信息消失了。

向量相似度检索只能回答"找和问题语义相似的 chunk",但无法回答"从公司 X 出发,沿关系链找到机构 Z"这种需要图遍历的问题。

什么场景会遭遇这个限制

单文档内的事实查询,标准 RAG 没有问题。问题出现在需要跨文档多跳推理的场景:

  • "这家公司的创始人,和另一家公司的 CEO 有什么共同背景?"
  • "这篇研究报告里引用的三篇论文,它们的核心观点是否一致?"
  • "分散在五份合同中的甲方乙方关系,整体呈现什么格局?"

回答这类问题,需要先找到相关实体,再沿着实体之间的关系边遍历——这是图问题,不是相似度搜索问题。

知识图谱是什么,它为什么能解决这个问题

知识图谱(Knowledge Graph)的核心思想是:把知识表示为"实体 + 关系"的有向图,而不是文本片段的集合。节点是实体(人、公司、地点、概念),边是关系(创立、属于、竞争、投资)。

这种表示方式天然支持关系查询:从任意节点出发,沿着特定类型的边遍历,可以精确找到所有满足关系条件的实体,而不需要猜测哪个 chunk 可能相关。

GraphRAG 的核心思路就是:先用 LLM 从文档中自动抽取实体和关系构建知识图谱,再用图遍历替代向量相似度检索来回答跨文档关系问题。


标准 RAG 能回答"某个文档里说了什么",但对于跨文档的复杂推理——"这三份报告里提到的公司,它们之间有什么关联?"——标准 RAG 几乎无能为力。这不是检索质量的问题,而是架构层面的根本限制。

GraphRAG 通过在标准 RAG 之前加入知识图谱(Knowledge Graph,一种把实体和实体之间的关系结构化存储成图的数据库,节点是实体,边是关系)构建步骤,来解决这个限制。


1.2 标准 RAG 的架构局限

标准 RAG 把文档切成 chunk,每个 chunk 独立向量化存储。检索时找最相似的 chunk,送给 LLM 回答。

这个架构有一个根本限制:chunk 之间的关联关系在向量化时就丢失了

举一个具体的例子。三份文档:

  • 文档 A:介绍某公司的创始人张伟
  • 文档 B:介绍张伟创立的另一家公司
  • 文档 C:介绍该公司的竞争对手

用户问:"张伟创立的公司和其主要竞争对手,有哪些共同的投资人?"

这个问题需要:

  1. 从文档 A 找到张伟的信息
  2. 从文档 B 找到张伟的公司
  3. 从文档 C 找到竞争对手
  4. 推断出三者之间的关系
  5. 在这个关系网络上查找共同投资人

标准 RAG 最多能帮你找到相关 chunk,但它不知道这些 chunk 里的实体是怎么关联的。LLM 要做这种跨文档推理,需要把所有相关 chunk 都塞进 context,而且还需要 LLM 自己做关系推断——这是对 LLM 推理能力的重大考验,效果不稳定。

知识图谱把这些关系显式地结构化表示出来,查询时可以直接沿着关系边遍历,而不是靠语义相似度猜。


1.3 知识图谱的基本概念

知识图谱由三种基本要素构成:

实体(Entity):现实世界中的"物"。人名、公司名、地名、产品名、概念名。例:张伟OpenAIGPT-4

关系(Relation):实体之间的连接。例:创立了竞争对手投资于属于

三元组(Triple):一个完整的知识单元,格式为 (主体, 关系, 客体),是知识图谱中存储信息的基本单位。例:(张伟, 创立了, OpenAI分部X)(GPT-4, 属于, OpenAI)

知识图谱就是这些三元组的集合,构成一个有向图,节点是实体,边是关系。

创立了

任职于

竞争对手

投资了

投资了

产品

产品

张伟
人物

公司A
公司

公司B
公司

公司C
公司

投资人X
机构

产品P
产品

产品Q
产品

用上面的关系图,"张伟的公司和竞争对手有共同投资人吗"这个问题就变成了一个图遍历问题:从 张伟 出发,找到 公司A,再找到 公司C,再看两者是否共享同一个 投资人X 节点。这是确定性的图查询,不依赖向量相似度。


1.4 Microsoft GraphRAG 框架

2024 年,Microsoft Research 发布了 GraphRAG 论文和开源实现,系统性地解决了基于知识图谱的 RAG 问题。

Microsoft GraphRAG 的核心贡献:

  1. 自动化知识图谱构建:用 LLM 自动从文档中抽取实体和关系,不需要手动标注
  2. 社区检测:对图中的实体做聚类,找出"社区"(在图结构中联系紧密的实体集合,类似社交网络中的圈子)
  3. 层级摘要:为每个社区生成摘要,形成层级化的知识表示
  4. 两种查询模式:Local Search(精确实体查询)和 Global Search(全局主题查询)

1.4.1 Local Search vs Global Search

查询模式 原理 适用场景 代价
Local Search 从特定实体出发,沿关系边检索相关信息 针对具体实体的问题:"某公司的业务范围?" 低,精确检索
Global Search 扫描所有社区摘要,做跨社区综合 全局主题问题:"整个行业的趋势?" 高,需要处理大量摘要

Local Search 适合"关于 X 的问题",Global Search 适合"关于整个语料的问题"。两种模式在同一个图上运行,由查询类型自动选择。


1.5 GraphRAG vs 标准 RAG 架构对比

GraphRAG

具体实体

全局主题

文档集合

LLM抽取
实体和关系

知识图谱
Neo4j/NetworkX

社区检测
+摘要生成

知识摘要库

用户查询

查询类型

Local Search
图遍历

Global Search
摘要聚合

拼Prompt

LLM回答

标准 RAG

相关Chunks

文档集合

文本分块

向量化

向量数据库

用户查询

查询向量化

相似度检索

拼Prompt

LLM回答


1.6 用 LangChain + Neo4j 实现 Graph RAG

下面是一个完整可运行的示例,演示用 Neo4j 存储知识图谱,用 LangChain(一个构建 AI 应用的 Python 框架,提供图数据库连接、LLM 调用等现成组件)实现 Graph RAG 查询。

验证目的:用内存版知识图谱演示从文档抽取三元组、构建图结构、执行图遍历查询的完整流程,验证"跨文档关系问题可以通过图查询而非向量相似度检索来准确回答"。

python
# graph_rag_demo.py
# 演示基于知识图谱的 RAG:实体抽取 → 图存储 → 图检索
# 依赖:pip install langchain langchain-openai langchain-community neo4j

from typing import List, Dict, Tuple, Optional
import json


# ============================================================
# 第一步:模拟文档集合
# ============================================================

SAMPLE_DOCUMENTS = [
    {
        "id": "doc1",
        "content": "OpenAI 由 Sam Altman 担任 CEO,公司总部位于旧金山。OpenAI 开发了 GPT-4 和 ChatGPT 等产品。微软向 OpenAI 投资了超过 100 亿美元。"
    },
    {
        "id": "doc2",
        "content": "Anthropic 由前 OpenAI 员工 Dario Amodei 创立。Anthropic 开发了 Claude 系列 AI 助手。亚马逊向 Anthropic 投资了 40 亿美元。"
    },
    {
        "id": "doc3",
        "content": "Google DeepMind 是 Alphabet 的子公司,开发了 Gemini 模型。Google 同时也是 Anthropic 的投资方,投资金额约 3 亿美元。"
    },
]


# ============================================================
# 第二步:用 LLM 从文档中抽取三元组
# 实际使用时接入真实 LLM,这里用模拟数据演示结构
# ============================================================

def extract_triples_with_llm(text: str) -> List[Tuple[str, str, str]]:
    """
    从文本中抽取知识三元组 (主体, 关系, 客体)。
    实际场景调用 LLM:

    from langchain_openai import ChatOpenAI
    from langchain.prompts import ChatPromptTemplate

    llm = ChatOpenAI(model="gpt-4o")
    prompt = ChatPromptTemplate.from_template(
        "从以下文本中抽取所有实体关系三元组,格式为 JSON 数组:\n"
        "[{'subject': '主体', 'relation': '关系', 'object': '客体'}]\n"
        "文本:{text}"
    )
    result = (prompt | llm).invoke({"text": text})
    triples = json.loads(result.content)
    return [(t['subject'], t['relation'], t['object']) for t in triples]
    """
    # 模拟抽取结果(结构与真实 LLM 输出一致)
    if "OpenAI" in text and "Sam Altman" in text:
        return [
            ("Sam Altman", "担任CEO", "OpenAI"),
            ("OpenAI", "总部位于", "旧金山"),
            ("OpenAI", "开发了", "GPT-4"),
            ("OpenAI", "开发了", "ChatGPT"),
            ("微软", "投资了", "OpenAI"),
        ]
    elif "Anthropic" in text and "Dario Amodei" in text:
        return [
            ("Dario Amodei", "创立了", "Anthropic"),
            ("Anthropic", "开发了", "Claude"),
            ("亚马逊", "投资了", "Anthropic"),
        ]
    elif "DeepMind" in text:
        return [
            ("Google DeepMind", "是子公司", "Alphabet"),
            ("Google DeepMind", "开发了", "Gemini"),
            ("Google", "投资了", "Anthropic"),
        ]
    return []


# ============================================================
# 第三步:内存知识图谱(生产环境替换为 Neo4j)
# ============================================================

class InMemoryKnowledgeGraph:
    """
    简单的内存知识图谱实现,用于演示。
    生产环境使用 Neo4j:

    from langchain_community.graphs import Neo4jGraph
    graph = Neo4jGraph(url="bolt://localhost:7687", username="neo4j", password="password")
    """

    def __init__(self):
        # 邻接表:entity -> [(relation, target_entity), ...]
        self.edges: Dict[str, List[Tuple[str, str]]] = {}
        # 反向索引:entity -> [(source_entity, relation), ...]
        self.reverse_edges: Dict[str, List[Tuple[str, str]]] = {}
        # 所有三元组
        self.triples: List[Tuple[str, str, str]] = []

    def add_triple(self, subject: str, relation: str, obj: str):
        """添加一个三元组到图中。"""
        self.triples.append((subject, relation, obj))

        # 正向边
        if subject not in self.edges:
            self.edges[subject] = []
        self.edges[subject].append((relation, obj))

        # 反向边(方便从客体查主体)
        if obj not in self.reverse_edges:
            self.reverse_edges[obj] = []
        self.reverse_edges[obj].append((subject, relation))

    def get_neighbors(self, entity: str, max_hops: int = 2) -> List[str]:
        """
        获取实体的邻居节点(多跳)。
        max_hops 控制图遍历深度,深度越大获取的上下文越多,但也越慢。
        """
        visited = {entity}
        current_level = {entity}
        context_triples = []

        for _ in range(max_hops):
            next_level = set()
            for node in current_level:
                # 获取以当前节点为主体的边
                for relation, target in self.edges.get(node, []):
                    context_triples.append(f"({node}) -[{relation}]-> ({target})")
                    if target not in visited:
                        next_level.add(target)
                        visited.add(target)

                # 获取以当前节点为客体的边
                for source, relation in self.reverse_edges.get(node, []):
                    context_triples.append(f"({source}) -[{relation}]-> ({node})")
                    if source not in visited:
                        next_level.add(source)
                        visited.add(source)

            current_level = next_level

        return context_triples

    def search_by_relation(self, relation_keyword: str) -> List[str]:
        """
        按关系类型搜索三元组。
        例如搜索 "投资" 找出所有投资关系。
        """
        return [
            f"({s}) -[{r}]-> ({o})"
            for s, r, o in self.triples
            if relation_keyword.lower() in r.lower()
        ]

    def find_common_neighbors(self, entity1: str, entity2: str) -> List[str]:
        """
        找出两个实体的共同邻居。
        这是 Graph RAG 解决"共同投资人"类问题的核心操作。
        """
        def get_all_neighbors(entity):
            neighbors = set()
            for _, target in self.edges.get(entity, []):
                neighbors.add(target)
            for source, _ in self.reverse_edges.get(entity, []):
                neighbors.add(source)
            return neighbors

        neighbors1 = get_all_neighbors(entity1)
        neighbors2 = get_all_neighbors(entity2)
        return list(neighbors1 & neighbors2)


# ============================================================
# 第四步:Graph RAG 查询管道
# ============================================================

class GraphRAGPipeline:
    """
    Graph RAG 完整查询管道:
    1. 从问题中识别关键实体
    2. 在知识图谱中检索相关三元组(图遍历)
    3. 把三元组转为自然语言上下文
    4. 送给 LLM 生成答案
    """

    def __init__(self, kg: InMemoryKnowledgeGraph):
        self.kg = kg

    def extract_entities_from_query(self, query: str) -> List[str]:
        """
        从查询中识别关键实体。
        实际场景用 NER 模型或 LLM 来做,这里用简单规则演示。
        """
        # 简单规则:检查查询中是否包含图中已知的实体
        known_entities = set(self.kg.edges.keys()) | set(self.kg.reverse_edges.keys())
        found = [e for e in known_entities if e in query]

        # 如果没找到已知实体,返回空(后续使用全局搜索)
        return found

    def retrieve_graph_context(self, query: str) -> str:
        """
        核心检索函数:从知识图谱中获取与查询相关的上下文。
        根据查询类型选择 Local Search 或关系搜索。
        """
        entities = self.extract_entities_from_query(query)

        context_parts = []

        if entities:
            # Local Search:从识别出的实体出发做图遍历
            for entity in entities:
                neighbors = self.kg.get_neighbors(entity, max_hops=2)
                if neighbors:
                    context_parts.append(f"关于「{entity}」的关系:")
                    context_parts.extend(neighbors[:10])  # 限制数量,避免 context 太长

        # 如果问题涉及"共同"关系,做共同邻居查询
        if "共同" in query or "都" in query:
            if len(entities) >= 2:
                common = self.kg.find_common_neighbors(entities[0], entities[1])
                if common:
                    context_parts.append(f"\n{entities[0]}{entities[1]} 的共同关联实体:{common}")

        # 如果没有找到具体实体,做关系类型搜索
        if not context_parts:
            for keyword in ["投资", "创立", "开发", "竞争"]:
                if keyword in query:
                    triples = self.kg.search_by_relation(keyword)
                    if triples:
                        context_parts.append(f"与「{keyword}」相关的关系:")
                        context_parts.extend(triples[:10])

        return "\n".join(context_parts) if context_parts else "未找到相关知识图谱信息"

    def answer_with_graph_context(self, query: str) -> Dict:
        """
        完整的 Graph RAG 问答:检索图上下文 → 生成答案。
        """
        graph_context = self.retrieve_graph_context(query)

        # 实际场景调用 LLM:
        # from langchain_openai import ChatOpenAI
        # llm = ChatOpenAI(model="gpt-4o")
        # prompt = f"基于以下知识图谱信息回答问题:\n{graph_context}\n\n问题:{query}"
        # answer = llm.invoke(prompt).content

        # 模拟 LLM 回答
        answer = f"基于知识图谱分析,{query.rstrip('??')}的情况如下:\n从图谱中提取的相关信息显示了实体间的关联关系。"

        return {
            "query": query,
            "graph_context": graph_context,
            "answer": answer,
        }


# ============================================================
# 第五步:使用 Neo4j 的生产级实现(代码框架)
# ============================================================

def build_neo4j_graph_rag():
    """
    生产级 Graph RAG,使用 Neo4j 作为图数据库。
    需要安装:pip install langchain-community neo4j
    需要运行 Neo4j 实例:docker run -p 7474:7474 -p 7687:7687 neo4j
    """
    try:
        from langchain_community.graphs import Neo4jGraph
        from langchain_community.chains.graph_qa.cypher import GraphCypherQAChain
        from langchain_openai import ChatOpenAI

        # 连接 Neo4j
        graph = Neo4jGraph(
            url="bolt://localhost:7687",
            username="neo4j",
            password="your_password"
        )

        # 插入知识三元组(Cypher 是 Neo4j 的查询语言,类似 SQL 但专为图数据库设计)
        # Neo4j(一种专门存储图结构数据的数据库)使用 Cypher 语言,MERGE 表示"不存在则创建"
        graph.query("""
            MERGE (sam:Person {name: 'Sam Altman'})
            MERGE (openai:Company {name: 'OpenAI'})
            MERGE (sam)-[:CEO_OF]->(openai)
            MERGE (microsoft:Company {name: '微软'})
            MERGE (microsoft)-[:INVESTED_IN {amount: '100亿美元'}]->(openai)
        """)

        # 用 GraphCypherQAChain 实现自然语言转 Cypher 查询
        # 这个 Chain 会把用户的自然语言问题转成 Cypher 查询,在 Neo4j 上执行,再用 LLM 总结答案
        llm = ChatOpenAI(model="gpt-4o", temperature=0)
        chain = GraphCypherQAChain.from_llm(
            llm=llm,
            graph=graph,
            verbose=True,   # 显示生成的 Cypher 查询,方便调试
        )

        result = chain.invoke({"query": "谁投资了 OpenAI?"})
        return result

    except ImportError:
        return "需要安装:pip install langchain-community neo4j langchain-openai"


# ============================================================
# 演示运行
# ============================================================

def run_demo():
    print("=" * 60)
    print("GraphRAG 演示:知识图谱构建与查询")
    print("=" * 60)

    # 构建知识图谱
    kg = InMemoryKnowledgeGraph()

    print("\n[第一步] 从文档中抽取三元组...")
    for doc in SAMPLE_DOCUMENTS:
        triples = extract_triples_with_llm(doc["content"])
        for s, r, o in triples:
            kg.add_triple(s, r, o)
            print(f"  + ({s}) -[{r}]-> ({o})")

    print(f"\n知识图谱构建完成:{len(kg.triples)} 个三元组")

    # 初始化查询管道
    pipeline = GraphRAGPipeline(kg)

    # 测试不同类型的查询
    test_queries = [
        "OpenAI 开发了哪些产品?",
        "谁投资了 AI 公司?",
        "Anthropic 和 OpenAI 有哪些共同关联?",
    ]

    print("\n[第二步] 执行 Graph RAG 查询...")
    for query in test_queries:
        print(f"\n查询:{query}")
        result = pipeline.answer_with_graph_context(query)
        print(f"图谱上下文:\n{result['graph_context']}")
        print(f"回答:{result['answer']}")
        print("-" * 40)


if __name__ == "__main__":
    run_demo()

1.7 GraphRAG 的代价

GraphRAG 的效果好,但代价也是真实的:

构建时间:对一个中等规模的文档集(100 份文档),知识图谱构建需要大量 LLM 调用来做实体抽取和关系识别。每份文档调用一次 LLM,100 份就是 100 次调用,加上社区检测和摘要生成,总调用次数可能达到几百次。

Token 成本:Microsoft 的基准测试显示,对一个百万 token 的语料库,构建 GraphRAG 的成本约为 $3-5(使用 GPT-4o-mini),但使用 GPT-4o 会高出 10 倍以上。

维护成本:文档更新时,知识图谱需要对应更新——不只是添加新实体,还需要处理已有实体的关系变化。标准 RAG 只需重新 embedding,GraphRAG 还需要重新跑实体抽取和图更新。

适用判断

场景 推荐方案 原因
问题只涉及单个文档内的信息 标准 RAG GraphRAG 的开销不值得
需要跨文档推理和关系查询 GraphRAG 标准 RAG 做不好
文档频繁更新(每天) 标准 RAG 图维护成本高
文档相对稳定、关系密集 GraphRAG 一次构建,长期使用
语料规模小(< 50 文档) 标准 RAG + 全文塞 context 直接全塞更简单

GraphRAG 解决的是标准 RAG 的架构缺陷:chunk 切碎了,实体间的关系就断了。知识图谱把这些关系显式存下来,查询时直接沿边遍历,而不是靠语义相似度猜。

代价是真实的:构建成本高、图的维护比向量索引复杂。不要为了用 GraphRAG 而用 GraphRAG——先确认你的问题确实需要跨文档多跳推理,再考虑引入它。

本页目录