GraphRAG:普通RAG只能回答"点"的问题,我踩了4个坑才搞懂

第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步:

  1. 从文本中抽实体和关系(调LLM就行,不需要训练NER模型)
  2. 把实体和关系存成图(用字典就行,不一定要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+。 因为:

  1. 抽实体的Prompt很长(系统提示词+文档内容)
  2. LLM输出的JSON也长
  3. 合并实体还要再调一次
  4. 有些文档要重试

构建图谱的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对比感受如何?评论区聊聊 👇

相关推荐
SimonKing1 小时前
Google第三方授权登录
java·后端·程序员
明月光8181 小时前
从一行 @Builder 说起:重新拾起 Java 的 Lombok、注解与 Builder 模式
java
考虑考虑10 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯11 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
金銀銅鐵12 小时前
[Python] 基于欧几里得算法,实现分数约分计算器
python·数学
Lyn_Li14 小时前
Kaggle Top 5 | 198只股票、200条数据的金融预测——BattleFin高分方案从零复现
python·kaggle·比赛复盘·金融预测
青石路15 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
像我这样帅的人丶你还18 小时前
Java 后端详解(五):Redis 缓存
java·后端·全栈
小九九的爸爸18 小时前
前端想要入门Agent开发,要具备哪些Python基础?
python·agent·ai编程