LangChain与RAG实战:从架构拆解到代码落地
发布时间:2026/10/18 19:21:06 锦皓数字建站

大模型应用要真正落地绕不开两件事怎么把内部资料和私有知识喂给模型怎么把多步复杂的任务串成一套自动化流程。LangChain 和 RAG检索增强生成正是目前解决这两个问题的主流组合拳。这篇文章我会从 LangChain 的架构设计讲起再到 RAG 核心工作流的每个环节逐个拆解最后给出一套可以直接跑通的代码实现。适合已经会调用大模型 API、但对 LangChain 内部机制和 RAG 工程化细节还不熟悉的开发者看完你会发现很多网上传得神乎其神的东西拆开来其实就那么几块积木。1. 先看清全局LangChain 到底在解决什么问题1.1 大模型应用的普遍痛点在 LangChain 出现之前用大模型 API 写业务逻辑大家普遍在忍受三件事一是 prompt 拼接全靠手写每次调不同模型都要重新组装一套消息格式二是业务逻辑和模型调用深度耦合换个模型恨不得把代码翻一遍三是外部数据接入很别扭想让模型引用你自己的文档、数据库、工具结果得自己处理切分、检索、拼装那一大堆杂活。这三个痛点叠加起来导致早期大模型项目的代码库结构极其混乱。我当时接过一个内部问答系统主文件三千多行一半是 prompt 模板字符串一半是各种 if-else 判断该调哪个接口维护成本非常高。LangChain 本质上做的事情就是把“大模型应用开发”这件事抽象成几个稳定的组件让我们像搭积木一样组织代码同时屏蔽掉底层模型的差异。1.2 LangChain 的核心抽象五类积木LangChain 的架构设计核心是把大模型应用拆成五个抽象层次这也是理解整个框架的钥匙。第一层是 Model I/O负责模型输入输出的标准化封装。不管是 OpenAI、国产开源模型、本地部署的推理服务在 LangChain 里都可以被封装成统一的接口。你换模型的时候只需要换实例化的对象下游的 prompt 组装和输出解析逻辑基本不用动。第二层是 Retrieval也就是检索模块。它把文档加载、文本切分、向量化存储、相似度检索这四步封装成一套标准链路。RAG 的核心逻辑就在这一层后面我会专门展开讲。第三层是 Memory处理多轮对话的历史记忆。它解决了无状态 API 的痛点——大模型本身不记上下文每次调用都是全新的Memory 模块负责帮你把聊天记录、中间状态管理起来并且会智能地处理“哪些历史信息需要带进当前轮次”。第四层是 Chains它是我个人认为 LangChain 最核心的抽象。Chain 的本质是把多个步骤串成一个管道前一个环节的输出自动成为下一个环节的输入。比如一个简单的 QA Chain就是“检索增强的 prompt 输入 - 模型调用 - 输出解析”这三步串起来。复杂一点的 Chain 还会带条件分支比如先判断用户问题是否需要查库再决定走哪条生成路径。第五层是 Agents代理机制是在 Chain 的基础上加入了“模型自主决策”的能力。Agent 会让模型自己观察当前状态、决定调用哪些工具、解析工具返回的结果然后循环这个过程直到完成任务。说白了Chain 是固定流程Agent 是动态决策流程。这五个抽象不是凭空设计的它们恰好对应了大模型应用开发的几个必经环节跟模型打交道、跟数据打交道、跟上下文打交道、跟流程打交道、跟外部工具打交道。理解了这五个层次你在看任何 LangChain 项目代码时就能迅速定位每段代码属于哪个层级。1.3 为什么值得选 LangChain而不是自己封装有人可能会说直接用 Requests 调 API再自己写一个检索函数几十分钟也能搞定一个小项目何必引入一个框架。这个说法对 Demo 场景成立但对生产级项目不成立。我在实际项目里体会到 LangChain 最大的价值不是那些 API 封装而是生态里的标准接口。如果我用 LangChain 的向量存储接口写好了文档索引模块后续要换向量数据库、换 embedding 模型、加一个重排器都只需要替换独立组件索引逻辑本身不用改动。这种可替换性才是框架真正的护城河。另外LangChain 的抽象让团队成员之间的协作更顺畅。代码评审的时候大家看到 Retriever、Chain、Agent 这些概念就大概知道模块的职责边界不需要从头到尾读一遍业务代码。这个价值在多人协作时会被放大得特别明显。当然LangChain 也不是没有缺点版本迭代快、部分 API 命名变化大、抽象层存在泄漏问题。这些坑我在第 4 部分会结合排查经验详细说这里先不展开。2. RAG 核心工作流拆解从文档到答案的完整链路2.1 RAG 是什么为什么必须用它RAG 全称 Retrieval-Augmented Generation检索增强生成。它解决的是大模型的三个硬伤知识截止时间带来的信息过时、训练数据里没有你的私有知识、以及纯靠模型参数记忆知识导致的幻觉问题。有个很直观的类比如果大模型是一个刚毕业但记忆力极好的实习生RAG 就是你在开会前塞给他的一叠参考资料。他不依赖脑子里那点模糊记忆去回答问题而是先翻资料找到相关内容再结合自己的表达能力给出回答。这样既保证了答案的时效性和内部一致性又减少了他自己编造内容的概率。RAG 的标准流程可以划分为索引、检索、生成三大阶段。索引阶段负责把文档处理成可搜索的形式检索阶段根据用户问题找出最相关的文本片段生成阶段把检索到的片段和用户问题一起交给模型组织答案。看起来不复杂但每个阶段里都有很多影响最终效果的细节。2.2 索引链路加载、切分、向量化索引链路是 RAG 的地基。先把各种格式的文档加载进来这一步在 LangChain 里通过不同的 Document Loader 完成。文本文件用 TextLoaderPDF 用 PyPDFLoader 或 PyMuPDFLoader网页用 WebBaseLoader结构化数据可以走 CSVLoader。每种加载器都会返回统一的 Document 对象里面有 page_content 和 metadata 两个字段便于下游处理。加载之后是文本切分。很多人在这一步草草了事直接用固定长度硬切导致检索质量很差。我强烈推荐用 RecursiveCharacterTextSplitter它支持按一组分隔符递归切分优先按段落切段落太长再按句子切这样能最大限度保持语义的完整性。切分参数里最重要的两个是 chunk_size 和 chunk_overlap。chunk_size 决定每个块的最大字符数chunk_overlap 是相邻块之间的重叠字符数。为什么需要重叠因为如果一段完整的语义恰好被切分点从中间截断两个块各占一半检索时都不完整召回效果自然差。重叠能让上下文信息在相邻块之间传递。我常用的配置是 chunk_size500、chunk_overlap100配合上层级分隔符切分文本召回效果明显优于固定切分。切分完之后就是向量化也就是 Embedding 过程。这一步把每个文本块变成一个高维向量语义越接近的文本向量距离越近。向量化的关键决策点是选 embedding 模型和向量数据库。embedding 模型的选择直接影响检索质量通用场景可以用 OpenAI 的 text-embedding-3-small 或国产的 bge-m3 系列领域特定场景微调过的 embedding 模型往往效果显著更好。向量数据库方面项目初期数据量不大时完全可以用 FAISS 这类轻量本地库数据量到了百万级再考虑上 Milvus 或者 ES 的向量检索能力。2.3 检索与增强召回、重排、注入提示词检索阶段的核心目标是“找到最相关的信息”。最简单的方式是用 embedding 做相似度搜索把用户问题向量化在向量数据库里找出余弦相似度最高的 k 个文本块。k 值的选择是个权衡太小可能漏信息太大容易把不相关的内容也带进来干扰模型组织答案。我一般从 k4 开始调根据实际效果调整。现实场景下纯向量检索往往不够。这里有两个常见的优化手段一是混合检索向量相似度和 BM25 关键词匹配同时进行再合并结果。向量检索擅长捕捉语义相似BM25 擅长精确匹配领域术语和专有名词两者结合能提升召回的全面性。二是重排序Re-Rank第一轮先扩大召回量比如召回 20 个块然后用一个专门的交叉编码器重排序模型对问题与每个块的相关性重新打分取分数最高的 4-5 个进入生成阶段。重排环节的效果提升非常明显是 RAG 系统性能优化的关键手段。检索完成后是增强阶段核心是把检索到的内容注入到 prompt 里。在 LangChain 中这一步通过 PromptTemplate 完成你需要设计一个清晰的指令模板告诉模型它是基于给定上下文回答问题的助手、要求它只基于上下文回答不得发挥、规定如果上下文没有答案就明确说不知道。这一步的 prompt 设计直接关系到最后输出的可控性和准确度。这里要特别提一个常被忽视的细节在 prompt 中要求模型输出时标注信息来源比如“根据文档第 3 节”或列出引用的文档片段。这不仅能减少幻觉还能让用户对答案有信任感。做法很简单检索阶段保留每个文本块的 metadata注入 prompt 时连同来源一起给到模型。2.4 进阶思路Query 改写、HyDE、多路召回基础 RAG 在用户问题表述模糊、问题包含代词指代或用户问题和文档表述差异大时召回效果会很差。比如用户问“那个新出的压缩技术怎么样”如果没有上下文这句话向量化后跟你的技术文档大概率匹配不上。针对这类问题有几套成熟的处理思路。Query 改写是最实用的一个先用大模型对原始问题做一遍改写把伪指代补全、把口语转成书面语、把一句话扩展成更详细的检索 query。比如上面的问题可以被改写成“XX 公司于 2024 年发布的 XX 压缩技术的性能表现和技术原理是什么”。然后拿改写后的 query 去做向量检索。这个方法成本很低但对召回率的提升很可观。HyDE假设性文档嵌入的思路更激进一些让模型先基于问题生成一篇假设性的回答文档再用这篇假设文档去做向量检索。原理是假设文档和真实文档的措辞通常比问题和文档的措辞更接近从而提升向量匹配的准确度。代价是多了一次模型调用速度会慢一些。多路召回则是组合多种检索方式的结果。比如向量检索一路、BM25 一路、按 metadata 过滤的规则检索再一路合并结果后统一去重再进重排模型选优。这个方法的效果很稳适合对准确率要求高的业务场景代价是系统复杂度上升需要额外做合并和秩序逻辑。3. 实战用 LangChain 搭建一套可运行的 RAG 系统3.1 环境准备与分析动手之前先确认环境。我用的 Python 版本是 3.10LangChain 版本以 0.1.x 为例因为 0.2/0.3 之后部分 API 有变化不过本节的核心思路是通用的。安装依赖主要分三类一是 LangChain 基础包和文档加载相关包二是向量存储和 embedding 相关的包三是大模型推理的库。下面是我惯用的依赖清单。pip install langchain langchain-community langchain-openai chromadb faiss-cpu pip install pypdf tiktokenembedding 模型我优先推荐本地方案可以避免网络请求带来的延迟和成本问题。使用 HuggingFace 的 bge-small-zh 或者使用 OpenAI 的 text-embedding-3-small 都是可行的选择取决于你的部署环境。为了示例的通用性下面的代码以 OpenAI 兼容接口为例子你在实际项目里可以替换成本地模型。3.2 完整代码实现这套代码实现的是完整的 RAG 链路加载本地 PDF 文档 - 切分 - 向量化 - 存储到 FAISS - 构建检索器 - 组装生成链 - 问答验证。整个流程用 LangChain 的组件串联你只需要准备好一个 PDF 文件路径即可跑通。from langchain_community.document_loaders import PyPDFLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import FAISS from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI # 1. 加载文档 loader PyPDFLoader(tech_manual.pdf) documents loader.load() print(f加载完成共 {len(documents)} 页内容) # 2. 切分文本优先按层级分隔符递归切分兼顾语义完整性和块大小 text_splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap100, separators[\n\n, \n, 。, , , , , , ] ) chunks text_splitter.split_documents(documents) print(f切分完成共 {len(chunks)} 个文本块) # 3. 向量化并存入向量库 embeddings OpenAIEmbeddings(modeltext-embedding-3-small) vectorstore FAISS.from_documents(chunks, embeddings) # 4. 构建检索器设定召回数量 retriever vectorstore.as_retriever(search_kwargs{k: 4}) # 5. 设计提示词模板约束模型基于上下文作答 prompt ChatPromptTemplate.from_messages([ (system, 你是一个严谨的技术问答助手。请严格基于给定的上下文内容回答用户问题。 如果上下文中没有相关内容请直接回答根据现有资料无法回答该问题。 回答中适当引用上下文中的表述。), (human, 上下文信息{context}\n\n用户问题{question}) ]) # 6. 组装 RAG 链路 llm ChatOpenAI(modelgpt-3.5-turbo, temperature0) rag_chain ( { context: retriever, question: lambda x: x[question] } | prompt | llm | StrOutputParser() ) # 7. 执行问答验证 result rag_chain.invoke({question: 这套系统的故障恢复机制是什么}) print(回答结果, result)3.3 关键步骤的逐一解读这段代码的每一步都有值得说的细节。第一步加载 PDF 时PyPDFLoader 会把每个页面解析成一个 Document 对象保留每页的内容和页码 metadata。如果你后续要把答案定位到具体页码这个 metadata 非常有用。第二步切分时我用了层级分隔符列表。优先按双换行符切分保持段落完整段落太长再按单个换行符然后逐级降级到标点符号。这样切出来的文本块大多是语义完整的自然段落而不是生硬的固定长度字符。chunk_size500 换算成中文大概是一段 300-400 字的文字chunk_overlap100 保证跨块上下文不丢失。这套参数适合技术文档类的中文和英文混合内容如果是代码文档或者表格密集的文档建议把 chunk_size 调低到 300 左右避免单个块里塞进太多不相关的代码片段。第三步向量化和存储是整个链路的关键。FAISS.from_documents 会先逐条调用 embedding 模型把每个 chunk 变成向量后建索引。这里有个容易踩的坑embedding 模型必须保持前后一致。如果你用 text-embedding-3-small 建了索引检索时也必须用同一个模型否则向量空间不一致检索效果会完全不可用。第五步提示词模板我特意把三个核心约束都写进去角色定位、基于上下文回答、不知道就不硬编。temperature 参数在第六步设置为 0确保生成的确定性。对于事实性问答场景temperature 调高会引入无谓的随机性产生幻觉的概率明显上升。第七步调用时LangChain 会自动执行整个管道先拿问题去向量库检索出 top 4 相关文本块把文本拼接成 context 字段填进 prompt再传给模型最后解析输出。这里我用了新的 LCEL 语法和管道操作符相比旧版的 Chain 写法更直观也更容易做并行化改造。3.4 检索效果的可视化验证直接看最终答案还不够我习惯在跑通后先看看检索器到底召回了什么内容。这能帮助判断问题出在检索环节还是生成环节。# 查看检索器召回的原始文本块 docs retriever.invoke(这套系统的故障恢复机制是什么) for i, doc in enumerate(docs): print(f--- 召回片段 {i1} ---) print(doc.page_content[:300]) print(f来源页码: {doc.metadata.get(page, 未知)}) print()这一步非常实用。你会发现有时候问题根本不在生成阶段而是检索器压根没有召回相关文档这种时候再怎么调 prompt 都没用要先优化切分参数、改造查询或者引入重排。实际开发中我会把这段验证代码封装成一个工具函数方便在迭代中随时查看召回质量。4. 常见问题排查与工程踩坑记录4.1 召回结果差答案总是张冠李戴这是 RAG 项目里最高频的问题。排查的顺序要先看检索再把关生成。第一步确认 embedding 模型选择是否合适。通用模型在垂直领域的效果通常一般比如医疗、法律、编程领域的专业术语通用 embedding 无法很好区分语义差异。建议先拿一批典型问题做召回测试看看召回的文本块是不是自己期望看到的。如果召回的块牛头不对马嘴考虑换领域内效果更好的 embedding 模型。第二步调整切分策略。chunk_size 太大会让每个向量块包含太多冗余信息稀释核心语义太小则导致块内信息不完整。chunk_overlap 设置为 0 时被切散的语义片段容易两个都召回不到。我自己调试时的经验是先用一组典型问题跑基线效果然后分别调整 chunk_size 和 chunk_overlap每次只变一个参数记录相应的召回准确率变化。这个笨办法比盲目参考网上的配置可靠得多。第三步考虑用户问题的特殊性。如果用户问题是长句、口语化或者省略了大量上下文直接拿原问题进行向量搜索效果普遍不好。建议增加 Query 改写环节让模型先把问题规范化再检索。我在一个内部知识库项目中加入 Query 改写后首轮检索的相关性分数平均提升了约 12%效果非常明显。4.2 模型回答出现幻觉或者回答太发散幻觉问题的根源往往不在模型而在 prompt 约束不够硬和检索上下文不精准。最常见的处理方式是把 prompt 变成强约束模板给模型限定只能使用上下文中的信息并且要求答案附上上下文片段引用同时把 temperature 调低到 0 左右压制自由发挥的空间。有一种更实用的思路是把“不知道”变成合法答案。很多模型幻觉是因为它被训练成无论如何都要回答用户强制它说“不知道”反而违背了它的训练习惯。所以 prompt 里要明确告诉它当上下文与问题无关时建议直接回答“根据现有资料无法回答该问题建议补充相关文档”并给出上下文中的相似或相关内容。这样的回答虽然不够“好看”但胜在安全可靠。最后检索环节如果召回的 4 个块里只有 1 块与问题强相关其他 3 块都是边缘内容模型很容易被噪声内容带偏。这时候可以在生成前列一轮相关性过滤用简单规则删除与问题相似度评分过低的内容块或者引入重排模型只保留高相关的片段。这个动作能显著减少幻觉。4.3 LangChain 版本变更带来的兼容性坑LangChain 的 API 变化频繁这是被吐槽最多的地方。我在 0.1 到 0.2 的升级中踩过不少坑部分顶级导入路径发生变化比如某些链从 langchain.chains 迁移到了 langchain_community 或者 langchain_experimental一些旧方法被标记为废弃但还能用过几个版本直接删掉还有内部依赖的 pydantic 版本冲突导致某些类实例化时报错。我的经验是项目创建时锁定版本号比如 langchain0.1.0升级前先跑一遍全量测试。如果代码报错优先去官方迁移文档搜 API 变更而不是在网上找零散的方案。生产环境尽量慢半拍升级等社区把新版本的坑填得差不多了再动。另一个常见问题是过度使用实验性 API。Agent 相关的许多新功能和 super-tools 都标着 experimental线上稳定性难以保证。这类功能适合在不影响核心业务流的场景做预研不要一上来就引入核心链路。4.4 成本与性能优化RAG 项目的成本大头通常在 embedding 调用次数和大模型 token 消耗。embedding 方面文档切分后会产生大量文本块每个块都要过一遍 embedding 模型。如果文档量很大每次都实时计算非常浪费。我推荐做一次本地持久化把向量库保存到磁盘。FAISS 支持 save_local 和 load_local 方法索引构建一次后反复加载即可。代码如下# 保存向量库到本地 vectorstore.save_local(faiss_index) # 下次使用时加载 vectorstore FAISS.load_local( faiss_index, embeddings, allow_dangerous_deserializationTrue )注意 load_local 时有个 allow_dangerous_deserialization 参数。FAISS 反序列化原理上存在潜在的安全风险这个参数默认是 False你必须显式设为 True 才能加载。如果你使用的是不受信任的 FAISS 索引文件建议先检查来源再加载。这个安全设计有其合理性不要为了方便直接设 True 而无视风险。生成阶段的 token 成本控制核心方法是控制上下文长度。检索到的 4 个块每个 500 字符加上 prompt 模板就是 2000 多字符多轮对话还会累加历史消息每轮调用的输入 token 数会越来越膨胀。通常用两种策略缓解限制检索块数量和长度把默认 4 个块改为 3 个并在提示词里要求模型只引用最相关的片段多轮对话时只把最近两轮历史放入上下文更早的内容交给 Memory 模块做摘要压缩。这两种方法在保证效果的前提下能把单轮成本降下来 20% 到 30%。5. 再往前走一步从固定流程到 Agent 化改造5.1 把 RAG 从链路升级成工具基础的 RAG 管道是固定的用户问题进答案出。但在真实业务里用户问题远不止“查一下文档然后回答”这么简单。比如用户问“帮我对比一下方案 A 和方案 B 的优缺点”一个 RAG 链路只能分别检索两部分的上下文却难以完成对比分析的多步推理再比如“先查一下最近的故障记录再写一份周报”这明显是个多工具的复合任务。这类场景适合把 RAG 包装成 Agent 可调用的工具。LangChain 里的 Tool 抽象允许你在 Agent 的工具列表里注册一个 Retriever 工具模型自主决定什么时候调用它、调用几次、拿到结果后如何继续推理。改造之后原来的 RAG 链路从“唯一执行者”变成“众多工具之一”系统的适用范围扩展了很多。我在一个运维知识库项目中做过类似改造Agent 手里有三个工具——RAG 检索器、故障记录查询器、日志分析引擎。用户提问后Agent 会先规划步骤必要时多次调用不同工具最后汇总推理出答案。相比原先固定链路这类 Agent 化 RAG 能处理的问题类型多了不少但相应地对调用过程的调试和可观测性要求也高了很多。5.2 工程化落地时的一些建议最后分享几个我在多个 RAG 项目的生产环境中总结出来的经验不算什么高大上的理论但都是实打实节省过时间的细节。日志和可观测性一定要提前做。每个查询、每次检索召回、每轮模型调用的输入输出都记录下来。一旦线上有问题没有日志基本就是盲人摸象。我在生产环境会给每个请求分配一个 trace_id贯穿检索和生成全链路。排查时顺着 trace_id 看整个请求链路定位问题远比翻终端日志高效。检索链路和生成链路分开评估。很多团队评估 RAG 系统只看最终答案的准确率但这样会掩盖问题的真正来源。更好的做法是分别评估检索命中率和生成正确率先构造一批问题和对应相关文档片段作为评测集单独测试检索器能不能召回正确的块再固定检索结果测试生成模块能不能正确组织答案。两段分别调优整个系统的优化效率会高很多。RAG 系统的优化没有银弹。我在不同项目里试过换 embedding 模型、调切分参数、加重排器、做 Query 改写、改 prompt 模板这些手段在不同领域的知识库上效果差异很大。同一套配置在技术文档类知识库上表现很好换到合同文本或者对话记录上可能效果骤降。行之有效的做法是搭建一套评测基准准备几十条有标准答案的问题每次改动配置后跑一遍基准测试让数据告诉你哪个方案值得投入。我个人的体会是RAG 工程与其说是算法问题不如说更多是数据工程和评测体系的问题。各项优化手段到底值不值得上用评测数据说话远比拍脑袋可靠。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。