GraphRAG 实战:从向量检索到知识图谱增强检索

传统 RAG 用向量相似度检索文档片段,但在回答"谁和谁有关系"“某事件的完整影响链"这类需要多跳推理的问题时力不从心——关键信息可能分散在不同文档里,向量检索只能局部命中。GraphRAG 将知识表示为实体-关系图谱,用图遍历补足向量检索缺失的关联推理能力。本指南从图谱建模出 …

传统 RAG 用向量相似度检索文档片段,但在回答"谁和谁有关系"“某事件的完整影响链"这类需要多跳推理的问题时力不从心——关键信息可能分散在不同文档里,向量检索只能局部命中。GraphRAG 将知识表示为实体-关系图谱,用图遍历补足向量检索缺失的关联推理能力。本指南从图谱建模出发,系统覆盖实体抽取、图谱构建、检索策略(局部与全局)、与向量检索的混合架构,并给出 Neo4j + LLM 的完整实战。

一、为什么需要 GraphRAG:向量检索的瓶颈

1.1 向量检索的三大局限

局限场景示例根因
局部性“A 公司的竞争对手是谁”——信息分散在多篇文档相似度只匹配片段,跨文档关系断裂
缺乏多跳“哪些供应商同时服务 B 和 C”——需要两跳推理向量无结构,无法遍历关系
聚合盲区“统计 2024 年所有部门的预算总额”——需要全图聚合只取 Top-K,看不到全局

ℹ️ 核心洞察:向量检索擅长"找出像什么的文档”,图谱检索擅长"找出有什么关系的实体"。二者互补而非互斥——混合架构是生产答案。

1.2 向量 vs 图谱的能力矩阵

能力向量 RAG知识图谱GraphRAG(结合)
语义相似匹配⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
实体精确查找⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
多跳关系推理⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
聚合统计⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
构建成本低高(抽取+建模)高
可解释性中高(结构可见)高

二、知识图谱建模基础

2.1 图谱的三要素

知识图谱 = 节点(实体)+ 边(关系)+ 属性(标签/值)

示例(保险理赔场景):
  (客户:Person {name:"王强", age:42})
  (保单:Policy {id:"P-8842", amount:500000})
  (险种:Product {name:"重疾险"})
  (王强)-[:购买]->(P-8842)
  (P-8842)-[:属于]->(重疾险)
  (王强)-[:申请]->(理赔:Claim {id:"C-331", status:"审核中"})

2.2 Schema 设计三原则

原则说明反例
实体可唯一标识每个节点有业务主键同名节点重复出现
关系有方向与语义边带谓词,如 购买/属于一律用 关联
属性只放原子值不要塞长文本到属性把回答全文塞进节点
// Neo4j Schema 约束示例
CREATE CONSTRAINT person_id IF NOT EXISTS
  FOR (p:Person) REQUIRE p.id IS UNIQUE;
CREATE CONSTRAINT policy_id IF NOT EXISTS
  FOR (p:Policy) REQUIRE p.id IS UNIQUE;
CREATE INDEX policy_product IF NOT EXISTS
  FOR (p:Policy) ON (p.product_id);

三、图谱构建:从文档到实体关系

3.1 LLM 驱动的实体与关系抽取

GraphRAG 的核心构建步骤:文档 → LLM 抽取三元组 → 消歧 → 入库。

# graph_builder.py — LLM 抽取实体关系三元组
import json
from typing import List, Dict

EXTRACT_SYSTEM = """你是知识图谱构建助手。从给定文本中抽取实体与关系,
输出 JSON:{"entities": [{"name", "type", "description"}],
            "relations": [{"from", "to", "type", "description"}]}
规则:
- 实体类型限:Person/Organization/Product/Event/Location/Concept
- 关系类型限:购买/属于/参与/导致/位于/服务/竞争
- 只抽取文本明确表达的信息,不要臆造
"""

def extract_triples(text: str, llm_call) -> dict:
    raw = llm_call(EXTRACT_SYSTEM, f"文本:\n{text}")
    return json.loads(raw)


