RAG检索优化与实战复盘

发布时间:2026/6/16 21:42:20

RAG检索优化与实战复盘 这段时间一直在学习RAG相关内容发现普通的基础检索效果真的很差经常出现召回不准、缺上下文、答非所问的问题。单纯靠提升模型参数根本解决不了检索层面的硬伤。所以我系统学习了常用的七大RAG检索优化策略并编写实战代码统一用本地 Ollama 模型Chroma 向量库测试。在这里记录一下我的学习过程、每种方案的核心逻辑、实操心得和适用场景方便后续复盘复用。一、RAG痛点在没做优化之前我用的是最基础的RAG链路文档切片→向量化入库→问题向量匹配→召回作答。实操中踩了很多坑总结下来主要有这几个问题切片粒度很难权衡切太小丢失完整上下文切太大文本冗余、语义杂乱匹配精度骤降检索方式太单一只能匹配用户提问的近似语义模糊提问、口语化提问基本检索不到有效内容向量匹配有缺陷只看语义相似度无法处理结构化筛选条件容易召回大量无关内容多检索结果杂乱多路检索结果直接堆砌优质内容被冗余信息覆盖作答质量不稳定针对这些问题RAG的优化可以贯穿全流程我把它分为三个阶段索引前数据优化、索引结构优化、查询后检索优化下面逐一记录我的学习和实战过程。二、七大RAG优化策略学习实战记录本次实战统一环境本地 Ollamaqwen:7b大模型、Qwen3-Embedding-0.6B 嵌入模型、LangChain Chroma 向量库。所需依赖pip install langchain langchain-community langchain-ollama chromadb python-dotenv pydantic2.1 摘要索引多向量检索个人理解这是一个轻量化优化方案核心是「分开存储、分层检索」。基础检索是直接把长文本切片向量化检索长文本向量不仅占用Token多语义也不够聚焦。摘要索引的思路很简单先用大模型给每一段切片生成精简摘要把摘要存入向量库用来做语义检索原始完整文档单独存在内存中。检索时先匹配精准简短的摘要再通过关联ID调取完整原文作答。个人感受很适合长文档知识库既能减少检索阶段的计算量又不会丢失作答需要的完整上下文性价比很高。实战代码import uuid from langchain_community.document_loaders import WebBaseLoader from langchain_core.documents import Document from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnableMap from langchain_ollama import OllamaLLM, OllamaEmbeddings from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_core.prompts import ChatPromptTemplate from langchain_chroma import Chroma from langchain_core.stores import InMemoryByteStore from langchain.retrievers import MultiVectorRetriever # 初始化本地模型 llm OllamaLLM(modelqwen:7b) embedding_model OllamaEmbeddings(modeldengcao/Qwen3-Embedding-0.6B:Q8_0) # 加载并清洗文档 web_docs WebBaseLoader(https://www.zgkpw.org.cn/notice/content_529245.html).load() data [Document(page_contentdoc.page_content.replace(\n, ).replace( ,).replace(\t, ), metadatadoc.metadata) for doc in web_docs] # 文档切片 text_splitter RecursiveCharacterTextSplitter( chunk_size1024, chunk_overlap100, length_functionlen, is_separator_regexFalse, ) texts text_splitter.split_documents(data) # 批量生成切片摘要 summary_chain ( {doc: lambda x: x.page_content} | ChatPromptTemplate.from_template(总结文档内容 {doc}) | llm | StrOutputParser() ) summaries summary_chain.batch(texts, {max_concurrency: 2}) # 初始化双存储结构 vectorStore Chroma(collection_namesummary_db, embedding_functionembedding_model) store InMemoryByteStore() id_key doc_id retriever MultiVectorRetriever(vectorstorevectorStore, byte_storestore, id_keyid_key) # 绑定摘要与原文 doc_ids [str(uuid.uuid4()) for _ in texts] summary_docs [Document(page_contents, metadata{id_key: doc_ids[i]}) for i, s in enumerate(summaries)] retriever.vectorstore.add_documents(summary_docs) retriever.docstore.mset(list(zip(doc_ids, texts))) # 链式问答调用 prompt ChatPromptTemplate.from_template(根据{doc}回答{question}) chain RunnableMap({ doc: lambda x: retriever.invoke(x[question]), question: lambda x: x[question], }) | prompt | llm | StrOutputParser() # 测试问答 res chain.invoke({question: 2026年科学传播人才高级研修班研修目的}) print(最终回答, res)2.2 父子索引父子文档检索个人理解专门解决「切片精度和上下文不可兼得」的问题是我个人很喜欢的一种优化方式。核心是双层切片把文档先切成大粒度的父文档保留完整上下文用来最终作答再把父文档切成小粒度子文档语义更精准用来检索匹配。检索时用子文档精准匹配内容自动关联返回对应的父文档。个人感受完美解决了小切片缺内容、大切片不准的痛点大部分常规问答场景用这个优化就足够好用。实战代码import uuid from langchain_community.document_loaders import WebBaseLoader from langchain_core.documents import Document from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnableMap from langchain_ollama import OllamaLLM, OllamaEmbeddings from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_core.prompts import ChatPromptTemplate from langchain_chroma import Chroma from langchain_core.stores import InMemoryStore from langchain.retrievers import ParentDocumentRetriever # 初始化模型 llm OllamaLLM(modelqwen:7b) embedding_model OllamaEmbeddings(modeldengcao/Qwen3-Embedding-0.6B:Q8_0) # 加载文档 web_docs WebBaseLoader(https://www.zgkpw.org.cn/notice/content_529245.html).load() data web_docs # 定义父子切片规则 text_splitter_son RecursiveCharacterTextSplitter(chunk_size500, chunk_overlap50) text_splitter_parent RecursiveCharacterTextSplitter(chunk_size1000) # 初始化存储 vectorStore Chroma(collection_nameparent_db, embedding_functionembedding_model) store InMemoryStore() # 初始化父子检索器 retriever ParentDocumentRetriever( child_splittertext_splitter_son, parent_splittertext_splitter_parent, vectorstorevectorStore, docstorestore, ) # 写入清洗后文档 retriever.add_documents([ Document( page_contentdata[0].page_content.replace(\n, ).replace( , ), metadata{} ) ]) # 构建问答链路 prompt ChatPromptTemplate.from_template(根据{doc}回答{question}) chain RunnableMap({ doc: lambda x: retriever.invoke(x[question]), question: lambda x: x[question], }) | prompt | llm | StrOutputParser() # 测试 res chain.invoke({question: 2026年科学传播人才高级研修班研修目的}) print(最终问答结果, res)2.3 假设性问题索引个人理解专门解决「用户提问和原文表述不一致」导致检索失败的问题。很多时候用户提问很口语化和文档原文的书面语完全不一样向量匹配不到内容。这个方案的思路是提前给每一段文档生成几个用户大概率会问的问题把这些问题向量化存入向量库。用户提问时匹配相似的预设问题就能间接召回对应原文。个人感受非常适配C端用户口语化提问场景能有效降低答非所问的概率。实战代码import uuid from typing import List from langchain_community.document_loaders import WebBaseLoader from langchain_core.documents import Document from langchain_core.output_parsers import StrOutputParser, PydanticOutputParser from langchain_core.runnables import RunnableMap from langchain_ollama import OllamaLLM, OllamaEmbeddings from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_core.prompts import ChatPromptTemplate from langchain_chroma import Chroma from langchain_core.stores import InMemoryByteStore from langchain.retrievers import MultiVectorRetriever from pydantic import BaseModel, Field # 初始化模型 llm OllamaLLM(modelqwen:7b) embedding_model OllamaEmbeddings(modeldengcao/Qwen3-Embedding-0.6B:Q8_0) # 加载清洗文档 web_docs WebBaseLoader(https://www.zgkpw.org.cn/notice/content_529245.html).load() data [Document(page_contentdoc.page_content.replace(\n, ).replace( ,).replace(\t, ), metadatadoc.metadata) for doc in web_docs] # 文档切片 text_splitter RecursiveCharacterTextSplitter(chunk_size1024, chunk_overlap100) texts text_splitter.split_documents(data) # 定义问题输出格式 class QuestionList(BaseModel): questions: List[str] Field(descriptionList of questions) parser PydanticOutputParser(pydantic_objectQuestionList) format_instructions parser.get_format_instructions() # 批量生成假设问题 prompt ChatPromptTemplate.from_template( 基于文档生成3个问题。 文档{doc} {format_instructions} 仅输出JSON。 ) question_chain ({doc: lambda x: x.page_content} | prompt.partial(format_instructionsformat_instructions)| llm | parser) questions question_chain.batch(texts[:1], {max_concurrency: 1}) # 初始化检索存储 vectorStore Chroma(collection_namequestion_db, embedding_functionembedding_model) store InMemoryByteStore() id_key doc_id retriever MultiVectorRetriever(vectorstorevectorStore, byte_storestore, id_keyid_key) # 绑定问题与原文 doc_ids [str(uuid.uuid4()) for _ in texts] question_docs [] for i, q_obj in enumerate(questions): content .join(q_obj.questions) question_docs.append(Document(page_contentcontent, metadata{id_key: doc_ids[i]})) retriever.vectorstore.add_documents(question_docs) retriever.docstore.mset(list(zip(doc_ids, texts))) # 问答链路 qa_prompt ChatPromptTemplate.from_template(根据{doc}回答{question}) chain RunnableMap({ doc: lambda x: retriever.invoke(x[question]), question: lambda x: x[question], }) | qa_prompt | llm | StrOutputParser() # 测试 res chain.invoke({question: 2026年科学传播人才高级研修班研修目的}) print(res)2.4 元数据索引个人理解解决纯语义检索无法做精准结构化筛选的短板。普通检索只能靠语义相似度匹配遇到带时间、分类、数值、类型等结构化条件的提问完全没办法精准过滤。元数据索引就是给文档打上结构化标签让模型自动解析用户问题中的筛选条件先过滤结构化数据再做语义匹配。个人感受适合知识库有明确分类、时间、属性的场景比如公告、课程、数据集检索精准度提升很明显。实战代码from langchain_chroma import Chroma from langchain_core.documents import Document from langchain_core.prompts import PromptTemplate from langchain_ollama import OllamaEmbeddings from langchain.chains.query_constructor.base import AttributeInfo from langchain.retrievers import SelfQueryRetriever from langchain_openai import ChatOpenAI import os from dotenv import load_dotenv load_dotenv(.env, overrideTrue) # 初始化通义千问解析模型 llm ChatOpenAI( api_keyos.getenv(DASHSCOPE_API_KEY), base_urlhttps://dashscope.aliyuncs.com/compatible-mode/v1, modelqwen3.6-plus, temperature0 ) embeddings OllamaEmbeddings(modeldengcao/Qwen3-Embedding-0.6B:Q8_0) # 测试数据集带元数据 docs [ Document(page_content科普人才研修班聚焦科学传播能力提升, metadata{year: 2026, type: 培训, num: 60}), Document(page_content人工智能前沿科技科普讲座解读AI应用场景, metadata{year: 2026, type: 讲座, num: 100}), Document(page_content中小学科学教育师资培训侧重科普教学实操, metadata{year: 2025, type: 培训, num: 80}), ] # 向量库初始化 vectorstore Chroma.from_documents(docs, embeddings) # 定义元数据字段规则 metadata_field_info [ AttributeInfo(nameyear, description活动举办年份, typeinteger), AttributeInfo(nametype, description活动类型, typestring), AttributeInfo(namenum, description招生/参与人数, typeinteger), ] document_content_description 科普相关活动介绍 # 自定义查询提示词 CUSTOM_PROMPT_TEMPLATE You are an expert at converting user questions into database queries. Rules: 1. Output ONLY a single standard JSON object. 2. Filter syntax: field operator value多条件用and/or组合 3. 无筛选条件使用 NO_FILTER Metadata fields: {field_info} User question: {question} prompt PromptTemplate( templateCUSTOM_PROMPT_TEMPLATE, input_variables[question], partial_variables{ field_info: \n.join([f{f.name}: {f.description} ({f.type}) for f in metadata_field_info]) } ) # 初始化自查询检索器 retriever SelfQueryRetriever.from_llm( llmllm, vectorstorevectorstore, document_contentsdocument_content_description, metadata_field_infometadata_field_info, promptprompt, verboseTrue ) # 测试结构化检索 res retriever.invoke(查询2026年举办的科普培训活动) print(res)2.5 查询优化多查询检索个人理解通过拓展查询维度弥补单一提问语义覆盖不足的问题。用户的提问往往很简短、模糊单一检索很容易遗漏关键内容。查询优化的思路很简单让大模型基于原始问题生成多个语义相近、表述不同的衍生问题多路检索后合并结果扩大召回范围。个人感受改造成本极低效果立竿见影适合所有模糊提问、极简提问的场景。实战代码from langchain_community.document_loaders import WebBaseLoader from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnableMap from langchain_ollama import OllamaLLM, OllamaEmbeddings from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_core.prompts import ChatPromptTemplate from langchain_chroma import Chroma from langchain.retrievers.multi_query import MultiQueryRetriever import logging # 开启日志 logging.basicConfig(levellogging.INFO) # 初始化模型 llm OllamaLLM(modelqwen:7b) embedding_model OllamaEmbeddings(modeldengcao/Qwen3-Embedding-0.6B:Q8_0) # 加载切片文档 web_docs WebBaseLoader(https://www.zgkpw.org.cn/notice/content_529245.html).load() text_splitter RecursiveCharacterTextSplitter(chunk_size1024, chunk_overlap100) texts text_splitter.split_documents(web_docs) vectorStore Chroma.from_documents(collection_namequery_db, embeddingembedding_model, documentstexts) # 普通检索 vs 多查询优化检索 base_retriever vectorStore.as_retriever() optim_retriever MultiQueryRetriever.from_llm(retrieverbase_retriever, llmllm) # 问答提示词 prompt ChatPromptTemplate.from_template(根据{doc}回答{question}) # 普通检索链路 base_chain RunnableMap({ doc: lambda x: base_retriever.invoke(x[question]), question: lambda x: x[question] }) | prompt | llm |StrOutputParser() # 优化检索链路 opt_chain RunnableMap({ doc: lambda x: optim_retriever.invoke(x[question]), question: lambda x: x[question] }) | prompt | llm |StrOutputParser() # 效果对比 print(普通检索结果, base_chain.invoke({question:研修班活动目的})) print(优化检索结果, opt_chain.invoke({question:研修班活动目的}))2.6 RAG-Fusion查询拓展RRF重排个人理解目前我学习到的精度最高的基础优化方案。它整合了查询拓展和RRF倒数排名融合算法。先多维度生成相似问题、多路检索再对所有检索结果统一打分重排自动过滤噪声、置顶高相关文档解决多路结果杂乱、优质内容下沉的问题。个人感受代码稍复杂但效果最好核心业务场景优先用这个方案。实战代码import uuid from json import dumps, loads from langchain_community.document_loaders import WebBaseLoader from langchain_core.documents import Document from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnableMap, RunnablePassthrough from langchain_ollama import OllamaLLM, OllamaEmbeddings from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_core.prompts import ChatPromptTemplate from langchain_chroma import Chroma # 初始化模型 llm OllamaLLM(modelqwen:7b) embedding_model OllamaEmbeddings(modeldengcao/Qwen3-Embedding-0.6B:Q8_0) # 加载切片文档 web_docs WebBaseLoader(https://www.zgkpw.org.cn/notice/content_529245.html).load() text_splitter RecursiveCharacterTextSplitter(chunk_size1024, chunk_overlap100) texts text_splitter.split_documents(web_docs) vectorStore Chroma.from_documents(collection_namefusion_db, embeddingembedding_model, documentstexts) base_retriever vectorStore.as_retriever() # 生成多查询链路 prompt ChatPromptTemplate.from_template(根据{question}生成3个语义不同的相似问题仅返回问题列表) prompt_chain prompt | llm | StrOutputParser() | (lambda x: x.split(\n)) # RRF重排算法 def reciprocal_rank_fusion(results, k60): fusion_scores {} for result_list in results: for rank, doc in enumerate(result_list): doc_str dumps({page_content: doc.page_content, metadata: doc.metadata}, ensure_asciiFalse) fusion_scores[doc_str] fusion_scores.get(doc_str, 0) 1 / (rank k) # 排序重排 sorted_items sorted(fusion_scores.items(), keylambda x: x[1], reverseTrue) reranked_results [] for doc_json, _ in sorted_items: doc_dict loads(doc_json) reranked_results.append(Document(**doc_dict)) return reranked_results[:4] # 完整问答链路 qa_prompt ChatPromptTemplate.from_template(根据{doc}回答{question}) chain ( RunnablePassthrough.assign(queriesprompt_chain) | RunnableMap({ doc: lambda x: reciprocal_rank_fusion([base_retriever.invoke(q) for q in x[queries]]), question: lambda x: x[question] }) | RunnableMap({ doc: lambda x: \n.join([d.page_content for d in x[doc]]), question: lambda x: x[question] }) | qa_prompt | llm | StrOutputParser() ) # 测试 res chain.invoke({question: 2026年科学传播研修班的举办目的是什么}) print(RAG-Fusion最终结果, res)2.7 混合检索个人理解互补两种检索方式的短板。向量检索擅长语义理解但对专有名词、关键词匹配不够精准BM25关键词检索擅长精准匹配实体、术语但没有语义能力。混合检索就是将二者结合结果融合重排兼顾语义和关键词精准度。个人感受适合政策文档、技术文档、专业术语较多的知识库落地稳定性最强。三、个人学习总结方案选型记录经过逐一实战测试我对七种优化方案的适用场景做了个人总结方便后续开发直接选型不用重复试错轻量化快速开发优先用摘要索引、查询优化改造成本低、效果稳定常规问答场景父子索引、假设性问题索引解决大部分检索不准、上下文缺失问题结构化数据检索元数据索引适配带时间、分类、属性的知识库生产级高精度场景RAG-Fusion、混合检索稳定性和准确率最优这次学习最大的感悟RAG效果的上限从来不是大模型而是检索质量。很多问答效果差的问题根本不需要微调模型做好检索优化就能解决80%的问题。

相关新闻