ARTICLE DETAIL

资讯详情

深耕网站SEO优化与搜索引擎排名提升的一线实战洞察。

RAG系统进阶:重排序、查询改写与混合检索实战

RAG系统进阶:重排序、查询改写与混合检索实战 1. 从“能用”到“好用”RAG系统进阶的必经之路上一章我们聊了如何从零搭建一个能跑起来的RAG系统就像盖房子我们打好了地基砌好了墙装上了门窗一个能遮风挡雨的毛坯房算是完成了。但如果你真的想住进去会发现很多问题窗户漏风、水管不通、电路不稳。一个基础的RAG系统也是如此它能把文档塞进向量库也能根据问题吐出答案但效果往往差强人意——答案可能不相关、不准确或者干脆就是一本正经地胡说八道。这就是从“能用”到“好用”的巨大鸿沟。很多团队在搭建完基础流程后就以为大功告成结果上线后用户反馈“AI在胡扯”项目价值大打折扣。问题的根源在于基础的RAG流程切块-向量化-检索-生成存在大量影响最终效果的“魔鬼细节”。比如你辛辛苦苦把一本几百页的技术手册切成块存了进去用户问一个非常具体的问题系统却检索出来一堆无关的“前言”和“目录”片段大模型拿着这些垃圾信息自然只能生成垃圾答案。所以这一章我们不谈从零搭建而是聚焦于如何将这个“毛坯房”精装修让它真正成为一个可靠、高效、智能的知识助手。核心思路是在检索和生成这两个核心环节之前、之中、之后增加一系列“质量控制”和“效果增强”的组件。这就像在生产线上的关键质检工位确保流入下一环节的“原材料”是合格的。我们将深入探讨重排序、查询改写、Hybrid Search、Query路由等关键技术并结合Milvus这样的向量数据库看看如何将它们有机地整合起来构建一个工业级的RAG应用。2. 检索质量提升的第一道防线重排序当你向向量数据库发起一次相似性搜索时数据库会返回一个按相似度分数如余弦相似度降序排列的文档块列表。但这里有一个根本性的假设向量空间的“相似”等于语义上的“相关”。这个假设在很多时候是脆弱的。2.1 为什么简单的向量检索会“失准”想象一下你的知识库里有关于“Python装饰器”和“室内装修装饰”的文档。当用户提问“如何用装饰器记录函数执行时间”时纯粹的基于嵌入向量的相似性搜索可能会因为“装饰”这个词的共性而把一些关于“室内装饰风格”的文档块也检索出来并且排名可能还不低。这是因为嵌入模型如text-embedding-ada-002在训练时会学习到“装饰器”和“装饰”在某种抽象语义上的关联但这种关联对于当前的具体问题来说是噪声。此外还有以下常见问题词汇不匹配用户问“怎么给汽车换机油”文档里写的是“车辆发动机润滑油的更换步骤”。虽然意思完全一样但用词不同可能导致向量相似度不高。语义稀释一个文档块可能包含多个主题其中只有一个子主题与查询相关但向量表征是整个块的“平均”导致相关性被稀释。缺少交互信息第一阶段的检索称为“召回”通常是独立进行的没有考虑候选文档之间的相互比较关系。重排序就是为了解决这些问题而生的。它的作用是在初步检索返回的Top K个候选文档比如K100的基础上使用一个更精细、更耗资源但更准确的模型对这些候选文档进行重新打分和排序最终筛选出Top N个比如N5最相关的文档再交给大模型生成答案。2.2 重排序的核心原理与模型选型重排序模型本质上是一个“文本对”分类或回归模型。输入是查询Query 文档Document对输出是一个相关性分数。这个模型比用于生成嵌入向量的模型要复杂得多因为它需要理解两个文本之间深层次的语义交互和逻辑关联。目前主流的重排序模型分为两类交叉编码器这是最经典和有效的重排序模型。它将查询和文档拼接在一起同时输入到一个Transformer模型如BERT、RoBERTa中。模型通过自注意力机制让查询和文档的每一个词都能进行充分的交互从而计算出最精确的相关性分数。它的效果好但计算成本高因为每次推理都需要处理查询文档这个长序列。因此它只适合对少量如20-100个候选文档进行精排。代表模型BAAI/bge-reranker-large,cross-encoder/ms-marco-MiniLM-L-6-v2。以BGE的重排序器为例它专门针对中文问答检索场景进行了优化效果显著。基于嵌入的轻量级重排序这类方法尝试平衡效果和效率。例如使用更强大的嵌入模型分别对查询和文档进行向量化然后计算改进的相似度分数如使用ColBERT模型中的“迟交互”机制。或者使用一个双塔模型但引入更复杂的交互计算。它们的速度比交叉编码器快但效果通常稍逊一筹。实操建议对于生产环境我强烈推荐使用交叉编码器作为重排序器。虽然它慢但我们对大量文档的“粗筛”已经由高效的向量数据库如Milvus完成了重排序只需要处理前100个左右的文档这个计算开销在大多数场景下是可以接受的。用一点延迟换取答案准确率的大幅提升这笔买卖非常划算。2.3 在LangChain中集成重排序以LangChain框架为例集成重排序非常简单。你需要一个重排序模型的API如Hugging Face Inference API或自己部署的模型端点和一个对应的LangChain重排序器接口。from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import CrossEncoderReranker from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Milvus from langchain_huggingface import HuggingFaceEndpoint # 1. 初始化基础的向量检索器Milvus embedding_model HuggingFaceEmbeddings(model_nameBAAI/bge-small-zh-v1.5) vectorstore Milvus( embedding_functionembedding_model, connection_args{host: localhost, port: 19530}, collection_namemy_docs ) base_retriever vectorstore.as_retriever(search_kwargs{k: 50}) # 先召回50个 # 2. 初始化重排序模型这里假设使用HuggingFace Inference API # 你也可以使用 transformers 库本地加载模型如 CrossEncoder(BAAI/bge-reranker-large) reranker_endpoint HuggingFaceEndpoint( repo_idBAAI/bge-reranker-large, tasktext-classification, # 重排序本质是文本对分类 huggingfacehub_api_tokenyour_token ) compressor CrossEncoderReranker(modelreranker_endpoint, top_n5) # 3. 构建带重排序的检索器 compression_retriever ContextualCompressionRetriever( base_compressorcompressor, base_retrieverbase_retriever ) # 4. 使用这个检索器 docs compression_retriever.invoke(Python装饰器有什么作用) # 现在 docs 包含的是经过重排序后最相关的5个文档块关键点search_kwargs{“k”: 50}这里的k50很重要。这是交给重排序模型的候选文档数量。设置太小可能漏掉相关文档设置太大会增加重排序的计算负担。根据你的数据集和查询复杂度通常在20-100之间调整。3. 让问题问得更好查询理解与改写很多时候用户的问题是模糊的、简短的或者有歧义的的。直接拿这样的原始问题去检索效果很难保证。查询改写的目的就是将用户的原始查询转化为一个对检索系统更友好、更能命中相关文档的“增强版查询”。3.1 常见的查询改写策略查询扩展为原始查询添加相关的同义词、上位词或关联词。例如用户问“苹果”根据上下文可以扩展为“苹果 水果”或“苹果 iPhone 公司”。如何实现可以使用预定义的同义词词典或者更智能地用一个小语言模型如GPT-3.5-turbo根据对话历史或文档主题来生成扩展词。# 一个简单的示例使用大模型进行查询扩展 from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI llm ChatOpenAI(modelgpt-3.5-turbo) prompt ChatPromptTemplate.from_template( 你是一个专业的搜索查询优化助手。请根据用户问题生成2-3个与之高度相关的搜索关键词用于从技术文档库中检索信息。只输出关键词用空格分隔。 用户问题{query} 优化后的关键词 ) expanded_query llm.invoke(prompt.format(query怎么排查程序内存泄漏)) # 输出可能为“内存泄漏 检测工具 Valgrind 内存分析 排查方法”查询重写将口语化、简略的问题重写成更正式、更完整的陈述句。例如“咋装Docker” 重写为 “Docker的安装步骤与教程”。如何实现同样可以利用大模型的指令跟随能力。Prompt可以设计为“请将以下用户问题改写为一个适合用于文档检索的完整问句{query}”。多查询生成对于复杂问题可以将其分解成多个子问题分别检索再合并结果。例如“如何在CentOS 7上安装Milvus并连接Spring Boot” 可以分解为“CentOS 7安装Milvus步骤”和“Spring Boot连接Milvus配置”。LangChain支持MultiQueryRetriever可以自动从不同角度生成多个查询用这些查询并行检索然后取结果并集能有效提高召回率。3.2 结合对话历史的查询理解在聊天机器人场景中当前问题往往依赖于之前的对话上下文。例如用户介绍一下Milvus。 AIMilvus是一款开源的向量数据库... 用户它怎么安装这里的“它”指代Milvus。如果不结合历史第二个问题“它怎么安装”对检索系统是毫无意义的。因此我们需要一个查询路由或上下文感知改写的步骤。简单方法在Prompt中附带最近的几条对话历史让大模型生成一个独立的、包含指代信息的查询。例如“根据对话历史将当前问题改写为完整的、可独立检索的查询。历史{history}当前问题{question}改写后”进阶方法使用更复杂的Agentic框架判断当前问题是否需要检索需要则改写是否是闲聊是否需要执行特定工具等。个人经验查询改写模块是提升RAG体验的“低成本高收益”环节。一个简单的基于规则或轻量级模型的改写就能解决大量“答非所问”的问题。我通常会把它放在检索链的最前端作为标准预处理步骤。4. 混合检索向量搜索与关键词搜索的联姻尽管向量搜索在语义匹配上能力强大但传统的关键词搜索如BM25算法依然有其不可替代的优势精确匹配对于专有名词、产品型号、错误代码、函数名等需要精确匹配的术语BM25效果往往更好。例如搜索“ERR_CODE_404”BM25能精准命中而向量搜索可能找到一些关于“网络错误”的泛化内容。词汇敏感性对拼写错误、缩写、词形变化的容忍度不同。BM25对词频和文档频率敏感而向量搜索更看重语义。可解释性关键词匹配的结果更容易向用户解释“因为您的查询中包含了‘安装’和‘Milvus’”。Hybrid Search混合检索就是将向量搜索和关键词搜索的结果结合起来取长补短。常见的融合方式有加权求和分别计算向量搜索的相似度分数归一化到0-1和BM25的分数归一化然后按一定权重如0.7 vs 0.3相加得到最终分数并重新排序。final_score α * normalized_vector_score (1-α) * normalized_bm25_score倒数融合将两个结果列表中的每个文档的排名位置取倒数如第1名得1分第2名得0.5分...然后加权求和再排序。这种方式更关注排名而非绝对分数。再排序先用BM25快速召回一批候选文档比如前200个再在这批文档内部用向量相似度进行精排或者反过来。4.1 使用Milvus实现混合检索Milvus从2.3版本开始原生支持了混合查询功能。它允许你在一次搜索请求中同时执行向量相似度搜索和标量字段上的表达式过滤可以模拟简单关键词匹配。但对于更复杂的BM25全文检索通常需要借助其他组件。一个更通用的架构是方案A应用层融合使用Elasticsearch或Apache Solr执行BM25检索同时用Milvus执行向量检索。在应用层如Python服务中获取两者结果进行分数归一化和融合。这种方式灵活但需要维护两个系统并在应用层实现融合逻辑。方案B使用集成工具使用LangChain的EnsembleRetriever或Weaviate其本身支持向量和关键词混合搜索。Milvus社区也有与Apache OpenSearch集成的方案。这里给出一个使用LangChain EnsembleRetriever的示例思路from langchain.retrievers import EnsembleRetriever from langchain_community.retrievers import BM25Retriever from langchain_community.vectorstores import Milvus from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceEmbeddings # 1. 准备文档假设doc_list是原始文档列表 text_splitter RecursiveCharacterTextSplitter(chunk_size500, chunk_overlap50) doc_splits text_splitter.split_documents(doc_list) # 2. 初始化BM25检索器基于内存适用于中小型库 bm25_retriever BM25Retriever.from_documents(doc_splits) bm25_retriever.k 10 # 设置召回数量 # 3. 初始化Milvus向量检索器 embedding HuggingFaceEmbeddings(model_nameBAAI/bge-small-zh-v1.5) vectorstore Milvus.from_documents(doc_splits, embedding, connection_args{host: localhost, port: 19530}) milvus_retriever vectorstore.as_retriever(search_kwargs{k: 10}) # 4. 构建混合检索器 ensemble_retriever EnsembleRetriever( retrievers[bm25_retriever, milvus_retriever], weights[0.4, 0.6] # 调整权重这里更侧重向量语义 ) # 5. 使用混合检索 docs ensemble_retriever.invoke(Milvus在Windows上的安装教程)注意自带的BM25Retriever适用于文档量不大、可全量加载到内存的场景。对于大规模生产环境你需要一个独立的全文检索引擎如Elasticsearch并实现一个自定义的Retriever来连接它。5. 智能路由与查询分类让系统学会“分诊”不是所有用户输入都适合走“检索-生成”这条管道。一个成熟的RAG系统需要具备一定的“判断力”这就是查询路由。5.1 路由的目的与策略路由的核心思想是在调用昂贵的检索和生成流程之前先对用户意图进行快速分类并决定最合适的处理路径。这能极大提升系统效率和用户体验。常见的路由分类包括知识库问答问题明确需要从自有知识库中寻找答案。这是RAG的主路径。闲聊/问候例如“你好”、“谢谢”。这类应该由大模型直接生成友好回应无需检索。通用知识问答例如“太阳为什么是圆的”。这类问题可能不在你的专有知识库内但大模型本身的世界知识足以回答。可以选择不检索或先检索知识库无结果后再用大模型通用能力回答。多轮对话中的指代澄清用户说“上面那个方法”需要结合历史判断指代可能不需要发起新检索。请求调用工具/API例如“帮我查一下北京的天气”。这需要触发Agent的Tool Calling功能。5.2 实现路由的两种方式基于规则的简单路由使用关键词或正则表达式匹配。例如检测到“你好”、“嗨”等词进入闲聊路径。这种方法简单快速但不够灵活。基于模型的智能路由使用一个轻量级的文本分类模型如微调的BERT或者Prompt大模型来判断意图。这是更鲁棒的方法。使用大模型Promptrouter_prompt ChatPromptTemplate.from_messages([ (system, 你是一个查询分类器。请将用户问题分类到以下类别之一\n1. knowledge_base_qa: 需要从特定知识库中检索答案的技术或专业问题。\n2. chitchat: 问候、感谢、闲聊等。\n3. general_qa: 通用知识问题无需特定知识库。\n4. clarification: 需要澄清上下文的多轮对话。\n只输出类别名称。), (human, {query}) ]) route_result llm.invoke(router_prompt.format(queryuser_input)) # 根据 route_result.content 的值决定后续流程使用本地小模型为了更低延迟和成本可以收集一些数据训练一个简单的意图分类模型如用fasttext或scikit-learn。将分类模型集成到服务入口。5.3 设计一个带路由的RAG流程一个整合了上述多种优化技术的RAG系统高级流程如下用户输入 | v [查询路由] -- 闲聊/通用QA -- 直接LLM生成回复 | v (知识库QA路径) [查询改写/扩展] -- 增强的查询 | v [混合检索] -- (BM25 向量搜索) 召回Top K候选文档 | v [重排序] -- 对Top K文档精排选出Top N最相关文档 | v [上下文构造] -- 将Top N文档格式化为LLM的上下文 | v [提示工程] -- 设计包含上下文、指令、问题的Prompt | v [大模型生成] -- 生成最终答案 | v [后处理/引用溯源] -- 格式化答案标注引用来源在这个流程中每一步都是一个可插拔的模块。你可以根据业务需求的复杂度决定启用哪些模块。对于核心的内部知识助手我建议至少要实现查询改写、重排序和引用溯源。6. 生产环境部署与性能考量当你的RAG系统在本地跑通效果也调优得不错之后下一步就是把它部署上线接受真实用户的检验。这里有几个关键的生产级考量点。6.1 向量数据库选型与Milvus部署我们一直以Milvus为例它确实是生产级向量数据库的佼佼者尤其适合大规模、高并发的场景。对于部署你有几种选择Docker Compose开发/测试这是最快的方式。Milvus官网提供了完整的docker-compose.yml文件一键启动包含Milvus、etcd和MinIO的所有服务。但请注意这不适用于生产环境因为数据持久化、高可用、监控都无法保证。Kubernetes Helm生产推荐对于生产环境使用Helm在K8s集群中部署Milvus是标准做法。这能方便地管理副本、伸缩、配置和监控。你需要仔细规划集群规模、存储类PVC、资源请求与限制。云托管服务如果你不想自己维护基础设施Zilliz CloudMilvus官方云服务或各大云厂商的向量数据库服务如阿里云、腾讯云的相关产品是最省心的选择但需要付费。部署 checklist[ ] 启用身份验证用户名/密码或API Key。[ ] 配置网络策略仅允许应用服务器访问。[ ] 规划存储向量索引和元数据需要持久化存储。确保PVC有足够的容量和IOPS。[ ] 设置资源限制为Milvus组件DataNode, QueryNode, IndexNode等设置合理的CPU和内存限制。[ ] 配置监控集成Prometheus和Grafana监控QPS、延迟、内存使用、磁盘IO等关键指标。6.2 服务化与API设计你的RAG系统应该以一个微服务的形式提供。通常包含以下核心接口知识库管理接口POST /v1/documents/ingest: 接收文档PDF、Word、TXT等进行切分、向量化存入Milvus。DELETE /v1/documents/{doc_id}: 删除特定文档及其所有向量块。GET /v1/documents: 列出已入库的文档。问答接口POST /v1/chat/completions: 核心问答接口。接收用户消息可能包含历史返回AI回复。请求体应包含query当前问题、conversation_id会话ID用于多轮、stream是否流式输出等。响应体应包含answer生成的答案、source_documents引用的文档片段及来源、conversation_id。技术栈选择Web框架FastAPIPython是当前最流行的选择异步性能好自动生成API文档。异步处理对于文档解析、向量化等耗时操作一定要使用异步任务如Celery Redis或直接使用FastAPI的BackgroundTasks避免阻塞HTTP请求。流式输出如果使用支持流式输出的LLM如GPT-4 Llama 2务必实现Server-Sent Events (SSE) 接口让用户能实时看到生成过程体验大幅提升。6.3 性能优化与缓存策略RAG的延迟主要来自LLM生成、向量检索、重排序。优化手段包括检索缓存对于相同或相似的查询其检索结果在一定时间内是稳定的。可以缓存(query_embedding, top_k_documents)对。注意缓存键需要归一化如对查询文本小写、去空格。LLM输出缓存对于常见、确定性的问题如“你们公司是做什么的”答案可以缓存。但要注意如果底层知识库更新了缓存需要失效。索引优化Milvus支持多种向量索引IVF_FLAT, HNSW, SCANN等。HNSW在查询速度和召回率上通常有较好的平衡是通用场景的首选。创建集合时需要根据向量维度和数据量调整M层内最大连接数和efConstruction构建时的搜索范围参数。批处理在文档入库阶段对文本块进行批量向量化如一次处理100个块比逐个处理效率高得多。6.4 可观测性与评估系统上线后你必须能看清它的运行状况。日志结构化日志JSON格式是关键。记录每一次请求的请求ID、用户ID、查询文本、检索到的文档ID、LLM使用的Token数、总耗时、各阶段改写、检索、重排、生成耗时。这便于问题排查和性能分析。监控指标业务指标每日问答量、平均响应时间、缓存命中率。质量指标需要人工标注一部分问答对定期如每周计算检索命中率检索到的文档是否相关和答案满意度生成的答案是否准确有用。可以设计一个简单的内部打分工具。系统指标服务CPU/内存、Milvus集群状态、LLM API调用错误率。反馈循环提供“点赞/点踩”功能收集用户反馈。这些负面反馈是优化系统最宝贵的资料可以用于持续改进重排序模型、优化提示词。7. 避坑指南与实战心得走过从零到生产的完整流程我踩过不少坑也积累了一些不一定写在官方文档里的经验。坑一文档切分的“黄金法则”不存在“分块大小到底设多少”这是最常见的问题。答案是取决于你的文档类型和查询模式。没有放之四海而皆准的值。技术文档/手册查询通常比较具体。建议使用较小的块256-512字符配合较大的重叠50-100字符。重叠能防止一个概念被切到两个块中间。长篇文章/报告查询可能涉及整体观点或总结。可以使用较大的块1024-2048字符重叠可以小一些。实战建议准备一个包含各种典型问题的测试集。用不同的分块策略大小、重叠处理你的文档然后看哪种策略下问题的检索结果平均相关度最高。这是一个需要实验的超参数。坑二嵌入模型不是越新越好新的嵌入模型如OpenAI的text-embedding-3往往在通用基准测试上表现更好。但在你的特定领域数据上它不一定是最优的。特别是对于中文垂直领域很多针对中文优化的开源模型如BGE、M3E表现可能远超通用模型。做法在决定使用哪个嵌入模型前用你的业务数据做一个简单的评估。随机采样一些查询相关文档对计算不同模型下查询与相关文档、查询与不相关文档的相似度差值。选择区分度最大的模型。坑三忽略元数据的力量Milvus除了存储向量还能存储每个向量对应的元数据如doc_id,chunk_index,source,title等。这些元数据在后期过滤和精排中极其有用。场景1用户问“在第三章里提到的那个工具是什么”。你可以在向量检索时添加一个元数据过滤条件source “第三章.pdf”大幅缩小搜索范围。场景2在重排序或最终呈现时你可以根据chunk_index对相邻的块进行简单的合并提供更连贯的上下文。入库时务必规划好元数据 schema把可能有用的信息都存进去。坑四Prompt设计中的“遗忘指令”当你把检索到的文档上下文塞进Prompt时大模型有时会“忘记”你的指令而是模仿上下文文档的写作风格来回答问题。对策在Prompt中使用强分隔符来明确区分指令、上下文和问题。例如请严格根据以下提供的信息来回答问题。如果信息不足以回答问题请直接说“根据已知信息无法回答该问题”。 信息{context}问题{question}此外在指令中明确要求“用中文回答”、“保持简洁”、“以列表形式呈现”等也能更好地控制输出格式。构建一个生产可用的RAG系统远不止是拼接几个开源组件。它需要你深入理解每一个环节的原理根据业务数据反复实验和调优并具备工程化部署和运维的能力。从基础的检索生成到加入重排序、查询改写、混合检索等增强组件再到设计路由、实现服务化、建立监控每一步都是在为系统的可靠性、准确性和用户体验添砖加瓦。这个过程没有银弹唯有持续迭代和打磨。希望本章讨论的这些进阶思路和实战细节能帮助你少走弯路更快地打造出真正解决业务问题的智能知识引擎。
返回列表