聊《GraphRAG并不难,难的是知道什么时候不该用》之前,先说一句实在的:别急着背概念,先看它在真实项目里到底解决什么问题。
摘要
GraphRAG把知识图谱和RAG结合,听起来是个合理的升级方案。但我在最近一次企业知识库项目中,GraphRAG的Demo表现完美,联调时却频频翻车——不是模型能力不够,而是权限、日志、可观测性这些"基础设施"没跟上。本文复盘这次项目,从传统RAG瓶颈讲到GraphRAG建模,最后重点讲联调失败的真实排查路径。
---
目录
1. 传统RAG的瓶颈
2. 知识图谱建模
3. 实体关系抽取
4. 图检索增强
5. 评估与优化
6. 总结
---
1. 传统RAG的瓶颈

做过企业知识库的应该都遇到过这个问题:用户问"这个项目的预算审批流程是什么",系统返回的文档片段里确实有相关内容,但信息是分散的——预算金额在财务文档里,审批人在人事制度里,流程节点在OA手册里。传统RAG只能做向量相似度检索,无法跨越文档边界做关联推理。
另一个典型场景是"实体歧义"。用户问"苹果最新发布什么产品",系统可能把水果公司和手机公司混在一起。传统RAG没有实体消歧能力,召回结果经常跑偏。
我之前的项目里,用户反馈最多的就是这两类问题:跨文档信息整合差、实体指代不清。这也是为什么后来团队决定上GraphRAG——用知识图谱的结构化关系来弥补传统RAG的短板。
---
2. 知识图谱建模

GraphRAG的核心在于"图"。但图怎么建,决定后期检索质量的上限。
我们当时用的是Neo4j作为图数据库,Schema设计遵循以下原则:
- 实体类型尽量粗粒度:不要设计成"预算申请单"、"预算审批单"这种细粒度,而是统一为"文档",通过属性区分类型
- 关系类型要稳定:用"包含"、"引用"、"属于"这类通用关系,避免业务词汇
- 属性尽量存原文片段:方便后续溯源
from neo4j import GraphDatabase
class KnowledgeGraph:
def __init__(self, uri, user, password):
self.driver = GraphDatabase.driver(uri, auth=(user, password))
def create_entity(self, name: str, label: str, properties: dict = None):
"""创建实体节点"""
with self.driver.session() as session:
session.execute_write(
self._upsert_node, name, label, properties
)
def create_relationship(self, from_name: str, to_name: str,
rel_type: str, properties: dict = None):
"""创建关系边"""
with self.driver.session() as session:
session.execute_write(
self._upsert_relationship, from_name, to_name,
rel_type, properties
)
def query_graph(self, entity_name: str, depth: int = 2):
"""多跳图查询"""
with self.driver.session() as session:
result = session.run(
"""
MATCH (start {name: $name})
MATCH path = (start)-[*1..$depth]-(related)
RETURN path
""",
name=entity_name, depth=depth
)
return [record["path"] for record in result]
这里有个取舍:深度设为2还是3?2跳能覆盖大部分场景,3跳召回噪音会明显增加。我们最终选了2跳,因为业务文档的关系链通常不会超过2层。
---

