
1. 项目概述从零到一构建RAG管道如果你已经对RAG检索增强生成的基本概念有所了解知道它能让大语言模型LLM突破自身知识局限从外部知识库中“找答案”那么下一步最实际的问题就是这东西到底怎么搭起来理论听起来很美但代码怎么写流程怎么串坑在哪里这正是我们这次要解决的问题。我将以一个最常见的场景——基于文档的智能问答为例带你用LangChain这个目前最流行的框架亲手搭建一个可运行、可调试的RAG Pipeline。这个管道将涵盖从文档加载、文本分割、向量化存储到检索、生成的全流程。无论你是想为自己的项目快速集成一个知识库问答功能还是想深入理解RAG的工程实现细节这篇手把手的指南都能让你获得可以直接复用的代码和避坑经验。2. 核心组件选型与设计思路在动手写代码之前花点时间思考组件选型至关重要。RAG Pipeline不是一堆技术的简单堆砌每个环节的选择都会直接影响最终效果和系统复杂度。我的设计思路遵循“核心流程标准化关键组件可替换”的原则确保管道既健壮又灵活。2.1 为什么选择LangChain作为框架市面上有LlamaIndex、Haystack等多个优秀的LLM应用框架。我选择LangChain作为入门和实战的首选主要基于以下几点考量1. 生态成熟度与社区活跃度LangChain拥有目前最庞大的用户和开发者社区。这意味着当你遇到一个具体问题时比如“如何用LangChain连接某国产模型”极大概率能在GitHub Issues、Discord或Stack Overflow上找到现成的解决方案或讨论。丰富的第三方集成超过数百种工具、数据源和模型让你能快速对接现有系统避免重复造轮子。2. 声明式编程与LCELLangChain Expression Language这是LangChain的核心魅力。LCEL允许你用链式调用的方式像搭积木一样声明式地组合各种组件模型、提示词、检索器、输出解析器等。代码非常直观易于理解和调试。例如一个简单的RAG链可以写成retriever | prompt | llm | output_parser逻辑一目了然。3. 良好的抽象与灵活性LangChain在提供高层抽象如RetrievalQA链的同时也允许你深入到每一层进行定制。你可以轻松替换向量数据库、文本分割器、嵌入模型或LLM。这种灵活性对于后续优化和应对不同场景需求至关重要。4. 完善的文档与教程尽管LangChain更新很快但其官方文档相对全面并且有大量的第三方博客、视频教程作为补充学习曲线相对平缓。注意LangChain因其抽象层较多有时会被诟病“黑盒”或性能开销大。但对于快速构建原型、理解流程和大多数生产场景来说其便利性远大于弊端。性能关键环节可以通过自定义组件或直接调用底层库来优化。2.2 管道核心组件拆解一个基础的RAG Pipeline通常包含以下五个核心环节我将逐一说明选型理由文档加载器Document Loader负责从各种来源PDF、TXT、网页、数据库加载原始文档。这里我选择PyPDFLoader和TextLoader因为它们足够简单能处理本地文件适合入门。在生产中你可能需要UnstructuredFileLoader来处理格式复杂的文档。文本分割器Text Splitter将长文档切割成适合嵌入和检索的“块”Chunks。这是影响检索精度的关键步骤。我选择RecursiveCharacterTextSplitter它是LangChain的默认推荐通过递归尝试不同的分隔符如“\n\n”, “\n”, “.”, “ ”来切割文本能在尽量保持语义完整性的前提下生成块。你需要关注两个核心参数chunk_size块大小和chunk_overlap块间重叠。chunk_overlap能防止关键信息被割裂在两个块的边界。嵌入模型Embedding Model将文本块转换为向量 embeddings。我选择text-embedding-ada-002OpenAI作为示例因为它效果稳定、API易用。但务必注意这会产生API调用费用且数据会发送到OpenAI。对于本地或隐私要求高的场景强烈推荐使用开源模型如BAAI/bge-small-zh-v1.5中文效果好或sentence-transformers/all-MiniLM-L6-v2英文通用。LangChain对Hugging Face等开源模型有很好的支持。向量数据库Vector Store存储和检索向量。我选择ChromaDB因为它轻量、无需外部服务、纯内存或持久化到磁盘均可特别适合原型开发和中小型项目。它的API与LangChain集成得非常好。其他选择包括Pinecone云服务适合大规模、Qdrant开源性能强、Weaviate开源带图数据库特性。大语言模型LLM负责最终的答案生成。示例中使用gpt-3.5-turbo原因同样是易用性。同理你可以替换为ChatGLM、Qwen、DeepSeek等任何LangChain支持的本地或API模型。设计思路总结本管道采用“本地处理云端智能”的混合模式。文档加载、分割、向量存储使用Chroma均在本地完成保障了原始数据隐私。仅在进行语义检索需要嵌入模型和答案生成需要LLM时根据选型可能调用云端API。你可以通过更换嵌入模型和LLM为本地部署的版本实现完全本地化的私有部署RAG系统。3. 环境准备与依赖安装工欲善其事必先利其器。我们先来搭建一个干净、可复现的Python环境。我强烈建议使用conda或venv创建独立的虚拟环境避免包依赖冲突。# 1. 创建并激活虚拟环境 (以conda为例) conda create -n rag_langchain python3.10 conda activate rag_langchain # 2. 安装核心依赖 pip install langchain langchain-community langchain-openai # langchain: 核心框架 # langchain-community: 社区维护的第三方集成 # langchain-openai: OpenAI模型官方集成 # 3. 安装文档处理、向量数据库等依赖 pip install chromadb pypdf sentence-transformers # chromadb: 向量数据库 # pypdf: PDF解析 # sentence-transformers: 用于运行开源嵌入模型备用 # 4. 安装可能用到的工具链 pip install tiktoken # OpenAI分词器用于精确计算token和文本分割 pip install unstructured # 强大的文档解析库可选用于复杂文档 pip install unstructured[pdf] # 如果需要PDF解析支持版本兼容性提示LangChain版本迭代较快某些接口可能发生变化。本文基于langchain0.1.0的较新版本编写。如果你遇到import错误或方法不存在请查阅对应版本的官方文档。一个常见的技巧是使用pip install langchain0.1.0来固定版本确保代码稳定运行。关于OpenAI API Key如果你选择使用OpenAI的嵌入模型或LLM需要准备一个API Key。请妥善保管不要直接硬编码在代码中。# 在Linux/Mac的终端中设置环境变量 export OPENAI_API_KEY你的-api-key-here # 在Windows的PowerShell中设置环境变量 $env:OPENAI_API_KEY你的-api-key-here在代码中更安全的方式是使用python-dotenv从.env文件加载。pip install python-dotenv然后在项目根目录创建.env文件OPENAI_API_KEYsk-...4. 分步实现你的第一个RAG Pipeline现在让我们开始真正的编码。我会将整个过程分解为清晰的步骤并附上完整的代码片段和解释。4.1 第一步加载与处理原始文档假设我们有一个名为knowledge.pdf的PDF文件作为知识库。我们首先需要将它加载进来并转换成LangChain能处理的Document对象列表。# rag_pipeline.py import os from dotenv import load_dotenv from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter # 加载环境变量 load_dotenv() # 1. 指定文档路径 pdf_path ./knowledge.pdf # 2. 使用PDF加载器 loader PyPDFLoader(pdf_path) documents loader.load() print(f成功加载了 {len(documents)} 页PDF文档。) # 注意PyPDFLoader按页加载每个页面是一个Document对象。 # 3. 初始化文本分割器 text_splitter RecursiveCharacterTextSplitter( chunk_size500, # 每个块的最大字符数 chunk_overlap50, # 块之间的重叠字符数 length_functionlen, # 计算长度的方法这里用简单的字符数 separators[\n\n, \n, 。, , , ] # 分割符优先级 ) # 4. 执行分割 split_docs text_splitter.split_documents(documents) print(f文档被分割成 {len(split_docs)} 个文本块。) # 打印第一个块看看效果 print(\n--- 第一个文本块预览 ---) print(split_docs[0].page_content[:200]) # 打印前200个字符关键参数解析与避坑指南chunk_size500这个值需要权衡。太小如100会导致信息碎片化检索到的块可能缺乏上下文太大如2000可能让单个块包含过多无关信息稀释核心语义并且可能超过LLM的上下文窗口限制。一般从300-1000开始尝试。对于中文由于字符承载信息量大可以稍大一些。chunk_overlap50这是保证检索质量的关键重叠确保了句子或关键概念不会被生硬地切断在两个块之间。例如一个重要的定义恰好位于块A的末尾和块B的开头重叠部分能使其在两个块中都出现提高了被检索到的概率。重叠大小通常设为chunk_size的10%-20%。separators默认的分隔符列表对英文友好。对于中文文档我调整了顺序加入了中文句号“。”和逗号“”这能让分割更符合中文语言习惯尽可能在语义边界处切割。实操心得分割效果需要肉眼检查。运行后务必随机抽查几个split_docs中的块看看是否在完整的句子或段落处断开。如果发现一个句子被拦腰截断就需要调整separators顺序或增加chunk_overlap。4.2 第二步向量化与存储构建知识库文本块准备好后我们需要将它们转化为向量并存入向量数据库以便后续进行相似性检索。# 接上一段代码 from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma # 1. 初始化嵌入模型 # 使用OpenAI的嵌入模型 embeddings OpenAIEmbeddings(modeltext-embedding-ada-002) # 注意此处会调用OpenAI API产生费用并上传数据。 # 如果你想使用本地开源模型推荐用于隐私数据可以这样 # from langchain_community.embeddings import HuggingFaceEmbeddings # model_name BAAI/bge-small-zh-v1.5 # embeddings HuggingFaceEmbeddings(model_namemodel_name, # model_kwargs{device: cpu}, # 或 cuda # encode_kwargs{normalize_embeddings: True}) # 2. 创建向量数据库并持久化 # persist_directory 指定数据库存储的本地路径 persist_directory ./chroma_db # 从分割好的文档创建向量存储 vectordb Chroma.from_documents( documentssplit_docs, embeddingembeddings, persist_directorypersist_directory ) # 3. 显式持久化到磁盘 vectordb.persist() print(f向量数据库已创建并保存到{persist_directory}) print(f共计存储了 {vectordb._collection.count()} 个向量。)代码细节与选择OpenAIEmbeddings使用非常简单但务必确认环境变量OPENAI_API_KEY已正确设置。text-embedding-ada-002是目前性价比和效果综合较好的选择。HuggingFaceEmbeddings这是完全本地的方案。首次运行时会从Hugging Face下载模型需要一定时间和磁盘空间。参数normalize_embeddingsTrue通常能提升相似度计算的效果。选择模型时需考虑语言中/英和性能模型大小。Chroma.from_documents这个方法一次性完成了向量化和存储。对于大量文档可以考虑分批处理避免内存溢出。persist()调用此方法后向量数据会保存到persist_directory指定的文件夹中。下次启动时可以直接加载无需重新计算嵌入节省时间和API费用。如何加载已存在的向量数据库# 后续运行直接加载已有的数据库 vectordb Chroma( persist_directorypersist_directory, embedding_functionembeddings # 必须使用与创建时相同的嵌入模型 )4.3 第三步构建检索器Retriever检索器是向量数据库的抽象接口它定义了如何从知识库中获取相关文档。我们可以对检索器进行配置以控制返回结果的数量和方式。# 接上一段代码 # 从向量数据库创建检索器 retriever vectordb.as_retriever( search_typesimilarity, # 检索类型相似度搜索 search_kwargs{k: 4} # 返回最相似的4个文本块 ) # 测试检索器 query 什么是机器学习 test_docs retriever.get_relevant_documents(query) print(f对于问题 {query}检索到 {len(test_docs)} 个相关文档块) for i, doc in enumerate(test_docs): print(f\n--- 块 {i1} (相关性分数估算) ---) print(doc.page_content[:300]) # 打印前300字符 print(...)检索器配置详解search_type默认为similarity即余弦相似度搜索。另一个常用选项是mmr最大边际相关性它会在考虑相关性的同时兼顾结果之间的多样性避免返回内容高度重复的块。search_kwargs最重要的参数是k它决定了返回多少个相关块。这个值需要根据LLM的上下文窗口和问题的复杂度来定。太少可能信息不足太多可能引入噪声并消耗大量token。通常设置在3-6之间。如果使用mmr还可以设置fetch_k初步获取的候选数量和lambda_mult多样性权重。实操心得检索器的k值不是一成不变的。对于简单事实性问题k2或3可能就够了。对于需要综合多个段落信息的复杂问题可以尝试k5或6。最好的方法是准备一组测试问题观察不同k值下检索到的内容是否真正相关。4.4 第四步组装RAG链使用LCEL这是最精彩的部分我们将使用LangChain Expression Language (LCEL) 将检索器、提示模板和LLM优雅地组合成一个可执行的“链”。# 接上一段代码 from langchain_openai import ChatOpenAI from langchain.prompts import ChatPromptTemplate from langchain.schema.runnable import RunnablePassthrough from langchain.schema.output_parser import StrOutputParser # 1. 初始化LLM llm ChatOpenAI(modelgpt-3.5-turbo, temperature0) # temperature0 使输出更确定、更少随机性适合事实性问答。 # 2. 定义提示模板 template 你是一个专业的问答助手。请严格根据以下提供的上下文信息来回答问题。 如果你无法从上下文中找到答案请诚实地回答“我不知道”不要编造信息。 上下文 {context} 问题 {question} 请根据上下文提供准确的答案 prompt ChatPromptTemplate.from_template(template) # 3. 使用LCEL组装链 rag_chain ( {context: retriever, question: RunnablePassthrough()} | prompt | llm | StrOutputParser() ) # 4. 进行问答测试 question 机器学习的主要类型有哪些 answer rag_chain.invoke(question) print(f\n问题{question}) print(f答案{answer})LCEL链拆解分析{context: retriever, question: RunnablePassthrough()}这是一个字典定义了链的输入结构。retriever会被调用其返回的文档列表将填入context变量RunnablePassthrough()表示将用户输入的原始问题直接传递给question变量。| prompt将上一步的输出包含context和question的字典传递给提示模板prompt。模板会将其渲染成完整的提示文本。| llm将渲染后的提示文本发送给LLM。| StrOutputParser()将LLM的复杂响应对象如AIMessage解析成简单的字符串答案。这种声明式的写法非常清晰链的每一步都明确可见也易于替换其中的任何一个组件比如换一个提示模板或LLM。4.5 第五步优化与增强基础版一个最基本的管道已经完成。但要让其更实用我们还需要做一些优化。优化1格式化检索到的上下文默认情况下retriever返回的是Document对象列表。在放入提示词前我们需要将它们合并成一个格式良好的字符串。def format_docs(docs): 将Document列表格式化为一个字符串。 return \n\n.join([doc.page_content for doc in docs]) # 改进后的链 rag_chain_with_source ( {context: retriever | format_docs, question: RunnablePassthrough()} | prompt | llm | StrOutputParser() )优化2追根溯源引用来源对于知识库应用知道答案来自哪份文档的哪个部分至关重要。我们可以修改流程让链同时返回答案和来源。from langchain.schema import Document def rag_chain_with_sources(input_question): # 1. 检索相关文档 retrieved_docs retriever.get_relevant_documents(input_question) # 2. 格式化上下文 context format_docs(retrieved_docs) # 3. 构建提示并调用LLM formatted_prompt prompt.format(contextcontext, questioninput_question) answer llm.invoke(formatted_prompt).content # 4. 返回答案和来源文档 source_docs [{content: doc.page_content[:200], metadata: doc.metadata} for doc in retrieved_docs] return {answer: answer, sources: source_docs} # 测试 result rag_chain_with_sources(请解释一下监督学习。) print(答案, result[answer]) print(\n--- 来源信息 ---) for i, source in enumerate(result[sources]): print(f来源{i1} (页码{source[metadata].get(page, N/A)}): {source[content]}...)这样我们就有了一个具备基础溯源能力的RAG系统。Document对象的metadata中通常包含了来源文件路径和页码如果加载器支持这对于定位原文非常有帮助。5. 常见问题、调试技巧与进阶方向即使按照步骤搭建你也可能会遇到各种问题。这里我总结了一些常见坑点和排查方法。5.1 检索效果不佳怎么办这是RAG系统最常见的问题。答案不准很多时候问题出在检索环节而不是LLM。症状LLM的回答胡言乱语或者明显不是基于提供的上下文。排查步骤检查检索结果本身像我们之前测试retriever一样把你的问题直接丢给检索器看看返回的文本块是否真的相关。如果不相关问题出在前端。调整文本分割策略这是首要怀疑对象。尝试增大或减小chunk_size。对于概念定义类问题可能需要较大的块来包含完整描述对于具体数据查找可能需要较小的块来精确定位。增加chunk_overlap。确保关键信息不被切断。更换separators。对于中文尝试[\n\n, \n, 。, , , , , ]。审视嵌入模型如果你用的开源模型尝试换一个更适配你语料领域和语言的模型。例如中文问答换用BAAI/bge系列。尝试不同的检索类型将search_type从similarity换成mmr看看是否能通过提升结果多样性来间接提升相关性。检查查询本身用户的问题可能太模糊。可以考虑引入“查询重写”或“查询扩展”步骤利用LLM将用户问题改写成更利于检索的形式。5.2 答案出现幻觉Hallucination即使检索到了相关文档LLM有时也会忽略上下文根据自己的知识生成答案甚至编造内容。应对策略强化提示词Prompt Engineering这是最直接有效的方法。在提示词中采用更严厉的指令“你必须仅使用提供的上下文来回答问题。”“如果答案不在上下文中请直接说‘根据提供的资料无法回答此问题’。”在提示词末尾加入“请再次确认你的答案完全基于上述上下文。”在上下文中加入“引用标记”在格式化上下文时给每个文本块加上编号如[1] ...text... [2] ...text...。然后要求LLM在回答时引用这些编号例如“根据[1]和[3]所述...”。这不仅能减少幻觉还能让溯源更精确。使用“Refine”或“Map-Reduce”链LangChain提供了更复杂的链来处理长上下文。RefineDocumentsChain会迭代处理每个检索到的文档逐步完善答案对控制幻觉有一定帮助。5.3 性能与成本优化嵌入模型成本如果使用OpenAI等付费API构建大型知识库的嵌入向量成本可能很高。解决方案优先使用开源模型在本地生成嵌入。对于更新不频繁的知识库这是一次性投入。检索速度ChromaDB在内存中检索很快但如果向量数量极大百万级可能需要考虑Pinecone、Qdrant等专业向量数据库它们支持索引和分布式搜索。LLM调用成本与延迟GPT-4效果虽好但成本高、速度慢。解决方案对于简单问题使用gpt-3.5-turbo对于高精度要求使用GPT-4。或者积极探索本地LLM如Qwen、DeepSeek它们在某些垂直领域经过微调后效果可能不输于通用API。5.4 进阶方向探索当你掌握了基础管道后可以考虑以下方向来提升系统能力多路召回与重排序Rerank不要只依赖向量检索。可以同时使用关键词检索如BM25进行“多路召回”然后将所有候选结果混合用一个更精细的“重排序模型”进行打分和排序再将Top-K结果送给LLM。这能显著提升召回率。Agentic RAG让RAG系统具备“思考”和“工具使用”能力。例如当用户问题复杂时系统可以自动将问题拆解成多个子问题分别检索再综合答案或者在无法直接回答时自动调用搜索引擎工具查找最新信息。图数据库增强Graph RAG将知识库中的实体和关系抽取出来构建成知识图谱。检索时既检索向量也检索图谱中的关联路径让答案更具逻辑性和推理能力。对话历史与上下文管理将当前的RAG链升级为一个能够处理多轮对话的智能体。这需要维护对话历史并将历史信息巧妙地融入到检索和生成环节中。搭建第一个可运行的RAG管道只是起点。它为你提供了一个坚实的实验平台你可以在此基础上针对具体的业务场景和数据特点对每一个环节进行深度优化和定制。真正的挑战和乐趣在于如何让这个管道从“能跑”变得“好用”、“精准”和“高效”。