当 RAG 遇上知识图谱:GraphRAG 原理与 Java 实战

系列文章

本篇是 AI Agent 系列的第 12 篇。前 11 篇我们搭建了 Agent 的核心能力——推理、记忆、工具、规划、协作、评估,今天来聊一个让 RAG 能力再上一个台阶的技术:GraphRAG

  1. 当 RAG 遇上知识图谱:GraphRAG 原理与 Java 实战(本文)
  2. 理解 AI Agent 的大脑:ReAct 模式从入门到实战
  3. AI Agent 的灵魂对话:Prompt Engineering 系统提示词设计的艺术与工程
  4. AI Agent 的规划大脑:从任务分解到自适应执行策略
  5. 让 AI 学会”说人话”——Spring AI 结构化输出实战
  6. AI Agent 的工具箱:深入理解 Tool Use 与 Spring AI Function Calling 实战
  7. AI Agent 的记忆力是怎么实现的——LangChain4j Memory 机制深度解析
  8. MCP 模型上下文协议:AI 的万能接口与 MCP Server 实战
  9. AI Agent 的记忆系统:从 ChatMemory 到持久化记忆的 Java 实战
  10. 从零理解 RAG:检索增强生成完整指南
  11. AI Agent 团队协作:多 Agent 系统架构设计与 Java 实战
  12. AI Agent 评估与优化:从基准测试到生产环境的质量守护实战

引言:传统 RAG 的”阿喀琉斯之踵”

在之前的 RAG 完整指南 中,我们详细拆解了 RAG 的全流程——分块、向量化、召回、精排、生成。那套方案在处理精确查找类问题时表现出色,比如:

  • “Spring Boot 怎么配置数据源?”
  • “Redis 的持久化方式有哪些?”
  • “Netty 的 EventLoop 是什么?”

这类问题有一个共同特点:答案集中在一个或少数几个文档片段中,只要检索命中,LLM 就能准确回答。

但当你问一个完全不同类型的问题时,传统 RAG 就露出了疲态:

  • “这份技术文档的整体架构是什么?”
  • “这些论文中关于 Transformer 的核心观点有哪些共识和分歧?”
  • “这家公司过去三年的战略变化脉络是什么?”
  • “系统中所有涉及用户权限的模块之间是什么关系?”

这类问题叫做全局性查询(Global Query)。它们不是在找某个具体的片段,而是在找分散在整个语料中的关联信息,然后要求模型做综合归纳

传统 RAG 为什么搞不定?原因很直观:向量检索是基于语义相似度的,它擅长找到”跟问题最像的片段”,但不擅长找到”跟问题相关但分散在不同地方的所有片段”。你把一个问题向量化后去匹配,Top-K 返回的片段往往集中在一两个局部,无法覆盖全局信息。

打个比方:传统 RAG 像一个记忆力很好的图书管理员,你问他”第 3 章第 2 节讲了什么”,他翻得又快又准。但你问他”这本书的主旨是什么”,他就懵了——因为他只能一次翻几页,没法把整本书读完再给你总结。

GraphRAG 就是来解决这个问题的。


知识图谱:用”关系”组织世界

在讲 GraphRAG 之前,我们得先理解它依赖的核心数据结构——知识图谱(Knowledge Graph)

什么是知识图谱

知识图谱是一种用图(Graph)来表示知识的方式。它由三种基本元素组成:

  • 实体(Entity):现实世界中的事物,比如”Spring Boot”、”Redis”、”张三”
  • 关系(Relation):实体之间的联系,比如”Spring Boot 依赖 Redis”、”张三 开发了 项目X”
  • 属性(Attribute):实体的特征,比如”Spring Boot 的版本是 3.2”

用图的语言来说,实体是节点(Node),关系是边(Edge)

1
2
3
4
[Spring Boot] --依赖--> [Redis]
[Spring Boot] --依赖--> [MySQL]
[张三] --开发了--> [项目X]
[项目X] --使用了--> [Spring Boot]

这张图看起来简单,但它蕴含的信息密度远超传统文档。当你看到”Spring Boot”这个节点,顺着边走下去,就能发现它和 Redis、MySQL 的依赖关系,谁在用它,用在什么项目里——这种关系链路正是全局性查询所需要的

