RAG实战:从零搭建检索增强生成系统,解决大模型幻觉问题

📅 2026/8/8 5:04:37
RAG实战:从零搭建检索增强生成系统,解决大模型幻觉问题
1. 项目概述当大模型遇上“开卷考试”最近和几个做AI应用落地的朋友聊天大家普遍有个痛点大模型LLM在通用知识上对答如流像个博学的“闭卷考生”但一涉及到企业内部文档、最新行业报告或者某个特定领域的私有知识库它就常常开始“一本正经地胡说八道”要么回答得模棱两可要么干脆编造业内叫“幻觉”。这就像让一个学生去参加一场他完全没复习过的考试结果可想而知。于是一个叫RAG检索增强生成的技术就火了起来。你可以把它理解成给大模型配了一个“智能小抄”或者“开卷考试”的外挂。它的核心思路非常直观当用户提出一个问题时系统不是让大模型凭空回忆而是先从你准备好的、可靠的知识库比如公司文档、产品手册、法律条文里快速检索出与问题最相关的几段资料。然后把这些资料和用户的问题一起“喂”给大模型并指令它“请基于以下资料来回答问题。”这样一来大模型生成答案的准确性和可靠性就得到了质的提升因为它有了明确的依据。这个“RAG实战”项目就是一次从零到一搭建一个可用的RAG系统的过程。它不只是一个理论概念而是包含了文档处理、向量检索、提示工程和效果评估等一系列环环相扣的实操环节。无论你是想为自己的团队构建一个智能客服知识库还是想打造一个能快速查询内部技术文档的助手这套流程都能给你提供一个清晰的路线图。接下来我就把自己趟过的路、踩过的坑以及最终跑通的方案毫无保留地分享出来。2. 核心架构与组件选型解析一个完整的RAG系统可以拆解成三个核心阶段索引Indexing、检索Retrieval和生成Generation。每个阶段的技术选型都直接影响到最终效果。2.1 索引阶段把文档变成机器能懂的语言这个阶段的目标是把非结构化的文本如PDF、Word、网页处理成便于后续高效检索的结构化数据。关键步骤是分块Chunking和向量化Embedding。分块策略是第一个关键决策点。你不能简单地把整本100页的PDF扔给系统。分得太细比如每句话一块会丢失上下文分得太大比如每10页一块检索精度会下降且可能超出大模型的上下文窗口限制。经过多次试验我总结出几种策略固定大小分块比如每块512个字符或token。这是最简单的方法用LangChain的RecursiveCharacterTextSplitter可以轻松实现。但缺点也很明显它可能会把一个完整的段落或表格从中间切断。基于分隔符的分块按照段落\n\n、标题##、句号.等自然语言边界进行分割。这种方式能更好地保持语义完整性。语义分块这是更高级的方法使用嵌入模型计算句子间的相似度在语义发生较大变化的地方进行切割。虽然效果更好但实现更复杂。实操心得对于大多数中文文档我推荐采用“重叠分块”策略。例如设置块大小为1000字符重叠部分为200字符。这样既能保证块内信息的相对完整又能在检索时通过重叠部分提供一定的上下文避免信息被硬生生割裂。这个重叠的“缓冲区”对于提高召回率非常有效。向量化是第二个核心。分块后的文本需要被转换为数值向量即嵌入向量这个过程由嵌入模型Embedding Model完成。向量的质量直接决定了检索的准确性。选型时主要看几点支持语言处理中文必须选择对中文语义理解好的模型。向量维度常见的有384维、768维、1024维等。更高的维度通常能承载更多信息但计算和存储成本也更高。模型性能包括准确度和推理速度。我对比了几个主流选择OpenAI的text-embedding-ada-002效果公认很好使用简单但需要网络调用有延迟和成本且数据需出境。开源模型如BAAI/bge-large-zh、moka-ai/m3e-base。这些是本地部署的首选。bge-large-zh在中文检索任务评测中表现突出m3e-base则在通用性和速度上比较均衡。最终我选择了bge-large-zh-v1.5因为它专门针对中文检索进行了优化在MTEB等基准测试上中文表现最佳。向量数据库的选择生成的向量需要被存储和快速检索。这就用到了向量数据库。我评估了Chroma轻量、简单、Milvus功能强大、适合生产级和Qdrant性能优异、API友好。对于快速原型和中小规模应用Chroma的内存模式非常方便如果需要持久化、分布式和更高级的过滤功能Qdrant和Milvus是更好的选择。本项目为演示完整性选择了支持持久化的Chroma。2.2 检索与生成阶段精准查找与智能作答索引建好后就进入了实时查询阶段。检索环节当用户提问时系统首先用同样的嵌入模型将问题转换为向量然后在向量数据库中进行相似度搜索通常使用余弦相似度找出前k个例如k4最相关的文本块。这里有一个进阶技巧叫重排序Re-ranking。初次向量检索返回的top-k结果可能只考虑了语义相似度而忽略了与问题在逻辑、关键词匹配上的精确度。可以引入一个专门的交叉编码器模型如BAAI/bge-reranker-large对这k个结果进行二次精排选出最相关的2-3个再送给大模型。这能显著提升最终答案的质量但会增加少量延迟。生成环节这是最后一步也是体现“智能”的一步。我们将检索到的相关文本块和用户问题组合成一个精心设计的提示词Prompt发送给大语言模型。提示词的设计至关重要。一个糟糕的提示词会让大模型忽略你提供的资料。一个有效的提示词模板通常包含角色设定你是一个专业的XX领域助手。指令请严格根据以下提供的上下文信息来回答问题。上下文将检索到的文本块用明确的标记如context.../context包起来。问题用户的原问题。约束如果上下文信息不足以回答问题请明确回答“根据已知信息无法回答该问题”禁止编造。大模型的选择同样灵活可以是云端API如GPT-4、文心一言、通义千问也可以是本地部署的开源模型如ChatGLM3、Qwen、Llama 3。选择时需权衡效果、成本、数据隐私和响应速度。3. 从零搭建一个可运行的RAG系统实战理论讲完了我们动手搭一个。假设我们的知识库是几份关于“机器学习项目管理”的PDF文档。目标是构建一个能回答相关问题的助手。3.1 环境准备与依赖安装首先创建一个干净的Python环境并安装核心库。# 创建并激活虚拟环境可选但推荐 python -m venv rag_env source rag_env/bin/activate # Linux/Mac # rag_env\Scripts\activate # Windows # 安装核心包 pip install langchain langchain-community langchain-chroma # LangChain核心及Chroma集成 pip install sentence-transformers # 用于运行开源嵌入模型 pip install pypdf # 用于读取PDF文档 pip install tiktoken # 用于文本分词计算token长度 pip install openai # 如需使用OpenAI的模型如果计划使用本地开源LLM可能还需要安装transformers,torch等。这里我们先以使用OpenAI API为例因为部署最简单效果也稳定。3.2 文档加载、分块与向量库构建我们创建一个build_vectorstore.py脚本完成索引的构建。import os from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceEmbeddings from langchain.vectorstores import Chroma # 1. 加载文档 documents [] pdf_folder ./knowledge_base for file in os.listdir(pdf_folder): if file.endswith(.pdf): file_path os.path.join(pdf_folder, file) loader PyPDFLoader(file_path) docs loader.load() # 每个页面变成一个Document对象 documents.extend(docs) print(f已加载 {len(documents)} 页文档。) # 2. 文本分块 text_splitter RecursiveCharacterTextSplitter( chunk_size1000, # 每个块约1000字符 chunk_overlap200, # 块间重叠200字符 length_functionlen, separators[\n\n, \n, 。, , , , ] # 中文友好分隔符 ) chunks text_splitter.split_documents(documents) print(f分块后得到 {len(chunks)} 个文本块。) # 3. 初始化嵌入模型使用本地开源模型 embed_model HuggingFaceEmbeddings( model_nameBAAI/bge-large-zh-v1.5, model_kwargs{device: cpu}, # 如有GPU可改为 cuda encode_kwargs{normalize_embeddings: True} # 归一化方便余弦相似度计算 ) # 4. 创建并持久化向量数据库 vectorstore Chroma.from_documents( documentschunks, embeddingembed_model, persist_directory./chroma_db # 向量数据库保存路径 ) vectorstore.persist() print(向量数据库构建完成已保存至 ./chroma_db)注意bge-large-zh-v1.5模型第一次运行时会从Hugging Face下载需要一定时间。chunk_size需要根据你选用的大模型的上下文窗口来调整。例如如果后续使用GPT-3.5-turbo上下文约4k token你提供的上下文检索到的块加上问题本身不能超过这个限制。3.3 检索链与问答接口实现接下来我们创建query_rag.py实现检索与生成的全流程。from langchain.vectorstores import Chroma from langchain_community.embeddings import HuggingFaceEmbeddings from langchain.chains import RetrievalQA from langchain_openai import ChatOpenAI from langchain.prompts import PromptTemplate import os # 0. 设置OpenAI API Key (如果使用本地模型这部分不同) os.environ[OPENAI_API_KEY] your-api-key-here # 1. 加载已构建的向量数据库 embed_model HuggingFaceEmbeddings(model_nameBAAI/bge-large-zh-v1.5) vectorstore Chroma( persist_directory./chroma_db, embedding_functionembed_model ) # 2. 定义提示词模板 prompt_template 你是一个专业的机器学习项目管理助手。请严格根据以下提供的上下文信息来回答问题。如果上下文信息不足以回答问题请明确回答“根据已知信息无法回答该问题”禁止编造任何信息。 上下文 {context} 问题{question} 请根据上下文给出答案 PROMPT PromptTemplate( templateprompt_template, input_variables[context, question] ) # 3. 初始化大语言模型 llm ChatOpenAI( model_namegpt-3.5-turbo, # 也可用 gpt-4 temperature0.1 # 温度调低让输出更确定、更基于事实 ) # 4. 构建检索问答链 qa_chain RetrievalQA.from_chain_type( llmllm, chain_typestuff, # 最简单的方式将所有检索到的上下文“塞”进提示词 retrievervectorstore.as_retriever(search_kwargs{k: 4}), # 检索4个最相关块 chain_type_kwargs{prompt: PROMPT}, return_source_documentsTrue # 返回源文档便于追溯 ) # 5. 问答函数 def ask_question(question): result qa_chain.invoke({query: question}) answer result[result] sources result[source_documents] print(f\n问题{question}) print(f\n答案{answer}) print(f\n参考来源) for i, doc in enumerate(sources): print(f [{i1}] 来源文件{doc.metadata.get(source, N/A)}, 页码{doc.metadata.get(page, N/A)}) # 打印来源片段预览 print(f 片段{doc.page_content[:150]}...) return answer # 6. 测试 if __name__ __main__: while True: user_q input(\n请输入您的问题输入quit退出: ) if user_q.lower() quit: break ask_question(user_q)运行这个脚本你就可以通过命令行与你的知识库对话了。它会先检索再生成答案并附上答案的来源片段这大大增加了可信度和可追溯性。4. 效果优化与高级技巧基础流程跑通后你会发现一些痛点比如答案可能不够精准或者检索到了资料但模型没用好。下面分享几个提升效果的进阶技巧。4.1 检索优化不止于语义相似度单纯的向量相似度检索有时会漏掉关键信息特别是当问题表述和文档表述差异较大时。混合检索Hybrid Search结合稠密检索向量相似度和稀疏检索如BM25关键词匹配。LangChain可以很方便地集成BM25Retriever。这样既能捕捉语义关联又能保证关键词的精确命中。多向量检索除了对文本块本身做嵌入还可以对块的摘要、提出的问题甚至是假设的答案做嵌入建立多个向量索引从不同角度检索。元数据过滤在检索时加入过滤条件。例如你可以在索引时为每个块添加元数据如{“doc_type”: “用户手册”, “year”: “2023”}。查询时可以要求“只检索2023年用户手册中的内容”这能极大提升检索的精准度。4.2 提示工程与链式调用优化chain_typestuff是最简单的方式但如果检索到的上下文总长度超过模型限制就会失败。还有其它几种链类型Map-Reduce将每个检索到的文档块单独发送给LLM生成一个答案Map然后将所有初步答案汇总再让LLM合成一个最终答案Reduce。适合处理大量文档但调用成本高、速度慢。Refine迭代式处理。用第一个文档块生成初始答案然后依次用后续文档块去优化和精炼这个答案。生成的答案可能更连贯但速度也慢。ReAct让LLM以“思考-行动-观察”的循环来推理可以主动决定何时检索、检索什么。这更智能但实现复杂。对于大多数场景“stuff”配合一个高质量的提示词模板已经足够。提示词中可以加入更明确的指令例如“请以要点列表的形式总结答案”、“请比较上下文中的A方案和B方案”。4.3 评估与迭代如何知道系统变好了这是最容易忽视但至关重要的一环。你不能靠感觉优化系统。需要建立评估体系。人工评估构建一个测试问题集QA对人工评判答案的准确性、相关性和流畅度。这是黄金标准但成本高。自动评估指标检索相关度计算检索到的文档与标准答案的相似度如使用嵌入模型。答案忠实度生成的答案是否严格基于提供的上下文可以用另一个LLM来判断。答案相关性生成的答案是否直接回答了问题A/B测试在生产环境中可以分流少量用户请求到不同配置的系统如不同分块大小、不同检索策略对比关键指标如用户满意度、问题解决率。建立一个持续迭代的闭环数据准备 → 构建索引 → 评估效果 → 分析问题是检索不准还是生成不好→ 调整策略 → 重新构建。5. 常见问题、踩坑记录与排查指南在实际搭建过程中我遇到了不少问题这里列出来帮你避坑。5.1 检索不到相关内容或精度差可能原因1分块策略不当。块太大包含无关信息稀释了核心语义块太小上下文不完整。排查检查几个典型问题的检索结果看返回的文本块是否真的包含了答案。解决调整chunk_size和chunk_overlap。对于技术文档500-1500字符是常用范围。尝试基于章节标题分块。可能原因2嵌入模型不匹配。使用的嵌入模型对中文语义理解不佳或者训练领域与你的知识库领域差异太大。排查用几个简单的同义词或相关词在向量库中做相似度搜索看结果是否合理。解决更换更强大的中文嵌入模型如bge-large-zh。如果领域特殊如医学、法律可以考虑用领域数据对开源嵌入模型进行微调。可能原因3问题表述与文档表述不一致。用户问“怎么部署模型”文档里写的是“模型上线步骤”。解决实施查询扩展或查询重写。在检索前用LLM将用户问题扩展成几个相关的问法或用更正式的术语重写再用这些查询去检索。5.2 大模型忽略上下文依然胡编乱造可能原因1提示词不够强硬。模型没有收到必须依据上下文的强指令。解决强化提示词。使用“必须”、“严格禁止”、“只能”等词语。在提示词中明确写出惩罚项如“如果你编造信息将会产生严重后果”。可能原因2上下文信息过多或噪声大。检索到的4个块里可能只有1个是真正相关的其他3个干扰了模型。解决引入前文提到的重排序模型只将最相关的1-2个块送给LLM。或者在提示词中明确要求模型“只根据第X段和第Y段上下文回答”。可能原因3LLM本身“幻觉”倾向强。解决降低LLM的temperature参数如设为0.1让它更“保守”。或者换用“幻觉”更少的模型。5.3 系统响应速度慢可能原因1嵌入模型推理慢。特别是大型嵌入模型在CPU上运行。解决使用GPU运行嵌入模型。或者换用更轻量的模型如m3e-base在精度和速度间权衡。可能原因2向量数据库检索慢。当向量数量达到百万级时简单暴力搜索会很慢。解决向量数据库使用索引如HNSW。确保Chroma/Qdrant等配置了正确的索引参数。对于超大规模数据考虑分布式向量数据库。可能原因3LLM API调用延迟高。解决考虑对答案进行缓存。对于相同或相似的问题直接返回缓存答案。或者部署本地LLM消除网络延迟。5.4 表格、代码等非连续文本处理效果差PDF中的表格和代码块用常规分块方式会被打乱失去结构。解决使用专门的文档加载器或解析库。例如unstructured库对表格的识别能力较强。对于代码可以按函数或类进行分块。另一种思路是将这些非连续文本先渲染成图片再用多模态模型如GPT-4V进行识别和描述将描述文本纳入向量库。当然这复杂度会高很多。6. 生产环境部署考量想把原型变成真正可用的服务还需要考虑以下几点数据更新与增量索引知识库不是一成不变的。需要设计一个流程当有新文档加入或旧文档修改时能够只对变动的部分进行重新分块和向量化并更新向量数据库而不是全量重建。多路召回与融合排序如前所述结合关键词、向量、甚至图数据库等多种检索方式并对结果进行智能排序是提升召回率和准确率的关键。可观测性与日志记录每一次问答的原始问题、检索到的文档、生成的答案、耗时以及用户反馈如果有。这些日志是分析和优化系统最宝贵的资料。安全与权限确保RAG系统只能检索到用户有权访问的文档。这需要在检索时加入基于元数据如部门、权限等级的强过滤。成本控制如果使用商用API需要监控token消耗对长文档进行智能摘要后再索引或者设置使用频率限制。给大模型装上“开卷考试”的外挂RAG技术让大模型从“通才”变成了特定领域的“专家”。这个过程就像教一个聪明的学生如何高效使用参考资料首先要帮他建立一套整理有序的档案系统索引然后训练他快速找到相关文件的能力检索最后教会他如何精准地归纳文件内容来答题生成。这套方法目前是解决大模型知识滞后和幻觉问题最实用、最流行的路径之一。我自己的体会是开始不必追求最复杂的架构先用最简单的“Stuff”链和本地嵌入模型跑通全流程看到效果建立信心。然后再从评估中发现瓶颈有针对性地引入重排序、混合检索这些进阶技术。每一次迭代你都会对这个系统的“脾气”更了解最终让它成为你工作中得心应手的智能伙伴。