ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

十分钟搭建RAG Pipeline:LangChain与ChromaDB实战入门

十分钟搭建RAG Pipeline:LangChain与ChromaDB实战入门 1. 从零到一为什么你的第一个RAG Pipeline应该从这里开始如果你最近在关注大模型应用开发RAG检索增强生成这个词一定高频出现在你的视野里。它被看作是解决大模型“幻觉”和知识过时问题的银弹但很多人在动手时往往会陷入一个误区一上来就研究复杂的架构、对比各种向量数据库、纠结于重排序和Agent。结果就是看了无数教程依然搭不出一个能跑起来的Demo。我的建议是先忘掉那些复杂的术语用LangChain这个“脚手架”工具快速搭建一个最小可行产品MVP。这就像学编程你得先写出“Hello World”理解程序是怎么跑起来的再去研究设计模式和架构。今天我就带你用LangChain配合一个轻量级的向量数据库ChromaDB在十分钟内搭建起你的第一个RAG Pipeline。这个Pipeline虽然简单但它包含了RAG最核心的三个环节文档加载与处理、向量化存储与检索、以及与大模型的结合生成。走通这个闭环你才算真正摸到了RAG的门槛后续所有的优化和复杂化都是在这个坚实的基础上进行的。2. 环境准备与工具选型为什么是LangChain ChromaDB在开始敲代码之前我们需要把“厨房”收拾好。这里的核心是LangChain和向量数据库。你可能听过LlamaIndex、Haystack等其他框架也见过Milvus、Pinecone、Qdrant等一众向量数据库。为什么我推荐新手从LangChain和ChromaDB开始这背后有几个非常实际的考量。首先看LangChain。它本质上是一个编排框架把LLM应用开发中常见的环节如调用模型、处理文档、管理记忆、构建链抽象成了标准化的模块。它的优势不在于某个单一功能最强而在于“开箱即用”和“快速集成”。对于搭建第一个Pipeline来说我们最需要的是减少心智负担快速看到结果。LangChain提供了清晰的、高级的API让我们能用很少的代码就把文档加载、文本分割、向量化、检索、提示词组装、模型调用这一串流程串起来。你不用自己去写HTTP请求调用Embedding接口也不用手动管理检索出来的上下文如何拼接到提示词里这些繁琐但通用的步骤LangChain都帮你封装好了。当然LangChain的抽象有时会带来黑盒感和灵活性不足的问题但那是进阶时才需要权衡的对于入门和验证想法它是最高效的工具。然后是向量数据库。我们的目标是把文档转换成向量一组数字存起来之后用问题向量去库里找最相似的文档向量。ChromaDB是一个轻量级、嵌入优先的向量数据库。它最大的优点就是简单可以完全在内存中运行也可以持久化到磁盘无需复杂的服务部署比如Milvus。对于本地开发和第一个Demo来说这种零依赖、零配置的特性是无可比拟的。你只需要pip install chromadb然后在代码里初始化一个客户端它就能工作了。它默认使用余弦相似度作为距离函数这对于大多数文本语义检索场景已经足够好。我们第一步的目标是验证流程而不是追求极致的检索性能或处理海量数据因此ChromaDB的轻便性完美匹配了我们的需求。注意虽然ChromaDB默认使用余弦相似度但在初始化其向量存储组件时你也可以通过collection_metadata参数中的hnsw:space来指定使用l2欧氏距离等其他距离度量方式。不过对于文本余弦相似度关注向量方向而非长度通常效果更好。具体到环境我们需要安装以下包。我强烈建议使用虚拟环境如venv或conda来管理依赖避免包冲突。pip install langchain langchain-community langchain-openai chromadb tiktoken这里解释一下langchain: LangChain核心框架。langchain-community: 社区维护的第三方集成工具比如一些文档加载器。langchain-openai: OpenAI模型的官方LangChain集成我们将用其Embedding和Chat模型。chromadb: 向量数据库。tiktoken: OpenAI用于计算Token的库某些文本分割器会用到。安装完成后别忘了准备好你的OpenAI API Key并设置为环境变量这是后续调用模型服务的通行证。export OPENAI_API_KEY你的-api-key # 或者在代码中通过os.environ设置3. 构建核心流水线三步拆解与逐行代码解读现在我们进入核心环节用代码把RAG Pipeline的三个核心步骤串联起来。我会把代码分成块并详细解释每一行在做什么以及为什么这么做。3.1 第一步文档加载与智能切片RAG的第一步是处理你的“知识库”——通常是文本文件。这里我们用一个简单的Markdown文件作为示例。文档加载后不能直接把一整本书扔给模型需要切成小块这叫“文本分割”或“分块”。from langchain_community.document_loaders import TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter # 1. 加载文档 loader TextLoader(./my_knowledge_base.md) # 替换为你的文件路径 documents loader.load() # 2. 分割文本 text_splitter RecursiveCharacterTextSplitter( chunk_size500, # 每个块的最大字符数 chunk_overlap50, # 块与块之间的重叠字符数 length_functionlen, # 用于计算长度的函数 separators[\n\n, \n, 。, , , ] # 分割符优先级 ) chunks text_splitter.split_documents(documents) print(f原始文档被分割成了 {len(chunks)} 个块。)关键参数解读与避坑指南chunk_size: 这是最重要的参数。设置太小会丢失上下文信息比如一个完整的步骤被拆散设置太大检索精度会下降且可能超过模型的上下文窗口。500-1000字符是一个常见的起始点。你需要根据文档类型技术文档、小说、对话和后续模型窗口来调整。chunk_overlap: 重叠是为了防止一个完整的句子或概念被硬生生切断。例如一个段落如果刚好在500字符处被切断重叠50字符能确保下一块的开头包含上一块的结尾部分保持语义连贯。通常设置为chunk_size的10%-20%。separators: 分割符列表定义了切分的优先级。RecursiveCharacterTextSplitter会按顺序尝试用这些分隔符去分割直到每个块都小于chunk_size。这里的顺序意味着先尝试按双换行段落分不行再按单换行分再按句号、逗号、空格最后按字符。这个策略对中文和英文混合文档比较友好。避坑点不要迷信默认值。对于中文文档你可能需要把“。”、“”等加入separators。分割后务必打印几个块出来看看检查是否把完整的句子或表格拆得支离破碎。这是影响后续检索质量的第一步也是最容易出错的一步。3.2 第二步向量化存储与检索器搭建文本块准备好后我们需要把它们变成向量存进数据库并创建一个检索器。from langchain_openai import OpenAIEmbeddings from langchain.vectorstores import Chroma # 1. 初始化嵌入模型 embeddings OpenAIEmbeddings(modeltext-embedding-3-small) # 使用OpenAI的Embedding模型 # 2. 创建向量数据库并存储向量 # persist_directory 指定持久化目录如果为空则仅在内存中 vectorstore Chroma.from_documents( documentschunks, embeddingembeddings, persist_directory./chroma_db # 数据将保存到此目录 ) vectorstore.persist() # 显式持久化到磁盘 # 3. 从向量库创建检索器 retriever vectorstore.as_retriever( search_typesimilarity, # 相似度检索 search_kwargs{k: 3} # 返回最相似的3个块 )核心原理与选型思考嵌入模型这里我们用了OpenAI的text-embedding-3-small。它速度快、成本低且效果对于入门场景足够好。Embedding模型负责把文本转换成高维空间中的点语义相近的文本其向量在空间中的距离如余弦相似度也更近。这是RAG能进行语义检索的数学基础。向量数据库Chroma.from_documents这个方法一气呵成它内部做了三件事a) 调用Embedding模型将每个文本块转为向量b) 将这些向量和对应的原始文本元数据存储到ChromaDB中c) 创建索引以便快速检索。指定persist_directory后数据会保存到本地下次运行可以直接加载无需重新计算向量这对迭代开发非常友好。检索器检索器是对向量存储的封装提供了统一的搜索接口。search_typesimilarity表示使用向量相似度搜索即余弦相似度。k3表示每次检索返回最相关的3个文本块。这个k值是个超参数太小可能信息不全太大可能引入噪声并增加模型处理负担。从2-5开始尝试是合理的。3.3 第三步组装提示词与大模型调用这是最后一步也是最体现LangChain价值的一步它帮我们把检索到的上下文和用户问题组装成一个结构化的提示词Prompt然后调用大模型生成答案。from langchain_openai import ChatOpenAI from langchain.chains import RetrievalQA from langchain.prompts import PromptTemplate # 1. 定义提示词模板 prompt_template 请根据以下上下文信息来回答问题。如果你不知道答案就诚实地回答不知道不要编造信息。 上下文 {context} 问题{question} 请给出答案 PROMPT PromptTemplate( templateprompt_template, input_variables[context, question] ) # 2. 初始化大语言模型 llm ChatOpenAI(model_namegpt-3.5-turbo, temperature0) # 3. 创建检索增强生成链 qa_chain RetrievalQA.from_chain_type( llmllm, chain_typestuff, # 最常用的类型将所有检索到的文档“塞”进提示词 retrieverretriever, chain_type_kwargs{prompt: PROMPT}, # 使用我们自定义的提示词 return_source_documentsTrue # 非常重要返回检索到的源文档用于验证 ) # 4. 进行问答 question LangChain是什么 result qa_chain.invoke({query: question}) print(f问题{question}) print(f答案{result[result]}) print(\n--- 参考来源 ---) for i, doc in enumerate(result[source_documents]): print(f[来源{i1}] {doc.page_content[:200]}...) # 打印前200字符深度解析与经验之谈提示词工程模板中的{context}和{question}是占位符LangChain会在运行时用检索到的文本和用户问题替换。清晰的指令如“根据上下文”、“不要编造”能显著提升模型回答的准确性和可靠性。这是控制模型行为的关键。Chain类型chain_typestuff是最简单直接的方式它把所有检索到的文档内容拼接起来一并放入提示词的上下文窗口。它的优点是简单信息完整缺点是可能超过模型的上下文限制。如果检索到的文档总长度很长可以考虑map_reduce或refine等更复杂的链类型它们会对文档进行分组合并或迭代精炼但复杂度也更高。对于第一个Pipeline“stuff”足矣。模型参数temperature0使得模型的输出尽可能确定和一致适合事实性问答。如果你希望答案更有创意可以适当调高。return_source_documentsTrue这是我强烈建议你始终开启的选项。它让链返回检索到的原始文档片段。这有两个巨大好处1)可解释性/可验证性你可以看到模型是基于哪些材料得出的答案判断答案是否可靠是否存在“幻觉”。2)调试如果答案不对你可以立刻检查是不是检索到的文档本身就不相关从而定位问题是出在检索阶段还是生成阶段。调用方式我们使用了.invoke()方法这是LangChain新版本v0.1.0推荐的统一接口。传入一个字典键为“query”。4. 从Demo到实用你可能遇到的坑与进阶方向如果你的代码成功运行并得到了答案恭喜你你已经搭建了一个最基础的RAG系统。但这仅仅是开始。在实际应用中你会立刻遇到一系列问题下面我分享几个最常见的“坑”和对应的解决思路。4.1 检索质量不佳为什么总是找不到对的文档这是RAG系统最核心的挑战。表现就是你明明知识库里有答案但系统检索出来的都是不相关的文档导致模型要么胡说要么说不知道。排查与优化思路检查文本分割回到第一步打印出你的chunks。看看是不是把完整的句子、表格或代码块切碎了一个被切碎的概念其向量表示会失真。尝试调整chunk_size和chunk_overlap或者换用更智能的分割器比如按语义分割的SemanticChunker需要额外的模型。审视Embedding模型OpenAI的Embedding模型对英文优化更好。如果你的知识库主要是中文或者有大量专业术语可以考虑专门针对中文优化的开源Embedding模型如BGE、M3E系列。在LangChain中你可以轻松替换OpenAIEmbeddings为HuggingFaceEmbeddings。优化检索策略多路召回不要只依赖向量相似度。可以结合关键词搜索如BM25进行混合检索。LangChain的EnsembleRetriever可以支持。重排序向量检索初步返回了K个结果比如k10但这10个结果里可能混杂着一些相关性不高的。可以使用一个更精细的、计算量更大的重排序模型如BGE-Reranker对这10个结果重新打分和排序只取Top 3给模型。这能显著提升最终上下文的质量。元数据过滤如果你的文档有天然结构如章节、标签、日期可以在存储时为每个块添加元数据如{“source”: “chapter1”}。检索时可以要求retriever只检索特定元数据的文档缩小范围提升精度。评估检索效果构建一个小的测试集包含一些问题和对应的标准答案文档。计算你的检索器召回这些标准文档的准确率命中率。没有评估优化就是盲目的。4.2 回答冗长或偏离重点如何让模型更听话即使检索到了对的文档模型也可能生成啰嗦、包含无关信息或者格式不符合要求的答案。提示词与链的调优强化提示词指令在提示词模板中给出更明确的指令。例如请严格仅根据提供的上下文回答问题。 答案应简洁明了不超过三句话。 如果上下文信息不足请直接回答“根据已知信息无法回答此问题”。 请勿在答案中添加任何上下文以外的信息。使用更强大的模型gpt-3.5-turbo有时在遵循复杂指令和长上下文理解上不如gpt-4-turbo。如果条件允许升级模型是立竿见影的方法。后处理在得到模型输出后可以增加一个后处理步骤比如用规则或另一个小模型来提取答案中的核心部分或者格式化输出。4.3 效率与成本考量如何应对大量文档和频繁查询当你的知识库从几个文档变成成千上万个文档时简单的Pipeline会遇到挑战。向量索引与持久化我们使用了本地持久化的ChromaDB。对于生产环境你可能需要考虑部署可扩展的向量数据库服务如Qdrant、Milvus或Pinecone云服务。它们支持分布式、高性能检索。异步处理文档加载、分割、向量化Embedding都是耗时的IO或计算操作。在构建知识库时可以使用异步编程来加速处理大量文件。缓存策略对于频繁出现的相同或相似问题可以将“问题-答案”对缓存起来直接返回缓存结果避免重复的检索和生成降低延迟和成本。LangChain提供了多种缓存组件。成本控制Embedding和LLM调用都按Token计费。监控Token使用量对文本进行适当的清洗和压缩如移除多余空格、换行符使用更高效的Embedding模型如text-embedding-3-small比-ada-002更便宜都能有效控制成本。4.4 架构演进从简单链到智能体Agent我们目前构建的是一个简单的RetrievalQA链它是线性的检索 - 组装提示词 - 生成。但现实世界的问题更复杂。例如用户问题可能需要多步推理、调用工具如计算器、搜索引擎、或者根据初步答案进行追问。这时你可以考虑引入智能体。LangChain提供了Agent框架它让大模型具备“思考-行动-观察”循环的能力。一个典型的RAG Agent流程可能是模型先判断是否需要检索知识库如果需要则调用检索工具就是我们上面构建的retriever获取资料然后综合资料和自身知识给出答案甚至可能进行多轮交互。LangGraph则是LangChain之上用于构建复杂、有状态的多智能体工作流的库。如果你的应用需要多个智能体协作或者有非常复杂的决策流程LangGraph提供了更强大的编排能力。但对于绝大多数入门和中等复杂度的RAG应用标准的Chain和Agent已经足够。搭建第一个能跑的RAG Pipeline就像拼好了乐高套装最核心的骨架。它可能不完美但它是你理解所有概念和进行后续所有优化的基石。接下来你要做的不是急于把它复杂化而是拿着这个最简单的版本去反复测试观察它在不同问题、不同文档下的表现记录下它失败的情况。每一次失败都指向一个明确的优化方向是分割问题就去调分割器是检索问题就去换Embedding或加召回策略是生成问题就去优化提示词或换模型。这个迭代的过程才是真正掌握RAG技术的路径。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进