为什么知识图谱能补 RAG 的短板

传统 RAG 的索引是扁平的:文档被切成块,每块独立向量化,块与块之间的关系丢失了。而知识图谱的索引是结构化的:实体和关系被显式建模,你可以沿着关系链路”走”出一条完整的信息路径。

举个具体例子。假设你的文档库里有 100 篇技术博客,其中 15 篇提到了”Redis”。传统 RAG 在回答”Redis 在我们的技术栈中扮演什么角色”时,可能只检索到最相关的 3-5 篇。但知识图谱可以把 15 篇中提到的所有 Redis 相关实体和关系连成一张网——Redis 用了哪些数据结构、部署在哪些服务中、跟哪些中间件配合使用——全局视图自然浮现


GraphRAG:微软的解法

2024 年,微软研究院发表了一篇论文《From Local to Global: A Graph RAG Approach to Query-Focused Summarization》,提出了 GraphRAG 框架。这个方案的核心思想是:

在索引阶段,用 LLM 从文档中提取实体和关系,构建知识图谱,然后对图做社区检测和层级摘要。在查询阶段,根据问题类型选择局部搜索或全局搜索。

这段话信息量很大,我们拆开来看。

索引阶段:从文本到图谱

GraphRAG 的索引过程分为四步:

Step 1:实体和关系提取

对每个文档块,调用 LLM 执行信息抽取:

1
2
3
4
5
6
请从以下文本中提取所有实体和它们之间的关系。

实体类型:人物、组织、技术、项目、概念
关系类型:使用、开发、依赖、属于、合作

文本:...

LLM 会返回结构化的实体-关系三元组:

1
2
3
4
5
6
7
8
9
10
11
12
{
"entities": [
{"name": "Spring Boot", "type": "技术"},
{"name": "Redis", "type": "技术"},
{"name": "订单服务", "type": "项目"}
],
"relations": [
{"source": "订单服务", "target": "Spring Boot", "type": "使用"},
{"source": "订单服务", "target": "Redis", "type": "使用"},
{"source": "Spring Boot", "target": "Redis", "type": "依赖"}
]
}

关键设计决策:为什么用 LLM 而不是传统的 NER(命名实体识别)工具?

传统 NER 工具(如 spaCy、Stanford NER)擅长识别”人名、地名、组织名”这类通用实体,但在专业领域(技术文档、法律合同、医学文献)中表现很差。LLM 的优势在于零样本泛化——你只需要在 prompt 中定义实体类型,它就能提取,不需要标注数据训练。

当然,代价是成本。每处理一个文档块都要调用一次 LLM API,对于大规模语料来说,索引成本可能非常高。后面我们会讨论如何优化。

Step 2:实体消歧与合并

不同文档块可能用不同的名字指代同一个实体。比如”Spring Boot”、”SpringBoot”、”SB”可能是同一个东西。GraphRAG 用 LLM 做实体消歧:

1
2
3
4
5
6
7
以下是多个实体描述,请判断哪些指的是同一个实体:

1. "Spring Boot" - Java 微框架
2. "SpringBoot" - 快速开发框架
3. "SB 框架" - 团队内部简称

请返回合并后的实体列表。

这一步对最终图谱质量影响巨大。如果消歧做得不好,同一个实体被拆成多个节点,图谱的连通性就会下降,社区检测的效果也会打折扣。

Step 3:构建知识图谱

把提取到的实体作为节点、关系作为边,构建一张图。GraphRAG 的实现中使用的是NetworkX(Python 图计算库),在内存中维护图结构。

1
2
3
4
5
6
import networkx as nx

G = nx.Graph()
G.add_node("Spring Boot", type="技术", description="Java微框架")
G.add_node("Redis", type="技术", description="内存数据库")
G.add_edge("Spring Boot", "Redis", relation="依赖")

Step 4:社区检测与层级摘要

这是 GraphRAG 最核心的创新。

知识图谱建好后,GraphRAG 对图做Leiden 社区检测算法。社区检测的目的是把图中紧密连接的节点分成一组——每组就是一个”社区”,代表一个主题或知识簇。

1
2
3
社区 1: [Spring Boot, Redis, MySQL, MyBatis] → "后端技术栈"
社区 2: [React, Vue, TypeScript, Webpack] → "前端技术栈"
社区 3: [Docker, Kubernetes, Jenkins, GitLab CI] → "DevOps 工具链"