def build_graph(documents: List[str], llm_call, neo4j_driver):
    """批量抽取并写入图谱,带实体消歧。"""
    with neo4j_driver.session() as session:
        for doc in documents:
            triples = extract_triples(doc, llm_call)
            for ent in triples["entities"]:
                # MERGE 保证同名实体合并,避免重复节点
                session.run(
                    "MERGE (e:{type} {{name:$name, description:$desc}})"
                    .format(type=ent["type"]),
                    name=ent["name"], desc=ent.get("description", ""))
            for rel in triples["relations"]:
                session.run(
                    """MATCH (a {name:$from}), (b {name:$to})
                       MERGE (a)-[:{type} {{description:$desc}}]->(b)"""
                    .format(type=rel["type"]),
                    from=rel["from"], to=rel["to"], desc=rel.get("description",""))
// 抽取结果示例(保险条款文本)
{
  "entities": [
    {"name": "王强", "type": "Person", "description": "投保人"},
    {"name": "意外险A", "type": "Product", "description": "一年期意外险"}
  ],
  "relations": [
    {"from": "王强", "to": "意外险A", "type": "购买",
     "description": "2025-03-01 投保,保额 50 万"},
    {"from": "意外险A", "to": "高风险运动免责", "type": "属于",
     "description": "含免责条款"}
  ]
}

3.2 实体消歧与归一化

同一实体在不同文档可能写法不同(“谷歌” vs “Google”),需要消歧:

def disambiguate_entities(entities: List[dict], existing_aliases: dict,
                          embed_fn) -> List[str]:
    """将抽取实体映射到既有规范名。alias 表:别名 → 规范名。"""
    resolved = []
    for ent in entities:
        name = ent["name"]
        # 1. 精确别名表
        if name in existing_aliases:
            resolved.append(existing_aliases[name])
            continue
        # 2. 向量相似度匹配(阈值 0.92 才合并)
        best, best_score = find_similar_entity(name, embed_fn, existing_aliases)
        if best and best_score >= 0.92:
            resolved.append(best)
        else:
            resolved.append(name)   # 新实体,保留原名
    return resolved

3.3 增量更新与一致性

生产图谱需要增量构建——只处理新增/变更文档,避免全量重建:

def incremental_update(driver, changed_docs: List[str], llm_call,
                       doc_ids: List[str]):
    """按文档级增量更新:先删旧子图再重建,保持一致性。"""
    with driver.session() as session:
        for doc_id in changed_docs:
            # 1. 删除该文档对应的旧子图(通过文档锚点节点)
            session.run(
                """MATCH (d:Document {id:$doc_id})-[r]-(e)
                   DETACH DELETE d, r""", doc_id=doc_id)
            # 2. 重建该文档的三元组
            triples = extract_triples(load_doc(doc_id), llm_call)
            write_triples(session, doc_id, triples)

四、图谱检索策略:局部与全局

4.1 局部检索(Local):实体为中心的遍历

适用于面向具体实体的问答——先定位实体,再沿关系扩展邻域:

// 局部检索:找到王强及其 2 跳内的全部关联
MATCH (p:Person {name: '王强'})-[r*1..2]-(neighbor)
RETURN p.name AS start, type(r) AS rel, neighbor.name AS target
LIMIT 100

// 更实用的:返回带描述的子图供 LLM 理解
MATCH (p:Person {name:'王强'})-[r*1..2]-(neighbor)
RETURN p.name + ' -[' + type(r) + ']-> ' + neighbor.name AS path,
       neighbor.description AS desc
LIMIT 50
def local_graph_search(driver, entity_name: str, hops: int = 2) -> list[dict]:
    """局部检索:从实体出发做 BFS 遍历,返回邻域路径。"""
    query = """
        MATCH (e)-[r*1..{hops}]-(n)
        WHERE e.name = $name
        RETURN collect(DISTINCT e.name + ' --' + head(r).type + '--> ' + n.name)
               AS paths
        LIMIT 200
    """.format(hops=hops)
    with driver.session() as session:
        result = session.run(query, name=entity_name).single()
        return [{"path": p} for p in (result["paths"] if result else [])]

检索结果注入 LLM:

