前言你的 RAG 应用是否经常答非所问明明文档里就有答案模型却总是“自由发挥”这些问题往往不出在大模型身上而是出在检索这一环。基础的向量检索虽然好用但面对真实、复杂的业务场景就像穿了“新手村装备”——能打怪但打不了 BOSS。本文将从检索策略升级和重排序优化两个核心维度带你系统性地提升RAG系统性能。一、RAG系统的核心痛点分析一个能用的RAG距离一个好用的生产级系统通常存在以下问题检索不准简单的向量相似度搜索常常找不到最关键的信息信息丢失文本块被切得太碎关键定义断成两截答案幻觉模型拿到正确资料却依然自由发挥问题通常出在三个环节文档切片太粗暴按固定字数切块把表格切烂、把代码切断只用向量检索对专有名词、ID、日期的语义召回极差没有重排序Top-K向量召回的结果里真正相关的段落可能排在第8位二、检索策略升级从单通道到多路召回2.1 向量检索的局限性原始的单向量召回代码如下defretrieve_docs(query,k3):检索最相关的k个段落query_vecmodel.encode([query])D,Iindex.search(query_vec,kk)return[paragraphs[i]foriinI[0]]这种方式能理解语义能找到与query含义相近的段落但容易忽略语义不相近、但有明显关键词重合的文档。例如用户输入单向量召回的问题“2024年湖北省竞赛奖金”文中是省部级奖励金额向量可能找不到“奖助学金政策”向量找奖学金段落但助学金被忽略“获奖后的政策支持”多个段落提到奖项但真正写支持的被漏掉2.2 混合检索Hybrid Search双剑合璧解决方案是把关键词检索BM25和向量检索结合起来实现11 2的效果。fromlangchain.retrieversimportBM25Retriever,EnsembleRetrieverfromlangchain_community.vectorstoresimportFAISSfromlangchain_openaiimportOpenAIEmbeddings# 准备文档docs[华为云ModelArts是面向AI开发者的平台。,昇思MindSpore是一个全场景AI框架。,ModelArts Pro是企业级AI应用开发套件。]# 1. 向量检索器embeddingsOpenAIEmbeddings()vector_storeFAISS.from_texts(docs,embeddings)vector_retrievervector_store.as_retriever(search_kwargs{k:2})# 2. BM25关键词检索器bm25_retrieverBM25Retriever.from_texts(docs)bm25_retriever.k2# 3. 集成检索器权重可调ensemble_retrieverEnsembleRetriever(retrievers[bm25_retriever,vector_retriever],weights[0.5,0.5]# BM25和向量各占一半权重)# 测试queryModelArts平台是做什么的retrieved_docsensemble_retriever.invoke(query)print(retrieved_docs)混合检索的核心价值在于BM25确保包含关键词的文档被高优先级召回向量检索则补充那些语义相关但可能没有直接出现关键词的文档。三、重排序Rerank从海选到决赛3.1 为什么需要Rerank混合检索解决了召回的广度但不够精度。返回的5个文档里也许只有2个是真正高度相关的如果这2个排在后面就会影响LLM最终生成答案的质量。Rerank模型就像一位专业质检员初始检索如FAISS像海选快速选出100位可能符合条件的Rerank模型像专家评审仔细面试这100位挑出最顶尖的5位LLM像终极BOSS基于最顶尖的5位的信息做出最终决策3.2 Embedding模型 vs Rerank模型特性Embedding模型Rerank模型输入单段文本(Query, Document)对输出一个高维向量一个相关性分数计算方式双编码器独立编码交叉编码器深度交互速度非常快相对慢精度良好非常高用途从海量数据中快速召回对候选集精细重排序3.3 Rerank实战代码fromsentence_transformers.cross_encoderimportCrossEncoderfromlangchain.retrievers.document_compressorsimportCrossEncoderRerankerfromlangchain.retrieversimportContextualCompressionRetriever# 1. 初始化Rerank模型推荐BGE-Reranker中英双语效果好cross_encoder_modelCrossEncoder(BAAI/bge-reranker-large)# 2. 包装成LangChain组件compressorCrossEncoderReranker(modelcross_encoder_model,top_n3# 精排后只取前3名)# 3. 创建精排检索器先用混合检索召回再精排compression_retrieverContextualCompressionRetriever(base_compressorcompressor,base_retrieverensemble_retriever# 使用前面创建的混合检索器)# 4. 执行检索重排序query介绍一下ModelArts Pro套件reranked_docscompression_retriever.invoke(query)print(reranked_docs)3.4 两阶段检索完整流程importnumpyasnpimportfaissclassHybridRetrieverWithRerank:def__init__(self,embedding_model,paragraphs,bm25,rerank_model):self.embedding_modelembedding_model self.paragraphsparagraphs self.bm25bm25 self.rerank_modelrerank_model# 构建FAISS索引self.embeddingsembedding_model.encode(paragraphs)self.indexfaiss.IndexFlatL2(self.embeddings.shape[1])self.index.add(self.embeddings)defretrieve(self,query,k5,bm25_k10):# Step 1: 向量召回query_vecself.embedding_model.encode([query])D,Iself.index.search(query_vec,kk)vec_topk_idxI[0].tolist()# Step 2: BM25关键词召回bm25_scoresself.bm25.get_scores(query.split())bm25_topk_idxnp.argsort(bm25_scores)[::-1][:bm25_k].tolist()# Step 3: 合并候选集去重candidate_idxslist(set(bm25_topk_idxvec_topk_idx))candidate_paragraphs[self.paragraphs[i]foriincandidate_idxs]# Step 4: Rerank精排pairs[[query,para]forparaincandidate_paragraphs]rerank_scoresself.rerank_model.predict(pairs)# Step 5: 按分数排序取Top-Ksorted_pairssorted(zip(candidate_paragraphs,rerank_scores),keylambdax:-x[1])return[paraforpara,_insorted_pairs[:k]]四、查询转换Query Transformation让LLM先翻译用户问题在多轮对话中用户经常会问它怎么样“或具体说说第二个”这种依赖上下文的查询如果直接扔给检索系统效果必然很差。解决方案在检索之前先让LLM把用户的口语化查询改写成一个独立的、信息完整的查询语句。fromlangchain_core.promptsimportChatPromptTemplatefromlangchain_openaiimportChatOpenAI# 定义翻译官指令rewrite_promptChatPromptTemplate.from_messages([(system,你是一个精通信息检索的助手。请根据对话历史将用户的最新问题改写成一个独立的、对检索系统更友好的问题。),(user,对话历史:\n{chat_history}\n\n最新问题: {question})])modelChatOpenAI()rewriterrewrite_prompt|model# 模拟对话场景chat_history用户: 给我介绍下Text2SQL技术。\nAI: Text2SQL能将自然语言转化为SQL查询语句。question它主要用在哪些场景rewritten_questionrewriter.invoke({chat_history:chat_history,question:question})print(f原始:{question})print(f改写后:{rewritten_question.content})# 输出: Text2SQL技术主要应用在哪些业务场景除了基础改写还有更高级的策略HyDE假设性文档嵌入让LLM根据用户问题先生成一个假设性答案再用这个答案的向量去检索效果往往优于原始问题多查询Multi-Query将一个复杂问题分解成多个子问题分别检索汇总结果五、向量存储优化别让Embedding成为性能瓶颈5.1 向量缓存很多人只关注LLM的Token消耗其实Embedding的调用量往往是LLM的10倍以上每次入库、更新都要重新编码。importpickle# 保存向量到本地withopen(docs.pkl,wb)asf:pickle.dump({paragraphs:paragraphs,embeddings:doc_embeddings},f)# 加载withopen(docs.pkl,rb)asf:cachepickle.load(f)paragraphscache[paragraphs]doc_embeddingscache[embeddings]indexfaiss.IndexFlatL2(dimension)index.add(doc_embeddings)5.2 向量压缩适用于百万级数据当保存的向量超过几千上万条内存和查询速度都会成为问题需要引入压缩策略算法核心思想适用场景PQ乘积量化将向量拆分为子向量用8bit表示每块超大数据量压缩存储IVF倒排文件建立聚类中心只搜索最近的几个中心百万级向量库加速检索HNSW建图搜索多层邻居结构加速实时性要求高、近似搜索# PQ压缩示例d384# 向量维度indexfaiss.IndexPQ(d,M8,nbits8)# IVF加速示例quantizerfaiss.IndexFlatL2(d)indexfaiss.IndexIVFFlat(quantizer,d,nlist100)index.train(doc_embeddings)# 需要训练index.add(doc_embeddings)六、LLM生成优化防幻觉Prompt工程即使检索对了模型也可能脑补。我的Prompt模板强制要求模型只基于提供的上下文回答并输出引用标记SYSTEM_PROMPT你是一个企业知识库助手。请严格根据以下参考资料回答用户问题。 如果资料中不包含答案请明确回答根据现有资料无法确认。 要求 1. 回答需简洁不超过200字 2. 在答案末尾标注引用来源格式如 [来源: 《XXX》第3节] 3. 禁止编造资料中未提及的信息 参考资料 {retrieved_chunks} 用户问题{query} 七、性能调优总结优化环节核心目标关键手段查询转换让检索听懂用户HyDE、多查询、对话历史改写混合检索召回广而全向量检索 BM25关键词检索重排序排序精而准Cross-Encoder精排优中选优向量存储查询快而省向量缓存、PQ压缩、IVF加速Prompt工程生成稳而真强制引用、低温度、防幻觉指令结语RAG的落地是个系统工程。向量检索负责找得到混合检索负责找得全重排序负责排得对Prompt负责不乱说。缺一不可。建议从MVP开始逐步叠加优化不要一上来就追求大而全。先用混合检索 简单Rerank跑通再根据实际业务数据逐步调优最终打造一个稳定、高效的企业级RAG系统。