RAG 系统进化论(六):GraphRAG(基于知识图谱的 RAG),从相似文本走向实体关系

0 阅读16分钟

RAG 系统进化论(六):GraphRAG(基于知识图谱的 RAG),从相似文本走向实体关系

本文导读: 当答案分散在多份文档中,并需要沿着人物、产品、组织或事件关系进行推理时,单纯寻找相似文本往往不够。本文将介绍实体与关系抽取、实体消歧、知识图谱、图遍历、局部与全局检索,以及向量检索与图检索怎样配合;同时梳理 Neo4j、LangChain、LangGraph 等技术在 GraphRAG 中分别承担什么职责。

上一篇介绍了 Corrective RAG(纠错式 RAG)和 Self-RAG(自反思式 RAG)。

系统开始检查证据是否相关、是否充分,并在出错时重新检索或拒绝回答。

但无论怎样重排和纠错,传统 RAG 主要检索的仍是一个个独立文本块:

问题 → 找到相似 Chunk(文本块)→ 根据 Chunk 回答

如果答案分散在多份文档中,而且需要沿着实体关系连接起来,仅靠文本相似度就不够了。

例如,知识库中分别记录着:

供应商资料:
远星电子负责生产 R7 无线接收器。

产品清单:
AX-2048-C 使用 R7 无线接收器。

售后工单:
AX-2048-C 的部分批次出现 E1007 配对故障。

生产记录:
批次 B2407 使用了远星电子 7 月交付的 R7 接收器。

用户问:

哪个供应商可能与 AX-2048-C 的 E1007 故障有关?

没有任何一个文本块直接包含完整答案。

系统需要连接一条路径:

远星电子
  └── 生产 → R7 接收器
                └── 使用于 → AX-2048-C
                               └── 出现 → E1007

这就是 GraphRAG 要解决的问题:

不只查找语义相似的文本,还要理解实体之间怎样连接,并沿关系寻找跨文档证据。

什么是 GraphRAG?

传统向量 RAG 主要保存:

Chunk A:[向量]
Chunk B:[向量]
Chunk C:[向量]

GraphRAG 还会从文本中抽取:

Entity(实体):
远星电子、R7 接收器、AX-2048-C、E1007、批次 B2407

Relationship(关系):
远星电子 —生产→ R7
AX-2048-C —使用→ R7
AX-2048-C —出现→ E1007
批次 B2407 —使用→ R7

再将它们保存为 Knowledge Graph(知识图谱):

Node(节点)表示实体
Edge(边)表示实体之间的关系
Property(属性)保存名称、类型、时间和来源

把这三个元素组合起来,原本分散在文本中的事实就变成了一张可以沿关系查询的网络:

Node(节点)保存实体,Edge(边)表达实体之间的关系,Property(属性)补充实体的详细信息;每条关系仍然可以关联回原始文档中的证据。

图谱并不一定替代原始文档。

更常见的做法是:

图谱负责表达“谁和谁有什么关系”
原始文档负责提供完整语境和可引用证据
向量检索负责从自然语言问题找到可能相关的入口

GraphRAG 的完整过程

GraphRAG 通常分成两个阶段:

离线构建:
文档 → 实体与关系抽取 → 实体消歧 → 图谱 → 社区摘要

在线查询:
问题 → 找到入口实体 → 图遍历 → 取回原文证据 → 生成答案

第一步:抽取实体和关系

Entity Extraction(实体抽取)从文档中识别人、组织、产品、零部件、错误码、地点和事件。

Relationship Extraction(关系抽取)识别实体之间的连接。

例如:

原文:
AX-2048-C 使用远星电子生产的 R7 无线接收器。

实体:
AX-2048-C,类型为 Product(产品)
远星电子,类型为 Supplier(供应商)
R7,类型为 Component(零部件)

关系:
远星电子 —生产→ R7
AX-2048-C —使用→ R7

抽取时还要保存 Provenance(来源信息),也就是这条关系来自哪份文档、哪一段文字。

