第3篇搭了RAG,第5篇测了Embedding,用了一段时间发现一个问题:RAG回答"点"的问题很好,回答"面"的问题不行。
什么叫"点"和"面"?
点的问题:退货流程是什么? → RAG答得很好
点的问题:订单ORD123到哪了? → RAG答得很好
面的问题:公司各部门的退货处理有什么差异? → RAG答得稀碎
面的问题:整个供应链有哪些关键节点? → RAG答不上来
为什么?因为RAG是按文本相似度 检索的------你问"退货流程",它找到最相关的几段文字拼给你。但"各部门差异"、"供应链全貌"这类问题,答案分散在文档的各个角落,RAG根本找不到、也拼不起来。
GraphRAG就是解决这个问题的------用知识图谱把分散的信息连成网,让AI能看到全局。
我花了3周从普通RAG升级到GraphRAG,踩了4个坑。每个坑都让我重新理解了"知识图谱到底比向量检索强在哪"。
先说结论
| 维度 | 普通RAG | GraphRAG |
|---|---|---|
| 擅长回答 | 具体事实、流程步骤 | 全局概览、关系对比、趋势总结 |
| 检索方式 | 文本相似度(向量) | 图谱遍历 + 向量检索 |
| 信息关联 | 无,每段文本独立 | 实体间有关系,能跨文档关联 |
| 成本 | 低 | 高,构建图谱要调大量LLM |
| 维护 | 更新文档重新切片 | 更新图谱更复杂 |
用Java人的理解:普通RAG ≈ 全文检索(Elasticsearch),关键词匹配找最相关的文档;GraphRAG ≈ 关系型数据库(MySQL),实体之间有外键关联,能做JOIN查询。一个适合"搜",一个适合"查关系"。
一句话:查事实用RAG,查关系用GraphRAG。别什么都上GraphRAG------构建图谱的成本是RAG的5-10倍。
先看全貌:GraphRAG和普通RAG的区别
普通RAG的流程
文档 → 切片 → Embedding → 向量库
↓
用户提问 → Embedding → 相似度检索 → 拼上下文 → LLM回答
问题:每段文本是独立的,段落之间没有关联。问"全局"问题,检索到的片段拼不出完整答案。
GraphRAG的流程
文档 → 切片 → LLM抽取实体和关系 → 知识图谱(节点+边)
↓
用户提问 → 识别问题中的实体 → 图谱遍历找关联 → 拼上下文 → LLM回答
↘ 向量检索补充 ↗
多了两步:从文本中抽取实体和关系 、用图谱遍历替代纯相似度检索。
用Java人的理解:普通RAG是List,只能遍历和匹配;GraphRAG是建了索引的关系表,能JOIN、能GROUP BY、能做关联查询。
坑1:以为GraphRAG很难,其实核心就2步
我的第一反应
看GraphRAG的论文和文档,上来就是实体识别、关系抽取、知识图谱构建、社区检测、全局摘要......我脑子里的画面:
这不就是NLP课程里的命名实体识别 + 关系抽取 + 图数据库吗?
我连Neo4j都没装过,这怎么搞?
实际上GraphRAG核心就2步:
- 从文本中抽实体和关系(调LLM就行,不需要训练NER模型)
- 把实体和关系存成图(用字典就行,不一定要Neo4j)
最简示例:5行代码抽实体
ini
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
llm = ChatOpenAI(model="qwen-plus")
# 提示词:从文本中抽实体和关系
extract_prompt = ChatPromptTemplate.from_messages([
("system", """从以下文本中抽取实体和关系,输出JSON格式。
实体格式:{{"name": "实体名", "type": "类型"}}
关系格式:{{"source": "实体1", "target": "实体2", "relation": "关系"}}
只输出JSON,不要解释。"""),
("human", "{text}"),
])
chain = extract_prompt | llm
text = "张三是销售部的经理,他负责华东区域的客户管理,华东区域是公司最大的市场。"
result = chain.invoke({"text": text})
print(result.content)
输出:
json
{
"entities": [
{"name": "张三", "type": "人物"},
{"name": "销售部", "type": "部门"},
{"name": "华东区域", "type": "区域"}
],
"relations": [
{"source": "张三", "target": "销售部", "relation": "所属"},
{"source": "张三", "target": "华东区域", "relation": "负责"},
{"source": "华东区域", "target": "公司", "relation": "最大市场"}
]
}
用LLM抽实体和关系,不需要训练模型,不需要标注数据。 这和第3篇RAG的思路一样------用现成的大模型能力,别自己造轮子。
把抽取结果存成图
不需要Neo4j,Python字典就够了:
python
from collections import defaultdict
class SimpleGraph:
"""最简单的知识图谱实现"""
def __init__(self):
self.entities = {} # {name: {type, ...}}
self.relations = [] # [{source, target, relation}, ...]
self.neighbors = defaultdict(list) # {entity: [(neighbor, relation), ...]}
def add_entity(self, name, entity_type, **attrs):
self.entities[name] = {"type": entity_type, **attrs}
def add_relation(self, source, target, relation):
self.relations.append({"source": source, "target": target, "relation": relation})
self.neighbors[source].append((target, relation))
self.neighbors[target].append((source, relation)) # 双向
def get_related(self, entity, depth=1):
"""获取与某实体相关的所有实体(BFS遍历)"""
visited = {entity}
result = []
queue = [(entity, 0)]
while queue:
current, d = queue.pop(0)
if d >= depth:
continue
for neighbor, relation in self.neighbors[current]:
if neighbor not in visited:
visited.add(neighbor)
result.append({"entity": neighbor, "relation": relation, "from": current})
queue.append((neighbor, d + 1))
return result
# 使用
graph = SimpleGraph()
graph.add_entity("张三", "人物")
graph.add_entity("销售部", "部门")
graph.add_entity("华东区域", "区域")
graph.add_relation("张三", "销售部", "所属")
graph.add_relation("张三", "华东区域", "负责")
graph.add_relation("华东区域", "公司", "最大市场")
# 查询:和"张三"相关的所有实体
related = graph.get_related("张三", depth=2)
for r in related:
print(f"{r['from']} --[{r['relation']}]--> {r['entity']}")
输出:
css
张三 --[所属]--> 销售部
张三 --[负责]--> 华东区域
华东区域 --[最大市场]--> 公司
depth=2意味着从张三出发,走2步能到的所有实体都找到了。 这就是图谱遍历------普通RAG做不到这个。
用Java人的理解:这和图的BFS/DFS一模一样,数据结构课学的知识图谱真的能用上。
坑2:实体抽取质量差,图谱全是垃圾数据
翻车现场
我把公司20份文档丢进去抽实体,结果:
arduino
实体1:"退货"(类型:动作)
实体2:"退回"(类型:动作)
实体3:"退款"(类型:动作)
实体4:"退换货"(类型:动作)
同一个概念,被抽成了4个不同的实体。 图谱里一堆重复和歧义节点,查询时根本关联不起来。
原因
LLM抽实体没有"去重"概念------它不知道"退货"和"退回货"是同一个东西。每次调用都是独立的,没有全局视角。
解决方案:两步走------先抽实体,再合并同类
python
MERGE_PROMPT = """以下是多段文本中抽取出的实体列表,有些实体实际上是同一个东西。
请合并同义实体,输出合并后的列表。
原始实体列表:
{entities}
输出格式:
- 保留:该实体名(理由)
- 合并:实体A、实体B → 合并为:实体C(理由)
最终输出合并后的实体列表JSON。"""
def merge_entities(raw_entities: list[dict]) -> list[dict]:
"""合并同义实体"""
# 把所有实体拼成文本给LLM判断
entity_text = "\n".join([f"- {e['name']}({e['type']})" for e in raw_entities])
# 实际项目:调LLM判断哪些实体是同一个
# 这里用简单规则演示
merged = {}
synonyms = {
"退货": ["退回", "退回货", "退换货"],
"退款": ["退钱", "返还金额"],
}
for entity in raw_entities:
name = entity["name"]
# 检查是否是某个标准实体的同义词
matched = False
for standard, syns in synonyms.items():
if name in syns or name == standard:
if standard not in merged:
merged[standard] = entity.copy()
merged[standard]["name"] = standard
matched = True
break
if not matched and name not in merged:
merged[name] = entity
return list(merged.values())
# 示例
raw = [
{"name": "退货", "type": "动作"},
{"name": "退回", "type": "动作"},
{"name": "退款", "type": "动作"},
{"name": "退钱", "type": "动作"},
]
print(merge_entities(raw))
# 输出:[{"name": "退货", "type": "动作"}, {"name": "退款", "type": "动作"}]
实体合并是GraphRAG最费精力的步骤。 实际项目中我这样处理:
| 策略 | 说明 | 适用规模 |
|---|---|---|
| LLM合并 | 把所有实体丢给LLM判断同义 | < 100个实体 |
| 规则合并 | 用同义词表+编辑距离 | 100-1000个实体 |
| Embedding聚类 | 先向量化,再DBSCAN聚类 | > 1000个实体 |
用Java人的理解:这就是数据清洗------和ETL里的"去重+标准化"一模一样,脏数据进来,图谱就是垃圾。
坑3:构建图谱太贵,20份文档花了30块API费
翻车现场
我算了下构建图谱的成本:
ini
20份文档 × 平均15个切片 = 300个切片
每个切片调1次LLM抽实体 = 300次LLM调用
每次调用约1000 token = 30万token
qwen-plus价格:¥0.4/百万token输入 + ¥1.2/百万token输出
总成本 ≈ 30万 × (0.4+1.2)/100 = 约 ¥5
看起来不多?但实际是¥30+。 因为:
- 抽实体的Prompt很长(系统提示词+文档内容)
- LLM输出的JSON也长
- 合并实体还要再调一次
- 有些文档要重试
构建图谱的API成本是普通RAG的5-10倍。 普通RAG只需1次Embedding调用,GraphRAG需要N次LLM调用。
省钱策略
| 策略 | 能省多少 | 代价 |
|---|---|---|
| 先用Ollama本地模型抽实体 | 省80%+ | 抽取质量稍差,需人工抽查 |
| 批量处理 | 省30-50% | 需要控制批次大小 |
| 只对关键文档建图谱 | 省50-70% | 非关键文档只能RAG |
| 增量更新 | 省60%+ | 只处理新增/修改的文档 |
我推荐的组合方案:
python
# 用Ollama本地模型抽实体,省钱
from langchain_community.chat_models import ChatOllama
local_llm = ChatOllama(model="qwen2.5:7b") # 本地模型,免费
cloud_llm = ChatOpenAI(model="qwen-plus") # 云端模型,效果好
def smart_extract(text, use_local=True):
"""智能抽取:先本地,失败再用云端"""
if use_local:
try:
result = extract_with(local_llm, text)
# 简单校验:输出是否是合法JSON
if validate(result):
return result
except:
pass
# 本地失败,回退到云端
return extract_with(cloud_llm, text)
用Java人的理解:这和微服务里的降级策略一样------先试本地缓存,失败再查数据库。GraphRAG构建时用本地模型降级,查询时用云端模型保质量。
坑4:GraphRAG查询结果比RAG还差,差点放弃
翻车现场
图谱建好了,兴冲冲地测试:
问:退货流程是什么?
RAG答:退货流程:登录→我的订单→申请退货→填原因→提交,3个工作日审核。
GraphRAG答:退货与订单、审核、退款相关。具体流程请参考相关文档。
GraphRAG的回答反而更差了! 因为图谱遍历返回的是"实体和关系",不是完整的文本内容。AI拿到的上下文是"退货→关联→订单、审核、退款",它只能基于这些关系来概括,看不到具体的流程步骤。
根本原因
GraphRAG和普通RAG不是替代关系,是互补关系。 图谱擅长查关系,向量擅长查内容。我的错误是"只用图谱遍历,丢弃了向量检索"。
正确做法:GraphRAG = 图谱遍历 + 向量检索,两者结合
python
class HybridRAG:
"""混合检索:图谱遍历 + 向量检索"""
def __init__(self, graph, vector_store, llm):
self.graph = graph
self.vector_store = vector_store
self.llm = llm
def query(self, question: str) -> str:
# 1. 识别问题中的关键实体
entities = self._extract_entities_from_question(question)
# 2. 图谱遍历:找相关实体和关系
graph_context = []
for entity in entities:
related = self.graph.get_related(entity, depth=2)
for r in related:
graph_context.append(
f"{r['from']} --[{r['relation']}]--> {r['entity']}"
)
# 3. 向量检索:找相关文本内容
vector_context = self.vector_store.similarity_search(question, k=3)
vector_texts = [doc.page_content for doc in vector_context]
# 4. 合并两种上下文
full_context = f"""## 相关实体与关系(来自知识图谱)
{chr(10).join(graph_context) if graph_context else '无'}
## 相关文档内容(来自向量检索)
{chr(10).join(vector_texts)}
请基于以上信息回答问题:{question}"""
# 5. 调LLM生成回答
response = self.llm.invoke(full_context)
return response.content
def _extract_entities_from_question(self, question: str) -> list[str]:
"""从问题中识别实体名"""
# 简单实现:和图谱中的实体名做匹配
found = []
for entity_name in self.graph.entities:
if entity_name in question:
found.append(entity_name)
return found
两种上下文的分工:
| 上下文来源 | 提供什么 | 适合什么问题 |
|---|---|---|
| 图谱遍历 | 实体间的关系和全局结构 | "XX和YY有什么关系?""整体结构是什么?" |
| 向量检索 | 具体的文本内容和细节 | "XX的流程是什么?""XX的具体步骤?" |
混合查询的效果对比:
diff
问:退货流程是什么?
- 纯RAG:退货流程:登录→我的订单→申请退货...... ✅ 具体步骤
- 纯GraphRAG:退货与订单、审核、退款相关...... ❌ 太笼统
- 混合:退货流程:登录→我的订单→申请退货→填原因→提交,
3个工作日审核。该流程涉及退货审核部门和财务退款部门。 ✅✅ 既有步骤又有关系
问:退货和财务退款有什么关系?
- 纯RAG:找不到相关文档 ❌ 关键词不匹配
- 纯GraphRAG:退货 --[触发]--> 审核 --[通过]--> 退款 ✅ 关系清晰
- 混合:退货申请通过审核后,财务部门在3-5个工作日内处理退款。
退款金额按原支付方式返还。 ✅✅ 有关系有细节
混合方案才是正解。 图谱查关系,向量查内容,两者互补。
完整代码:GraphRAG混合检索系统
python
"""
GraphRAG混合检索系统
依赖:pip install langchain langchain-openai langchain-community chromadb
"""
import json
from collections import defaultdict
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
# ============ 知识图谱 ============
class KnowledgeGraph:
"""简单知识图谱"""
def __init__(self):
self.entities = {}
self.relations = []
self.neighbors = defaultdict(list)
self.entity_texts = {} # 实体对应的原始文本
def add_entity(self, name, entity_type, source_text=""):
self.entities[name] = {"type": entity_type}
if source_text:
self.entity_texts[name] = source_text
def add_relation(self, source, target, relation):
self.relations.append({
"source": source, "target": target, "relation": relation
})
self.neighbors[source].append((target, relation))
self.neighbors[target].append((source, relation))
def get_related(self, entity, depth=2):
"""BFS遍历相关实体"""
visited = {entity}
result = []
queue = [(entity, 0)]
while queue:
current, d = queue.pop(0)
if d >= depth:
continue
for neighbor, relation in self.neighbors[current]:
if neighbor not in visited:
visited.add(neighbor)
result.append({
"entity": neighbor,
"relation": relation,
"from": current,
})
queue.append((neighbor, d + 1))
return result
def get_entity_context(self, entity_name):
"""获取实体的完整上下文:关系 + 原始文本"""
lines = []
# 关系
related = self.get_related(entity_name, depth=2)
for r in related:
lines.append(f"{r['from']} --[{r['relation']}]--> {r['entity']}")
# 原始文本
if entity_name in self.entity_texts:
lines.append(f"\n关于{entity_name}的详细信息:{self.entity_texts[entity_name]}")
return "\n".join(lines)
# ============ 构建图谱 ============
def build_graph_from_documents(documents: list[str], llm) -> KnowledgeGraph:
"""从文档中构建知识图谱"""
graph = KnowledgeGraph()
extract_prompt = ChatPromptTemplate.from_messages([
("system", """从以下文本中抽取实体和关系,输出JSON格式。
实体:{{"name": "名称", "type": "类型"}}
关系:{{"source": "实体1", "target": "实体2", "relation": "关系"}}
只输出JSON,不要解释。如果无法抽取,输出空列表。"""),
("human", "{text}"),
])
chain = extract_prompt | llm
for doc in documents:
try:
result = chain.invoke({"text": doc})
data = json.loads(result.content)
for entity in data.get("entities", []):
graph.add_entity(
entity["name"],
entity.get("type", "未知"),
source_text=doc[:200], # 保留来源文本
)
for relation in data.get("relations", []):
graph.add_relation(
relation["source"],
relation["target"],
relation["relation"],
)
except Exception as e:
print(f"处理文档时出错:{e}")
continue
return graph
# ============ 混合检索 ============
class HybridRAG:
"""GraphRAG + 向量检索 混合系统"""
def __init__(self, graph: KnowledgeGraph, vector_store, llm):
self.graph = graph
self.vector_store = vector_store
self.llm = llm
def query(self, question: str) -> str:
# 1. 识别问题中的实体
entities = self._match_entities(question)
# 2. 图谱上下文
graph_context = []
for entity in entities:
ctx = self.graph.get_entity_context(entity)
if ctx:
graph_context.append(f"【{entity}的关系网络】\n{ctx}")
# 3. 向量上下文
vector_docs = self.vector_store.similarity_search(question, k=3)
vector_context = [doc.page_content for doc in vector_docs]
# 4. 合并
full_context = ""
if graph_context:
full_context += "## 知识图谱信息\n" + "\n\n".join(graph_context) + "\n\n"
if vector_context:
full_context += "## 文档内容\n" + "\n\n".join(vector_context)
# 5. 生成回答
prompt = f"""基于以下信息回答问题。如果图谱信息和文档内容有冲突,以文档内容为准。
{full_context}
问题:{question}"""
response = self.llm.invoke(prompt)
return response.content
def _match_entities(self, question: str) -> list[str]:
"""匹配问题中的实体"""
found = []
for name in self.graph.entities:
if name in question:
found.append(name)
return sorted(found, key=len, reverse=True) # 长的优先匹配
# ============ 使用示例 ============
if __name__ == "__main__":
# 模拟文档
docs = [
"张三是销售部的经理,他负责华东区域的客户管理,华东区域是公司最大的市场。",
"李四是销售部的员工,负责华南区域的客户开发,华南区域今年增长最快。",
"王五是财务部的主管,负责退款审核和财务报表,退款审核需要3-5个工作日。",
"退货流程:用户提交退货申请 → 销售部初审 → 财务部审核退款 → 退款到账,整个流程约7个工作日。",
]
llm = ChatOpenAI(model="qwen-plus")
# 1. 构建知识图谱
print("正在构建知识图谱...")
graph = build_graph_from_documents(docs, llm)
print(f"抽取到 {len(graph.entities)} 个实体,{len(graph.relations)} 条关系")
# 2. 构建向量库(实际项目用Chroma/FAISS)
# vector_store = Chroma.from_texts(docs, OpenAIEmbeddings(), ...)
# 3. 混合检索
# rag = HybridRAG(graph, vector_store, llm)
# 简单测试:直接用图谱查询
print("\n--- 图谱查询测试 ---")
related = graph.get_related("张三", depth=2)
for r in related:
print(f" {r['from']} --[{r['relation']}]--> {r['entity']}")
print("\n--- 全局查询测试 ---")
# 问一个"面"的问题
# result = rag.query("公司各部门的退货处理有什么差异?")
# print(result)
4个坑的总结
| # | 坑 | 错误做法 | 正确做法 | 一句话 |
|---|---|---|---|---|
| 1 | 以为GraphRAG很难 | 被论文吓退 | 核心2步:LLM抽实体+存成图 | 用LLM抽实体,不需要训练模型 |
| 2 | 实体重复歧义 | 直接用原始抽取结果 | 合并同义实体+标准化 | 脏数据进=垃圾图谱出 |
| 3 | 构建成本太高 | 全用云端LLM | 本地模型抽实体+增量更新 | Ollama抽实体省80%+ |
| 4 | 查询效果不如RAG | 只用图谱遍历 | 图谱+向量混合检索 | 图谱查关系,向量查内容 |
什么场景该用GraphRAG
| 场景 | 用普通RAG | 用GraphRAG | 用混合方案 |
|---|---|---|---|
| 查具体事实/流程 | ✅ 足够 | 杀鸡用牛刀 | 可选 |
| 查实体间关系 | ❌ 找不到 | ✅ 图谱遍历 | ✅ |
| 查全局概览/对比 | ❌ 碎片化 | ✅ 全局视角 | ✅ |
| 文档量<50份 | ✅ RAG够用 | 不值得建图谱 | 可选 |
| 文档量>200份 | 检索噪音大 | ✅ 图谱降噪 | ✅ 推荐 |
| 需要实时更新 | ✅ 重新切片 | ❌ 图谱更新复杂 | ✅ 向量部分可快速更新 |
我的真实建议: 先用普通RAG跑起来,当发现"全局性问题答不好"时再升级GraphRAG。别一上来就搞图谱------构建和维护的成本不低。
升级路径: 普通RAG → RAG+重排 → GraphRAG混合检索
你用过GraphRAG吗?和普通RAG对比感受如何?评论区聊聊 👇