3. 实体关系抽取
图谱建好了,关系数据从哪来?我们有两条路径:
路径一:LLM抽取
用Prompt让大模型从文本中提取实体和关系。成本低,适合快速原型。
PROMPT = """
请从以下文本中提取实体和关系,以JSON格式输出:
文本:{text}
输出格式:
{{
"entities": [
{{"name": "实体名", "type": "类型"}}
],
"relationships": [
{{"from": "实体A", "to": "实体B", "type": "关系类型"}}
]
}}
"""
路径二:规则+LLM混合
对结构化文档(如制度文件),先用正则提取关键实体,再用LLM补全关系。这条路径召回更准,但开发成本更高。
我们项目用的是路径二。原因很简单:业务文档的实体类型相对固定(人名、部门、制度名、金额),规则能覆盖70%以上的场景,LLM只做补全,既省钱又可控。
---
4. 图检索增强
图检索的核心思路是:先用图结构做初步召回,再用向量检索做精排。
def graph_rag_query(query: str, k: int = 5):
"""GraphRAG检索流程"""
# Step 1: 从查询中提取实体
entities = extract_entities(query)
# Step 2: 图遍历召回相关节点
graph_contexts = []
for entity in entities:
paths = graph_db.query_graph(entity.name, depth=2)
for path in paths:
graph_contexts.append({
"source": entity.name,
"path": format_path(path),
"type": "graph"
})
# Step 3: 向量检索补充上下文
vector_results = vector_db.similarity_search(query, top_k=k)
# Step 4: 融合排序
final_contexts = rerank(graph_contexts, vector_results, query)
return final_contexts[:k]
图检索的优势在于"跨文档关联"。比如用户问"张三审批过哪些预算",图检索可以直接沿"审批人"关系遍历,召回所有相关文档,而不是靠关键词匹配。
但这里埋了一个隐患——我们当时没有做权限校验。图检索返回了10个文档片段,其中3个属于用户没有访问权限的部门。Demo阶段没人发现这个问题,因为测试账号是管理员权限。
---
5. 评估与优化
GraphRAG的评估不能只看准确率,还要看三个指标:
- 召回率:正确答案是否在候选集中
- 可解释性:用户能否理解为什么返回这些结果
- 权限合规:返回内容是否超出用户权限范围
我们当时用的是简单评测集,200个问答对,手动标注正确答案。GraphRAG在准确率上比传统RAG提升了约15%,这是Demo阶段的主要亮点。
但联调阶段暴露了三个真实问题:
问题一:权限越界
用户A能查到的内容,用户B也能查到。排查路径:
1. 检查图查询是否过滤了权限标签
2. 发现query_graph方法没有传入user_id参数
3. 修复方案:在图查询时加入WHERE doc.permission <= $user_level条件
问题二:日志缺失
用户反馈"查不到结果",但不知道是图检索没召回还是向量检索没匹配。排查路径:
1. 补充每次检索的中间产物日志(实体提取结果、图查询结果、向量召回结果)
2. 发现是实体提取失败,LLM没有从查询中提取出有效实体
3. 修复方案:增加查询预处理,对短查询做意图补全
问题三:性能瓶颈
图遍历在深度为2时,平均响应时间从Demo的200ms上升到联调的1.2s。排查路径:
1. 分析Cypher查询计划,发现全表扫描
2. 为name和label添加索引
3. 响应时间降到400ms,可接受
这三个问题,任何一个单独看都不大,但叠加起来就是"Demo能跑,上线就崩"的典型场景。
---
6. 总结
GraphRAG不是银弹。它的优势在于跨文档关联和实体消歧,劣势在于工程复杂度显著增加。我们在项目中学到的最深刻的一点是:
Demo跑通只证明了算法可行性,联调通过才证明工程可行性。
权限、日志、可观测性这些" boring engineering ",在Demo阶段容易被忽视,但在生产环境里是决定生死的关键。GraphRAG的图再精美,查不出权限也是白搭。
如果你正在考虑上GraphRAG,我的建议是:
1. 先评估你的业务场景是否真的需要跨文档关联
2. 如果只需要文档内检索,传统RAG足够
3. 如果确实需要,提前设计权限模型和日志体系
4. 实体抽取用规则+LLM混合,比纯LLM更可控
工具本身不复杂,难的是知道什么时候不该用它。
总结
本文完成了关键概念、工程实践和落地建议的梳理。
资料展示
下面是我整理的AI大模型学习资料和工具包预览,适合收藏后按主题逐步学习。



如果你想看完整资料目录,可以在评论区留言「资料」;也欢迎告诉我你更关注AI大模型里的哪类内容。

157



被折叠的 条评论
为什么被折叠?