def answer_with_local_graph(question: str, driver, llm_call, entity_extractor):
    """局部 GraphRAG 问答:先抽取问题中的实体,再查图谱,最后让 LLM 组织回答。"""
    # 1. 从问题抽取核心实体
    entities = entity_extractor(question)
    if not entities:
        return fallback_to_vector_rag(question)

    # 2. 图谱遍历,收集邻域
    graph_evidence = []
    for ent in entities:
        graph_evidence += local_graph_search(driver, ent["name"])

    # 3. 图谱证据 + 问题 → LLM
    evidence_text = "\n".join(g["path"] for g in graph_evidence[:50])
    return llm_call(
        f"根据以下知识图谱中的事实回答用户问题:\n{evidence_text}\n\n问题:{question}",
        system="回答要基于给定的图谱事实,不要臆造关系。")

4.2 全局检索(Global):社区检测与主题摘要

适用于面向整个知识库的开放问题(“我们的客户主要关注什么风险?")。微软 GraphRAG 的思路:分层社区检测 + 社区摘要。

全局检索流程:
文档 → 实体图 → Leiden 社区检测(分层) → 每层社区生成摘要
→ 全局问题时,检索相关社区摘要 → LLM 综合回答
# global_search.py — 基于社区摘要的全局检索
import pandas as pd

def detect_communities(driver) -> list[dict]:
    """用图算法做社区检测(Neo4j GDS 的 Leiden/Louvain)。"""
    query = """
        CALL gds.leiden.stream({nodeProjection:'*', relationshipProjection:'*'})
        YIELD nodeId, communityId
        RETURN communityId, count(*) AS size
        ORDER BY size DESC
    """
    with driver.session() as session:
        return [dict(r) for r in session.run(query)]

def summarize_community(driver, community_entities, llm_call) -> str:
    """对社区内的实体与关系生成主题摘要,供全局问答检索。"""
    # 取出社区子图,让 LLM 总结主题
    subgraph = extract_subgraph(driver, community_entities)
    return llm_call(
        f"总结以下实体的共同主题与关键关系:\n{subgraph}",
        system="输出 2-3 句话的主题摘要。")

def global_answer(question: str, community_summaries: list[str],
                  llm_call) -> str:
    """基于社区摘要回答全局性问题。"""
    matched = rank_communities(question, community_summaries, embed_fn)
    top_summaries = "\n\n".join(matched[:5])
    return llm_call(
        f"问题:{question}\n\n相关资料摘要:\n{top_summaries}",
        system="综合多份摘要回答,标注信息来自哪些主题。")

4.3 局部 vs 全局的选择策略

问题类型检索策略示例
面向具体实体局部遍历“王强的保单包含哪些免责条款?”
涉及少数关联局部遍历“A 和 B 是什么关系?”
跨全库主题全局社区摘要“客户投诉主要集中在哪些方面?”
统计聚合图谱聚合查询“有多少客户购买了重疾险?”
模糊开放混合(先全局后下钻)“我们的产品线整体布局如何?”

五、混合检索架构:向量 + 图谱 + 全文

5.1 混合检索的三种路由模式

模式 A:先向量后图谱(级联)
  问题 → 向量 Top-K 文档 → 抽取实体 → 图谱遍历补充关系
  │ 适用:文档为主、关系辅助

模式 B:先图谱后向量(级联)
  问题 → 抽取实体 → 图谱定位 → 缺失信息向量补全
  │ 适用:实体明确的领域

模式 C:并行融合(重排)
  问题 → 向量结果 + 图谱结果 → 统一评分器 → 合成排序
  │ 适用:综合场景,质量最好但开销最大

5.2 模式 C 的融合实现

# hybrid_retriever.py — 向量 + 图谱并行检索后融合
def hybrid_retrieve(question: str, vector_store, graph_driver,
                    embed_fn, entity_extractor,
                    alpha: float = 0.6, top_k: int = 10) -> list[dict]:
    """
    alpha 控制向量与图谱的权重配比。
    返回融合排序后的证据列表。
    """
    # 向量侧
    vector_hits = vector_store.search(
        embed_fn(question), top_k=top_k,
        payload=True)   # 每项含 score 0-1

    # 图谱侧:抽取实体后做局部遍历
    graph_hits = []
    entities = entity_extractor(question)
    for ent in entities:
        paths = local_graph_search(graph_driver, ent["name"], hops=2)
        for p in paths:
            graph_hits.append({"content": p["path"],
                               "score": estimate_relevance(p, question)})

    # 融合:归一化分数,按 alpha 加权排序
    merged = []
    for hit in vector_hits:
        merged.append({"content": hit["content"],
                       "score": alpha * hit["score"],
                       "source": "vector"})
    for hit in graph_hits:
        merged.append({"content": hit["content"],
                       "score": (1 - alpha) * hit["score"],
                       "source": "graph"})
    merged.sort(key=lambda x: x["score"], reverse=True)
    return merged[:top_k]

5.3 图谱证据的提示词编排

图谱检索出的路径对 LLM 不友好,需要转换为自然语言描述:

def serialize_graph_evidence(paths: list[dict]) -> str:
    """把图谱路径转成 LLM 可读的事实句。"""
    facts = []
    for p in paths:
        # "王强 --购买--> 意外险A" → "王强购买了意外险A"
        parts = p["path"].split("-->")
        if len(parts) == 2:
            facts.append(f"{parts[0].strip()} {verbalize(parts[1].strip())}")
    return "\n".join(facts)

def verbalize(rel_path: str) -> str:
    """关系动词化(简化实现,生产用映射表)。"""
    return {"--购买-->": "购买了", "--属于-->": "属于",
            "--参与-->": "参与了", "--导致-->": "导致",
            "--位于-->": "位于", "--服务-->": "服务"}

六、GraphRAG 的评估:不止是回答质量

6.1 三层评估体系

层评估对象指标方法
图谱质量构建是否正确三元组准确率、节点/边覆盖率抽样人工核对
检索质量召回是否相关图命中率、路径相关性构造图谱查询评测集
回答质量最终输出忠实度、相关性、多跳正确率RAGAS + 多跳测试集

6.2 多跳推理的专项评测

向量 RAG 评测不覆盖多跳能力,GraphRAG 需要专门的多跳问答集:

# multi_hop_eval.py — 多跳推理评测集
MULTI_HOP_SET = [
    {
        "query": "王强购买的意外险是否覆盖高风险运动?",
        # 需两跳:王强→意外险A→免责条款→高风险运动
        "hops": 3,
        "ground_truth": "不覆盖,高风险运动属于免责范围",
        "expected_path": ["王强", "购买", "意外险A", "免责", "高风险运动"],
    },
    {
        "query": "同时服务 A 公司和 B 公司的供应商有哪些?",
        "hops": 2,
        "ground_truth": "供应商 X",
        "expected_path": ["A公司", "服务", "供应商X", "服务", "B公司"],
    },
]

def eval_multi_hop(graphrag_system, eval_set, judge_fn):
    results = []
    for case in eval_set:
        answer = graphrag_system.answer(case["query"])
        score = judge_fn(case["query"], answer, case["ground_truth"])
        path_correct = verify_path_reconstructed(answer, case["expected_path"])
        results.append({**case, "quality_score": score, "path_correct": path_correct})
    pass_rate = sum(r["path_correct"] for r in results) / len(results)
    return {"pass_rate": pass_rate, "avg_quality": mean(s["quality_score"] for s in results)}

6.3 向量 vs GraphRAG 的消融对比

def ablate_rag_variants(question_set, vector_rag, graphrag, hybrid, judge_fn):
    """对比三种检索方案在评测集上的质量与成本。"""
    results = {}
    for name, system in [("vector", vector_rag), ("graph", graphrag),
                         ("hybrid", hybrid)]:
        scores = [judge_fn(q["query"], system.answer(q["query"]),
                           q["ground_truth"]) for q in question_set]
        results[name] = {
            "avg_quality": mean(s for s in scores),
            "multi_hop_pass": multi_hop_pass_rate(system, MULTI_HOP_SET),
            "avg_cost": system.avg_cost_per_query(),
        }
    return results

# 输出示例:
# vector:  avg_quality 0.78 | multi_hop_pass 0.35 | cost 0.011
# graph:   avg_quality 0.74 | multi_hop_pass 0.62 | cost 0.008
# hybrid:  avg_quality 0.86 | multi_hop_pass 0.71 | cost 0.013

七、工程化:规模、成本与性能

7.1 构建成本控制

图谱构建的 LLM 调用是主要成本。优化手段:

手段效果
文档分批 + 并行抽取缩短构建时间
缓存抽取结果相同文档不重复调用
仅对高价值文档建图分层:核心文档全建图,边缘文档走纯向量
增量更新避免全量重建
def build_with_budget(docs, llm_call, budget_usd, cost_per_doc=0.02):
    """预算内构建:优先处理高价值文档。"""
    affordable = int(budget_usd / cost_per_doc)
    # 按文档价值(点击率、更新频率)排序后截断
    ranked = sorted(docs, key=lambda d: d["priority"], reverse=True)
    selected = ranked[:affordable]
    build_graph(selected, llm_call, driver)
    print(f"本次构建 {len(selected)} 篇文档,剩余 {len(docs)-len(selected)} 篇走纯向量")

7.2 图谱规模对检索延迟的影响

规模节点数局部检索延迟全局检索延迟建议
小< 10K< 5ms< 50ms单机 Neo4j 足够
中10K-100K5-20ms50-200ms加索引 + 结果缓存
大> 100K20-100ms200ms-1s预计算社区摘要 + 只检索 Top 社区
# 缓存热门路径,降低重复遍历
class GraphPathCache:
    def __init__(self, redis):
        self.r = redis
    def get(self, entity: str, hops: int) -> list | None:
        cached = self.r.get(f"graph:{entity}:{hops}")
        return json.loads(cached) if cached else None
    def set(self, entity: str, hops: int, paths: list, ttl=3600):
        self.r.setex(f"graph:{entity}:{hops}", ttl, json.dumps(paths))

7.3 一致性维护:图谱 vs 源文档

图谱可能滞后于文档更新。维护策略:

def sync_check(driver, doc_store, sample_ratio=0.05) -> list[str]:
    """定期抽查:文档中的实体/关系是否在图谱中一致。"""
    inconsistencies = []
    for doc in doc_store.random_sample(sample_ratio):
        fresh = extract_triples(doc["text"], llm_call)
        in_graph = query_triples_in_graph(driver, fresh)
        if not in_graph:
            inconsistencies.append(doc["id"])
    return inconsistencies   # 非空则触发增量重建

八、Neo4j 实战:一个保险理赔知识图谱

8.1 场景建模与 Cypher 查询

// 场景:理赔风控。检查是否存在"客户同时购买互斥险种"
MATCH (c:Customer)-[:购买]->(p1:Policy)-[:属于]->(prod1:Product),
      (c)-[:购买]->(p2:Policy)-[:属于]->(prod2:Product)
WHERE prod1.name = '定期寿险' AND prod2.name = '终身寿险'
RETURN c.name AS customer, p1.id AS p1, p2.id AS p2
// 场景:识别"关联欺诈"——多个客户共享同一联系方式
MATCH (c1:Customer)-[:拥有]->(contact {value:$phone})
MATCH (c2:Customer)-[:拥有]->(contact)
WHERE c1.id <> c2.id
WITH c1, c2, count(contact) AS shared
RETURN c1.name, c2.name, shared
ORDER BY shared DESC LIMIT 20

8.2 Python 驱动集成

# fraud_graph_check.py — 图谱驱动的理赔风控
from neo4j import GraphDatabase

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

    def check_shared_contact(self, customer_id: str) -> list[dict]:
        """检查该客户是否与其他人共享联系方式(欺诈信号)。"""
        query = """
            MATCH (c:Customer {id:$cid})-[:拥有]->(contact)
            MATCH (other:Customer)-[:拥有]->(contact)
            WHERE other.id <> c.id
            RETURN other.name AS name, contact.value AS shared
        """
        with self.driver.session() as s:
            return [dict(r) for r in s.run(query, cid=customer_id)]

    def check_exclusive_policies(self, customer_id: str) -> list[dict]:
        """检查是否持有互斥险种(销售合规问题)。"""
        query = """
            MATCH (c:Customer {id:$cid})-[:购买]->(:Policy)-[:属于]->(p:Product)
            WITH c, collect(p.name) AS products
            WHERE '定期寿险' IN products AND '终身寿险' IN products
            RETURN c.name AS customer, products
        """
        with self.driver.session() as s:
            return [dict(r) for r in s.run(query, cid=customer_id)]

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

8.3 测试图谱查询

import pytest

def test_fraud_check_detects_shared_contact(fixture_graph):
    checker = ClaimFraudChecker(uri="bolt://localhost:7687",
                                user="neo4j", password="test")
    # 构造已知风险数据:王强与李雷共享手机号
    risks = checker.check_shared_contact("customer-wangqiang")
    assert any(r["name"] == "李雷" for r in risks), "应识别共享联系方式"

def test_exclusive_policy_detection(fixture_graph):
    checker = ClaimFraudChecker(...)
    found = checker.check_exclusive_policies("customer-chen")
    assert found, "持有互斥险种应被标记"

九、GraphRAG 与 Agent 记忆的协同

9.1 图谱作为长期语义记忆

将 Agent 记忆中的实体关系沉淀为图谱,让 Agent 拥有结构化的长期记忆:

def memorize_to_graph(conversation, driver, llm_call):
    """从对话中抽取实体关系,写入 Agent 记忆图谱。"""
    triples = extract_triples(
        json.dumps(conversation, ensure_ascii=False), llm_call)
    write_triples(driver, "agent_memory", triples)

def recall_from_graph(question, driver, llm_call):
    """Agent 回答前,先查记忆图谱补充背景。"""
    entities = extract_entities(question, llm_call)
    return local_graph_search(driver, entities[0]["name"], hops=2)

9.2 图谱驱动的规划

Agent 做复杂任务时,可用图谱做任务分解与依赖分析:

def plan_with_graph(task: str, domain_graph, llm_call):
    """利用图谱中的实体依赖关系生成执行计划。"""
    # 例如:任务"发布新功能" → 图谱中前置节点(测试→部署)
    entities = extract_entities(task, llm_call)
    deps = []
    for ent in entities:
        deps += query_predecessors(domain_graph, ent["name"])
    return llm_call(
        f"任务:{task}\n依赖关系:{deps}\n请按依赖顺序给出执行步骤。",
        system="步骤必须满足给定的依赖顺序。")

9.3 架构融合方向

前沿方向:向量-图谱统一存储(如 Memgraph + 向量索引)、图检索与 Agent 工具调用集成(Cypher 作为 Agent 工具)、自动图谱自愈(定期校验并修复缺失关系)。GraphRAG 的价值不在于替代向量检索,而在于为 Agent 与问答系统补上结构化推理这条腿——当问题需要"关系、路径、聚合"时,图谱是唯一的高质量答案来源。

总结:GraphRAG 落地的五个关键决策

决策点选项建议
是否建图全量建图 / 分层建图 / 不建图高价值核心文档建图,边缘文档走纯向量
图谱存储Neo4j / Memgraph / 自研团队熟 Cypher 选 Neo4j,需向量融合看 Memgraph
检索模式局部 / 全局 / 混合实体型问题局部,主题型问题全局,综合场景混合
构建方式离线批量 / 增量生产必须增量 + 一致性校验
评估只评回答 / 三层评估图谱质量 + 检索质量 + 回答质量三层缺一不可

GraphRAG 的本质,是把"藏在文本里的关系"显式化为可查询、可遍历、可推理的图结构,再与向量语义检索融合。它不解决所有问题——语义模糊匹配仍是向量的主场——但它补上了向量检索最薄弱的一环:多跳关联推理。掌握局部检索、全局社区摘要、混合融合三种策略,你就拥有了从"文档问答"进化到"知识推理"的完整路径。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「LLM」更多文章

  1. 模型评估与 LLMOps:从离线评测到生产监控的闭环体系
  2. 上下文工程实战:从上下文窗口到长上下文管理的工程体系
  3. LLM 语义缓存与模型路由:成本治理的两大杠杆