
现在很多做实际业务的人一看到“大模型”三个字第一反应都是“我能不能拿它来问答我的私有文档”。真把这个需求做一遍就会发现直接丢给大模型硬答结果基本不能看——要么胡说八道要么答非所问要么就是“我不知道”。我自己在这个系列的前两篇聊了Transformer基础和解码策略说实话那都是“让模型把话说顺”的阶段到了真正面向业务落地绕不开的其实是知识怎么进来、怎么被检索、怎么被引用。这篇我打算把RAG和LangChain放到一起讲透不是给你念概念而是把我自己从“能跑通”到“能上线”这一段踩过的路拆开给你看。RAG全称是Retrieval-Augmented Generation中文叫检索增强生成。LangChain则是我在这类项目里用得最多的编排框架。这两个词放在一起代表的是一条非常具体的技术路线先把外部知识切块、向量化、存起来再在每次问答时先检索相关内容最后把检索结果拼进上下文让大模型生成答案。整篇文章适合那些已经写过Prompt、调过模型接口但还没真正把RAG跑通的人。我会尽量把链路拆到每一步都讲清楚包括一些文档里不会写的细节。1. RAG为什么会出现先把问题定义清楚再谈解决1.1 大模型硬伤不止一个但RAG针对的是“知识”这一项大模型再强本质上也只是一个“很会接话”的参数化系统。它所能输出的内容全部来自训练时见过的数据。这带来两个致命问题。第一是知识陈旧。模型训练时间一旦过去之后发生的事情它完全不知道。第二是幻觉。当问题超出它知识边界的时候它不会老老实实说“我不知道”而是倾向于用语言惯性编一个听起来合理的答案。这两个问题叠加起来正是企业内部文档问答、客服助手、私有知识库应用落地时最大的拦路虎。我当时遇到的需求很典型客户手里有几百份产品手册和内部流程文档要做一个能回答员工问题的助手。如果直接基于大模型做模型没见过这些文档内容回答全是编的。用微调也能做但要为几百份文档不断迭代模型权重成本高不说每次文档更新还要重新训练现实里根本跑不动。RAG正是从这个痛点里长出来的思路——把知识外置到模型权重之外在问答时临时“翻书”再让模型根据翻到的内容作答。1.2 RAG和微调的本质区别权重内化还是上下文外挂很多人容易混淆RAG和微调以为都是“让模型知道我的数据”。其实两者解决问题的层面完全不同。微调是在调整模型本身的参数把知识“内化”进权重里。它的优势是回答风格、专业术语可以深度定制但劣势也明显训练成本高、数据更新要重新训、知识规模受参数容量限制、幻觉问题依然存在。RAG则完全不改模型参数它是在推理阶段把检索到的内容临时追加到Prompt里让模型基于这段外部资料进行生成。知识是“外挂”的换一批文档就等于换了一个知识库成本几乎为零。我做一个比喻你就懂了。微调像给一个专家重新回炉培训逼他把新知识记在脑子里RAG像给这位专家配了一个专属资料室他每次回答前先查资料再开口。大多数企业场景下后者显然更划算。当然RAG也有短板——如果资料室的检索员能力不行找不到对的内容专家再厉害也白搭。这也是后面我们要花大量精力调检索质量的原因。1.3 什么样的问题适合用RAG什么样的不适合RAG不是万能药我见过不少项目硬上RAG结果效果反而更差。其实判断标准很简单看你需要的答案是否基于“特定、可检索的事实”。适合RAG的场景通常具备三个特征知识域封闭、答案有据可查、数据频繁更新。比如产品手册问答、法律法规检索、故障排查库、教学答疑。这些问题答案就在文档里只是用户自己找不到RAG帮他把相关段落捞出来。不适合的场景也有两类。一类是高逻辑推理性问题比如“A方案和B方案结合起来对公司未来三年战略有什么影响”这类问题就算检索到几段资料模型也做不好综合分析。另一类是纯粹创造性任务比如“给我们的新品写一句广告语”这时候RAG检索出来的文档反而会限制模型发挥。判断项目该不该上RAG不要看它热不热门先拿这三个特征套一遍自己的场景。2. 最小可用的RAG链路拆开看每一环在干什么2.1 整体流程索引阶段和查询阶段一条完整的RAG链路要分成两个阶段来理解。索引阶段是离线的做一次就够了读文档、切文本、向量化、建索引。查询阶段是在线的用户每次提问都会触发问题向量化、检索相似内容、拼Prompt、生成答案。这两个阶段的逻辑必须分开想清楚。很多新手容易搞混以为RAG就是“向量数据库检索”。实际上真正决定成败的不只是检索还有前面文档怎么切、后面生成怎么拼。任何一个环节薄弱整条链路都会被拖垮。我后面讲优化会一条条展开这里先让你脑子里有一个全局地图。2.2 文本加载与切分chunk_size和overlap是绕不过去的参数文档加载通常是整个RAG里最枯燥但又最容易出事的一环。PDF、Word、HTML、Markdown都有各自的解析库但不管什么格式最终都要归一化成纯文本块。我自己的经验是先把原始文档做强解析把标题、表格、段落结构保留下来再喂给切分器。如果一上来就用通用方法切表格结构信息很容易丢后面检索精度会受很大影响。真正考验水平的是文本切分。切分的目标是生成若干大小适中的chunk文本块每个chunk在未来检索时作为一个独立条目参与匹配。chunk_size块大小和chunk_overlap块间重叠是两个决定性的参数。切得太小比如100字符每个块承载的语义信息太少检索容易只命中只言片语答案缺乏上下文连贯性。切得太大比如2000字符一个块里包含多个主题向量化后主题被稀释检索精度下降而且填充进Prompt后Token浪费严重。从我的实践看中文场景下500到800字符是比较稳妥的区间。overlap一般设为chunk_size的10%到20%目的是避免一句话被从中间截断导致语义残缺。2.3 嵌入模型和向量数据库选型的真实考量chunk确定之后接下来要做向量化。这一步要选择一个嵌入模型把文本转换为高维向量。嵌入模型的核心指标是“语义相似度——向量空间距离的一致性”。也就是说语义接近的句子向量距离应该近语义无关的句子向量距离应该远。选嵌入模型时不要只看排名还要看你实际的语料领域。通用领域用通用模型专业医疗、法律、代码领域最好用对应领域微调过的模型否则相似度排序会明显失真。向量数据库负责存储这些向量并提供相似度检索。关于选型我直接说结论。小项目、几十万条文本以下用本地嵌入式向量库最划算比如Chroma或FAISS部署简单、零运维。百万级以上的向量或者需要实时写入和高并发查询再考虑独立的向量数据库服务。不要把选型搞复杂你的首个Demo环境只需要一个能在本地跑起来的方案。2.4 检索与生成不是搜到什么给什么而是给什么样的上下文当用户发起提问系统会用同一个嵌入模型把问题向量化再去向量库中查找最相似的chunk。这一步检索出的通常是TopK个结果比如取Top4或者Top6。但注意检索结果不能直接一股脑塞给模型要做三步后处理。第一步是去重多路检索经常召回同一段内容的不同切分版本。第二步是按相关性截断有些结果距离阈值太低宁可少给也不要干扰生成。第三步是组织顺序把最相关的内容放在Prompt中靠前的位置让模型清楚哪些是主要依据。做完这三步再拼Prompt生成的准确率会有一个肉眼可见的提升。3. 用LangChain把它串起来时哪些细节真正影响结果3.1 为什么选LangChain而不是自己从零组装说到这里你可能会想这些流程我用脚本一遍遍调用接口不也能串起来吗确实能但LangChain的价值在于把每条链路上最常用、最折腾的部分都封装好了并且提供了统一抽象。最核心的抽象是Chain也就是把“取文档—切分—向量化—检索—调用模型—生成答案”串成的流水线。传统写法里你要自己维护中间结果传递、异常处理、上下文拼装用LangChain你只需要声明链路的输入输出框架帮你组装。另一个好用的抽象是DocumentLoader几十种格式的解析都给封装了不至于为了一种文件格式就去现学解析器。当然也不能无脑吹LangChain它的问题是版本迭代快、API变动频繁网上很多旧教程里的代码跑起来全是错。所以下面我给的示例是基于当前主流写法但我会同步说明新旧版本的核心差异点避免你被网上过期代码带偏。3.2 一个可以直接跑通的最小示例那我直接上一个可运行的例子。假设你有一堆Markdown文档存在./docs目录要做一个能回答文档内容的问答机器人最精简的代码长这样from langchain_community.document_loaders import DirectoryLoader, TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough # 1. 加载文档 loader DirectoryLoader(./docs, glob**/*.md, loader_clsTextLoader) docs loader.load() # 2. 切分文本 splitter RecursiveCharacterTextSplitter( chunk_size600, chunk_overlap80, separators[\n\n, \n, 。, , , , , , ], ) chunks splitter.split_documents(docs) # 3. 向量化并写入向量库 embeddings HuggingFaceEmbeddings(model_name你的嵌入模型路径) vectorstore Chroma.from_documents(chunks, embeddings, persist_directory./chroma_db) # 4. 构建检索器 retriever vectorstore.as_retriever(search_kwargs{k: 4}) # 5. 定义Prompt模板 prompt ChatPromptTemplate.from_template( 请基于以下资料回答问题。 如果资料中没有相关内容请直接说“资料中没有找到相关信息”不要编造。 资料 {context} 问题{question} ) # 6. 组装RAG链路 def format_docs(docs): return \n\n---\n\n.join(doc.page_content for doc in docs) rag_chain ( {context: retriever | format_docs, question: RunnablePassthrough()} | prompt | llm | StrOutputParser() ) # 7. 调用 answer rag_chain.invoke(你们的退货政策是什么) print(answer)这里面最值得解释的是第6步。它用了LangChain表达语言LCEL即LangChain Expression Language通过|管道符把组件连接起来。整个链路的逻辑是先用retriever完成检索经过format_docs把多个chunk拼接成一块上下文再和用户问题一起填入Prompt模板然后交给大模型生成最后解析输出。LCEL写法看起来有些陌生但它比老版Chain对象更灵活也更便于做流式输出和异步调用。3.3 最容易踩的三个坑嵌入模型加载、API变更和流式输出第一个坑是嵌入模型加载。官方文档里很多示例直接让你用OpenAI的Embedding接口但企业内部环境通常没有外网访问权限。我建议你本地部署一个开源嵌入模型并用HuggingFaceEmbeddings加载。这里有个细节model_name要填完整的本地模型路径而不是名称缩写否则它会尝试连外网下载直接报错。第二个坑是版本API变了。以Chroma为例老代码习惯在创建时传persist_directory但是不同的LangChain版本对这个参数的名称和存储逻辑处理有差异。如果你的代码跑起来报“unexpected keyword argument”第一反应应该是去查当前版本的源码或迁移说明而不是硬改参数。我还遇到过很多次Retriever和VectorStore方法名变动导致代码失效的情况这类问题排查起来非常耗时。第三个坑是流式输出。Demo阶段大家习惯用invoke一次性拿结果但生产环境用户期待的是打字机效果。用LCEL链路时只要把最后的rag_chain.invoke(...)换成rag_chain.stream(...)大部分组件都天然支持流式。不过要注意文档加载和检索这两个环节发生在生成之前它们是无法流式的真实的流式体验从“大模型开始生成”那一刻才启动。这不算问题只需要你在产品层面对用户做适当的等待提示。4. 跑通之后质量上不去从召回和排序两个方向找问题4.1 先量化再优化你要盯住的几个指标RAG效果差是一个模糊的感受。落到工程上我们要把它拆成几个可度量的指标。第一是召回准确率用户问题真正对应的答案片段是否出现在检索结果里。第二是响应相关性模型最终生成的回答是否完全基于检索到的正确信息。第三是完整率多跳类型的问题比如“A产品的保修期是多久如果过了保修期怎么办”答案需要多个chunk才能拼全模型是否做到了。我的做法是每次专项优化前先挑30到50个真实问题人工标注出标准答案所在的段落然后跑一遍系统看看这几十个问题里有多少被正确定位。这一步能帮你在几分钟内快速判断问题出在“检索没召回到对的内容”还是“召回了但模型没用对”。4.2 召回质量排查k值、相似度阈值和TopK结果的直接观察效果差的时候我永远先看检索结果而不是先去调Prompt。做法很简单把链路的检索结果单独打印出来逐个检查“问题对应的正确段落有没有出现在TopK结果里”。如果正确段落没出现问题在向量检索本身。你可以按这个顺序检查。第一k值是否太小。固定的知识库问题越复杂命中的正确片段可能越分散k3往往不够用我通常从k5起步调。第二相似度阈值是否过严或过松。很多向量库返回的相似度分数区间和你预期的不一样你先打印几个真实查询的score分布再定阈值而不是拍脑袋。第三嵌入模型是否不适合你的领域。这个就需要对比测试了换一个领域相关的模型跑同一批问题量化指标当场见分晓。如果正确段落出现在TopK结果里但最终答案还是错的那问题主要落在“上下文组织”和“大模型遵从度”上。优先检查Prompt里是否明确写了“没有资料就直接说不知道”以及你给到的chunk顺序是否把最关键的证据排在了前面。4.3 查询改写和HyDE当用户提问方式与文档写法不一致时还有一种很常见的情况用户问“怎么申请发票”但文档里写的是“报销单开具流程”。字面上完全不匹配但语义上高度相关。向量检索依赖的是嵌入模型的语义空间映射如果嵌入模型不够好这种“说法差异”很可能导致召回失败。一个有效但成本可控的手段是查询改写。在正式检索之前先用大模型把用户的提问改写成几个不同角度的检索式比如将口语改成书面语、将简写改成全称、将问题拆成子问题再分别检索最后合并结果。这能让检索覆盖面大幅提升。另一个进阶做法是HyDE即Hypothetical Document Embeddings。它的核心思路是拿到用户问题之后先让大模型“凭空写一段假设的理想答案”再用这段答案去做向量检索。原理是假想答案的语义分布比问题本身更接近真实文档的语言风格用它去匹配文档往往比直接用问题检索效果更好。HyDE有额外的一次模型调用成本但对部分场景的召回提升非常明显。我建议你在前面提到的30个测试问题上做A/B对比再决定要不要上线。4.4 重排与混合检索从“能搜到”到“排得准”在文档量级上去之后你会遇到另一个问题向量检索召回的TopK里有相关内容但混着大量不太相关的chunk模型生成时容易被干扰。这时候靠调k值已经解决不了需要引入重排环节。基本原理是第一轮用轻量的向量检索或关键词检索快速召回100条候选第二轮用一个更强的排序模型通常叫Cross-Encoder对这100条逐对计算与问题的相关度重新排序后只取前5条。重排模型比双塔式的向量模型计算量大但由于只处理第一轮筛出的候选整体耗时可控。加了重排之后我项目里的答案相关度通常能有明显提升效果非常直接。混合检索是另一个高性价比方案。不要只依赖向量语义检索把BM25这类基于关键词的传统检索也加进来。二者按权重混合得分比如向量0.7加关键词0.3能同时兼顾语义泛化和精确匹配。很多向量数据库原生支持混合检索不用你自己写融合逻辑。关键词匹配对产品型号、编号、人名这类专有名词尤其有效这是纯向量方案最大的短板。5. 从Demo到系统RAG落地还要补哪些课5.1 多知识库隔离、细粒度权限与更新策略Demo跑通只是万里长征第一步。真到了上线阶段第一个大问题是数据边界。企业的知识库通常不是一个池子而是分部门、分密级的多个库。你不能让所有人检索到所有文档。我建议在架构上把“向量化存储”和“业务权限”解耦每个知识库使用单独的数据表或用字段标记归属查询时带上权限过滤条件。哪怕一开始数据量不大也值得把这个隔离设计做进去否则后面用户一多就失控。知识更新是第二个容易踩的坑。RAG不是把向量库建好就结束的。文档改了旧chunk要删掉新chunk要写入。最简单的做法是按来源文档做整体替换删除某个文档对应的全部向量再重新切分嵌入。但要注意向量数据库的删除操作不是实时的删除后需手动持久化否则下次启动时旧数据又回来了。我一般会为知识库维护一个“文档版本表”记录每个文档的哈希值定时比对并触发现有文档的更新流程。5.2 流式输出、可观测性与Prompt稳定除了检索质量生产环境最影响观感的是响应速度。RAG链路天然多一跳检索比普通大模型问答多出几百毫秒延迟这是正常的真正的问题通常出在用户等待时的反馈体验。我的建议是产品层面区分两个状态检索阶段给“正在查找相关资料”的提示生成阶段再用流式输出打字机效果。不要把检索阶段伪装成生成阶段用户会觉得卡死了。可观测性也是容易被忽略的工程细节。RAG比普通接口多了一个“黑盒中的黑盒”模型生成了1和0但你说不清它到底引用了哪段资料。我在生产环境的做法是每次请求都把检索到的TopK chunk ID和相似度分数记录下来连同Prompt一起打入日志。一旦用户投诉某个答案有问题可以直接倒查是哪条chunk带偏了模型。这一步能让排查效率提升一个数量级。5.3 下一步扩展GraphRAG、Agent路由和评估平台如果你已经跑通了基础RAG并且检索质量和生成质量都稳定了接下来有几条路可以走。第一条是GraphRAG方向。普通向量检索只能做语义相似度匹配难以回答“A和B之间有什么关系”这类需要多跳推理的问题。把文档里的实体和关系抽取出来构建成知识图谱在检索时把向量召回和图谱路径召回结合能覆盖更多复杂问题。这个方向效果不错但工程复杂度也会上一个台阶。第二条是Agent化。不要把所有问题都丢进同一条RAG链路。先让一个路由模型判断问题类型简单问题直接回答需要查资料的问题走向量检索需要查结构化数据的问题走SQL查询。这就是Agent的基本雏形。RAG不是终点而是Agent的能力组件之一。第三条是建立评测平台不要让RAG效果永远停留在“感觉”。定期用你积累的标准测试集跑回归对比每次改动前后的指标变化。只有量化了你才知道自己到底是变好了还是变坏了也才能向团队其他人证明当前方案的价值。我在实际项目中感受最深的一件事是RAG落地难最难的从来不是跑通Demo也不是模型选型而是把“检索效果量化”这件事坚持下去。很多项目一开始尝到甜头后面就靠感觉修修补补最终效果不可控。如果你准备认真做一个RAG项目我真心建议从第一天就记录检索日志、沉淀测试集。有了这两样东西后面每次优化都是在资产上做加法而不是在黑暗中开盲盒。最后再分享一个我们内部一直在用的“效果检查单”每次改动代码之后先跑30个标准问题看检索结果TopK里正确文档的命中率再看最终回答里有没有出现幻觉内容最后看延迟有没有突破预期。三步都过了才放改动进正式环境。这套流程看着土但比任何花哨的架构都管用。