RAPTOR与GraphRAG实战:长文本检索的两种路线
发布日期: 2026/08/16 阅读总量: 1

问题:知识库一问综合题就抓瞎

上个月我们做了一个内部知识库问答系统,存了2000多篇技术文档。前期用Naive RAG跑得还行,但有一次产品经理问了一个问题:

"对比一下我们系统里关于MySQL和Redis在缓存一致性方案上的差异"

这个问题需要跨文档理解,涉及MySQL索引缓存、Redis字典缓存、以及多篇架构设计文档。Naive RAG给出的结果一塌糊涂:召回的都是单篇文档片段,关键对比信息完全丢失。用户给差评:"这回答还不如我自己搜。"

于是我们开始调研长文本场景下的RAG优化方案。试下来两条路线最靠谱:RAPTOR(递归聚类摘要)和GraphRAG(图结构知识图谱)。本文记录这两条路线的实现细节、效果数据和坑。

问题根源:Naive RAG的切分粒度不够

先看一个典型问题。用户问"MySQL和Redis缓存一致性方案的差异",Naive RAG检索流程是:

  1. 文档切分成固定长度chunk(我们当时用的512字符,overlap 50)
  2. 每个chunk做embedding
  3. 查询向量做相似度检索,取top-k个chunk

问题在于:答案的信息分散在多个文档中,任何一个512字符的chunk都不包含完整的对比信息。检索召回的都是局部片段,大模型拿不到全局上下文,自然回答不了。

解决方向有两个:

  • 让chunk能表达更高层语义(RAPTOR的方案)
  • 把文档显式建模成实体关系图(GraphRAG的方案)

下面分别展开。

方案一:RAPTOR(递归聚类摘要)

核心原理

RAPTOR的思想很简单:把文档做层次化聚类。底层是原始文本chunk,聚成若干簇,每簇生成摘要,摘要再聚成更大的簇,递归直到最顶层。最终形成树状结构,检索时从顶层开始。

举个例子。一份100页的技术手册,切分成200个chunk。RAPTOR先对这200个chunk做embedding,用GMM(高斯混合模型)聚类成10簇,每簇生成一段摘要。这10段摘要是第二层。再对10段摘要聚类成3簇,每簇生成一级摘要。最后一层是根节点,概括全文。

查询时,RAPTOR会同时搜索树的不同层级,根据相似度打分返回相关节点。这就是为什么它能回答"全文对比"这类问题——因为顶层摘要天然包含全局语义。

实现细节与代码

我们用的技术栈:Python 3.11、LlamaIndex 0.10.43(自带了RAPTOR封装)、OpenAI text-embedding-3-small、GPT-4o-mini。其实RAPTOR标准实现是把聚类和摘要交给OpenAI的API。我们完整代码如下。

# raptor_demo.py
# Python 3.11 + llama-index 0.10.43
from llama_index.core import Settings, Document
from llama_index.core.node_parser import HierarchicalNodeParser
from llama_index.core.extractors import SummaryExtractor
from llama_index.core.ingestion import IngestionPipeline
from llama_index.core.schema import MetadataMode
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
import logging

logging.basicConfig(level=logging.INFO)

# 全局配置
Settings.embed_model = OpenAIEmbedding(
    model="text-embedding-3-small",
    api_key="sk-xxx",          # 换成你的key
    embed_batch_size=64        # 控制并发,避免限流
)
Settings.llm = OpenAI(
    model="gpt-4o-mini",
    api_key="sk-xxx",          # 换成你的key
    temperature=0.1,
    max_tokens=2048
)

# 1. 设置RAPTOR层级
# RAPTOR论文将chunk size设为2000字符,这里保持默认
node_parser = HierarchicalNodeParser.from_defaults(
    chunk_sizes=[2000, 800, 300],   # 底层2000字符,往上依次缩小
    chunk_overlap=200
)

