ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

RAG检索优化实战:从混合检索到结果重排,打造更懂行的智能助手

RAG检索优化实战:从混合检索到结果重排,打造更懂行的智能助手 1. 项目概述从“能聊”到“懂行”的Agent进阶之路上次我们聊了RAG的基础搭建让Agent学会了“翻书找答案”。但很多朋友在实际操作后反馈这个“初级版”Agent有点“一根筋”检索出来的内容要么不相关要么信息太零散回答起来还是磕磕绊绊。这就像你让一个刚学会查字典的人去写专业论文他每个字都认识但就是组织不成有深度的观点。今天我们就来“手搓”Agent的进阶能力解决RAG系统中最核心也最让人头疼的问题检索质量。这直接决定了你的Agent是“人工智障”还是“智能助理”。简单来说基础RAG是“开卷考试”而进阶RAG则是“开卷考名师划重点考前串讲”。我们不仅要让Agent找到资料还要确保它找到的是最相关、最精华、并且能融会贯通的那部分。本次聚焦的“中篇”核心就是优化检索链路涉及查询理解、召回排序以及结果处理等多个环节。无论你是用LangChain、LlamaIndex还是自研框架这些原理都是相通的。我会结合Python代码和大量实战中的“坑”带你一步步构建一个更聪明、更可靠的RAG系统。2. 核心思路拆解优化检索的三大战役为什么简单的“用户提问 - 向量检索 - 返回Top K”效果往往不尽人意因为现实世界的问题和知识库都是非结构化的、充满歧义的。进阶优化的核心思路可以概括为应对三大挑战2.1 第一战查询意图的模糊性用户的问题可能很简短、口语化或者包含歧义。比如“苹果怎么种”指的是水果还是公司直接拿这个短句去检索效果很难保证。我们需要对原始查询进行“增广”或“改写”使其表达更清晰、包含更多潜在的相关信息点。2.2 第二战召回结果的混杂性单一的检索方法如纯向量检索有其局限性。向量检索擅长语义匹配但可能忽略精确的关键词传统关键词检索如BM25擅长精确匹配但无法理解语义。此外知识库中的信息可能有重叠或冲突。我们需要混合多种检索策略并对召回的大量候选文档进行精排序。2.3 第三战上下文信息的有效性即使检索到了相关文档直接全部扔给大模型LLM也可能导致信息过载、焦点分散或者因上下文长度限制而截断重要内容。我们需要对检索到的文档进行“提纯”例如摘要、过滤或去重只把最精华的部分放入最终的提示词Prompt中。基于这三大战役我们的技术方案路线图就清晰了查询处理 - 混合检索 - 结果重排与后处理。下面我们就深入每个环节看看具体怎么实现。3. 查询理解与增强让Agent“听懂”弦外之音这是优化流程的第一步也常常被忽略。目标是将用户原始的、可能模糊的查询转化为对检索更友好的形式。3.1 查询改写与扩展最直接的方法是使用LLM本身来改写查询。例如用户问“Python如何连接数据库”我们可以让LLM将其扩展为“Python连接数据库的方法包括使用sqlite3、pymysql、psycopg2等库的代码示例以及连接字符串的配置和常见错误处理。” 这样检索时就能覆盖更全面的相关片段。from langchain.prompts import ChatPromptTemplate from langchain.chat_models import ChatOpenAI # 示例可用其他模型 def query_expansion(original_query: str) - str: prompt_template ChatPromptTemplate.from_messages([ (system, 你是一个专业的查询优化助手。请将用户简短或模糊的问题扩展成2-3个更全面、更具体的搜索查询用于文档检索。直接输出扩展后的查询用分号分隔。), (human, {query}) ]) llm ChatOpenAI(temperature0.1, model_namegpt-3.5-turbo) chain prompt_template | llm expanded_queries_str chain.invoke({query: original_query}).content # 假设返回Python连接SQLite数据库的代码示例Python使用pymysql连接MySQL的教程数据库连接池的最佳实践 expanded_queries [q.strip() for q in expanded_queries_str.split(;)] # 可以将原始查询和扩展查询合并用于后续检索 all_queries [original_query] expanded_queries return all_queries # 实操心得temperature参数要设低如0.1确保改写稳定。对于专业领域可以在system prompt中加入领域知识引导LLM生成更专业的扩展词。3.2 查询路由对于拥有多个垂直知识库的系统例如一个公司内部有产品文档、技术API、人事制度三个独立知识库我们需要先判断用户问题属于哪个领域再定向检索。这同样可以用一个轻量级LLM或分类器来实现。def query_router(query: str, knowledge_base_names: list) - str: 根据查询内容路由到最相关的知识库。 返回知识库的名称。 prompt f 用户问题{query} 可选的知识库类别{, .join(knowledge_base_names)} 请判断用户问题最可能属于哪个知识库类别只输出类别名称。 llm ChatOpenAI(temperature0) response llm.invoke(prompt).content.strip() return response # 注意事项如果问题涉及多个知识库更复杂的策略是进行多路检索然后合并结果。Dify等工具中知识库检索效果差有时就是因为缺乏有效的查询路由或混合检索策略。3.3 关键信息抽取从查询中抽取实体、关键词用于后续的关键词检索。例如对于“帮我比较Spring Boot和Django在ORM方面的差异”可以抽出“Spring Boot”、“Django”、“ORM”作为关键词。提示查询增强不宜过度。过于冗长的生成查询可能会引入噪声。一个平衡的做法是将原始查询和1-2个最相关的扩展查询一起用于检索。4. 混合检索策略向量与关键词的“双剑合璧”单一检索方式如同“单腿走路”混合检索则是“两条腿跑步”。目前最有效的组合是语义检索向量词汇检索关键词。4.1 语义检索向量检索这是我们熟悉的环节使用文本嵌入模型如text-embedding-3-small、bge-m3将文档和查询转换为向量通过计算余弦相似度找到语义相近的文档。它的优势是能理解同义词、相关概念例如“编程”和“写代码”。4.2 词汇检索如BM25BM25是一种经典且强大的概率检索模型它基于查询中的关键词在文档中出现的频率和分布来计算相关性。它对精确匹配、技术术语、代码片段等效果非常好。在Python中我们可以使用rank_bm25这个库。from rank_bm25 import BM25Okapi import jieba # 中文分词示例英文可用nltk class BM25Retriever: def __init__(self, documents: list[str]): # 对文档进行分词 tokenized_docs [list(jieba.cut(doc)) for doc in documents] self.bm25 BM25Okapi(tokenized_docs) self.raw_docs documents def retrieve(self, query: str, top_k: int 5) - list[tuple[str, float]]: tokenized_query list(jieba.cut(query)) doc_scores self.bm25.get_scores(tokenized_query) # 获取top_k的索引和分数 top_indices np.argsort(doc_scores)[::-1][:top_k] results [(self.raw_docs[i], doc_scores[i]) for i in top_indices] return results # 注意事项BM25对分词质量敏感。中文需要可靠的分词工具jieba, hanlp等英文需处理词干化和停用词。预处理的一致性文档和查询使用相同的分词器至关重要。4.3 混合检索的实现混合检索不是简单地将两个结果集合并而是需要对它们的分数进行标准化和加权融合。def hybrid_retrieval(query: str, vector_retriever, bm25_retriever, top_k: int 10, alpha: float 0.5): 混合检索 :param alpha: 向量检索分数的权重(1-alpha)是BM25的权重。 # 1. 分别检索 vector_results vector_retriever.similarity_search_with_score(query, ktop_k*2) # 多召回一些 bm25_results bm25_retriever.retrieve(query, top_ktop_k*2) # 2. 构建文档到分数的映射 doc_score_map {} # 处理向量结果 (doc, score) for doc, score in vector_results: # 向量相似度分数通常越大越相似我们将其归一化到[0,1]或直接使用 normalized_score 1 / (1 abs(score)) if score 0 else score # 简单处理假设score是距离 doc_score_map[doc.page_content] doc_score_map.get(doc.page_content, 0) alpha * normalized_score # 处理BM25结果 (doc, score) for doc, score in bm25_results: # BM25分数可能很大需要归一化。这里采用最大最小归一化需提前知道全局最大最小或使用本次召回结果 # 简化使用相对排名或sigmoid normalized_bm25 1 / (1 np.exp(-score/10)) # 简单的sigmoid缩放 doc_score_map[doc] doc_score_map.get(doc, 0) (1 - alpha) * normalized_bm25 # 3. 按融合分数排序 sorted_docs sorted(doc_score_map.items(), keylambda x: x[1], reverseTrue) return sorted_docs[:top_k] # 实操心得权重alpha需要根据你的数据和任务调优。一个经验是对于事实性、术语性强的问题提高BM25权重alpha调小如0.3对于概念性、开放性问题提高向量检索权重alpha调大如0.7。可以准备一个测试集进行验证。4.4 进阶混合策略 Reciprocal Rank Fusion (RRF)RRF是一种无需分数标准化、仅基于排名的融合方法非常鲁棒尤其适合不同检索系统分数尺度差异大的情况。def rrf_fusion(ranked_lists: list[list[str]], k: int 60): Reciprocal Rank Fusion :param ranked_lists: 多个检索结果列表每个列表是文档ID或内容的排序列表。 :param k: 常数通常取60。 scores {} for rlist in ranked_lists: for rank, doc in enumerate(rlist): doc_id doc # 假设doc是可哈希的标识 scores[doc_id] scores.get(doc_id, 0) 1 / (k rank 1) # 按RRF总分排序 fused_ranking sorted(scores.items(), keylambda x: x[1], reverseTrue) return fused_ranking # 使用示例将向量检索和BM25检索得到的前N个结果按各自分数排序的列表输入RRF。注意混合检索会增加计算和复杂度但对于生产系统其带来的精度提升通常是值得的。Spring Boot Milvus LangChain4j 实现 RAG 问答这类架构中可以在LangChain4j侧实现自定义的Retriever来集成BM25。5. 重排序与结果后处理去粗取精去伪存真经过混合检索我们得到了一个相关性更高的候选文档列表。但直接把这些文档塞进上下文可能还不够我们需要进一步加工。5.1 基于LLM的重新排序使用一个更强大的LLM或同一个LLM对Top N例如20个的候选文档进行相关性精排。这比直接使用初始的相似度分数更准。def llm_rerank(query: str, candidate_docs: list[str], top_m: int) - list[str]: 使用LLM对候选文档进行重排序。 prompt_template 你是一个信息检索评估专家。请根据用户问题判断以下文档的相关性并按照相关性从高到低输出文档的编号只输出编号用逗号分隔。 用户问题{query} 候选文档 {docs} 请只输出排序后的编号例如3,1,2 # 将候选文档格式化为带编号的列表 formatted_docs \n.join([f[{i1}] {doc[:500]}... for i, doc in enumerate(candidate_docs)]) # 截断避免过长 prompt prompt_template.format(queryquery, docsformatted_docs) llm ChatOpenAI(temperature0, model_namegpt-4) # 重排序建议使用能力更强的模型 response llm.invoke(prompt).content.strip() try: ranked_indices [int(idx.strip()) - 1 for idx in response.split(,)] ranked_docs [candidate_docs[i] for i in ranked_indices] return ranked_docs[:top_m] except: # 如果LLM输出不符合预期退回原始顺序 print(重排序解析失败使用原始顺序) return candidate_docs[:top_m] # 注意事项这种方法成本较高每次检索需调用LLM且依赖LLM的指令遵循能力。通常用于对最终效果要求极高的场景或作为离线评估工具。对于“rag检索结果冲突怎么办”这类问题也可以在重排序的prompt中要求LLM识别并优先选择更权威、更新颖的文档。5.2 结果去重与多样性检索到的文档可能内容高度重复。我们需要进行去重同时可能希望保留一定多样性从不同角度阐述同一问题。一个简单的方法是基于文档向量或句子向量进行聚类。from sklearn.metrics.pairwise import cosine_similarity import numpy as np def deduplicate_documents(docs: list[str], embeddings: list[np.ndarray], threshold: float 0.9) - list[str]: 基于嵌入向量进行去重。 :param threshold: 余弦相似度阈值高于此值视为重复。 unique_indices [] for i in range(len(docs)): is_duplicate False for j in unique_indices: sim cosine_similarity([embeddings[i]], [embeddings[j]])[0][0] if sim threshold: is_duplicate True break if not is_duplicate: unique_indices.append(i) return [docs[i] for i in unique_indices] # 更高效的做法可以使用近似最近邻搜索或直接对嵌入进行聚类。5.3 上下文压缩与摘要当检索到的文档很长时我们可以先对其进行摘要再将摘要喂给LLM以节省上下文窗口并聚焦核心信息。这被称为“Map-Reduce”或“Refine”模式。def summarize_long_document(doc: str, max_length: int 500) - str: 使用LLM对长文档进行摘要 if len(doc) max_length: return doc prompt f 请将以下技术文档的核心内容摘要成不超过{max_length}字的关键信息保留事实、数据和关键结论。 文档 {doc} llm ChatOpenAI(temperature0.1, model_namegpt-3.5-turbo-16k) summary llm.invoke(prompt).content.strip() return summary # 实操心得摘要会损失细节对于需要精确引用如代码、参数的场景需谨慎。一种折中方案是“抽取式摘要”即直接提取原文中最相关的几个句子。6. 构建健壮的RAG Pipeline代码实战与整合现在我们把上述所有组件串联起来构建一个完整的、健壮的RAG检索流程。class AdvancedRAGPipeline: def __init__(self, vector_store, text_corpus: list[str], embedding_model, llm): self.vector_retriever vector_store.as_retriever(search_kwargs{k: 15}) self.bm25_retriever BM25Retriever(text_corpus) self.embedding_model embedding_model self.llm llm self.top_k_hybrid 15 self.top_m_final 5 def retrieve(self, query: str) - list[str]: # 1. 查询增强 (可选) expanded_queries self._query_expansion(query) all_queries [query] expanded_queries # 2. 多查询混合检索取并集 all_candidate_docs [] for q in all_queries: hybrid_results hybrid_retrieval( q, self.vector_retriever, self.bm25_retriever, top_kself.top_k_hybrid, alpha0.5 ) all_candidate_docs.extend([doc for doc, _ in hybrid_results]) # 3. 去重 (基于内容简单去重) unique_candidate_docs list(dict.fromkeys(all_candidate_docs))[:20] # 保留前20个唯一文档 # 4. LLM重排序 (可选成本高) if len(unique_candidate_docs) self.top_m_final: # 可以抽样或全部重排 final_docs llm_rerank(query, unique_candidate_docs, self.top_m_final) else: final_docs unique_candidate_docs # 5. 上下文压缩/摘要 (如果文档过长) compressed_docs [] for doc in final_docs: if len(doc) 1000: compressed_docs.append(summarize_long_document(doc, 500)) else: compressed_docs.append(doc) return compressed_docs def _query_expansion(self, query: str) - list[str]: # 简化的查询扩展实际应用可更复杂 # 这里返回一个空列表或简单的同义词扩展 return [] # 暂时关闭扩展可根据需要开启 def generate_answer(self, query: str, retrieved_docs: list[str]) - str: context \n\n---\n\n.join(retrieved_docs) prompt f 基于以下上下文信息回答用户问题。如果上下文信息不足以回答问题请如实告知你不知道不要编造信息。 上下文 {context} 用户问题{query} 请提供准确、清晰的回答 response self.llm.invoke(prompt).content.strip() return response # 使用示例 pipeline AdvancedRAGPipeline(vector_store, all_texts, embedding_model, chat_llm) docs pipeline.retrieve(Python中如何处理数据库连接池) answer pipeline.generate_answer(Python中如何处理数据库连接池, docs) print(answer)这个Pipeline包含了从查询到检索结果的核心优化步骤。你可以根据实际需求启用或禁用某些模块如查询扩展、重排序。7. 避坑指南与效果调优在实际搭建和调试过程中我踩过不少坑这里总结几个关键点7.1 分块策略是根基检索的质量上限在文档被切分Chunk的那一刻就决定了。不合理的分块会导致信息碎片化或上下文丢失。对于技术文档尝试按章节、子标题进行分块并适当重叠例如重叠100-200个字符。对于代码库可以按函数/类进行分块并附带必要的类/模块说明。通用建议使用递归分块RecursiveCharacterTextSplitter尝试不同尺寸如512 1024 tokens和重叠度并通过检索评估选择最佳组合。7.2 嵌入模型的选择与微调不同的嵌入模型在不同类型文本上表现差异巨大。通用领域OpenAI的text-embedding-3-*系列、BGE系列的bge-large-zh-v1.5中文或bge-base-en-v1.5英文都是很好的选择。专业领域考虑使用领域数据对开源嵌入模型如BGE进行微调哪怕只有几千条样本也能显著提升效果。评测使用MTEB等基准测试或构建自己的小规模测试集QA对来评估不同嵌入模型在你数据上的表现。7.3 混合检索的权重调优alpha参数向量vs关键词权重不是固定的。建议构建一个包含多种问题类型事实型、概念型、复杂型的测试集。遍历不同的alpha值如0.1, 0.3, 0.5, 0.7, 0.9。评估每个设置下的检索精度如RecallK, MRR。选择一个在多数问题上表现稳健的值或者实现更复杂的动态权重策略根据查询分类调整。7.4 处理“检索结果冲突”当不同来源的文档信息矛盾时LLM可能会混淆。解决方法在Prompt中明确指示“如果上下文信息存在矛盾请以[文档A]的信息为准”或“请指出信息存在不一致并分别说明”。实施来源优先级在检索后处理阶段给权威性更高的文档如官方文档、最新文档更高的权重或优先位置。使用LLM进行事实核查在生成最终答案前让LLM先识别并总结上下文中的矛盾点。7.5 评估体系不可或缺不要只凭感觉判断RAG系统好坏。建立量化评估体系检索阶段指标Hit Rate命中率、MRR平均倒数排名、NDCG归一化折损累计增益。生成阶段指标使用LLM作为裁判评估答案的忠实度是否基于上下文、相关性是否回答问题和完整性。端到端测试维护一个覆盖核心场景的QA测试集定期运行监控效果变化。7.6 关于Agentic RAG的思考“Agentic RAG”是更前沿的方向其核心是让Agent主动管理检索过程。例如迭代检索根据初步检索结果和LLM的思考动态生成新的、更精准的查询进行二次检索。工具调用Agent可以调用搜索引擎、数据库查询等工具作为检索的补充。自我验证Agent生成答案后可以自行检索验证答案中的关键事实。 这要求Agent具备规划、工具使用和反思能力是构建真正智能助手的关键一步我们将在后续的“手搓Agent”关卡中深入探讨。搭建一个高效的RAG系统是一个持续迭代和调优的过程。没有一劳永逸的银弹关键是根据你的具体数据、问题和资源有策略地选择和组合这些技术。从基础的向量检索到引入混合检索和重排序每一步都可能带来显著的体验提升。希望这篇“中篇”能帮你打好检索优化的地基下一篇我们将深入探讨如何利用这些高质量的检索结果设计更强大的Prompt和生成策略让Agent的回答不仅准确而且逻辑清晰、详略得当。
返回列表