从零构建RAG系统:基于LangChain与本地模型的检索增强生成实践

📅 2026/7/26 13:24:56
从零构建RAG系统:基于LangChain与本地模型的检索增强生成实践
1. 项目概述与核心价值如果你正在探索如何让大语言模型LLM更“靠谱”地回答你公司内部文档、产品手册或者某个专业领域的问题那么检索增强生成RAG技术几乎是你绕不开的路径。简单来说RAG就是给“记忆力”有限且可能“信口开河”的LLM配了一个外置的“知识库”和“搜索引擎”。当用户提问时系统不是让LLM凭空回忆或编造而是先从你的专属知识库里快速找到最相关的几段资料然后把问题和这些资料一起塞给LLM让它“基于给定材料”来组织答案。这就像考试时允许你带指定参考资料开卷答题答案的准确性和专业性自然大幅提升。我最初接触RAG是为了解决内部技术文档的问答需求。直接问ChatGPT某个内部API的细节它要么不知道要么就开始一本正经地胡说八道。而RAG的核心价值正在于此它极大地缓解了LLM的“幻觉”问题并使其能够利用私有的、最新的、高度专业的领域知识。你不再需要耗费巨资去微调一个专属模型只需要把你的文档处理好搭建一个检索管道就能立刻获得一个“懂你业务”的智能助手。无论是法律条款查询、技术支持、产品知识库还是学术文献摘要RAG都提供了一种成本可控、效果显著的实现方案。本文将从一个实践者的角度手把手拆解如何从零构建一个可运行的简单RAG系统重点不仅在于“怎么做”更在于“为什么这么做”以及过程中那些容易踩坑的细节。2. 系统核心原理与架构设计2.1 RAG 的工作流程拆解一个典型的RAG系统工作流可以清晰地分为“索引”和“查询”两个阶段理解这两个阶段是设计和优化的基础。索引阶段线下进行这是知识“入库”的过程。你的原始文档PDF、Word、TXT、网页等经过一系列处理转化为向量数据库里一条条可被快速检索的记录。这个过程是离线的一旦完成除非知识库更新否则不需要重复进行。其核心步骤包括文档加载使用相应的加载器Loader读取各种格式的文档提取出纯文本。文本分割将长文本切割成大小适中的“片段”Chunks。这是关键一步分割的大小和策略直接影响检索质量。向量化嵌入使用嵌入模型Embedding Model将每个文本片段转换为一个高维向量比如768或1536维。这个向量就像是这段文本的“数学指纹”语义相近的文本其向量在空间中的距离也更近。向量存储将文本片段、其对应的向量以及可能的元数据如来源文件名、页码一并存入向量数据库。查询阶段线上实时这是系统“工作”的过程响应用户的每次提问。问题向量化将用户输入的自然语言问题使用与索引阶段相同的嵌入模型转换为一个查询向量。相似性检索在向量数据库中寻找与查询向量“距离”最近即最相似的K个文本片段。这个“距离”通常用余弦相似度或欧氏距离来衡量。上下文构建将检索到的Top K个文本片段按照相关性或其他策略组合成一个“上下文”字符串。提示工程与生成精心设计一个提示词Prompt将用户问题和检索到的上下文一起提交给LLM。典型的Prompt模板是“请基于以下上下文信息回答问题。如果上下文不包含答案请说‘根据已知信息无法回答’。上下文{context} 问题{question}”。答案返回LLM基于指令和提供的上下文生成最终答案返回给用户。2.2 关键组件选型与考量构建RAG系统就像搭积木每个组件的选择都影响着最终系统的性能、成本和易用性。1. 嵌入模型这是系统的“语义理解引擎”。它的任务是把文本变成向量。选择时主要看性能在标准基准测试如MTEB上的综合排名和特定任务如检索的表现。维度向量维度越高通常表征能力越强但也会增加存储和计算开销。常用的有384维轻量、768维平衡、1536维如OpenAI的text-embedding-3-small。上下文长度模型单次能处理的最大文本长度决定了你文本分割的上限。部署方式本地部署如BAAI/bge-small-zh-v1.5还是调用API如OpenAI, Cohere。本地部署无网络延迟、成本固定但对计算资源有要求API调用方便但会产生持续费用和网络依赖。实操心得对于中文场景我强烈推荐优先测试智源开源的BGE系列模型如BAAI/bge-large-zh-v1.5它在中文语义匹配上表现非常出色且可以免费本地部署。起步阶段用bge-small-zh在CPU上也能跑起来是快速验证原型的利器。2. 向量数据库这是系统的“记忆仓库”负责高效存储和检索向量。选择时考虑易用性与集成是否提供Python SDK是否与LangChain等框架无缝集成。性能支持百万、千万级向量的毫秒级检索能力。功能是否支持过滤按元数据筛选、动态更新、持久化存储等。部署模式云服务、本地部署还是内存型。避坑指南初期验证或简单应用ChromaDB是绝佳选择。它轻量、纯Python、无需额外服务数据可持久化到磁盘。它的简单性让你能聚焦于RAG流程本身而不是折腾数据库配置。当数据量达到百万级或需要分布式时再考虑Milvus、Pinecone云服务或Qdrant。3. 大语言模型这是系统的“大脑”负责最终的理解和生成。选择取决于任务需求是否需要极强的推理、代码生成能力还是通用问答即可。成本与隐私是否接受调用GPT-4等闭源API的成本和潜在的数据出境风险。部署条件是否有GPU资源本地运行Llama、Qwen等开源模型。经验之谈对于企业内部RAG出于数据安全和长期成本考虑我倾向于使用开源模型本地部署。例如Qwen1.5-7B-Chat或Llama-3-8B-Instruct在消费级显卡如RTX 4090上即可流畅运行通过量化技术如GGUF, AWQ还能在更小的显存上部署。它们配合优质的上下文完全能满足专业领域的问答需求。4. 编排框架LangChain和LlamaIndex是两大主流选择。它们将上述组件模块化提供了构建RAG流程的高层抽象。LangChain更像一个“万能胶水”和“工作流编排器”组件极其丰富灵活性高适合构建复杂、定制化的AI应用链。LlamaIndex自称“数据框架”专精于RAG和数据索引在文档处理、索引结构、高级检索策略如分层索引、自动检索器上更深入、更“开箱即用”。选择建议如果你是初学者想快速搭建一个标准RAGLlamaIndex的教程和默认配置可能更直接。如果你预见到未来需要集成各种工具如搜索引擎、API、设计复杂逻辑链LangChain的抽象能力会更强大。本文为展示原理将使用LangChain因为它对每个步骤的暴露更清晰。3. 从零开始的详细实现步骤下面我们使用LangChain和ChromaDB构建一个基于本地嵌入模型和开源LLM的简易RAG系统。假设我们的知识库是一系列关于“机器学习基础”的Markdown文档。3.1 环境准备与依赖安装首先创建一个干净的Python环境推荐使用conda或venv然后安装核心库。# 创建并激活虚拟环境以conda为例 conda create -n rag_demo python3.10 conda activate rag_demo # 安装核心依赖 pip install langchain langchain-community langchain-chroma # LangChain核心及Chroma集成 pip install sentence-transformers # 用于运行本地嵌入模型如BGE pip install pypdf python-docx markdown # 支持多种格式的文档加载器 pip install unstructured[md] # 增强的文档解析能力 pip install tiktoken # 用于文本分割的令牌计数即使不用OpenAI模型其分割器也很好用 pip install accelerate # 运行本地LLM所需 # 注意本地LLM运行库如transformers, torch根据模型选择安装此处暂不安装后续选择模型时说明。3.2 文档加载与预处理我们创建一个docs文件夹放入我们的知识文档例如ml_basics.md,neural_networks.md。然后编写索引脚本index_docs.py。# index_docs.py from langchain_community.document_loaders import DirectoryLoader, TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_huggingface import HuggingFaceEmbeddings from langchain_chroma import Chroma import os # 1. 加载文档 documents_path ./docs # 使用通配符加载所有.md文件 loader DirectoryLoader(documents_path, glob**/*.md, loader_clsTextLoader) raw_documents loader.load() print(f成功加载 {len(raw_documents)} 个文档) # 2. 分割文本 text_splitter RecursiveCharacterTextSplitter( chunk_size500, # 每个片段的字符数约 chunk_overlap100, # 片段之间的重叠字符数避免上下文割裂 length_functionlen, separators[\n\n, \n, 。, , , , , , ] # 中文优先按句分割 ) all_splits text_splitter.split_documents(raw_documents) print(f文档被分割成 {len(all_splits)} 个文本片段) # 3. 初始化嵌入模型 # 使用开源的BGE模型首次运行会自动从HuggingFace下载 embed_model HuggingFaceEmbeddings( model_nameBAAI/bge-small-zh-v1.5, # 中文小模型适合快速实验 model_kwargs{device: cpu}, # 使用CPU如果有GPU可改为cuda encode_kwargs{normalize_embeddings: True} # 标准化向量便于使用余弦相似度 ) # 4. 创建并持久化向量存储 persist_directory ./chroma_db vectordb Chroma.from_documents( documentsall_splits, embeddingembed_model, persist_directorypersist_directory ) vectordb.persist() # 将数据持久化到磁盘 print(f向量索引已创建并保存至 {persist_directory})关键参数解析chunk_size500这是一个需要反复调试的超参数。太小会导致信息碎片化太大会引入噪声并影响检索精度。对于中文500-800字符是一个不错的起点。你需要根据你的文档类型技术文档段落长对话记录短和LLM的上下文窗口来调整。chunk_overlap100重叠是为了避免一个完整的句子或概念被硬生生切成两半导致检索时失去关键信息。重叠部分通常占chunk_size的10%-20%。normalize_embeddingsTrue将向量归一化为单位长度。这样向量点积就等于余弦相似度是向量检索中最常用的相似度度量方式。3.3 构建检索与问答链索引建好后我们编写查询脚本query_rag.py。这里我们先演示不调用外部LLM只完成检索部分以验证索引是否有效。# query_rag.py (版本1仅检索) from langchain_huggingface import HuggingFaceEmbeddings from langchain_chroma import Chroma # 加载相同的嵌入模型 embed_model HuggingFaceEmbeddings( model_nameBAAI/bge-small-zh-v1.5, model_kwargs{device: cpu}, encode_kwargs{normalize_embeddings: True} ) # 从磁盘加载已创建的向量数据库 persist_directory ./chroma_db vectordb Chroma( persist_directorypersist_directory, embedding_functionembed_model ) # 进行相似性检索 question 什么是过拟合 retriever vectordb.as_retriever(search_kwargs{k: 3}) # 检索最相似的3个片段 relevant_docs retriever.invoke(question) print(f问题: {question}\n) print(检索到的最相关文档片段) for i, doc in enumerate(relevant_docs): print(f\n--- 片段 {i1} (相关性分数: {doc.metadata.get(_score, N/A)}) ---) print(doc.page_content[:300] ...) # 打印前300个字符 print(f来源: {doc.metadata.get(source, Unknown)})运行这个脚本你应该能看到系统返回了与“过拟合”最相关的几个文本片段及其来源。这证明了我们的检索管道是通的。3.4 集成大语言模型生成答案现在我们把检索到的上下文和问题一起交给LLM来生成最终答案。这里我们以调用开源模型Qwen1.5-7B-Chat的本地部署为例。你需要确保有足够的GPU内存约8GB或使用CPU速度较慢。首先安装运行模型所需的库pip install transformers torch然后更新query_rag.py集成LLM。# query_rag.py (版本2完整RAG) from langchain_huggingface import HuggingFaceEmbeddings from langchain_chroma import Chroma from langchain.prompts import PromptTemplate from langchain_huggingface import HuggingFacePipeline from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline import torch # 1. 加载嵌入模型和向量数据库同上 embed_model HuggingFaceEmbeddings(model_nameBAAI/bge-small-zh-v1.5, ...) vectordb Chroma(persist_directory./chroma_db, embedding_functionembed_model) retriever vectordb.as_retriever(search_kwargs{k: 3}) # 2. 加载本地LLM model_id Qwen/Qwen1.5-7B-Chat # 使用Qwen 7B模型需提前从HuggingFace下载 tokenizer AutoTokenizer.from_pretrained(model_id, trust_remote_codeTrue) # 根据硬件情况选择加载方式 if torch.cuda.is_available(): model AutoModelForCausalLM.from_pretrained( model_id, torch_dtypetorch.float16, # 半精度减少显存占用 device_mapauto, # 自动分配多GPU trust_remote_codeTrue ) else: # CPU模式使用float32速度较慢 model AutoModelForCausalLM.from_pretrained( model_id, torch_dtypetorch.float32, device_mapcpu, trust_remote_codeTrue ) # 创建文本生成管道 pipe pipeline( text-generation, modelmodel, tokenizertokenizer, max_new_tokens512, # 生成答案的最大长度 temperature0.1, # 低温度使输出更确定、更聚焦 do_sampleTrue, ) llm HuggingFacePipeline(pipelinepipe) # 3. 设计提示词模板 prompt_template 请严格根据以下提供的上下文信息来回答问题。如果上下文中的信息不足以回答问题请直接说“根据提供的上下文我无法回答这个问题”。不要编造信息。 上下文 {context} 问题{question} 请基于上下文给出答案 PROMPT PromptTemplate( templateprompt_template, input_variables[context, question] ) # 4. 构建RAG链 from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough def format_docs(docs): return \n\n.join(doc.page_content for doc in docs) rag_chain ( {context: retriever | format_docs, question: RunnablePassthrough()} | PROMPT | llm | StrOutputParser() ) # 5. 提问并获取答案 question 什么是过拟合如何避免 answer rag_chain.invoke(question) print(f问题: {question}\n) print(f答案: {answer})重要提示首次运行加载Qwen 7B模型会下载约15GB的数据请确保网络通畅和磁盘空间。如果硬件资源有限可以考虑更小的模型如Qwen1.5-1.8B-Chat或使用量化版本GGUF格式通过llama.cpp或ctransformers库在CPU上高效运行。这是本地部署LLM的另一个常见优化路径。4. 高级优化与常见问题排查一个基础的RAG系统搭建完成后其效果往往距离“好用”还有差距。以下是几个关键的优化方向和常见问题的解决方法。4.1 检索质量优化技巧检索是RAG的基石检索不准后续生成再好也白搭。优化文本分割chunk_size是黄金参数。对于概念定义类问题较小的chunk300-500字符可能更精准对于需要推理、总结的问题较大的chunk800-1000字符能提供更完整的上下文。可以采用分层索引策略同时存储不同粒度的chunk检索时合并结果。改进检索器调整检索数量search_kwargs{k: n}。n太小可能信息不全n太大会引入噪声。通常从3-5开始测试。使用MMR最大边际相关性LangChain的retriever可以设置search_typemmr。它在保证相关性的同时增加结果之间的多样性避免返回多个高度重复的片段。retriever vectordb.as_retriever( search_typemmr, search_kwargs{k: 5, fetch_k: 20} # 先取20个再用MMR精选5个 )元数据过滤如果你的文档有清晰的元数据如章节、文档类型、日期可以在检索时加入过滤条件缩小搜索范围提升精度。retriever vectordb.as_retriever( search_kwargs{k: 3, filter: {section: 第三章}} )尝试重排序简单的向量相似度检索有时不够准。可以引入一个交叉编码器模型如BAAI/bge-reranker-large对初步检索到的Top N个结果进行重新打分和排序。交叉编码器同时编码问题和候选文档计算出的相关性分数通常比单纯的向量点积更精确但计算开销也更大。实操心得对于大多数中小型知识库万级文档以内精心调优的文本分割策略 合适的嵌入模型 MMR检索已经能取得不错的效果。重排序是追求极致精度时的“杀手锏”可以放在后期优化。4.2 提示工程与答案生成优化检索到正确的上下文后如何让LLM用好它们设计强约束的Prompt如示例中所示Prompt必须清晰、强硬地指令LLM“基于上下文回答”并明确告知“不知道就说不”。可以加入角色设定“你是一个严谨的客服专家”来进一步规范其行为。上下文管理LLM有上下文长度限制。当检索到的总上下文过长时需要做截断或智能摘要。可以按相关性分数对片段排序优先保留高分片段直到填满上下文窗口。让LLM引用来源要求LLM在答案中注明依据的文档片段编号或来源这不仅能增加可信度也方便用户回溯核查。可以在Prompt中加入“请在答案末尾以‘参考来源[片段编号]’的格式注明依据。”处理无答案情况除了在Prompt中约束还可以在程序逻辑上做判断。例如如果检索到的所有片段与问题的相似度都低于某个阈值如0.5可以直接返回“未找到相关信息”而无需调用LLM节省成本。4.3 常见问题与排查清单问题现象可能原因排查与解决方案答案与上下文无关胡编乱造1. Prompt约束力不够。2. 检索到的上下文完全不相关。3. LLM本身“幻觉”倾向强。1. 强化Prompt使用“必须”、“严格根据”等词并添加不答惩罚示例。2. 检查检索结果打印出relevant_docs看内容是否真的与问题相关。若不相关需优化嵌入模型或分割策略。3. 尝试降低生成温度temperature0.1或换用更“听话”的模型。答案遗漏了上下文中的关键信息1. 上下文太长LLM未关注到全部。2. 关键信息被分割到了不同的chunk中。1. 优化上下文组织将最相关的片段放在最前或最后有些模型对首尾位置更敏感。2. 调整chunk_size和chunk_overlap确保关键句子完整。尝试使用按句或按段分割的RecursiveCharacterTextSplitter。检索速度慢1. 向量数据库数据量大且未建索引。2. 嵌入模型推理慢特别是CPU运行。1. 确保向量数据库如Chroma使用了有效的索引如HNSW。对于大规模数据考虑专业向量数据库。2. 考虑使用更小的嵌入模型如BAAI/bge-small或启用GPU加速。对于API模型检查网络延迟。回答“根据上下文无法回答”但明明上下文里有1. LLM理解能力有限未能从上下文中提取信息。2. 上下文表述与问题措辞差异大。1. 尝试在Prompt中给出一个从给定上下文中成功找到答案的例子少样本提示。2. 使用重排序器提升最相关片段的排名或增加检索数量k。处理长文档时内存溢出1. 一次性加载整个大文档进行分割。2. 嵌入模型处理长序列超限。1. 使用流式加载或分块加载文档。2. 确保chunk_size小于嵌入模型的max_seq_length。对于超长文档先按章节分割再对每章进行chunk。5. 项目扩展与应用场景展望当你掌握了基础RAG的搭建后可以朝着更复杂、更实用的方向演进。1. 多轮对话带历史记忆当前的系统是单轮的。要实现多轮对话需要将历史对话记录也纳入上下文。一种简单有效的方法是将之前的“问答对”也作为文本片段存入向量库在检索时同时检索历史对话和知识库。更高级的做法是使用ConversationBufferMemory或ConversationSummaryMemory等LangChain记忆组件来管理对话历史。2. 混合检索策略除了向量相似度检索可以结合关键词检索如BM25。例如先用关键词快速筛选出一批候选文档再用向量检索进行精排。这种“粗排精排”的策略能兼顾召回率和准确率。LangChain的EnsembleRetriever可以支持这种混合检索。3. 代理Agent模式让RAG系统不再是被动问答而是能主动调用工具。例如用户问“我们去年Q3的销售额是多少”系统可以先检索内部知识库找到数据分析报告的位置和格式然后自动调用SQL查询工具去数据库获取具体数字最后组织成答案。这需要将RAG链与LangChain的Agent框架结合。4. 面向生产环境的考量异步处理文档索引和查询都可以异步化提升系统吞吐。缓存对常见问题的检索结果或最终答案进行缓存显著降低响应延迟和LLM调用成本。监控与评估建立评估体系监控检索命中率、答案准确率、用户反馈等指标。可以使用RAGAS等框架进行自动化评估。持续更新设计知识库的增量更新机制避免每次全量重建索引。从我自己的实践来看RAG的搭建是一个“快速验证、持续迭代”的过程。不要一开始就追求完美的架构。用最简单的流程如本文所述跑通整个管道用你的真实数据测试效果。然后从最痛的痛点开始优化——是检索不准那就调分割、换模型、加重排。是答案不好那就雕琢Prompt、换LLM、加后处理。每一次迭代都围绕具体的评估指标和用户反馈进行这样构建出来的系统才能真正解决业务问题。