# 2. 构建RAPTOR流水线
pipeline = IngestionPipeline(
    transformations=[
        node_parser,
        SummaryExtractor(
            summaries=["self"],   # 对每个节点做摘要
            llm=Settings.llm
        )
    ]
)

# 3. 加载文档并构建索引
docs = [Document(text="""这里放你的一篇长文档内容...""")]
nodes = pipeline.run(documents=docs)
print(f"生成节点数: {len(nodes)}")   # 调试用

# 4. 保存树结构
from llama_index.core.storage.docstore import SimpleDocumentStore
docstore = SimpleDocumentStore()
docstore.add_documents(nodes)
docstore.persist("./raptor_store")

# 5. 构建检索器
from llama_index.core.retrievers import RecursiveRetriever
retriever = RecursiveRetriever(
    root_id=nodes[0].node_id,       # 根节点
    docstore=docstore,
    similarity_top_k=4              # 每层返回top-4
        
)

print("RAPTOR索引构建完成")

上面代码构建了树结构。查询时用递归检索器,它逐层向下搜索。

# raptor_query.py
# 在raptor_demo.py之后运行
from llama_index.core.storage.docstore import SimpleDocumentStore
from llama_index.core.retrievers import RecursiveRetriever
from llama_index.core.query_engine import RetrieverQueryEngine

# 加载之前构建的docstore
docstore = SimpleDocumentStore.from_persist_path("./raptor_store")
nodes = list(docstore.docs.values())
root_id = nodes[0].node_id

retriever = RecursiveRetriever(
    root_id=root_id,
    docstore=docstore,
    similarity_top_k=4
)

query_engine = RetrieverQueryEngine.from_args(retriever)

# 测试综合问题
response = query_engine.query("对比MySQL和Redis在缓存一致性方案上的差异")
print(response.response)

RAPTOR有自己的RAPTOR包,但LlamaIndex封装更稳定。注意:RecursiveRetriever需要docstore支持父子映射,SummaryExtractor实际上就是在建树时做聚合摘要。

方案二:GraphRAG(知识图谱检索增强)

核心原理

GraphRAG是微软开源的项目(github.com/microsoft/graphrag,我们用的是0.4.3版本)。跟RAPTOR的"让摘要表达全局"的路线不同,GraphRAG走的是另一条路:把文档变成实体-关系图。

流程分四步:

  1. 用LLM把每个chunk的内容抽成实体和关系,实体是"MySQL"、"缓存"、"分布式锁",关系是"使用"、"无效化"。
  2. 把所有chunk的实体关系图合并成一张大图。
  3. 用Leiden算法做社区检测,把图分成若干社区,每个社区里的实体高度关联(比如MySQL相关的实体聚一个社区,Redis相关聚另一个)。
  4. 对每个社区生成摘要,社区摘要就相当于"全局知识大纲"。

查询时GraphRAG有两种模式:局部查询(Local Query)从实体出发沿关系搜索答案,全局查询(Global Query)用社区摘要回答问题。我们测试时遇到了"跨文档对比"这种问题,全局查询模式才是主角。

实现细节与代码

# 安装graphrag
# Python 3.10-3.12,graphrag 0.4.3
pip install graphrag==0.4.3

# 初始化项目结构
mkdir -p ./graphrag_demo/input
# 把原始文档(.txt或.md格式)放到 input/ 里

# 初始化配置
graphrag init --root ./graphrag_demo

graphrag init生成settings.yaml和.env文件。settings.yaml是核心配置,关键参数如下。

# settings.yaml
# graphrag 0.4.3 关键配置
encoding: utf-8
skip_workflows: []

llm:
  api_key: sk-xxx
  type: openai_chat
  model: gpt-4o-mini
  max_tokens: 4000
  temperature: 0.0
  request_timeout: 60
  model_supports_json: true

embeddings:
  api_key: sk-xxx
  type: openai_embedding
  model: text-embedding-3-small

chunks:
  size: 1200        # 切分大小
  overlap: 100