否则图谱只能给出结论,无法证明结论来自哪里。

async function extractGraphFacts(
  chunk: DocumentChunk,
) {
  // 从文本中识别产品、供应商、零部件、故障等业务实体
  const entities = await extractEntities(chunk.content);

  // 只在已识别实体之间抽取受支持的关系类型
  const relationships = await extractRelationships(
    chunk.content,
    entities,
  );

  // 为每个实体和关系记录原始文档、段落与更新时间
  return attachProvenance(
    { entities, relationships },
    chunk.metadata,
  );
}

关系类型应该预先设计或受到约束。

如果任由模型生成任意关系名称,图谱中可能同时出现:

生产、制造、供应、提供、负责生产

这些词可能描述同一种关系,却无法稳定查询。

第二步:Entity Resolution 合并同一个实体

Entity Resolution(实体解析或实体消歧)判断不同名称是否代表同一个对象。

例如:

星云定制键盘
AX-2048-C
AX2048C
定制版 2048

它们可能都是同一个产品。

如果不合并,图谱会出现多个孤立节点,查询关系时就会遗漏信息。

实体消歧常结合:

  • 业务唯一 ID;
  • 名称规范化;
  • 别名字典;
  • 型号和组织编码;
  • 文本上下文;
  • 人工审核。

确定性 ID 比模型猜测更可靠。只有缺少唯一标识时,才需要使用相似度或模型辅助判断。

async function resolveEntity(
  candidate: ExtractedEntity,
) {
  // 优先通过产品 ID、供应商编码等稳定标识查找现有实体
  const exactMatch = await findByBusinessId(candidate);
  if (exactMatch) return exactMatch;

  // 对名称进行大小写、空格、符号和别名规范化
  const normalizedName = normalizeEntityName(candidate.name);

  // 使用类型和上下文寻找可能相同的候选节点
  const possibleMatches = await findSimilarEntities(
    normalizedName,
    candidate.type,
  );

  // 置信度不足时保留为待审核项,避免错误合并污染整张图
  return decideOrQueueForReview(candidate, possibleMatches);
}

错误合并比重复节点更危险。

如果把两个不同供应商合并成一个,后续所有关系和答案都可能被错误连接。

第三步:构建 Knowledge Graph

完成抽取和消歧后,系统把实体和关系写入知识图谱。

每条边除了起点、终点和关系类型,还应保存:

来源文档
原文片段
生效时间
更新时间
置信度
数据权限

这样,图谱中的一条关系不是没有出处的事实,而是一条可以回查的证据索引。

async function updateKnowledgeGraph(
  documents: Document[],
) {
  // 将文档切成适合抽取事实的文本块
  const chunks = splitForGraphExtraction(documents);

  // 从每个文本块中抽取实体、关系和来源
  const extractedFacts = await Promise.all(
    chunks.map(extractGraphFacts),
  );

  // 合并别名和重复实体,获得稳定的图节点
  const resolvedFacts = await resolveAllEntities(
    extractedFacts.flat(),
  );

  // 增量写入节点和边,并删除已经失效的关系版本
  await upsertGraphFacts(resolvedFacts);
}

第四步:沿图关系进行多跳检索

Graph Traversal(图遍历)从一个或多个入口节点出发,沿关系查找相邻节点。

例如,用户问:

哪个供应商可能与 AX-2048-C 的 E1007 故障有关?

系统可以:

第一跳:AX-2048-C —使用→ R7 接收器
第二跳:R7 接收器 —由…生产→ 远星电子
第三跳:AX-2048-C —出现→ E1007

这类需要跨越多条边才能得到答案的问题称为 Multi-hop Question(多跳问题)。

图遍历不能无限扩散。

跳数越多,候选节点增长越快,也越容易把无关关系加入上下文。因此通常要限制:

  • 允许的关系类型;
  • 最大跳数;
  • 实体类型;
  • 时间范围;
  • 权限范围;
  • 每一跳保留的候选数量。
