从零搭建RAG系统:基于LangChain与Milvus的检索增强生成实践 1. 项目概述为什么RAG是当前AI应用落地的关键拼图如果你最近在关注大模型应用开发一定绕不开“RAG”这个词。它不再是实验室里的概念而是正在成为连接通用大模型与私有、精准业务知识的桥梁。简单来说RAG检索增强生成的核心思想是“先查后答”当用户提问时系统不是让大模型凭空想象而是先从你的专属知识库比如公司文档、产品手册、客服记录里检索出最相关的信息片段然后把这些信息作为“参考资料”喂给大模型让它基于这些资料生成最终答案。这就像一位经验丰富的顾问在回答客户问题前会先翻阅相关的案例档案和法规条文确保回答既专业又准确。我之所以花时间从零开始搭建一个RAG系统是因为在实际项目中我们遇到了大模型最典型的两个“硬伤”幻觉和知识滞后。幻觉是指模型会一本正经地胡说八道编造不存在的事实知识滞后则是指模型训练数据有截止日期无法知晓最新的信息。比如你问它公司上周刚发布的新产品特性它要么瞎编要么直接说不知道。RAG正是解决这两个痛点的利器。通过将最新的、经过验证的文档灌入向量数据库我们能让大模型“与时俱进”并且答案有据可查极大地提升了可信度。这个“从零到生产”系列的上篇我们将聚焦于RAG系统的基石部分知识库的构建与检索。我会带你用Python一步步搭建一个可用的RAG原型核心工具链包括LangChain应用框架、Sentence Transformers文本向量化模型和Milvus向量数据库。无论你是想为自己的团队构建一个智能问答机器人还是希望将内部知识库AI化这篇内容都能提供一个扎实的起点。我们不会停留在理论而是深入到每一步的配置、代码和踩坑经验里。2. 核心架构与工具选型为什么是它们在动手之前理清架构和选对工具至关重要。一个典型的RAG系统工作流可以拆解为几个核心环节文档加载与处理、文本分割、向量化嵌入、向量存储与检索、以及最终的提示工程与生成。我们的技术栈选择基于一个核心原则在保证功能强大、社区活跃的前提下尽可能降低入门和部署的复杂度。2.1 框架层为什么选择LangChainLangChain不是一个具体的模型或数据库而是一个用于构建大模型应用的框架。你可以把它想象成乐高积木的底板和通用连接件。它提供了大量标准化的“组件”比如文档加载器、文本分割器、各种模型的接口、以及预构建的链Chain。使用LangChain你不需要从零开始写网络请求、处理异步、或设计复杂的流程逻辑它能让你用声明式的方法快速组装出一个应用原型。对于RAG来说LangChain的价值尤其明显。它内置了RetrievalQA这样的链你只需要配置好检索器Retriever和语言模型LLM它就能自动完成“检索-组合提示-生成答案”的全过程。这极大地加速了开发迭代。当然LangChain也有其缺点比如抽象层次较高有时为了调试需要深入其源码在追求极致性能的生产环境中可能需要对其部分组件进行定制或替换。但对于从零到一的阶段它的效率优势无可比拟。2.2 嵌入模型Sentence Transformers的平衡之道文本向量化即把一段文字转换成计算机能理解的数字向量一组浮点数是检索的基石。向量的质量直接决定了检索的准确性。这里我们选择Sentence Transformers库它封装了基于Transformer架构的、专门为句子和段落嵌入设计的模型。为什么不直接用OpenAI的Embedding API原因有三成本、延迟和隐私。本地运行的Sentence Transformers模型一旦下载推理完全免费且没有网络延迟响应速度极快。更重要的是你的敏感数据无需离开本地环境这对于企业级应用是必须考虑的安全红线。我们通常会选择all-MiniLM-L6-v2这个模型作为起点它在精度和速度之间取得了很好的平衡模型文件小约80MB在CPU上也能快速运行非常适合原型开发和中小规模应用。2.3 向量数据库Milvus为何脱颖而出检索的核心是“相似度计算”。我们需要一个数据库能够高效存储百万甚至千万量级的向量并能快速找出与问题向量最相似的那些文档向量。这就是向量数据库的专长。在众多选择中如Pinecone、Weaviate、Qdrant我选择Milvus作为本次实践的载体主要基于以下几点考量开源与可自托管你可以完全在自己的服务器或笔记本上部署它拥有全部控制权无需担心云服务费用和数据出境问题。性能与成熟度Milvus是专门为向量搜索设计的数据库经历了多个版本的迭代在性能优化、索引类型如IVF_FLAT, HNSW支持方面非常成熟社区活跃。与LangChain生态集成良好LangChain提供了官方的Milvus集成使用起来非常方便。部署灵活支持Docker一键部署也支持分布式集群能从单机原型平滑扩展到生产集群。虽然Milvus的安装和配置相比简单的内存检索比如直接用Chroma要稍复杂但它为未来的扩展性打下了坚实基础。一旦你的知识库从几百条文档增长到几十万条Milvus的优势就会凸显出来。注意工具选型没有银弹。如果你的数据量很小1万条追求极简快速上手Chroma这类轻量级嵌入式向量数据库是更好的选择。但如果你预期数据会增长或需要更强大的检索功能如过滤、混合搜索从Milvus开始学习是更有远见的投资。3. 环境准备与Milvus部署实战工欲善其事必先利其器。我们将在一个干净的Python虚拟环境中开始并完成Milvus数据库的部署。这是后续所有工作的基础。3.1 Python环境与依赖安装首先确保你的系统已安装Python 3.8-3.11版本这是大多数AI库兼容性最好的范围。我强烈建议使用conda或venv创建独立的虚拟环境避免包版本冲突。# 使用conda创建环境推荐 conda create -n rag_demo python3.9 conda activate rag_demo # 或者使用venv python -m venv rag_demo source rag_demo/bin/activate # Linux/Mac # rag_demo\Scripts\activate # Windows接下来安装核心依赖。我们将使用pip进行安装。这里列出了关键库及其作用pip install langchain langchain-community # LangChain核心及社区组件 pip install sentence-transformers # 用于生成文本向量 pip install pymilvus # Milvus的Python客户端 pip install unstructured[all-docs] # 强大的文档加载器支持PDF、Word、PPT等 pip install python-dotenv # 管理环境变量如需接入OpenAI等API pip install tiktoken # 用于文本分割时计算Token可选但推荐unstructured库可能需要一些系统依赖比如用于处理PDF的poppler。在Ubuntu/Debian上可以运行sudo apt-get install poppler-utils在Mac上可以用brew install poppler。Windows用户可能需要从官网下载并配置环境变量或者考虑使用Docker来规避系统依赖问题。3.2 使用Docker Compose部署Milvus单机版Milvus官方提供了极简的Docker Compose部署方式非常适合开发和测试。请确保你的机器上已经安装了Docker和Docker Compose。下载配置文件在项目根目录下创建一个docker-compose.yml文件。最简单的方式是从Milvus GitHub仓库获取最新的单机版配置。你可以直接运行以下命令# 下载最新的docker-compose配置文件 wget https://github.com/milvus-io/milvus/releases/download/v2.4.0/milvus-standalone-docker-compose.yml -O docker-compose.yml启动服务在包含docker-compose.yml的目录下执行docker-compose up -d这个命令会在后台启动三个容器milvus-standalone: Milvus核心服务。etcd: 用于元数据存储。minio: 用于对象存储存储插入的向量和索引文件。验证安装运行以下命令检查容器状态确保所有服务都是Up状态。docker-compose ps你也可以通过访问http://localhost:9091来打开Milvus的图形化管理界面Attu如果配置中包含的话进行更直观的管理。默认情况下Milvus的服务端口是19530。实操心得第一次启动时由于需要拉取镜像可能会花费几分钟。如果遇到端口冲突比如19530、9091端口被占用你需要修改docker-compose.yml文件中的端口映射。另外确保你的Docker分配了足够的内存建议至少4GB否则Milvus可能无法正常启动。4. 知识库构建从原始文档到向量存储有了运行中的Milvus我们就可以开始处理文档了。这一步的目标是将非结构化的文档如PDF、TXT转化为结构化的向量并存入数据库。这是RAG系统中工作量最大、也最需要细心的一环。4.1 文档加载与文本提取我们使用LangChain的文档加载器。这里以处理一个包含多份产品手册的目录为例。from langchain_community.document_loaders import DirectoryLoader, UnstructuredFileLoader from langchain.text_splitter import RecursiveCharacterTextSplitter import os # 假设你的文档都放在 ./knowledge_base 目录下 documents_path ./knowledge_base # 使用DirectoryLoader加载所有支持格式的文件 # 这里使用通配符但更推荐为不同格式指定不同的loader loader DirectoryLoader(documents_path, glob**/*.pdf, loader_clsUnstructuredFileLoader) # 如果你有多种格式可以组合使用 # loaders [DirectoryLoader(...), DirectoryLoader(..., glob**/*.docx)] # docs [] # for loader in loaders: # docs.extend(loader.load()) raw_documents loader.load() print(f成功加载了 {len(raw_documents)} 份文档)UnstructuredFileLoader是功能强大的加载器它能解析PDF中的文字、表格甚至简单的布局。但要注意对于扫描版PDF图片格式它需要OCR支持否则无法提取文字。此时你可能需要先使用pytesseract等OCR库处理图片。4.2 文本分割的艺术与技巧你不能将整本100页的PDF作为一个向量存入数据库。这会导致检索精度极低检索出整本书和提示词上下文浪费。因此必须进行文本分割。分割不是简单按固定字符数切割而是要尽可能保证语义的完整性。LangChain的RecursiveCharacterTextSplitter是常用选择它会优先按段落、句子等自然分隔符进行切割。text_splitter RecursiveCharacterTextSplitter( chunk_size500, # 每个文本块的最大字符数或Token数 chunk_overlap50, # 块与块之间的重叠字符数避免上下文断裂 length_functionlen, # 计算长度的方法这里用字符数。也可用 tiktoken 计数 separators[\n\n, \n, 。, , , , , , ] # 分割优先级 ) split_documents text_splitter.split_documents(raw_documents) print(f原始文档被分割成了 {len(split_documents)} 个文本块)关键参数解析chunk_size这是最重要的参数。设置太小会丢失上下文设置太大检索会不精准且会挤占LLM的上下文窗口。一般建议在200-1000之间。对于技术文档500-800是个不错的起点。你可以用tiktoken来按Token计数这更符合LLM的视角。chunk_overlap重叠部分能防止一个完整的句子或概念被生生切断。例如一个段落正好在500字符处结束下一个段落从501开始如果没有重叠这两个段落的语义连接就丢失了。通常设置为chunk_size的10%-20%。separators分割符列表按优先级尝试。这里配置为先按双换行段落再按单换行再按句号等。实操心得没有通用的最佳分割策略。你需要根据你的文档类型进行试验。对于QA格式的文档可以尝试按问题分割对于长技术文档按二级/三级标题分割可能效果更好。分割后最好随机抽样检查几个块确保它们语义上是完整的单元。4.3 文本向量化与Milvus集合创建现在我们将分割后的文本块转化为向量并存入Milvus。首先初始化嵌入模型和Milvus连接。from sentence_transformers import SentenceTransformer from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Milvus from pymilvus import connections, utility # 1. 初始化嵌入模型 # 使用Sentence Transformers的模型LangChain做了封装 embed_model HuggingFaceEmbeddings( model_namesentence-transformers/all-MiniLM-L6-v2, model_kwargs{device: cpu}, # 如果GPU可用可改为 cuda encode_kwargs{normalize_embeddings: True} # 归一化向量有利于余弦相似度计算 ) # 2. 连接Milvus connections.connect(hostlocalhost, port19530) # 3. 定义集合Collection名称和参数 collection_name rag_demo_collection vector_dim 384 # all-MiniLM-L6-v2 模型的向量维度是384 # 检查集合是否存在如果存在则删除仅用于演示生产环境慎用 if utility.has_collection(collection_name): utility.drop_collection(collection_name)接下来使用LangChain的Milvus.from_documents方法它内部会自动完成向量化、创建集合、定义Schema包含向量字段和文本元数据字段以及插入数据的所有步骤。# 4. 将文档向量化并存入Milvus # 这一步耗时取决于文档数量和你的硬件性能 vector_db Milvus.from_documents( documentssplit_documents, embeddingembed_model, collection_namecollection_name, connection_args{host: localhost, port: 19530}, drop_oldTrue # 如果集合已存在则删除重建。生产环境应设为False采用增量插入。 ) print(f知识库构建完成共存入 {vector_db.collection.num_entities} 条向量数据。)幕后发生了什么LangChain会为每个文本块调用embed_model生成一个384维的向量。在Milvus中创建一个名为rag_demo_collection的集合。Schema通常包含id: 主键自动生成。vector: 384维的浮点向量字段。text: 存储原始文本的VarChar字段。可能还有其他元数据字段如source文档来源、page页码等这些来自Document对象的metadata属性。将所有(向量, 文本, 元数据)插入集合。默认情况下Milvus会在插入后自动为向量字段创建索引使用IVF_FLAT索引类型和余弦相似度度量以加速后续检索。你也可以在from_documents中通过index_params自定义索引参数。5. 检索器配置与相似度搜索调优数据存进去了如何高效准确地检索出来是关键。LangChain的Milvus对象本身就是一个检索器Retriever但我们可以对其进行配置以优化检索效果。5.1 基础检索与参数理解首先我们从已存在的集合中加载这个向量库。# 重新连接并加载已有的向量库 vector_db Milvus( embedding_functionembed_model, collection_namecollection_name, connection_args{host: localhost, port: 19530}, ) # 将其转换为检索器 retriever vector_db.as_retriever( search_kwargs{k: 4} # 每次检索返回最相似的4个文本块 )现在我们可以进行检索测试query 我们产品的最新版本有哪些安全特性 docs retriever.get_relevant_documents(query) print(f针对问题 {query} 检索到 {len(docs)} 个相关片段) for i, doc in enumerate(docs): print(f\n--- 片段 {i1} (相似度得分: {doc.metadata.get(score, N/A)}) ---) print(doc.page_content[:200] ...) # 打印前200个字符 print(f来源: {doc.metadata.get(source, Unknown)})search_kwargs中的k参数控制返回结果的数量。这个值需要权衡太小可能遗漏关键信息太大会引入噪声并增加后续LLM处理的负担和成本。通常结合LLM的上下文窗口大小来设定。5.2 高级检索策略超越简单向量搜索单纯的向量相似度搜索语义搜索有时并不足够。我们需要引入更多策略来提升召回率和准确率。1. 混合搜索Hybrid Search这是将基于关键词的搜索如BM25和向量搜索结合起来的方法。BM25擅长精确匹配关键词而向量搜索擅长捕捉语义相似度。两者结合可以取长补短。Milvus从2.3版本开始支持混合搜索。在LangChain中你可能需要直接使用pymilvus进行更底层的调用来实现。# 示例使用pymilvus进行混合搜索概念性代码 from pymilvus import Collection, connections connections.connect(...) collection Collection(“rag_demo_collection”) collection.load() # 定义搜索参数 search_params { “metric_type”: “IP”, # 或“COSINE” “params”: {“nprobe”: 10} # IVF索引的搜索参数 } # 假设我们有一个关键词搜索的权重分数列表 keyword_scores # 实际实现中需要先用BM25算法计算query与所有文档的关键词匹配分 hybrid_results collection.search( data[query_vector], # 查询向量 anns_field“vector”, paramsearch_params, limit5, exprNone, # 可以添加过滤表达式 # 实际混合搜索需要更复杂的逻辑这里仅为示意 )2. 重排序Re-ranking初步检索例如召回10个片段后使用一个更精细但可能更慢的模型称为重排序器对这10个结果重新计算相关性并排序只保留最顶部的几个如3个送入LLM。这能显著提升最终答案的质量。Cohere和BGE等公司提供了专门的重排序模型API你也可以用交叉编码器Cross-Encoder模型本地实现。3. 元数据过滤在检索时加入业务过滤条件能极大提升精准度。例如只检索“产品A”的“用户手册”中“2024年”发布的文档。# 在as_retriever中通过search_kwargs传递过滤表达式 retriever vector_db.as_retriever( search_kwargs{ “k”: 4, “expr”: “source \‘产品A用户手册.pdf\’ and page 10 and page 20” # 元数据过滤表达式 } )注意事项过滤表达式依赖于插入数据时提供的元数据。确保在文档加载和分割阶段就将有用的元数据如文件名、标题、页码、日期等存入Document.metadata中。Milvus支持对标量字段如字符串、整数建立索引并进行高效过滤。6. 构建检索增强生成链让LLM基于资料回答检索到相关文档后最后一步是将它们组合成一个清晰的提示Prompt交给大语言模型生成最终答案。LangChain的RetrievalQA链封装了这个过程。6.1 使用本地开源模型以Ollama为例为了完全在本地运行我们可以使用Ollama来部署和运行开源LLM如Llama 3、Qwen等。首先确保你安装了Ollama并拉取了模型。# 安装Ollama (参见官网) # 拉取一个模型例如 Llama 3 8B ollama pull llama3:8b然后在Python中使用LangChain的Ollama集成。from langchain_community.llms import Ollama from langchain.chains import RetrievalQA from langchain.prompts import PromptTemplate # 1. 初始化本地LLM llm Ollama(model“llama3:8b”, temperature0.1) # temperature调低让输出更确定 # 2. 定义自定义提示模板 # 这是控制答案质量的关键 custom_prompt PromptTemplate( input_variables[“context”, “question”], template“”“你是一个专业的助手请严格根据以下提供的上下文信息来回答问题。如果上下文信息不足以回答问题请直接说‘根据现有资料无法回答该问题’不要编造信息。 上下文 {context} 问题{question} 请根据上下文回答”“” ) # 3. 创建RetrievalQA链 qa_chain RetrievalQA.from_chain_type( llmllm, chain_type“stuff”, # 最简单的方式将所有检索到的文档内容拼接后放入提示词 retrieverretriever, # 我们之前配置的检索器 chain_type_kwargs{“prompt”: custom_prompt}, return_source_documentsTrue # 返回参考来源便于追溯 ) # 4. 进行问答 result qa_chain({“query”: “我们产品的最新版本有哪些安全特性”}) print(“答案”, result[“result”]) print(“\n--- 参考来源 ---”) for doc in result[“source_documents”]: print(f“- {doc.metadata.get(‘source’, ‘N/A’)} (页码: {doc.metadata.get(‘page’, ‘N/A’)})”)关键点解析chain_type“stuff”这是最直接的方法将所有检索到的文档内容context全部塞进提示词。它的优点是简单信息完整。缺点是受限于LLM的上下文窗口长度如果检索到的文档总长度超过窗口就会失败。适用于检索片段较少、较短的情况。自定义提示模板这是RAG的灵魂。模板必须清晰指令模型“根据上下文回答”并明确告知在上下文不足时如何处理这是对抗幻觉的第一道防线。模板的质量极大影响最终答案的准确性和格式。temperature设置为较低值如0.1可以减少模型的随机性让答案更稳定、更基于事实。6.2 使用云API如OpenAI如果你希望使用更强大的模型如GPT-4或者本地硬件资源有限可以轻松切换到云API。from langchain_openai import ChatOpenAI import os os.environ[“OPENAI_API_KEY”] “your-api-key-here” # 请替换为你的密钥 llm ChatOpenAI(model_name“gpt-3.5-turbo”, temperature0.1) # 后续的RetrievalQA链创建方式与本地模型完全相同实操心得提示工程是RAG的“放大器”。除了基础的指令你还可以在提示词中加入答案格式要求例如“请用分点列表的形式回答”。引用要求例如“在答案中的关键事实后面用【来源X】标明出处”这需要检索器返回的文档有编号。角色设定例如“你是一位资深的安全专家...”。 多迭代几次提示词用一些典型问题测试观察答案的变化是提升系统效果性价比最高的方法。7. 常见问题、排查技巧与性能优化实录在实际搭建和运行过程中你一定会遇到各种问题。下面是我踩过的一些坑和解决方案。7.1 检索效果不佳怎么办症状检索到的文档与问题不相关导致LLM胡言乱语或答非所问。检查文本分割这是最常见的原因。chunk_size可能太大或太小。打印出检索到的文档块内容看它们是否是语义完整的单元。调整chunk_size和chunk_overlap或者尝试按标题分割MarkdownHeaderTextSplitter。审视嵌入模型all-MiniLM-L6-v2是通用模型对于特定领域如医学、法律效果可能打折扣。可以尝试领域内微调的模型如BGE系列、text2vec系列。在 Hugging Face MTEB排行榜 上可以找到适合你任务的模型。引入查询转换有时用户的问题太简短或模糊。可以尝试在检索前对查询进行扩展或重写。例如使用LLM将“它怎么用”重写为“[产品名]的使用方法是什么”。LangChain提供了Query expansion等工具链。启用混合搜索或重排序如前所述这是提升召回精度的有效手段。7.2 运行速度慢尤其是插入数据时症状向量化并插入上万条数据耗时过长。批量操作确保使用批量插入。Milvus.from_documents内部是批量的但如果你自己写插入逻辑应避免逐条插入。利用GPU如果机器有GPU将嵌入模型加载到GPU上model_kwargs{‘device’: ‘cuda’}向量化速度会有数量级提升。调整Milvus索引对于大规模数据集在插入数据前不建索引auto_idFalse等所有数据插入完毕后再一次性创建索引速度更快。生产环境通常采用这种“先插入后建索引”的流式处理方式。索引类型选择IVF_FLAT是精度和速度的平衡。对于十亿级数据可以考虑HNSW索引它搜索更快但占用更多内存。创建索引时调整nlistIVF或M/efConstructionHNSW参数可以权衡速度和精度。7.3 LLM生成答案时忽略上下文依然幻觉症状即使检索到了完美答案LLM仍然自己编造。强化提示词在提示词中用更强烈的语气如“你必须且只能使用以下上下文信息。上下文信息中没有提到的内容绝对不允许出现在你的答案中。”调整链类型chain_type“stuff”可能因为上下文太长模型忽略了中间部分。可以尝试“map_reduce”或“refine”。“map_reduce”会对每个文档片段先单独生成答案再汇总适合处理大量文档。“refine”则迭代式地完善答案质量可能更高但速度慢。后处理验证设计一个验证步骤用另一个LLM或规则判断生成的答案是否能在提供的上下文中找到明确支持。如果找不到则返回“无法回答”。7.4 Milvus连接或操作失败症状Python客户端无法连接或插入/搜索时报错。检查服务状态运行docker-compose ps确保所有容器正常运行。查看Milvus日志docker-compose logs milvus-standalone。确认端口和主机确保连接参数host和port正确。在Docker容器内运行时host不能是localhost而应是宿主机IP或服务名。集合未加载Milvus的集合在搜索前需要加载到内存。Milvus.from_documents和Milvus()初始化时通常默认加载。但如果手动操作记得collection.load()。版本兼容性确保pymilvus版本与Milvus服务器版本兼容。版本不匹配是常见错误源。7.5 一个简易的RAG系统诊断清单当你发现系统回答不好时可以按以下步骤隔离问题步骤检查点工具/方法1. 检索隔离检索到的文档真的相关吗直接调用retriever.get_relevant_documents(query)人工检查返回的文本块。2. 嵌入隔离向量相似度计算合理吗计算问题与检索到的片段的向量手动计算余弦相似度或检查Milvus返回的score。3. 提示词隔离给LLM的输入上下文问题清晰吗打印出即将发送给LLM的完整提示词模板prompt_template.format(...)检查上下文是否完整、格式是否正确。4. LLM隔离LLM本身有能力回答吗将一段包含明确答案的文本作为上下文连同问题直接提问LLM看它能否正确提取信息。5. 综合测试整个链路在简单case上是否工作构造一个“大海捞针”测试在知识库中插入一句独特的话如“张三的生日是1990年1月1日”然后问“张三的生日是哪天”看系统能否准确回答。这套从零搭建的RAG系统已经具备了核心的“检索-增强-生成”能力。然而这仅仅是生产可用的起点。在实际项目中我们还需要考虑更多工程化问题例如如何设计异步管道来处理持续增长的文档流如何实现多路检索向量关键词数据库并合并结果如何加入对话历史实现多轮问答如何对系统回答进行置信度评估和事实性核查如何监控检索命中率和用户反馈这些将是“从零到生产”下篇要探讨的核心内容。