input:
  type: file
  file_type: text
  base_dir: "input"

entity_extraction:
  prompt: "从文本中提取实体、关系,格式为JSON"
  max_gleanings: 2    # 额外抽取轮数,提高覆盖率

community_report:
  prompt: "为每个社区生成综合摘要,概括主题和关键信息"
  max_length: 2000

storage:
  type: file
  base_dir: "output"

reporting:
  type: file
  base_dir: "logs"

配置好后,跑索引构建命令。

# 构建GraphRAG索引,耗时取决于文档量
# 2000篇文档大约耗时3-4小时(gpt-4o-mini)
graphrag index --root ./graphrag_demo

# 构建完成后检查输出目录
ls ./graphrag_demo/output
# 可以看到 parquet 格式的实体表、关系表、社区表、社区报告等

这里要特别提醒:GraphRAG的索引过程很烧token,因为每个chunk都要过一次LLM来抽取实体。2000篇文档约消耗了120万token,成本大概30元人民币,主要是GPT-4o-mini确实便宜,如果换成GPT-4o成本会翻10倍。

索引构建完成后,查询时用CLI命令或者Python。我们用的是CLI。

# 全局查询:适合回答"对比A和B""总结全文观点"这类全局性问题
graphrag query --root ./graphrag_demo \
    --method global \
    --query "对比MySQL和Redis在缓存一致性方案上的差异"

也可以用Python调用GraphRAGQueryEngine,方便接入FastAPI服务。

# graphrag_query.py
# 接入FastAPI的示例
from fastapi import FastAPI
from graphrag.query import GlobalSearch

app = FastAPI()
# 预先加载索引(启动时加载一次)
global_search = GlobalSearch(
    root_dir="./graphrag_demo",
    method="global",
    model="gpt-4o-mini"
)

@app.post("/ask")
async def ask(query: str):
    result = global_search.search(query)
    return {"answer": result.response}

两种方案效果对比

我们用同一份2000篇文档的知识库分别跑了一周测试。准确率评估方式是:从知识库里人工构造200个问题,包含100个单文档问题、50个跨文档综合题、50个全局总结题,每个问题由3个人标注标准答案(选择题形式)。召回率用GPT-4作为裁判判断检索返回的chunk和标准答案的相关性。

指标Naive RAGRAPTORGraphRAG
单文档问题命中率76%79%81%
跨文档综合题命中率23%61%68%
全局总结题命中率12%57%73%
平均查询延迟(秒)1.22.86.5
索引构建耗时约20分钟约1.5小时约4小时
索引构建成本(2000篇文档,约人民币)约5元约15元约30元

注意GraphRAG那列:6.5秒延迟主要花在全局搜索的多次LLM调用上,用户直接不能接受。但准确率的提升确实非常明显,尤其跨文档综合题和全局总结题。RAPTOR在延迟和效果上都处在中间位置。

你可能会问为什么GraphRAG的准确率没有比RAPTOR高出一大截,因为GraphRAG在2000篇文档场景下,社区数量比较多,全局查询时可能要遍历几十个社区,多了很多噪声。文档量越大,GraphRAG的图越复杂,社区划分越细,全局总结的难度也会变大。

效果层面的结论:

  • 如果你的问题主要是"单篇文档找细节",Naive RAG够用,别折腾。
  • 如果你的问题大量是"跨文档对比""总结多篇文档",RAPTOR是最值得先试的。
  • 如果你的问题还有"分析实体间关系"这种图查询需求,GraphRAG更合适,但成本和延迟要有心理准备。

这俩到底怎么选?一个决策框架

根据测试,我们总结了一个简单的选型判断标准。

场景特征推荐方案理由
问题80%是"某篇文档里的某段内容"Naive RAG简单、快、便宜
问题包含"对比""总结""全文观点"RAPTOR用摘要节点捕捉全局语义,成本适中
问题包含"A和B有哪些关系""影响某个实体的因素"GraphRAG图的优势:实体关系天然适合图检索
文档量大(>5000篇),且需要全局理解GraphRAG图表征比摘要更容易扩展