async function retrieveGraphEvidence(
  question: string,
) {
  // 从问题中识别产品、故障码等入口实体
  const seedEntities = await identifySeedEntities(question);

  // 只沿与问题有关的关系类型执行有限跳数遍历
  const subgraph = await traverseGraph(seedEntities, {
    relations: ["USES", "PRODUCED_BY", "HAS_ERROR"],
    maxHops: 3,
  });

  // 根据图中的来源标识取回原始文档片段
  const sourceChunks = await loadSourceEvidence(subgraph);

  // 返回关系路径和原文证据,而不是只返回节点名称
  return { subgraph, sourceChunks };
}

第五步:Local Search 与 Global Search

GraphRAG 不只适合多跳事实查询,还可以处理全局性问题。

Local Search:围绕具体实体查找

Local Search(局部搜索)从问题中的具体实体出发,查找相邻关系和原文证据。

适合:

AX-2048-C 使用哪个供应商的接收器?
E1007 还影响了哪些产品?
供应商远星电子关联了哪些故障批次?
Global Search:理解整个知识库的主题

Global Search(全局搜索)处理需要总结大量文档的问题:

过去半年主要产品故障集中在哪些类型?
哪些供应链问题影响了最多产品?
售后知识库中有哪些反复出现的风险主题?

直接把整张图和所有文档交给大模型不可行。

常见做法是先使用 Community Detection(社区发现)识别连接紧密的实体群组,再为每个群组生成 Community Summary(社区摘要)。

社区一:无线接收器、配对错误、固件版本
社区二:轴体供应商、按键失灵、特定生产批次
社区三:电池供应商、温度异常、运输条件

全局查询先检索相关社区摘要,再汇总多个社区的结论。

async function globalGraphSearch(
  question: string,
) {
  // 根据问题检索最相关的社区摘要,避免读取整张知识图谱
  const communities = await searchCommunitySummaries(
    question,
    { limit: 8 },
  );

  // 分别根据每个社区的实体、关系和来源形成局部结论
  const partialAnswers = await Promise.all(
    communities.map((community) =>
      summarizeCommunityForQuestion(question, community),
    ),
  );

  // 合并局部结论,并保留每个结论对应的社区和原始来源
  return combineGlobalEvidence(partialAnswers);
}

GraphRAG 解决了什么?

1. 连接跨文档知识

实体和关系把分散在不同文档中的事实连接起来。

2. 支持多跳问题

系统可以沿“产品—零部件—供应商—批次—故障”路径寻找证据,而不是依赖某个文本块直接包含完整答案。

3. 支持全局总结

社区发现和社区摘要让系统能够回答整个知识库的主题、趋势和风险问题。

4. 让推理路径更明确

答案可以展示经过了哪些实体和关系,并回到对应原文,而不只是给出若干相似文本。

向量检索和图检索怎样配合?

GraphRAG 不是“用图数据库替换向量数据库”。

两者适合解决不同问题:

检索方式更擅长
Vector Retrieval(向量检索)根据自然语言找到语义相近的文本或入口实体
Graph Retrieval(图检索)沿明确关系查找邻居和多跳路径
Hybrid Vector-Graph Retrieval(向量与图混合检索)先语义定位,再扩展关系,最后取回原文

一个常见流程是:

用户问题
  ↓
向量检索找到相关 Chunk 和实体
  ↓
从实体进入知识图谱
  ↓
沿允许关系扩展一到三跳
  ↓
取回关系对应的原始文档
  ↓
根据文本证据生成答案
async function answerWithGraphRAG(
  question: string,
) {
  // 使用向量检索寻找语义相关文本和可能的入口实体
  const semanticSeeds = await vectorSearch(question);

  // 将文本中的实体映射到知识图谱中的稳定节点
  const graphSeeds = await linkChunksToGraph(
    semanticSeeds,
  );

  // 沿与问题有关的关系进行受限扩展
  const graphEvidence = await retrieveGraphEvidenceFromSeeds(
    question,
    graphSeeds,
  );

  // 取回原文并与关系路径一起组成可引用上下文
  const evidence = await combineTextAndGraphEvidence(
    semanticSeeds,
    graphEvidence,
  );

  // 根据证据回答,并展示关键关系路径和文档来源
  return generateGraphGroundedAnswer(question, evidence);
}

