RAG系统搭完其实才是工作的开始,实际跑起来你会发现,答案质量参差不齐,有时候精准得吓人、有时候又会非常离谱。这个问题往往不模型本身,而是在检索环节的那些"小细节"。
这篇文章整理了七个在LlamaIndex里实测有效的检索优化点,每个都带代码可以直接使用。

固定长度切分文档是最省事的做法,但问题也很明显:这样经常把一句话从中间劈开,上下文断裂,检索器只能硬着头皮匹配这些残缺的片段。
所以LlamaIndex提供了两个更聪明的解析器。SemanticSplitter会在语义边界处切分,不再机械地按字数来;SentenceWindow则给每个节点附加前后几句话作为上下文窗口。并且这两者还可以组合使用,能达到不错的效果:
# pip install llama-index from llama_index.core import VectorStoreIndex, SimpleDirectoryReader from llama_index.core.node_parser import ( SemanticSplitterNodeParser, SentenceWindowNodeParser ) docs = SimpleDirectoryReader("./knowledge_base").load_data() # Step 1: Semantically aware base chunks semantic_parser = SemanticSplitterNodeParser(buffer_size=1, breakpoint_percentile_threshold=95) semantic_nodes = semantic_parser.get_nodes_from_documents(docs) # Step 2: Add sentence-window context to each node window_parser = SentenceWindowNodeParser(window_size=2, window_metadata_key="window") nodes = window_parser.get_nodes_from_documents(semantic_nodes) index = VectorStoreIndex(nodes)
检索模型打分的对象是单个节点,所以让每个节点包含完整的语义单元,再带上一点其他的附加信息,命中率自然就上去了。
2、BM25 + 向量的混合检索向量嵌入擅长捕捉语义相似性,但碰到专业缩写、产品型号这类精确匹配场景就容易翻车。老牌的BM25算法恰好补上这个短板,它对精确词项敏感,长尾术语的召回能力很强。
把两种检索方式融合起来,LlamaIndex的QueryFusionRetriever可以直接搞定:
from llama_index.core.retrievers import QueryFusionRetriever from llama_index.core import StorageContext from llama_index.core.indices.keyword_table import SimpleKeywordTableIndex # Build both indexes vector_index = index # from above keyword_index = SimpleKeywordTableIndex.from_documents(docs) retriever = QueryFusionRetriever( retrievers=[ vector_index.as_retriever(similarity_top_k=5), keyword_index.as_retriever(similarity_top_k=5) ], num_queries=1, # single query fused across retrievers mode="simple", # RRF-style fusion )
BM25抓精确匹配,向量抓语义关联,RRF融合后的top-k质量通常比单一方法好一截,而且不用写多少额外代码。
3、多查询扩展用户的提问方式千奇百怪,同一个意图可以有很多种表达方法。所以单一query去检索很可能漏掉一些相关但措辞不同的文档。
多查询扩展的思路就是:自动生成几个query的变体,分别检索,再把结果融合起来。
from llama_index.core.retrievers import QueryFusionRetriever multi_query_retriever = QueryFusionRetriever.from_defaults( retriever=vector_index.as_retriever(similarity_top_k=4), num_queries=4, # generate 4 paraphrases mode="reciprocal_rerank", # more robust fusion )
如果业务场景涉及结构化的对比类问题(比如"A和B有什么区别"),还可以考虑query分解:先拆成子问题,分别检索,最后汇总。
不同的表述会激活embedding空间里不同的邻居节点,所以这种融合机制保留了多样性,同时让多个检索器都认可的结果排到前面。
4、reranker初筛拿回来的top-k结果,质量往往是"还行"的水平。如果想再往上提一个档次reranker是个好选择。
和双编码器不同,交叉编码器会把query和passage放在一起过模型,对相关性的判断更精细。但是问题就是慢,不过如果只跑在候选集上延迟勉强还能接受:
from llama_index.postprocessor.cohere_rerank import CohereRerank # or use a local cross-encoder via Hugging Face if preferred reranker = CohereRerank(api_key="COHERE_KEY", top_n=4) # keep the best 4 query_engine = vector_index.as_query_engine( similarity_top_k=12, node_postprocessors=[reranker], ) response = query_engine.query("How does feature X affect Y?")
先用向量检索快速圈出候选(比如top-12),再用交叉编码器精排到top-4。速度和精度之间取得了不错的平衡。
5、元数据过滤与去重不是所有检索回来的段落都值得信任,文档有新有旧,有的是正式发布版本,有的只是草稿。如果语料库里混着不同版本、不同产品线的内容,不加过滤就是给自己挖坑。
元数据过滤能把检索范围限定在特定条件内,去重则避免相似内容重复占用上下文窗口,时间加权可以让新文档获得更高权重:
from llama_index.core.retrievers import VectorIndexRetriever from llama_index.postprocessor import ( SimilarityPostprocessor, DuplicateRemovalPostprocessor ) retriever = VectorIndexRetriever( index=vector_index, similarity_top_k=12, filters={"metadata": {"product": "alpha"}} # simple example ) post = [ DuplicateRemovalPostprocessor(), SimilarityPostprocessor(similarity_cutoff=0.78), ] nodes = retriever.retrieve("Latest install steps for alpha build?") nodes = [p.postprocess_nodes(nodes) for p in post][-1]
过滤器挡住不相关的文档,相似度阈值过滤掉弱匹配,去重保证多样性。这套组合操作下来,检索结果的下限被抬高了。
6、响应合成模式的选择检索只是手段,最终目的是生成靠谱的答案。如果合成阶段没控制好,模型很容易脱离检索内容自由发挥,幻觉就来了。
LlamaIndex的"compact"模式会让模型更紧密地依赖检索节点,减少跑题的概率:
from llama_index.core.response_synthesizers import TreeSummarize, CompactAndRefine # Balanced, citation-friendly option qe = vector_index.as_query_engine( similarity_top_k=8, response_mode="compact", # leans terse & grounded use_async=False, ) ans = qe.query("Summarize the security model, cite sources.") print(ans) # includes source refs by default
严格来说这不算检索优化,但它形成了一个反馈闭环——如果发现答案经常跑偏,可能需要回头调整top-k或者相似度阈值。
7、持续评估没有量化指标,优化就是在黑箱里瞎摸。建议准备一个小型评估集,覆盖核心业务场景的10到50个问题,每次调参后跑一遍,看看忠实度和正确率的变化。
from llama_index.core.evaluation import FaithfulnessEvaluator, CorrectnessEvaluator faith = FaithfulnessEvaluator() # checks grounding in retrieved context corr = CorrectnessEvaluator() # compares to reference answers eval_prompts = [ {"q": "What ports do we open for service Z?", "gold": "Ports 443 and 8443."}, # add 20–50 more spanning your taxonomy ] qe = multi_query_retriever.as_query_engine(response_mode="compact", similarity_top_k=6) scores = [] for item in eval_prompts: res = qe.query(item["q"]) scores.append({ "q": item["q"], "faithful": faith.evaluate(res).score, "correct": corr.evaluate(res, reference=item["gold"]).score }) # Now look at averages, find weak spots, iterate.
当你发现系统在某类问题上总是出错:比如漏掉具体数字、把策略名称搞混等等,就就可以根据问题来进行调整了,比如加大BM25权重?提高相似度阈值?换个更强的reranker?
几个容易踩的坑分块太长会拖累召回率,节点应该保持聚焦,让句子窗口来承担上下文补充的任务。
Rerank不要对全量结果做,应该只在初筛的候选集上。
语料库如果混着多个产品版本,一定要在建索引时就加好version、env、product这些元数据字段,否则检索回来的可能是过时内容。
最后别凭感觉判断效果好不好,维护一个评估用的表格,记录每次调参后的分数变化,时间长了你会发现哪些参数对哪类问题影响最大。
总结RAG的答案质量不靠单一银弹,而是一系列合理配置的叠加。建议先从混合检索和句子窗口两个点入手,观察效果,再逐步加入多查询扩展和reranker。
量化、调整、再量化,循环往复。
https://avoid.overfit.cn/post/507a074851c5480a818e67374aecddd6
作者:Modexa