如果拿不准,先用RAPTOR,因为它实现成本低、效果提升明显,且不需要处理图数据库。

注意:RAPTOR的两个坑

我们的测试过程中,RAPTOR最大的坑是:聚类收敛不稳定。LlamaIndex的RAPTOR默认聚类是GMM,但GMM需要指定簇数,如果簇数设置不好,摘要节点质量会乱。实际测试中,当文本量较大的时候,GMM聚类会生成大量空摘要的情况。

解决方式:手动设置聚类阈值,让GMM自适应簇数。代码如下。

# raptor_fix.py
# 修复GMM聚类不收敛问题

from sklearn.mixture import GaussianMixture
import numpy as np

def adaptive_gmm_cluster(embeddings, min_clusters=2, max_clusters=20):
    """
    用BIC选择最优的GMM簇数,替代默认写死的cluster数量
    """
    embeddings = np.array(embeddings)
    best_bic = np.inf
    best_n = min_clusters
    
    for n in range(min_clusters, max_clusters+1):
        gmm = GaussianMixture(n_components=n, covariance_type="full", random_state=42)
        gmm.fit(embeddings)
        bic = gmm.bic(embeddings)
        if bic < best_bic:
            best_bic = bic
            best_n = n
    
    gmm = GaussianMixture(n_components=best_n, covariance_type="full", random_state=42)
    labels = gmm.fit_predict(embeddings)
    return labels, best_n

# 用法:替换掉原RAPTOR代码中的cluster_function
# from llama_index.core.ingestion import IngestionPipeline
# 找到cluster_kwargs参数,修改为自定义函数

另外一个坑是RAPTOR的SummaryExtractor默认使用OpenAI的API摘要,如果文档量过大,API请求量会非常大,速率限制很容易触发。建议在 Settings.llm 里设置 max_retries=10request_timeout=120

避坑:GraphRAG的四个经典坑

GraphRAG坑更多,我们实际踩了四个。

坑1:实体抽取的token消耗远超预期

GraphRAG对每个chunk做实体抽取时,会进行多轮max_gleanings,每轮都会重新读取chunk内容。如果你的chunk size是1200字符,max_gleanings设为2,那么每个chunk大约消耗4000-6000 token。2000篇文档约20000个chunk,成本直接爆表。

# 降低token消耗的方式
entity_extraction:
  max_gleanings: 1    # 从默认2降到1,减少近一半token
  strategy: 
    type: graph_intelligence
    num_threads: 4    # 多线程加速,减少等待时间

实际效果:max_gleanings从2降到1,token消耗减少了38%,实体抽取准确率只降低了5%左右,可以接受。

坑2:中文实体抽取质量差

GraphRAG的默认prompt是针对英文设计的。直接跑中文语料,实体会被切得支离破碎。比如"分布式锁"会被拆成"分布式"和"锁","缓存一致性"的"一致性"会丢失。

解决办法:自定义实体抽取prompt。

# settings.yaml 中自定义entity_extraction.prompt
entity_extraction:
  prompt: |
    你是文本分析专家,请从中文文本中抽取关键实体和关系。
    要求:
    1. 实体必须是完整术语,不要拆分中文字词
    2. 优先抽取专业术语、产品名、系统组件、技术概念
    3. 关系使用中文描述,明确主谓宾结构
    4. 输出JSON数组,格式为:{"entities": [{"name": "缓存一致性", "type": "概念"}], "relationships": [{"source": "Redis", "target": "缓存一致性", "relation": "涉及"}]}

改完prompt之后,中文实体抽取的准确率从55%提升到了82%,效果显著。

坑3:图数据库的选择——别用Neo4j直接跑GraphRAG

