☰
LangChain与RAG实战:从架构拆解到代码落地
2026/10/18 19:20:52 网站建设 项目流程

大模型应用要真正落地,绕不开两件事:怎么把内部资料和私有知识喂给模型,怎么把多步复杂的任务串成一套自动化流程。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 完成。文本文件用 TextLoader,PDF 用 PyPDFLoader 或 PyMuPDFLoader,网页用 WebBaseLoader,结构化数据可以走 CSVLoader。每种加载器都会返回统一的 Document 对象,里面有 page_content 和 metadata 两个字段,便于下游处理。

加载之后是文本切分。很多人在这一步草草了事,直接用固定长度硬切,导致检索质量很差。我强烈推荐用 RecursiveCharacterTextSplitter,它支持按一组分隔符递归切分,优先按段落切,段落太长再按句子切,这样能最大限度保持语义的完整性。

切分参数里最重要的两个是 chunk_size 和 chunk_overlap。chunk_size 决定每个块的最大字符数,chunk_overlap 是相邻块之间的重叠字符数。为什么需要重叠?因为如果一段完整的语义恰好被切分点从中间截断,两个块各占一半,检索时都不完整,召回效果自然差。重叠能让上下文信息在相邻块之间传递。我常用的配置是 chunk_size=500、chunk_overlap=100,配合上层级分隔符切分,文本召回效果明显优于固定切分。

切分完之后就是向量化,也就是 Embedding 过程。这一步把每个文本块变成一个高维向量,语义越接近的文本,向量距离越近。向量化的关键决策点是选 embedding 模型和向量数据库。embedding 模型的选择直接影响检索质量,通用场景可以用 OpenAI 的 text-embedding-3-small 或国产的 bge-m3 系列;领域特定场景微调过的 embedding 模型往往效果显著更好。向量数据库方面,项目初期数据量不大时完全可以用 FAISS 这类轻量本地库,数据量到了百万级再考虑上 Milvus 或者 ES 的向量检索能力。

2.3 检索与增强:召回、重排、注入提示词

检索阶段的核心目标是“找到最相关的信息”。最简单的方式是用 embedding 做相似度搜索,把用户问题向量化,在向量数据库里找出余弦相似度最高的 k 个文本块。k 值的选择是个权衡:太小可能漏信息,太大容易把不相关的内容也带进来,干扰模型组织答案。我一般从 k=4 开始调,根据实际效果调整。

现实场景下,纯向量检索往往不够。这里有两个常见的优化手段:一是混合检索,向量相似度和 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.10,LangChain 版本以 0.1.x 为例,因为 0.2/0.3 之后部分 API 有变化,不过本节的核心思路是通用的。

安装依赖主要分三类:一是 LangChain 基础包和文档加载相关包;二是向量存储和 embedding 相关的包;三是大模型推理的库。下面是我惯用的依赖清单。

pip install langchain langchain-community langchain-openai chromadb faiss-cpu pip install pypdf tiktoken

embedding 模型我优先推荐本地方案,可以避免网络请求带来的延迟和成本问题。使用 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_size=500, chunk_overlap=100, separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""] ) chunks = text_splitter.split_documents(documents) print(f"切分完成,共 {len(chunks)} 个文本块") # 3. 向量化并存入向量库 embeddings = OpenAIEmbeddings(model="text-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(model="gpt-3.5-turbo", temperature=0) 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_size=500 换算成中文大概是一段 300-400 字的文字,chunk_overlap=100 保证跨块上下文不丢失。这套参数适合技术文档类的中文和英文混合内容,如果是代码文档或者表格密集的文档,建议把 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"--- 召回片段 {i+1} ---") 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 版本冲突,导致某些类实例化时报错。

我的经验是:项目创建时锁定版本号,比如 langchain==0.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_deserialization=True )

注意 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 工程与其说是算法问题,不如说更多是数据工程和评测体系的问题。各项优化手段到底值不值得上,用评测数据说话,远比拍脑袋可靠。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询