然后,GraphRAG 对每个社区用 LLM 生成摘要

1
2
3
社区 1 摘要:该社区围绕 Java 后端技术栈构建。Spring Boot 作为核心框架,
依赖 Redis 做缓存、MySQL 做持久化、MyBatis 做 ORM。这套组合在团队的
订单服务和用户服务中广泛使用。

更妙的是,Leiden 算法是层级化的——它可以在不同粒度上做社区检测。底层社区是小而精的主题簇,上层社区是更大的知识域。GraphRAG 为每一层都生成摘要,形成层级化的知识索引

1
2
3
Level 0 (最细): [Spring Boot, Redis] → [MySQL, MyBatis] → [React, Vue] → ...
Level 1 (中间): [后端技术栈] → [前端技术栈] → [DevOps工具链] → ...
Level 2 (最粗): [技术体系] → [业务系统] → ...

这个层级结构的意义在于:不同粒度的问题可以命中不同层级的社区摘要


查询阶段:局部搜索 vs 全局搜索

GraphRAG 在查询时提供两种搜索模式,分别应对不同类型的问题。

局部搜索(Local Search)

适合精确查找类问题——跟传统 RAG 类似,但利用了图谱的结构信息。

工作流程:

  1. 实体识别:从用户问题中提取实体(如”Spring Boot”)
  2. 图谱遍历:从该实体节点出发,沿边遍历 1-2 跳,收集关联实体和关系
  3. 文档检索:同时用向量检索找到相关文档块
  4. 上下文组装:把图谱信息(实体、关系、社区摘要)和文档块拼成 prompt
  5. LLM 生成:基于组装好的上下文回答问题

局部搜索的优势在于精确性——它利用图谱的结构关系来补充向量检索可能遗漏的上下文。

全局搜索(Global Search)

适合全局性查询——这是 GraphRAG 真正的杀手锏。

工作流程:

  1. 选择社区层级:根据问题的粒度选择合适的社区层级
  2. 并行摘要:对每个社区的摘要,用 LLM 生成一个中间答案
  3. Map-Reduce 聚合:把所有中间答案汇总,生成最终回答

这个过程本质上是一个 Map-Reduce 模式:

1
2
3
4
5
6
7
Map 阶段:对每个社区摘要独立提问
社区1摘要 → "该社区关于后端技术栈的观点是..."
社区2摘要 → "该社区关于前端技术栈的观点是..."
社区3摘要 → "该社区关于 DevOps 的观点是..."

Reduce 阶段:汇总所有中间答案
"综合来看,技术体系的整体架构是..."

为什么这样设计? 因为 LLM 的上下文窗口是有限的。你不可能把整个文档库的所有内容一次性塞进去。但通过社区摘要的层级结构,你可以把海量信息压缩成可控数量的摘要,再分而治之。

这就解决了文章开头提到的”图书管理员”问题——GraphRAG 不需要一次读完整本书,它只需要读每一章的摘要,然后综合出全书主旨。


与其他 RAG 变体的横向对比

GraphRAG 并不是唯一的 RAG 增强方案。我们来对比几种主流变体:

方案 核心思路 擅长 不擅长
Naive RAG 文档分块 → 向量化 → Top-K 检索 精确查找、FAQ 全局性问题、多跳推理
HyDE 先让 LLM 生成假设性答案,再用答案做检索 问题模糊时提升召回 增加一次 LLM 调用成本
Multi-hop RAG 多轮检索,每轮基于上轮结果扩展 多步推理问题 延迟高、错误累积
GraphRAG 构建知识图谱 + 社区摘要 + Map-Reduce 全局性查询、关系推理 索引成本高、不适合频繁更新
LightRAG 轻量级图谱 RAG,双层索引(实体/关系 + 文档) 成本敏感场景 社区层级摘要能力弱

GraphRAG 的独特价值在于全局搜索能力。其他方案本质上还是在”找片段”,只有 GraphRAG 在索引阶段就把全局信息压缩好了。