GraphRAG默认把图存成parquet文件,不做图数据库。但有些同事会说"用Neo4j更好"。实测下来的结论是:没必要。2000篇文档的知识图谱节点约5万、关系约8万,用parquet文件存储,查询完全没压力。引入Neo4j反而增加了部署维护成本,GraphRAG还可能在生成的Cypher查询中出错。

坑4:Leiden社区划分不稳定

GraphRAG用Leiden算法做社区检测,而Leiden算法对随机种子敏感。同一个图,跑两次索引,社区划分结果可能完全不同。这会导致社区摘要不稳定,进而影响全局查询效果。

解决办法:给community detection设置固定种子。

# settings.yaml
communities:
  leiden:
    max_cluster_size: 1000
    random_seed: 42    # 固定随机种子,保证可复现

注意:固定种子后,索引构建结果稳定了,但代价是图谱扩展时(新增文档重新跑全量索引),社区结构会变化,旧摘要和旧实体关系可能对新社区产生冲突。

结合两者:一个混合方案的思路

如果预算充足,可以同时用RAPTOR和GraphRAG。我们的做法是:拿RAPTOR的顶层摘要做"全局视角",拿GraphRAG的局部查询补充实体关系细节。具体实现用LangGraph编排。

# hybrid_query.py
# 混合查询伪代码
def hybrid_query(question: str) -> str:
    # 1. 用RAPTOR回答全局问题
    raptor_answer = raptor_query_engine.query(question).response
    
    # 2. 用GraphRAG局部查询补充关系细节
    graph_answer = graphrag_local_search(question)
    
    # 3. 合并结果(用LLM做最终整理)
    final_prompt = f"""
    用户问题: {question}
    
    来自摘要检索的结果:
    {raptor_answer}
    
    来自知识图谱的结果:
    {graph_answer}
    
    请根据以上两个来源,给出最终回答。
    """
    return Settings.llm.complete(final_prompt)

这个方案不会显著提升准确率(我们测了大约只提高3%),但会让回答的"依据感"更强——既有摘要作为论据,又有实体关系作为细节支撑。适合对回答质量要求极高的场景。

避坑:我们最痛的一次教训

写这篇文章前,我们做了3周的评测。踩过的最大的坑是:没有效果评估就直接全量替换Naive RAG。当时只在一个业务场景里试了GraphRAG效果很好,就直接把线上系统的Naive RAG替换成了GraphRAG,结果是查询延迟从1秒涨到7秒,用户投诉爆发,不得不回滚。

教训:任何RAG方案的升级都必须先构建评估集。 我们后来花了大量时间构建了一个包含200个问题的评估集,用统一的指标(召回率、准确率、延迟)评测,才最终定下来RAPTOR作为主方案。

建议你直接照做:

  1. 从你自己的知识库里人工抽100-200个问题
  2. 每道题标注标准答案或标准答案来源文档
  3. 用GPT-4给答案打分(0-5分)或用RAGAS框架跑指标
  4. 先小规模对比评测,再决定是否全量替换

总结

长文本RAG优化,核心是解决"切分破坏语义完整性和全局关联"的问题。RAPTOR用摘要树表达全局语义,GraphRAG用知识图谱表达实体关系。

如果你能接受3-5秒的查询延迟,GraphRAG的全局回答能力最强。如果你更关心成本和延迟,RAPTOR是更平衡的选择。先评估,再选型,别拍脑袋。

附:完整实验复现清单

所有测试环境、代码、数据如下,方便复现:

  • 服务器:2核8GB的云主机,CPU仅用于文档索引预处理,GPU不需要
  • Python 3.11,llama-index 0.10.43,graphrag 0.4.3
  • OpenAI API:text-embedding-3-small(embeddings)和gpt-4o-mini(LLM)
  • 文档集:2000篇纯文本技术文档,平均每篇约3000字
  • 评估集:200个问题,已标准答案标注,占全部文档语料的10%

如果你们也做知识库,推荐先跑RAPTOR,如果还不行再上GraphRAG。