实现 GraphRAG 需要哪些技术栈?

GraphRAG 不是安装一个组件就能获得的能力。

它通常由文档解析、实体关系抽取、实体消歧、图数据存储、图查询、检索编排和结果追踪等多层技术组成:

技术层负责解决的问题JS / TS 中可以使用的技术
文档解析与切块从 PDF、网页和业务文档中得到可处理的文本块LangChain Document Loaders(文档加载器)、@langchain/textsplitters;复杂 PDF 可以接入 Unstructured 解析服务
实体与关系抽取把自然语言转成结构化的实体和关系大模型的 Structured Output(结构化输出)、LangChain withStructuredOutput()、Zod 数据结构校验
实体消歧合并别名、重复名称和同一业务对象业务唯一 ID、别名字典、Elasticsearch(全文搜索引擎)、向量相似度和人工审核
图谱存储与查询保存节点、边和属性,并执行多跳查询Neo4j 图数据库、neo4j-driver、Cypher(图查询语言)
向量与图混合检索先用语义找到入口,再沿图关系扩展@langchain/neo4j、Neo4j Vector Index(向量索引)和 Full-text Index(全文索引)
图算法发现社区、中心节点和重要关系Neo4j GDS(图数据科学库)、Graphology(JavaScript 图计算库)
工作流编排串联抽取、消歧、入库、检索和失败重试LangGraph 的 StateGraph(状态图工作流)
前端图谱展示在浏览器中交互式展示节点和关系Cytoscape.js、Sigma.js;它们负责可视化,不负责持久化存储
评测与追踪定位抽取错误、查询错误和回答错误LangSmith;当流程开始包含多个节点和模型调用时再接入

可以把这些技术在系统中的位置理解为:

LangChain Document Loaders
        ↓
Text Splitter(文本切分)
        ↓
LLM + Structured Output + Zod
        ↓
Entity Resolution(实体消歧)
        ↓
Neo4j + Cypher
        ↓
向量检索 + 图遍历
        ↓
LangGraph 编排回答流程
        ↓
LangSmith 追踪每一步结果
Elasticsearch、BM25 和 IK 在这里处于什么位置?

它们不是图数据库,也不负责保存实体关系,而是 GraphRAG 的辅助检索层。

  • Elasticsearch(简称 ES)负责建立关键词索引和检索实体候选;
  • BM25 是关键词相关性排序算法,适合匹配产品型号、错误码和专有名词;
  • IK Analyzer(IK 中文分词器)负责把中文问题切成适合检索的词语。

它们可以在进入图谱前帮助系统找到入口实体:

用户问题
  ↓
IK 中文分词
  ↓
Elasticsearch + BM25 找到实体名称、别名或相关文档
  ↓
把结果映射到知识图谱节点
  ↓
使用 Cypher 沿关系继续查询

如果项目已经使用 Elasticsearch,可以直接复用这套能力;如果数据量不大,Neo4j 的全文索引和向量索引已经够用,就不必为了 GraphRAG 单独增加 Elasticsearch。

图数据库和图组件怎样选择?

Neo4j 是学习和实现第一版 GraphRAG 时比较直接的选择。

它使用 Property Graph(属性图)模型保存节点、关系和属性,使用 Cypher 查询关系路径,同时还可以保存向量索引和全文索引。对于 JS / TS 项目,可以使用:

pnpm add @langchain/core @langchain/community @langchain/textsplitters
pnpm add @langchain/langgraph @langchain/neo4j neo4j-driver zod

