用LlamaIndex搭多文档RAG:父子分块加混合检索的踩坑手记
为什么不是朴素RAG朴素RAG(一个chunk一个向量,top-k检索)跑了一周就发现三个问题:长文档被切碎上下文丢失,多个文档之间引用错乱,准确率不到60%。改用父子分块加混合检索后,准确率拉到85%,记录一下踩过的坑。
父子分块怎么搞
核心思路:检索用小块(200字),返回用大块(父块2000字)。LlamaIndex里用SentenceWindowNodeParser或者HierarchicalNodeParser都行。我用的是后者,配AutoMergingRetriever,相邻小块命中超过3个自动合并成父块,上下文不丢。
[*]分块:父块2000字,子块2000字再切200字,父子用关系链关联。
[*]索引:子块建向量索引,父块用docstore存原始文本。
[*]检索:query子块索引top-10,命中的子块去重取父,合并后塞给LLM。
混合检索的坑
只跑向量检索精度不够,加BM25关键词检索,做融合。LlamaIndex有QueryFusionRetriever,把vector加bm25两个检索器结果做RRF融合。权重默认各0.5,但我的文档专业词多,BM25提到0.6准确率高5个点。
坑在Embedding模型。我一开始用OpenAI text-embedding-3,中文文档准确率70%。换成BGE-M3中文专模型后到84%。BGE-M3同时支持稠密加稀疏加ColBERT三种向量,一个模型顶三个检索器,省事。
部署代码骨架
from llama_index.core import VectorStoreIndex, StorageContext, load_index_from_storage
from llama_index.core.node_parser import HierarchicalNodeParser
from llama_index.retrievers import AutoMergingRetriever
from llama_index.core.query_engine import RetrieverQueryEngine
parser = HierarchicalNodeParser.from_defaults(chunk_sizes=)
nodes = parser.get_nodes_from_documents(docs)
auto_retriever = AutoMergingRetriever.from_defaults(retriever, storage_context)
engine = RetrieverQueryEngine.from_args(auto_retriever)
response = engine.query("xxx条款怎么规定的")
最大成本坑是重排。top-10召回塞给LLM太多,加个bge-reranker-v2重排取top-3,token量砍60%,准确率不降反升。reranker跑CPU也行,单次重排200ms,能接受。
https://www.metk.cn/img/posts/llamaindex_rag_chunking.jpg 有个坑提醒一下,接口一变整套流程都得跟着改,最好提前做抽象层。 说实话这个投入产出比确实得看行业,我们做传统制造的,算下来还不如人工。 这个结论我保留意见,至少在我们行业不太成立,可能跟样本有关。 帖子里那个数字挺关键的,不过不同规模的团队差异会很大,不能直接套。 这个成本账算得很实在,我这边也是类似情况,量小的时候真没必要自己折腾。 写得挺到位的,不过我觉得还是要区分"能用"和"好用",中间差着一大截。 用过类似的,前期调优确实费人,但稳定跑了之后省心不少,值得。 我之前也研究过这个方向,最后放弃了,主要是我这边数据量根本不够。 成本这块还可以再拆细一点,隐性的人力投入其实占大头。