问题:知识库一问综合题就抓瞎
上个月我们做了一个内部知识库问答系统,存了2000多篇技术文档。前期用Naive RAG跑得还行,但有一次产品经理问了一个问题:
"对比一下我们系统里关于MySQL和Redis在缓存一致性方案上的差异"
这个问题需要跨文档理解,涉及MySQL索引缓存、Redis字典缓存、以及多篇架构设计文档。Naive RAG给出的结果一塌糊涂:召回的都是单篇文档片段,关键对比信息完全丢失。用户给差评:"这回答还不如我自己搜。"
于是我们开始调研长文本场景下的RAG优化方案。试下来两条路线最靠谱:RAPTOR(递归聚类摘要)和GraphRAG(图结构知识图谱)。本文记录这两条路线的实现细节、效果数据和坑。
问题根源:Naive RAG的切分粒度不够
先看一个典型问题。用户问"MySQL和Redis缓存一致性方案的差异",Naive RAG检索流程是:
- 文档切分成固定长度chunk(我们当时用的512字符,overlap 50)
- 每个chunk做embedding
- 查询向量做相似度检索,取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走的是另一条路:把文档变成实体-关系图。
流程分四步:
- 用LLM把每个chunk的内容抽成实体和关系,实体是"MySQL"、"缓存"、"分布式锁",关系是"使用"、"无效化"。
- 把所有chunk的实体关系图合并成一张大图。
- 用Leiden算法做社区检测,把图分成若干社区,每个社区里的实体高度关联(比如MySQL相关的实体聚一个社区,Redis相关聚另一个)。
- 对每个社区生成摘要,社区摘要就相当于"全局知识大纲"。
查询时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 RAG | RAPTOR | GraphRAG |
|---|---|---|---|
| 单文档问题命中率 | 76% | 79% | 81% |
| 跨文档综合题命中率 | 23% | 61% | 68% |
| 全局总结题命中率 | 12% | 57% | 73% |
| 平均查询延迟(秒) | 1.2 | 2.8 | 6.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=10 和 request_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作为主方案。
建议你直接照做:
- 从你自己的知识库里人工抽100-200个问题
- 每道题标注标准答案或标准答案来源文档
- 用GPT-4给答案打分(0-5分)或用RAGAS框架跑指标
- 先小规模对比评测,再决定是否全量替换
总结
长文本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。