GraphRAG 实战——知识图谱增强检索真的有用吗
GraphRAG 实战——知识图谱增强检索真的有用吗
适读人群:对 GraphRAG 感兴趣但不确定是否值得投入的开发者 | 阅读时长:约 16 分钟 | 核心价值:给你一个不无脑吹捧的真实测试结论
去年 Microsoft 发布 GraphRAG 论文的时候,技术群里炸了。各种"普通RAG已死"、"知识图谱才是未来"的声音满天飞。
我当时没急着跟风,因为我见过太多"改变一切"的技术出来后,在工程实践里一地鸡毛。
等我腾出时间,认真在两个真实项目里测试了一个多月,得出的结论比那些推文复杂得多。
GraphRAG 是什么,先说清楚
普通 RAG 的问题:文档被切成了独立的 chunk,chunk 之间的关系丢失了。
举个例子:
文档A(第2章):张工负责系统架构设计,
他设计的微服务方案于2023年上线。
文档B(第5章):该系统目前每日处理订单量超过100万。
文档C(FAQ):系统性能问题应联系架构组负责人。如果用户问:"负责这个百万订单系统的架构师是谁?"
普通 RAG 可能找到文档B(有"百万订单"),但文档B里没有人名。或者找到文档A(有"张工"),但文档A里没有"百万订单"的描述。两个文档的信息没法被自动关联起来。
GraphRAG 的思路是:从文档里提取实体和关系,建成知识图谱,查询时通过图谱的关系路径把分散的信息关联起来。
知识图谱的节点可能是:
- 张工 → 负责 → 系统架构
- 微服务系统 → 处理量 → 每日100万订单
- 性能问题 → 联系 → 架构组负责人
通过图谱推断:张工 = 架构组负责人 = 百万订单系统架构师。
我的测试环境
测试用 Microsoft GraphRAG 开源版本,对比普通 RAG(混合检索 + Re-ranking)。
项目一:企业内部技术文档(适合 GraphRAG 的场景)
- 文档量:约 500 篇,总字数约 80 万字
- 内容:技术架构文档、系统间依赖关系、人员职责说明
- 特点:文档间有大量隐式关联,单篇文档信息不完整
项目二:产品使用手册(不适合 GraphRAG 的场景)
- 文档量:约 200 篇,总字数约 30 万字
- 内容:产品功能说明、操作步骤、FAQ
- 特点:文档相对独立,每篇自成体系
GraphRAG 的构建过程
先把 GraphRAG 的工程实现说一下,很多文章跳过了这部分,让人误以为很简单。
步骤一:实体和关系抽取
这一步需要对每个文档 chunk 调用 LLM,让它提取出实体和关系。这是成本最高的步骤。
Microsoft GraphRAG 用的 Prompt 大概是这样的(简化版):
ENTITY_EXTRACTION_PROMPT = """
给定以下文本,提取所有实体和关系。
实体类型包括但不限于:人物、组织、系统、技术、概念
对于每个实体,返回:
- 实体名称
- 实体类型
- 描述(来自原文)
对于每个关系,返回:
- 源实体
- 目标实体
- 关系描述
- 权重(1-10,代表关系的重要程度)
文本内容:
{text}
以JSON格式返回结果。
"""
import json
from openai import OpenAI
client = OpenAI()
def extract_entities_and_relations(text: str) -> dict:
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "你是一个信息抽取专家,专注于准确提取实体和关系。"},
{"role": "user", "content": ENTITY_EXTRACTION_PROMPT.format(text=text)}
],
response_format={"type": "json_object"},
temperature=0
)
return json.loads(response.choices[0].message.content)步骤二:构建图谱
import networkx as nx
from collections import defaultdict
class KnowledgeGraph:
def __init__(self):
self.graph = nx.DiGraph()
self.entity_descriptions = defaultdict(list)
def add_extraction_result(self, extraction: dict, source_doc_id: str):
entities = extraction.get("entities", [])
relations = extraction.get("relations", [])
for entity in entities:
name = entity["name"]
entity_type = entity.get("type", "UNKNOWN")
desc = entity.get("description", "")
if not self.graph.has_node(name):
self.graph.add_node(name, type=entity_type, sources=[])
self.graph.nodes[name]["sources"].append(source_doc_id)
self.entity_descriptions[name].append(desc)
for relation in relations:
src = relation["source"]
tgt = relation["target"]
rel_desc = relation.get("description", "")
weight = relation.get("weight", 5)
if self.graph.has_edge(src, tgt):
self.graph[src][tgt]["weight"] += weight
self.graph[src][tgt]["descriptions"].append(rel_desc)
else:
self.graph.add_edge(src, tgt,
weight=weight,
descriptions=[rel_desc],
sources=[source_doc_id])
def get_neighbors(self, entity: str, depth: int = 2) -> set:
"""获取实体的 depth 跳邻居"""
if not self.graph.has_node(entity):
return set()
neighbors = set()
frontier = {entity}
for _ in range(depth):
new_frontier = set()
for node in frontier:
# 双向邻居
new_frontier.update(self.graph.successors(node))
new_frontier.update(self.graph.predecessors(node))
new_frontier -= neighbors # 去掉已访问的
new_frontier.discard(entity)
neighbors.update(new_frontier)
frontier = new_frontier
return neighbors
def get_paths(self, entity1: str, entity2: str, max_length: int = 3) -> list:
"""找两个实体之间的路径"""
try:
paths = list(nx.all_simple_paths(
self.graph.to_undirected(),
entity1, entity2,
cutoff=max_length
))
return paths
except nx.NodeNotFound:
return []步骤三:图谱增强的检索
def graph_enhanced_retrieve(
query: str,
kg: KnowledgeGraph,
vector_retriever,
top_k: int = 5
) -> list:
# 1. 从query中识别实体
entities_in_query = extract_entities_from_query(query)
# 2. 在图谱中找这些实体的邻居
related_entities = set()
for entity in entities_in_query:
neighbors = kg.get_neighbors(entity, depth=2)
related_entities.update(neighbors)
# 3. 构建增强查询(原始查询 + 相关实体描述)
if related_entities:
entity_context = "相关实体:" + "、".join(list(related_entities)[:10])
enhanced_query = query + "\n" + entity_context
else:
enhanced_query = query
# 4. 用增强查询做向量检索
results = vector_retriever.search(enhanced_query, top_k=top_k)
# 5. 找到实体关联的源文档,补充进结果
for entity in entities_in_query | related_entities:
if kg.graph.has_node(entity):
source_docs = kg.graph.nodes[entity].get("sources", [])
# 把这些文档加入候选(如果还没在结果里)
# ... 实际实现需要根据你的文档存储方式来
return results真实测试结果
项目一(技术文档,有复杂关联关系):
| 方案 | 多跳推理问题(30题) | 单文档直接问题(70题) |
|---|---|---|
| 普通混合 RAG | 43% | 78% |
| GraphRAG(Local模式) | 61% | 71% |
| GraphRAG(Global模式) | 57% | 62% |
多跳推理问题:GraphRAG 明显好,提升了 18 个百分点。 单文档直接问题:GraphRAG 反而差了,因为它引入了额外的噪声。
项目二(产品手册,文档独立):
| 方案 | 总体准确率 |
|---|---|
| 普通混合 RAG | 74% |
| GraphRAG | 68% |
产品手册场景,GraphRAG 全面输给了普通 RAG。
成本是真正的拦路虎
处理 80 万字的技术文档,GraphRAG 的构建成本:
- 实体抽取(GPT-4o):约 $45
- 社区摘要生成:约 $12
- 总计:约 $57
对比普通 RAG 的构建成本(只需要 Embedding):
- text-embedding-3-small:约 $0.08
GraphRAG 的构建成本是普通 RAG 的 700 倍。
更重要的是,文档更新时要重建图谱,每次更新都有成本。
我的结论
GraphRAG 不是"更好的RAG",它是一个针对特定场景的专用工具。
用 GraphRAG 的理由:
- 你的文档集有大量跨文档的实体关联关系
- 用户经常问需要多跳推理的问题("A 和 B 之间什么关系"、"负责 X 的人是谁")
- 你的文档集相对稳定,不需要频繁重建
- 有足够的预算承担构建成本
不用 GraphRAG 的理由:
- 文档是相对独立的(FAQ、手册、规范)
- 用户问题主要是直接查询,不需要关系推理
- 文档更新频繁(成本受不了)
- 预算有限
对于大多数企业知识库项目,我的建议是先用普通混合 RAG,把基础做好,再看是否有必要上 GraphRAG。
贸然上 GraphRAG 的代价是:70 倍的成本,两倍的工程复杂度,却在大多数问题上效果差不多甚至更差。