GraphRAG 的代价也很明显:

  1. 索引成本高:每个文档块都要调 LLM 做实体提取,一个 1000 篇文档的语料库可能需要数千次 LLM 调用
  2. 更新成本高:新增文档需要重新提取实体、合并到图谱、重新做社区检测
  3. 不适合实时场景:索引构建可能需要数小时,不适合需要即时更新的知识库

什么时候该用 GraphRAG?

  • 你的知识库相对稳定(不是每分钟都在变)
  • 用户经常问全局性、总结性的问题
  • 文档之间有丰富的实体关系
  • 你有预算承担索引阶段的 LLM 调用成本

什么时候不该用?

  • 知识库频繁更新(考虑传统 RAG + 增量索引)
  • 用户主要问精确查找类问题(传统 RAG 就够了)
  • LLM 调用预算有限(考虑 LightRAG 等轻量方案)

Spring Boot + LangChain4j 实战

理论讲够了,来写代码。我们用 LangChain4j 实现一个简化版的 GraphRAG,包含实体提取、图谱构建和局部搜索。

项目依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependencies>
<!-- LangChain4j 核心 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>1.0.0-beta1</version>
</dependency>
<!-- OpenAI 集成 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>1.0.0-beta1</version>
</dependency>
<!-- 嵌入模型 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-embeddings-bge-small-zh-v15</artifactId>
<version>1.0.0-beta1</version>
</dependency>
</dependencies>

实体-关系提取器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Component
public class EntityRelationExtractor {

private final ChatLanguageModel llm;

public EntityRelationExtractor(ChatLanguageModel llm) {
this.llm = llm;
}

/**
* 从文本块中提取实体和关系
* 核心思路:用结构化 prompt 让 LLM 返回 JSON 格式的三元组
*/
public ExtractionResult extract(String text) {
String prompt = """
请从以下文本中提取实体和关系,严格按 JSON 格式返回。

实体类型:人物、组织、技术、项目、概念、产品
关系类型:使用、开发、依赖、属于、包含、合作、竞争

文本:
%s

返回格式:
{
"entities": [
{"name": "实体名", "type": "类型", "description": "一句话描述"}
],
"relations": [
{"source": "源实体", "target": "目标实体", "type": "关系类型"}
]
}
""".formatted(text);

String response = llm.generate(prompt);
return parseResponse(response);
}

private ExtractionResult parseResponse(String json) {
// 解析 JSON,提取实体和关系
// 实际生产中建议用 Jackson 或 Gson 做健壮解析
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.readValue(json, ExtractionResult.class);
} catch (Exception e) {
// LLM 返回的 JSON 可能格式不标准,需要容错处理
String cleaned = json.replaceAll("```json\\s*", "").replaceAll("```", "").trim();
return mapper.readValue(cleaned, ExtractionResult.class);
}
}
}

源码解析:这段代码的核心是 prompt 设计。注意几个细节:

  1. 明确指定了实体类型和关系类型——这比让 LLM 自由发挥效果好得多,减少了歧义和不一致
  2. 要求返回 JSON 格式——配合 LangChain4j 的结构化输出能力(参见 结构化输出实战),可以直接映射到 Java 对象
  3. 容错处理——LLM 返回的 JSON 经常带有 markdown 代码块标记,需要清理

知识图谱存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
@Component
public class KnowledgeGraph {

// 用 ConcurrentHashMap 做内存存储,生产环境建议用 Neo4j
private final Map<String, EntityNode> entities = new ConcurrentHashMap<>();
private final List<RelationEdge> relations = new CopyOnWriteArrayList<>();

/**
* 添加实体,如果已存在则合并描述
*/
public void addEntity(String name, String type, String description) {
entities.merge(name, new EntityNode(name, type, description),
(existing, newNode) -> {
// 实体消歧:合并描述信息
existing.setDescription(existing.getDescription() + "; " + description);
return existing;
});
}

/**
* 添加关系,自动创建缺失的实体节点
*/
public void addRelation(String source, String target, String type) {
// 确保两端实体存在
entities.putIfAbsent(source, new EntityNode(source, "未知", ""));
entities.putIfAbsent(target, new EntityNode(target, "未知", ""));

// 避免重复关系
boolean exists = relations.stream()
.anyMatch(r -> r.getSource().equals(source)
&& r.getTarget().equals(target)
&& r.getType().equals(type));
if (!exists) {
relations.add(new RelationEdge(source, target, type));
}
}

/**
* 局部搜索:从指定实体出发,遍历 N 跳内的关联信息
*/
public SubGraph localSearch(String entityName, int hops) {
Set<String> visited = new HashSet<>();
Set<String> currentLevel = new HashSet<>();
currentLevel.add(entityName);
List<EntityNode> subEntities = new ArrayList<>();
List<RelationEdge> subRelations = new ArrayList<>();

for (int hop = 0; hop < hops; hop++) {
Set<String> nextLevel = new HashSet<>();
for (String name : currentLevel) {
if (visited.contains(name)) continue;
visited.add(name);

EntityNode entity = entities.get(name);
if (entity != null) subEntities.add(entity);

// 找到所有与该实体相关的关系
for (RelationEdge rel : relations) {
if (rel.getSource().equals(name) || rel.getTarget().equals(name)) {
subRelations.add(rel);
nextLevel.add(rel.getSource());
nextLevel.add(rel.getTarget());
}
}
}
currentLevel = nextLevel;
}

return new SubGraph(subEntities, subRelations);
}
}