这里需要特别区分:

  • neo4j-driver 是 Neo4j 官方 JavaScript 驱动,负责执行 Cypher、写入节点和遍历关系;
  • @langchain/neo4j 是 LangChain 的 Neo4j 集成,可以把文档向量和关键词索引接入检索流程;
  • @langchain/langgraph 负责组织“抽取—检查—入库—检索—回答”的状态和分支;
  • Cytoscape.js、Sigma.js 只负责浏览器中的图谱展示,不能代替图数据库。

除了 Neo4j,还可以根据现有基础设施选择其他图数据库:

技术更适合的场景
Memgraph需要低延迟、实时更新,并希望继续使用类似 Cypher 的查询方式
NebulaGraph图数据规模较大,需要分布式存储和遍历
Amazon Neptune系统部署在 AWS,希望使用托管式图数据库
Apache AGE已经大量使用 PostgreSQL,希望在现有数据库中增加图查询能力
RDF Store + SPARQL强调本体、行业标准和语义互操作的知识图谱

第一版不需要同时引入这些数据库。对本系列的 JS / TS 示例来说,选择 Neo4j 就足够完整:

文档处理:LangChain.js
结构化抽取:大模型 + withStructuredOutput() + Zod
图谱存储:Neo4j
图查询:neo4j-driver + Cypher
混合检索:@langchain/neo4j
流程编排:LangGraph
前端展示:Cytoscape.js(可选)
链路追踪:LangSmith(需要监测时接入)
有哪些现成的 GraphRAG 方案?

Microsoft GraphRAG 是较完整的现成实现之一。

它已经覆盖实体与关系抽取、Community Detection(社区发现)、Community Report(社区报告)、Local Search(局部搜索)和 Global Search(全局搜索)等环节,适合用来理解标准 GraphRAG 流程,或者快速验证全局检索效果。

不过,它的完整工具链更偏向 Python。JS / TS 项目不一定要重新实现它的全部内部流程,可以把离线构图作为独立任务或服务,前端和 Node.js 应用只调用构建结果。

另一种方式是使用 Neo4j 作为基础设施,根据自己的业务关系模型搭建 GraphRAG。这种方案没有一次性封装所有步骤,但实体类型、关系规则、数据权限和检索路径更容易按业务定制,也更适合和 LangChain.js、LangGraph 组合。

因此,本系列后面的伪代码默认采用下面这条路线:

LangChain.js 负责文档与模型调用,Neo4j 负责图存储和查询,LangGraph 负责流程编排,LangSmith 在需要监测时负责追踪。

GraphRAG 不适合所有问题

如果用户只问:

E1007 是什么意思?

一次关键词或向量检索就可能得到答案。

为这类问题构建图谱会增加不必要的抽取、存储和维护成本。

GraphRAG 更适合:

  • 答案分散在多份文档;
  • 问题经常涉及实体关系;
  • 需要多跳查找或影响分析;
  • 需要对整个语料进行全局总结;
  • 关系本身具有业务价值。

GraphRAG 还留下了什么问题?

图谱构建成本高

实体抽取、关系抽取、消歧、社区发现和摘要生成都需要额外计算。

抽取错误会沿图传播

一条错误关系可能影响多条查询路径。图谱不能只构建一次,还要持续更新、验证和删除失效关系。

全局摘要可能丢失细节

社区摘要提高了查询效率,也可能压缩掉重要例外,因此最终结论仍应回到原始来源。

文本仍然不是知识的全部

很多企业资料存在于:

  • PDF 页面布局;
  • 扫描件;
  • 数据表格;
  • 产品结构图;
  • 故障截图;
  • 图表和流程图。

如果只抽取纯文本,表格行列关系和图片信息仍然会丢失。

下一篇:Multimodal RAG(多模态 RAG)

下一篇进入 Multimodal RAG。

系统将从只检索文本:

问题 → 文本 Chunk → 答案

扩展到同时处理:

文本 + 表格 + 图片 + 页面布局 + 扫描文档

GraphRAG 让系统理解知识之间的关系。

Multimodal RAG 要解决的是:

当关键知识不只存在于文字中,系统怎样找到并理解表格、图片和完整页面?