从零搭建RAG系统:基于LangChain与Chroma的检索增强生成实践指南
在实际的大模型应用开发中一个核心的挑战是如何让模型能够准确、可靠地回答关于特定领域或私有知识的问题。直接依赖大模型的“记忆”或通用知识往往会导致幻觉Hallucination或信息过时。检索增强生成Retrieval-Augmented Generation, RAG正是为解决这一问题而生的关键技术范式。它通过将外部知识库的检索能力与大模型的生成能力相结合让模型能够“参考”最新、最相关的文档来生成答案从而显著提升回答的准确性和可信度。本文将以工程实践为导向从零开始手把手带你搭建一个功能完整的 RAG 系统。我们将聚焦于一个最经典的架构文档处理 - 向量化与索引 - 检索 - 生成。整个过程将使用当前主流且易于上手的工具链确保每一步都有清晰的代码、配置和验证环节。无论你是希望将 RAG 集成到现有业务中的开发者还是希望深入理解其内部机制的学习者本文都将提供一个可运行、可调试、可扩展的实践蓝本。1. 理解 RAG 的核心架构与工作流程在动手写代码之前必须清晰地理解 RAG 系统各个组件的作用和数据流向。一个典型的 RAG 流程可以分解为“索引构建”和“查询应答”两个阶段。1.1 索引构建阶段从原始文档到可检索的知识库这个阶段是离线的目的是将你的原始知识如 PDF、Word、TXT 文件处理成向量数据库能够高效检索的格式。其核心步骤包括文档加载从本地文件系统、网络或云存储中读取原始文档。不同格式PDF、DOCX、Markdown需要不同的解析器。文档分割大模型有上下文长度限制不能将整本书直接塞给模型。需要将长文档切割成语义相对完整的小片段Chunks。分割策略如按字符、按句子、按段落、重叠分割直接影响检索质量。文本向量化将文本片段转换为计算机可以理解的数值向量Embeddings。这个步骤通常使用预训练的嵌入模型如text-embedding-ada-002,BGE,Sentence Transformers。向量化模型的质量决定了语义检索的准确性。向量存储将生成的向量及其对应的原始文本片段元数据存储到专门的向量数据库如 Milvus, Pinecone, Weaviate, Qdrant中并建立索引以支持快速相似性搜索。1.2 查询应答阶段从用户问题到生成答案这个阶段是在线的响应用户的实时查询。问题向量化将用户的自然语言问题使用与索引阶段相同的嵌入模型转换为查询向量。语义检索在向量数据库中搜索与查询向量最相似的 K 个文本片段Top-K。相似度通常通过余弦相似度或点积计算。上下文构建将检索到的 Top-K 个文本片段按照相关性或其他策略如基于日期、来源的重排序进行排序和拼接组合成一个“增强的上下文”Context。提示工程与生成构建一个精心设计的提示词Prompt将用户问题和检索到的上下文一起提交给大语言模型如 GPT-4, Claude, 或本地部署的 Llama 3指令模型基于给定的上下文来回答问题。结果返回将模型生成的答案返回给用户。高级系统可能还会包含引用溯源显示答案来源于哪个文档片段或置信度评估。整个流程的核心思想是让模型“即查即用”而不是依赖其内部参数化的知识。下面我们将基于这个架构开始搭建我们的系统。2. 环境准备与工具链选型为了构建一个可运行的原型我们需要选择一组具体的技术栈。这里的选择平衡了流行度、易用性和学习成本。2.1 核心组件与工具选择组件选型说明备选方案编程语言与框架Python 3.10AI 生态最完善的语言。Node.js (LangChain.js)文档加载与处理LangChain / LlamaIndex提供了统一的文档加载、分割接口支持多种格式。直接使用 PyPDF2, docx 等库手动处理。嵌入模型sentence-transformers库的all-MiniLM-L6-v2轻量级效果不错可本地运行无需 API 密钥。OpenAItext-embedding-3-small, BGE (BAAI/bge-small-zh-v1.5中文优)。向量数据库Chroma(本地轻量版)简单易用无需额外服务适合学习和原型开发。Milvus(生产级需 Docker)Qdrant(云原生性能好)。大语言模型OpenAI GPT-3.5-Turbo (API)生成质量稳定易于集成。本文为演示使用 API。Ollama Llama 3(本地运行)Claude API,国内大模型 API。开发框架LangChain提供了 RAG 全链路的抽象和高阶 API极大简化开发。LlamaIndex(更专注于 RAG 索引) 或完全手动组装。注意本文选择 Chroma 作为向量数据库因为它可以作为一个 Python 库直接安装使用无需启动额外的数据库服务最适合快速上手。生产环境则需评估 Milvus、Qdrant 等具备持久化、高可用特性的方案。2.2 开发环境搭建首先确保你的 Python 版本在 3.10 及以上。然后创建一个新的虚拟环境并安装依赖。# 创建并激活虚拟环境 (以 conda 为例) conda create -n rag-demo python3.10 conda activate rag-demo # 安装核心依赖 pip install langchain langchain-community langchain-openai # 安装文档加载器 (支持 txt, pdf, docx 等) pip install pypdf python-docx # 安装句子转换器嵌入模型 pip install sentence-transformers # 安装向量数据库 Chroma 及其客户端 pip install chromadb # 安装 OpenAI SDK (如果你使用 OpenAI 模型) pip install openai如果你的网络环境访问 PyPI 或下载模型较慢请考虑配置镜像源。对于sentence-transformers模型首次运行时会从 Hugging Face 下载可能需要一些时间。验证关键库是否安装成功python -c “import langchain; print(‘LangChain version:‘, langchain.__version__)” python -c “import chromadb; print(‘ChromaDB version:‘, chromadb.__version__)”3. 构建知识库文档加载、分割与向量化索引我们将创建一个名为build_knowledge_base.py的脚本完成索引构建的所有工作。3.1 项目结构与文档准备创建一个项目目录结构如下rag-project/ ├── data/ # 存放原始知识文档 │ ├── company_handbook.pdf │ ├── product_spec.txt │ └── qa_pairs.docx ├── vector_db/ # Chroma 数据库将存储在这里自动生成 ├── build_knowledge_base.py # 索引构建脚本 ├── query_rag.py # 查询问答脚本 └── requirements.txt在data/目录下放置一些示例文档。为了演示你可以创建一个简单的demo.txt公司产品“智能助手”的最新版本是 v2.5.1于2024年10月发布。 该版本新增了多轮对话记忆功能并优化了响应速度。 技术支持的邮箱是 supportexample.com服务时间是工作日 9:00-18:00。 我们的总部位于北京市海淀区。 项目报销流程需要先提交电子申请经直属上级审批后交至财务部。3.2 编写索引构建脚本build_knowledge_base.py的完整代码如下我们分步解释import os from langchain_community.document_loaders import TextLoader, PyPDFLoader, Docx2txtLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.embeddings import HuggingFaceEmbeddings from langchain.vectorstores import Chroma # 1. 配置路径 DATA_PATH “./data” PERSIST_DIRECTORY “./vector_db/chroma_db” EMBEDDING_MODEL_NAME “sentence-transformers/all-MiniLM-L6-v2” def load_documents(data_path): “”“加载指定目录下的所有支持格式的文档。”“” documents [] for filename in os.listdir(data_path): file_path os.path.join(data_path, filename) try: if filename.endswith(‘.txt’): loader TextLoader(file_path, encoding‘utf-8’) loaded_docs loader.load() documents.extend(loaded_docs) print(f“已加载: {filename}”) elif filename.endswith(‘.pdf’): loader PyPDFLoader(file_path) loaded_docs loader.load() documents.extend(loaded_docs) print(f“已加载: {filename}”) elif filename.endswith(‘.docx’): loader Docx2txtLoader(file_path) loaded_docs loader.load() documents.extend(loaded_docs) print(f“已加载: {filename}”) # 可以继续添加其他格式的加载器如 CSV, Markdown 等 except Exception as e: print(f“加载文件 {filename} 时出错: {e}”) return documents def split_documents(documents): “”“将文档分割成适合检索的小片段。”“” # 创建文本分割器 text_splitter RecursiveCharacterTextSplitter( chunk_size500, # 每个片段的最大字符数 chunk_overlap50, # 片段之间的重叠字符数保持上下文连贯 length_functionlen, separators[“\n\n”, “\n”, “。”, “.”, “,”, “ “, “”] # 分割优先级 ) splits text_splitter.split_documents(documents) print(f“原始文档数: {len(documents)} 分割后片段数: {len(splits)}”) return splits def create_vectorstore(text_splits, persist_directory, embedding_model_name): “”“创建嵌入模型生成向量并存储到 ChromaDB。”“” # 初始化嵌入模型本地运行无需API embeddings HuggingFaceEmbeddings( model_nameembedding_model_name, model_kwargs{‘device’: ‘cpu’}, # 使用 GPU 可改为 ‘cuda’ encode_kwargs{‘normalize_embeddings’: True} # 归一化便于余弦相似度计算 ) # 创建向量存储。如果目录已存在Chroma 会尝试加载现有数据库。 # 这里我们强制重新创建生产环境可能需要增量更新逻辑。 if os.path.exists(persist_directory): print(f“检测到已有向量库目录 {persist_directory} 正在重新创建...”) import shutil shutil.rmtree(persist_directory) vectorstore Chroma.from_documents( documentstext_splits, embeddingembeddings, persist_directorypersist_directory ) print(f“向量数据库已创建并持久化到: {persist_directory}”) return vectorstore if __name__ “__main__”: print(“ 开始构建 RAG 知识库 ”) # 步骤1: 加载文档 raw_docs load_documents(DATA_PATH) if not raw_docs: print(“未加载到任何文档请检查 data/ 目录。”) exit(1) # 步骤2: 分割文档 all_splits split_documents(raw_docs) # 步骤3: 向量化并存储 vector_db create_vectorstore(all_splits, PERSIST_DIRECTORY, EMBEDDING_MODEL_NAME) # 可选简单测试一下检索功能 test_query “智能助手的最新版本是什么” results vector_db.similarity_search(test_query, k2) print(f“\n测试检索查询: ‘{test_query}’”) for i, doc in enumerate(results): print(f“[结果 {i1}] {doc.page_content[:150]}...”) # 打印前150个字符 print(“ 知识库构建完成 ”)关键参数与配置解释chunk_size500和chunk_overlap50这是分割的核心参数。chunk_size需要根据你使用的 LLM 的上下文窗口和文档特性调整。太小会丢失上下文太大会降低检索精度并增加提示词令牌消耗。chunk_overlap可以防止一个完整的句子或概念被硬生生切断。HuggingFaceEmbeddings我们使用all-MiniLM-L6-v2模型它是一个平衡了速度和效果的通用嵌入模型。normalize_embeddingsTrue意味着生成的向量会被归一化为单位长度此时向量点积等于余弦相似度是语义检索的常用度量方式。Chroma.from_documents这个方法完成了三件事1) 用嵌入模型将所有文本片段转换为向量2) 将这些向量存入 Chroma3) 将向量索引和原始文本元数据持久化到指定目录。后续查询时可以直接加载这个目录无需重新计算嵌入。运行此脚本python build_knowledge_base.py如果一切顺利你将看到加载、分割文档的日志并最终在./vector_db/chroma_db目录下生成 Chroma 数据库文件。同时控制台会打印出针对测试问题的检索结果验证索引是否有效。4. 实现问答链检索、提示与生成知识库构建好后我们需要实现查询流程。创建query_rag.py脚本。4.1 配置 LLM 并组装检索链我们将使用 LangChain 的RetrievalQA链它封装了检索、上下文构建和生成的过程。import os from langchain.embeddings import HuggingFaceEmbeddings from langchain.vectorstores import Chroma from langchain.chains import RetrievalQA from langchain_openai import ChatOpenAI from langchain.prompts import PromptTemplate # 配置 PERSIST_DIRECTORY “./vector_db/chroma_db” EMBEDDING_MODEL_NAME “sentence-transformers/all-MiniLM-L6-v2” # 注意使用 OpenAI 需要设置 API Key。建议从环境变量读取不要硬编码。 os.environ[“OPENAI_API_KEY”] “your-openai-api-key-here” # 替换为你的 key或通过环境变量设置 def load_vectorstore(persist_directory, embedding_model_name): “”“加载已构建的向量数据库。”“” embeddings HuggingFaceEmbeddings( model_nameembedding_model_name, model_kwargs{‘device’: ‘cpu’}, encode_kwargs{‘normalize_embeddings’: True} ) vectorstore Chroma( persist_directorypersist_directory, embedding_functionembeddings ) print(f“向量数据库已从 {persist_directory} 加载”) return vectorstore def create_qa_chain(vectorstore): “”“创建检索问答链。”“” # 1. 初始化 LLM # 使用 GPT-3.5-Turbo 你也可以替换为其他 LangChain 支持的 LLM llm ChatOpenAI( model“gpt-3.5-turbo”, temperature0.1, # 降低随机性使答案更确定 max_tokens500 ) # 2. 定义自定义提示模板 # 这是 RAG 效果好坏的关键清晰的指令能引导模型更好地利用上下文。 prompt_template “”“请根据以下上下文信息回答问题。如果上下文信息不足以回答问题请直接说“根据提供的信息我无法回答这个问题”不要编造信息。 上下文 {context} 问题{question} 请基于上下文给出答案”“” PROMPT PromptTemplate( templateprompt_template, input_variables[“context”, “question”] ) # 3. 创建 RetrievalQA 链 # chain_type“stuff” 是最简单的方式将所有检索到的上下文塞进提示词。 # retrievervectorstore.as_retriever(search_kwargs{“k”: 4}) 表示每次检索最相关的4个片段。 qa_chain RetrievalQA.from_chain_type( llmllm, chain_type“stuff”, retrievervectorstore.as_retriever(search_kwargs{“k”: 4}), chain_type_kwargs{“prompt”: PROMPT}, return_source_documentsTrue # 非常重要返回源文档用于溯源 ) return qa_chain def answer_question(qa_chain, question): “”“使用 QA 链回答问题并显示结果。”“” print(f“\n问题: {question}”) result qa_chain.invoke({“query”: question}) answer result[“result”] source_docs result[“source_documents”] print(f“答案: {answer}”) print(“\n--- 来源文档片段 (引用溯源) ---”) for i, doc in enumerate(source_docs): print(f“[来源 {i1}]: {doc.page_content[:200]}...”) # 展示片段前200字符 print(f“ (元数据: {doc.metadata})”) print() if __name__ “__main__”: print(“ 初始化 RAG 问答系统 ”) # 步骤1: 加载向量库 db load_vectorstore(PERSIST_DIRECTORY, EMBEDDING_MODEL_NAME) # 步骤2: 创建问答链 qa_chain create_qa_chain(db) # 步骤3: 交互式问答 print(“\n系统就绪请输入您的问题 (输入 ‘quit‘ 退出):”) while True: user_input input(“ “).strip() if user_input.lower() ‘quit’: break if user_input: answer_question(qa_chain, user_input) else: print(“请输入有效问题。”)4.2 关键代码解析与配置LLM 初始化 (ChatOpenAI)我们使用了 OpenAI 的 GPT-3.5-Turbo API。temperature0.1使输出更确定适合事实性问答。请务必将your-openai-api-key-here替换为你自己的 API 密钥最佳实践是从环境变量读取 (os.getenv(“OPENAI_API_KEY”))。提示词工程 (PromptTemplate)这是 RAG 的灵魂。我们给模型的指令非常明确指令“请根据以下上下文信息回答问题。”防幻觉“如果上下文信息不足以回答问题请直接说...不要编造信息。”结构化输入明确分隔{context}和{question}。 一个糟糕的提示词如“请回答{question}”会导致模型完全忽略你提供的上下文转而依赖自己的知识导致幻觉。检索器配置 (as_retriever)search_kwargs{“k”: 4}表示每次检索前 4 个最相关的文档片段。k值需要权衡太小可能信息不全太大会增加提示词长度和成本并可能引入噪声。溯源 (return_source_documentsTrue)这个参数至关重要它让链返回检索到的原始文档片段。在输出中展示这些片段可以验证答案是否真的来源于你的知识库增加可信度也便于调试检索质量。4.3 运行与验证首先确保已设置好OPENAI_API_KEY环境变量或修改代码中的密钥。然后运行python query_rag.py系统加载向量库后进入交互模式。尝试问几个问题“智能助手的最新版本是什么”“技术支持的邮箱是多少”“项目报销的流程是什么”“公司总部在哪里”观察输出你应该能看到准确的答案以及答案所引用的文档片段和元数据如来源文件名和页码。这证明你的 RAG 系统已经成功运行。5. 生产环境进阶考量与常见问题排查一个能跑通的 demo 距离生产可用还有距离。以下是几个关键的进阶方向和常见坑点。5.1 从原型到生产必须考虑的增强点文档预处理与清洗问题原始文档可能包含页眉、页脚、无关图表代码、特殊字符等噪声。方案在加载和分割之间加入清洗步骤使用正则表达式或专门库如html2text,markdownify去除无关内容提取纯文本。更智能的分块策略问题简单的按字符分割可能切断表格、代码块或一个完整概念。方案使用语义分割器如SemanticChunker或基于标记Markdown 标题、LaTeX 环境的分割。对于代码可以考虑按函数/类分割。检索优化混合检索结合密集向量检索语义相似和稀疏检索如 BM25关键词匹配兼顾语义和精确词匹配。LangChain 的EnsembleRetriever可以支持。重排序初步检索出 Top-K如 K20个结果后使用一个更精细的交叉编码器模型如BGE-reranker对它们重新排序选出最相关的 Top-N如 N4个送入 LLM显著提升精度。元数据过滤检索时加入过滤器如“只从某年之后的文档中检索”、“只检索某类别的文档”。这需要你在索引时为每个片段添加丰富的元数据如来源、日期、类别。提示词优化与链设计多步推理对于复杂问题可以使用LangChain Expression Language (LCEL)自定义更复杂的链例如先让 LLM 分解问题再并行检索多个子问题最后综合答案。历史上下文在链中加入对话记忆管理以实现多轮问答。评估与监控评估指标建立测试集评估答案的忠实度是否基于上下文、相关性是否答非所问和流畅度。日志与监控记录用户的查询、检索到的文档、生成的答案、令牌消耗和响应时间用于分析和优化。5.2 常见问题与排查清单在开发过程中你可能会遇到以下典型问题问题现象可能原因检查与解决思路答案与文档内容不符幻觉1. 提示词未强制模型使用上下文。2. 检索到的文档不相关。3. LLM 的temperature参数过高。1.检查提示词确保包含“根据上下文”等指令并测试将上下文置空时模型的反应。2.检查检索结果打印出source_documents看内容是否与问题相关。若不相关检查嵌入模型是否合适或调整分块大小。3.降低temperature设为 0.1 或 0。检索不到任何相关文档1. 向量数据库未正确构建或加载。2. 查询语句与文档表述差异太大。3. 嵌入模型不适合领域或语言。1.验证数据库运行vectorstore.similarity_search_with_score(“简单词”, k1)看能否返回结果。2.查询改写尝试用更接近文档表述的方式提问或使用 LLM 先对查询进行改写/扩展。3.更换嵌入模型中文场景尝试BAAI/bge系列模型。答案不完整或截断1. LLM 的max_tokens参数设置过小。2. 检索到的上下文总长度超过模型限制。1.增加max_tokens。2.减少检索数量k或使用Map-Reduce等能处理长上下文的链类型。处理速度慢1. 嵌入模型在 CPU 上运行。2. 向量数据库未建索引或配置不当。3. LLM API 调用网络延迟高。1.使用 GPU将嵌入模型设置为model_kwargs{‘device’: ‘cuda’}。2.优化数据库生产环境换用 Milvus/Qdrant 并配置索引。3.缓存对常见查询的嵌入向量或最终答案进行缓存。无法加载特定格式文档缺少对应的文档加载器库。安装对应库如pip install unstructured支持更多格式或使用UnstructuredFileLoader。5.3 安全与成本控制数据安全如果使用云端 LLM API如 OpenAI你的上下文和问题会被发送到第三方。处理敏感数据时务必使用本地模型如通过 Ollama 部署 Llama 3或私有化部署的模型。API 成本LLM API 按令牌收费。优化提示词长度、控制检索上下文的数量、对答案进行缓存都是有效的成本控制手段。输入审查对用户输入进行基本的审查和过滤防止提示词注入攻击。6. 扩展方向与学习路径完成基础搭建后你可以根据兴趣和需求向更深处探索探索高级 RAG 模式Self-RAG让模型在生成过程中自我评估并检索实现更动态的检索决策。Agentic RAG将 RAG 系统作为一个工具嵌入到智能体Agent中让 Agent 自主决定何时、如何检索。多模态 RAG索引和检索的对象不仅是文本还包括图片、音频、视频使用多模态嵌入模型。集成到现有架构Web 服务化使用 FastAPI 或 Flask 将你的 RAG 系统封装成 RESTful API。与 Spring Boot 集成如果你身处 Java 技术栈可以使用langchain4j在 Spring Boot 应用中实现类似的 RAG 逻辑并连接 Milvus 等向量数据库。前端界面构建一个简单的聊天界面提升用户体验。深入底层原理学习嵌入模型了解对比学习、BERT 等如何产生语义向量。研究向量索引学习 HNSW、IVF-PQ 等近似最近邻搜索算法是如何工作的。分析提示词技巧深入研究 Few-Shot、Chain-of-Thought 等如何提升 LLM 在 RAG 中的表现。构建 RAG 系统是一个迭代过程从最简单的流水线开始通过评估发现瓶颈是检索不准还是生成不好然后有针对性地引入更高级的组件和技术。始终以“让模型基于给定知识可靠回答”为目标不断调试你的文档处理、检索策略和提示词这才是掌握 RAG 技术的实践精髓。