设计要点

  • merge 操作实现了简单的实体消歧——同名实体的描述会被合并
  • localSearch 的 N 跳遍历是图搜索的经典模式,这里限制在 2 跳以内,避免结果爆炸
  • 生产环境建议用 Neo4j 做图存储,它的 Cypher 查询语言比手写遍历高效得多

全局搜索(简化版 Map-Reduce)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@Component
public class GlobalSearchEngine {

private final ChatLanguageModel llm;
private final KnowledgeGraph knowledgeGraph;

public GlobalSearchEngine(ChatLanguageModel llm, KnowledgeGraph knowledgeGraph) {
this.llm = llm;
this.knowledgeGraph = knowledgeGraph;
}

/**
* 全局搜索:对图谱中所有实体做 Map-Reduce 式摘要
*
* 完整版 GraphRAG 会先做社区检测,这里简化为按实体类型分组
*/
public String globalSearch(String query) {
// Map 阶段:按实体类型分组,每组生成中间答案
Map<String, List<EntityNode>> byType = knowledgeGraph.getAllEntities()
.stream()
.collect(Collectors.groupingBy(EntityNode::getType));

List<String> intermediateAnswers = new ArrayList<>();

for (Map.Entry<String, List<EntityNode>> entry : byType.entrySet()) {
String type = entry.getKey();
List<EntityNode> entities = entry.getValue();

// 拼接该类型所有实体的信息
StringBuilder context = new StringBuilder();
for (EntityNode entity : entities) {
context.append("- ").append(entity.getName())
.append(": ").append(entity.getDescription()).append("\n");

// 加入关系信息
knowledgeGraph.getRelationsFor(entity.getName())
.forEach(rel -> context.append(" 关系: ")
.append(rel.getSource()).append(" → ")
.append(rel.getType()).append(" → ")
.append(rel.getTarget()).append("\n"));
}

String mapPrompt = """
基于以下%s类别的知识信息,回答问题:
%s

知识信息:
%s

请从%s的角度,给出简洁但全面的回答(200字以内)。
""".formatted(type, query, context, type);

String answer = llm.generate(mapPrompt);
intermediateAnswers.add("【" + type + "视角】" + answer);
}

// Reduce 阶段:汇总所有中间答案
String reducePrompt = """
用户问题:%s

以下是不同视角的分析结果,请综合归纳,给出一个完整、有层次的回答:

%s

要求:
1. 提炼各视角的共性结论
2. 指出不同视角之间的关联
3. 给出全局性总结
""".formatted(query, String.join("\n\n", intermediateAnswers));

return llm.generate(reducePrompt);
}
}

与微软 GraphRAG 的差异:原版使用 Leiden 社区检测算法对图做聚类,每个社区对应一个主题簇。这里简化为按实体类型分组,效果不如社区检测精确,但实现复杂度低很多,适合中小规模知识库。如果需要完整版社区检测,可以引入 JGraphT 库的 Louvain 或 Leiden 算法实现。

端到端使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Service
public class GraphRAGService {

private final EntityRelationExtractor extractor;
private final KnowledgeGraph knowledgeGraph;
private final GlobalSearchEngine globalSearch;
private final EmbeddingStore<TextSegment> embeddingStore;

/**
* 索引构建:文档 → 实体提取 → 图谱构建 → 向量化存储
*/
public void buildIndex(List<Document> documents) {
for (Document doc : documents) {
// 1. 分块
List<TextSegment> chunks = new DocumentSplitter()
.split(doc, 500, 50); // 500 字一块,50 字重叠

for (TextSegment chunk : chunks) {
// 2. 实体-关系提取
ExtractionResult result = extractor.extract(chunk.text());

// 3. 写入知识图谱
result.getEntities().forEach(e ->
knowledgeGraph.addEntity(e.getName(), e.getType(), e.getDescription()));
result.getRelations().forEach(r ->
knowledgeGraph.addRelation(r.getSource(), r.getTarget(), r.getType()));

// 4. 同时做向量化存储(用于局部搜索的文档检索)
Embedding embedding = embeddingModel.embed(chunk).content();
embeddingStore.add(embedding, chunk);
}
}
log.info("索引构建完成。实体数:{},关系数:{}",
knowledgeGraph.getEntityCount(), knowledgeGraph.getRelationCount());
}

/**
* 智能查询:根据问题类型自动选择局部或全局搜索
*/
public String query(String question) {
// 简单启发式:包含"整体"、"全局"、"总结"、"概述"等词时用全局搜索
boolean isGlobal = containsGlobalKeywords(question);

if (isGlobal) {
return globalSearch.globalSearch(question);
} else {
// 局部搜索:先识别实体,再图谱遍历 + 向量检索
String entity = extractMainEntity(question);
SubGraph subGraph = knowledgeGraph.localSearch(entity, 2);
List<TextSegment> docs = vectorSearch(question, 5);

String context = buildContext(subGraph, docs);
return llm.generate("基于以下信息回答问题:\n" + context + "\n\n问题:" + question);
}
}
}

生产环境的坑与最佳实践

坑 1:实体提取成本爆炸

假设你的知识库有 1000 篇文档,每篇切成 10 个块,每个块调用一次 LLM 做实体提取。那就是 10000 次 LLM 调用。如果用 GPT-4 级别的模型,每次调用约 0.01-0.03 美元,索引成本就是 100-300 美元。

优化方案

  1. 用小模型做提取:实体提取不需要最强的模型。GPT-3.5 或开源的 Qwen-7B 就够用,成本降 90%+
  2. 批量处理:把多个文本块拼在一起,一次调用提取多个块的实体,减少 API 调用次数
  3. 增量更新:只对新增/修改的文档重新提取,而不是全量重建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 批量实体提取:将多个文本块合并为一次 LLM 调用
*/
public List<ExtractionResult> batchExtract(List<TextSegment> chunks) {
StringBuilder combined = new StringBuilder();
for (int i = 0; i < chunks.size(); i++) {
combined.append("--- 文档块 ").append(i + 1).append(" ---\n");
combined.append(chunks.get(i).text()).append("\n\n");
}

String prompt = """
请分别从以下 %d 个文档块中提取实体和关系。
对每个文档块,返回独立的 JSON 对象。

%s

返回格式:JSON 数组,每个元素对应一个文档块的提取结果。
""".formatted(chunks.size(), combined);

return llm.generate(prompt); // 一次调用,批量提取
}

坑 2:实体消歧不准

同一个实体可能有多种表述:”Spring Boot”、”SpringBoot”、”spring-boot”、”SB 框架”。如果消歧做不好,图谱中会出现大量孤立节点。

最佳实践

  1. 预处理规范化:提取前先做文本规范化——统一大小写、去除特殊字符
  2. 别名词典:维护一份实体别名映射表,硬编码高频别名
  3. 嵌入相似度消歧:对实体名做向量化,计算相似度,超过阈值的视为同一实体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 基于嵌入相似度的实体消歧
*/
public String resolveEntity(String newEntity, double threshold) {
Embedding newEmb = embeddingModel.embed(newEntity).content();

for (String existing : knowledgeGraph.getAllEntityNames()) {
Embedding existingEmb = embeddingModel.embed(existing).content();
double similarity = CosineSimilarity.between(newEmb, existingEmb);

if (similarity > threshold) {
return existing; // 返回已存在的规范名
}
}
return newEntity; // 没找到匹配,作为新实体
}

坑 3:社区检测的粒度选择

GraphRAG 的社区检测是层级化的,但选择哪个层级做全局搜索是个问题。层级太粗,摘要太笼统;层级太细,Map-Reduce 的中间结果太多,成本高且可能丢失全局视角。

经验法则

  • 全局性大问题(”整体架构是什么”)→ 用粗粒度(Level 2+)
  • 中等粒度问题(”后端技术栈的选型思路”)→ 用中等粒度(Level 1)
  • 具体问题(”Redis 在哪些服务中使用”)→ 用局部搜索,不需要社区摘要

坑 4:图谱更新的连锁反应

新增一篇文档,不只是加几个节点那么简单。新实体可能需要和已有实体合并,新关系可能改变社区结构,社区摘要需要重新生成。

生产建议

  • 小规模更新:增量添加实体和关系,定期(如每天凌晨)重新跑社区检测和摘要生成
  • 大规模更新:全量重建图谱。可以用异步任务队列处理,不影响在线查询
  • 版本化图谱:维护图谱的版本快照,更新失败时可以回滚

与 Agent 系统的整合

GraphRAG 不是一个独立系统,它是 Agent 的知识增强层。在我们的 AI Agent 系列 中,Agent 的核心能力是推理和工具调用,但推理的质量取决于它能获取多少高质量上下文。

GraphRAG 可以作为 Agent 的知识工具注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 将 GraphRAG 注册为 Agent 的工具
* 参考:AI Agent 的工具箱(/posts/fd311830.html)
*/
@Tool("搜索知识库中的信息。对于全局性问题使用全局模式,精确查找使用局部模式。")
public String searchKnowledge(
@P("查询内容") String query,
@P("搜索模式:local 或 global") String mode
) {
if ("global".equals(mode)) {
return globalSearchEngine.globalSearch(query);
} else {
return localSearchEngine.localSearch(query);
}
}

这样,Agent 在推理过程中(参考 ReAct 模式)可以自主决定何时使用全局搜索、何时使用局部搜索,甚至可以先用全局搜索了解整体概况,再用局部搜索深挖细节。


技术边界与未来展望

GraphRAG 不是万能的

  1. 不适合非结构化文本的实时场景:如果你的知识库每分钟都在变化(比如聊天记录),GraphRAG 的索引成本太高
  2. 依赖 LLM 质量:实体提取的质量直接取决于 LLM 的能力。如果 LLM 漏提了关键实体,图谱就不完整
  3. 可解释性有限:虽然图谱本身是可解释的,但社区检测和摘要生成是黑盒,你很难追溯”为什么这个社区被归为一组”

未来方向

  1. 多模态 GraphRAG:不只是文本,图片、表格、代码块也可以提取实体和关系
  2. 自适应索引:根据查询频率自动调整索引粒度,高频热点区域做更细的索引
  3. 增量社区检测:避免每次更新都全量重跑社区检测,研究增量图算法
  4. GraphRAG + Agent 联动:Agent 可以主动扩展图谱——当发现知识缺口时,自动触发信息采集和图谱更新

总结

GraphRAG 的核心贡献是把全局性查询从不可能变为可能。传统 RAG 只能在文档片段级别做检索,GraphRAG 通过知识图谱 + 社区摘要 + Map-Reduce,在索引阶段就把全局信息压缩好了,查询时可以直接调用。

但这并不意味着 GraphRAG 要取代传统 RAG。它们是互补关系:

  • 精确查找 → 传统 RAG(快、便宜、简单)
  • 全局性查询 → GraphRAG(慢、贵、但能力独特)
  • 混合方案 → 先用启发式判断问题类型,再路由到合适的检索策略

对于 Java 开发者来说,LangChain4j 已经提供了构建 GraphRAG 所需的基础组件(LLM 调用、嵌入模型、向量存储)。图谱存储可以选择 Neo4j(成熟稳定)或 JGraphT(轻量级内存方案)。社区检测可以用 JGraphT 内置的 Louvain 算法。

如果你的知识库有丰富的实体关系,用户经常问”整体情况”类的问题,GraphRAG 值得投入。如果你的需求主要是精确查找,先把传统 RAG 做好,别过度工程化。


参考资源