
直接开工这是一篇基于 LangChain 实现 RAG 问答库的实战分享方便大家参考整个搭建思路和落地经验。用 LangChain 搭建 RAG 问答库最容易被忽视的其实是检索链路的设计很多人一听到 LangChain 和 RAG第一反应是装个库、跑个 demo、接个大模型完事。但真到自己搭一个能用的问答库时才发现问题远不止有没有答出来这么简单切分粒度不对命中片段就是一堆碎片向量模型选得随意召回的相关性就飘检索策略只用了相似度同一个问题换种问法结果全变更不用说部署到生产之后文档更新、权限隔离、多轮对话这些问题一个一个往外冒。我做的这个 langchain-rag-chat 项目目标很朴素用最小成本跑通一条文档进来、答案出去的完整链路同时把检索质量做到能上生产的基本线。它适合两类人看一是刚接触 RAG、想快速理解整套机制的同学二是已经跑过 demo、但对检索效果不满意、想知道卡点在哪的开发者。下面从动机、选型、实现到优化把我踩过的坑和调参思路完整摊开。1. 为什么用 LangChain 搭 RAG开箱即用背后的取舍1.1 开箱即用的真实含义标题里写了开箱即用这四个字要看怎么理解。它不是说装完 LangChain 直接就能得到一个完美的问答系统而是指你不需要从零去写文档解析、文本切分、向量索引、相似度检索这些底层组件LangChain 把这些环节抽象成了组合式的模块像搭积木一样把整条管线拼出来。我自己之前用原生 Python 写过一版检索系统光是处理 PDF 表格和长文本分段就写了快两千行还要自己维护向量索引的读写逻辑。后来切到 LangChain文档加载用PyPDFLoader切分用RecursiveCharacterTextSplitter向量化用OpenAIEmbeddings或者HuggingFaceEmbeddings检索用VectorStoreRetriever每一块都是现成的替换某个环节只需要改几行代码。这个开箱即用的真正价值是让开发者把精力集中在调优检索效果和应用场景上而不是重复造轮子。1.2 但 LangChain 不是银弹版本和抽象层是个大坑先给一句大实话LangChain 的抽象层在带来便利的同时也带来了学习和排障成本。尤其是它的 API 在 0.1 到 0.2 版本期间改动频繁网上教程大量过时。我在项目里锁定的是langchain0.2.0和langchain-community的配套版本目的是避免教程里写的from langchain.vectorstores import Chroma在新版本里变成from langchain_chroma import Chroma时直接报错。还有一点要提醒不要急着上 LangChain 的LCELLangChain Expression Language花哨写法虽然链式|操作符很酷但调试起来不如普通函数直观。我在项目里先用最朴素的函数式写法把流程跑通再加上 LCEL 封装成接口这样每一环出问题能直接打印中间结果。提示如果你的环境里已经装过旧版 LangChain先pip uninstall langchain再重装避免版本冲突。这个项目里我统一用pip install langchain langchain-community langchain-openai langchain-chroma一条命令装齐基础依赖。2. 从文档到答案langchain-rag-chat 的整体架构和数据流2.1 完整链路拆解整个系统的数据流可以分成两条线一条是离线的索引构建另一条是在线的问答推理。离线阶段处理的是把文档变成可检索的向量索引这一步在线阶段处理的是把用户问题变成答案返回这一步。离线构建的核心步骤我大概列一下文档加载读取 PDF、Markdown、TXT、Word 等格式的文件提取纯文本内容。文本切分把长文档切成固定大小的 chunk文本块目的是让每个块在向量化之后有明确的语义边界也方便检索时精准定位。元数据打标给每个 chunk 附上来源文件、页码、标题等元数据这样答案生成时可以溯源回答也能带上引用来源。向量化用 embedding 模型把每个 chunk 转成向量我用的 1536 维的 OpenAI 向量也测试过 384 维的本地模型。写入向量库把向量和原文、元数据一起存入向量数据库同时建立索引。在线推理则是问题向量化用户输入问题后用同一个 embedding 模型转成向量。相似度检索在向量库里找出与问题最相似的 top-k 个 chunk。重排可选用 reranker比如 bge-reranker对召回结果做精排。构造 Prompt把问题 检索到的上下文拼进预设的提示模板。大模型生成交给 LLM 生成最终答案并要求它基于提供的上下文作答。2.2 技术选型三个核心决策先亮一下我在这个项目里的选型清单再逐个说理由环节选型理由编排框架LangChain 0.2.x生态成熟模块替换方便向量数据库Chroma轻量、零依赖、适合起步后续可平滑迁移到 Qdrant/MilvusEmbeddingOpenAI text-embedding-3-small对比过 BGE 中文模型中文效果稳定且 API 成本低LLMgpt-4o-mini 或本地 Qwen 系列按场景选择要速度选 4o-mini要私有化选 Qwen文档解析PyPDFLoader Unstructured 自研表格补丁解决 PDF 表格和复杂排版问题第一个决策是向量库选 Chroma 而不是 Milvus。很多教程一开始就让你上 Milvus但起步阶段没必要。Chroma 是嵌入式向量库不需要单独部署服务数据存在本地目录里对开发和单机场景非常友好。实测几千个 chunk 的检索延迟在几毫秒级别完全没有性能压力。真要到了几百万级向量、需要分布式部署的时候再迁移到 Milvus 也不迟因为 LangChain 对向量库的接口是统一的切换成本很低。第二个决策是embedding 模型的选择要匹配你的文档语言。我最初用 OpenAI 的text-embedding-ada-002后来换了text-embedding-3-small发现维度从 1536 降到 1536small 也是 1536但效果更好、价格更低。对于文档以中文为主的项目建议你同时用中文语料实测对比OpenAI 的向量在中英混合场景下确实稳但如果你必须私有化部署或追求零成本BAAI/bge-small-zh-v1.5或m3e-base这些开源模型效果也不差。第三个决策是LangChain 版本锁定。这一点在前面提过再强调一次社区里 90% 的过时教程都是因为langchain和langchain-community的模块拆分导致的。0.2 版本之后Chroma从langchain_community.vectorstores移出到langchain_chromaOpenAI的 LLM 和 Embedding 要分别从langchain_openai导入。搞清楚这个后面照着写代码才不会一路报错。3. 核心环节实现文档加载、切分策略与检索管线3.1 文档加载不同格式的姿势不一样文档加载是整个管线的入口很多人在这上面翻车。比如 PDF 文件直接用PyPDFLoader加载后遇到扫描版 PDF 拿到的全是空文本遇到排版复杂的表格文本会被拆得七零八落。我这个项目里的处理思路是分级处理对于文本型 PDF文字可选中、复制直接用PyPDFLoader搭配pdfplumber做表格内容提取。对于扫描版 PDF需要先接入 OCR。我调研了一圈最终选了开源的PaddleOCR识别中文效果不错但部署稍重如果文档量不大也可以直接用百度云的 OCR API 顶一下。实测下来扫描版 PDF 的识别准确率直接决定了后续检索质量这一步不能省。对于Markdown 和 TXTLangChain 的TextLoader就够用但注意指定编码Windows 上默认gbk容易乱码我在代码里强制写了encodingutf-8。对于Word 文档.docx用UnstructuredWordDocumentLoader最省心。注意unstructured这个包依赖一堆系统库在 Mac 上需要用brew install libmagic之类的命令先装依赖后面会踩到。这一步的核心原则是加载到什么质量的文本决定了后面检索效果的上限。文本都提取不清楚切分和向量化做得再好也白搭。3.2 切分策略chunk_size 和 overlap 不是拍脑袋定的文本切分是 RAG 管线中最容易被低估、对效果影响却最大的一环。切得太细每个 chunk 表达的含义不完整检索经常召回一些半句话大模型看着上下文也答不完整切得太粗一个 chunk 包含多个主题向量化之后语义被稀释检索时命中但不准。LangChain 默认给的RecursiveCharacterTextSplitter是一个递归式切分器它会按分层分隔符从段落到句子再到字符反复切分尽量减少语义断裂。我在项目里采用的参数组合是参数值说明chunk_size500中文约 500 字大约 3~5 个段落语义完整且利于定位chunk_overlap100相邻块之间保留重叠避免关键句被拦腰截断length_function按字符计中文场景按字符比按 token 更可控chunk_overlap设置的逻辑是如果文档里某一段关键的因果句子刚好落在两个 chunk 的边界上检索时可能两边都搜不到。加上重叠之后这个句子至少会完整出现在至少一个 chunk 里。但 overlap 不是越大越好太大会让相邻 chunk 大量重复检索时返回的几个结果内容几乎一样白白浪费上下文窗口。对于不同类型的文档我做了差异化调整代码文档把separators扩展成[\n, \n\n, \n, 。, . , ]优先保护代码块完整性。技术规范类文档chunk_size 适当调到 800因为这类文本句式长、逻辑链条长切得太碎会丢失上下文。FAQ 类文档按一问一答为单位做自适应切分避免一个问题被切成上下两半。实际调参方法建议这样操作把切分结果用表格形式打印出来chunk 序号、内容预览、字符数肉眼扫一遍重点检查有没有语义被腰斩的地方比看什么理论都有用。3.3 检索管线相似度之外还有 MMR 和重排检索是 RAG 问答库的核心能力但在很多入门教程里它就是向量库里查 top-k这么简单。实践中你会发现纯相似度检索的问题很明显同一个语义不同表达方式比如如何安装和安装步骤是什么在向量空间里可能距离并不近返回的 top-k 结果往往高度相似内容冗余度高相关性低但向量距离相近的噪声片段会混进来。我在项目里做了三层优化实测效果提升明显第一层采用 MMR最大边际相关性检索。LangChain 的Retriever支持search_typemmr在保证相关性的同时强制结果之间有一定多样性。核心参数lambda_mult我调到了 0.7这个值控制相似度和多样性的权重0.7 意味着更偏相关性但也留了 30% 的空间给多样性防止返回的几段内容一模一样。对于回答步骤类问题MMR 的效果比纯相似度好很多。第二层加一个 reranker 重排模型。一开始我嫌麻烦没上直到一次测试里发现纯向量检索召回的 4 个片段里经常有 1~2 个和问题毫无关系。后来接入了bge-reranker-base做二次精排逻辑是先向量召回 20 个候选片段再用重排模型逐条计算和问题的相关度打分取前 4 个进上下文。这一步让最终回答的准确率提升非常明显。重排模型跑在 CPU 上也不慢几百毫秒级别完全可以接受。第三层混合检索策略。对于技术文档库我额外加了一个 BM25 关键词检索通道和向量检索的结果做加权融合。向量检索擅长语义匹配但有时候用户会用很精准的术语提问比如LangChain LCEL 是什么关键词检索在这里命中率反而更高。两路结果取并集后进重排效果最稳。这三层优化下来我对检索质量的评价不再依赖单个问题的手感而是建了一套简单的评估方式准备 20 个有标准答案的问题人工标注每个问题在知识库中对应的原文片段然后计算召回率和命中率。开始调之前命中率大概 60% 左右调完之后稳定在 85% 以上。这个评估集很小但比随手问一个感觉不错靠谱得多。4. 问答生成链路与 Prompt 设计的几个关键细节4.1 让模型只依据上下文回答的技巧检索做得再好如果 Prompt 和生成链路没设计好最终答案照样翻车。RAG 的生成环节核心要解决两件事一是让模型严格基于检索到的资料作答不要信口开河二是让模型在资料不足时承认不知道而不是强行编造。我在项目里使用的 Prompt 模板核心结构如下你是一个企业内部知识问答助手。请基于以下【参考资料】回答问题。 【参考资料开始】 {context} 【参考资料结束】 要求 1. 回答时只使用资料中出现的信息不要引入资料之外的内容。 2. 如果资料中没有明确答案直接回答根据现有资料无法回答不要猜测。 3. 回答需要给出依据在回答末尾标注引用的资料来源文件名和页码。 4. 如果问题是开放性的结合资料给出条理清晰的回答。 问题{question}这个模板有几个细节值得注意用**【参考资料开始】/【参考资料结束】**这样明确的边界标记比简单的以下是参考资料效果好模型能更清晰地区分资料和指令。明确要求标注来源可以倒逼模型忠实于资料减少编造。即使模型偶尔编造至少引用的文件名和页码能帮我们快速定位答案的真实来源便于审核。加上没有明确答案就承认不知道的兜底指令是降低幻觉最有效的手段之一。实测中加上这条之后模型面对检索结果不相关的情况时不再强行回答而是措辞得体地拒绝整体可信度大幅提升。4.2 RetrievalQA 还是 ConversationalRetrievalChainLangChain 提供两种常见的问答链RetrievalQA和ConversationalRetrievalChain。前者适合单轮问答——问题直接、不依赖上下文后者支持多轮对话——能记住用户前面问过什么。我在这个项目里两条链都实现了。基础问答用的是RetrievalQA结构简单、好调试但要做到小助手级别的对话体验比如那第二步呢这个方案的缺点是什么这种追问就必须用ConversationalRetrievalChain。多轮对话的关键问题在于历史上下文和检索知识库的关系用户问那第二步呢本身不包含足够信息用于检索因此需要把历史对话也纳入检索范围或者将历史记录一并传入重写模块。LangChain 处理这件事的逻辑是在每次检索之前先压缩历史对话结合当前问题生成一个能独立检索的standalone question再拿这个重新表述后的问题去向量库检索。这个机制解决了多轮对话的检索漂移问题但代价是多一次 LLM 调用。如果你是本地模型延迟会明显增加用 API 模型时也要算好每次对话的成本。我给这个项目加了一个开关只在对话轮次超过 1 次时才启用历史压缩单轮问答直接走最快路径。4.3 流式输出与溯源格式化这个项目是聊天问答库所以我把流式输出streaming打好了。LangChain 的RetrievalQA默认不支持流式需要在构建llm时设置streamingTrue配合CallbackHandler把 token 逐段推给前端。这一步的效果非常直观用户看到的不再是转圈等 10 秒然后一大段文字突然出现而是像聊天软件一样逐字输出主观体验差异巨大。溯源格式化这块我倾向于在回答后面追加一个参考资料区块列出本次回答用到的 chunk 来源文件名 页码。这里有个细节不要把所有召回片段全部展示给用户我实测只展示实际被生成逻辑采纳的片段效果最好但 LangChain 默认不做这个区分。简单做法是把 top-k 里面重排分数最高的 2~3 个来源展示出来基本够用。5. 从能跑到好用检索质量评估与常见问题排查5.1 建立你自己的黄金问答集优化 RAG 系统最大的困难是没有量化指标所有调整都靠感觉。这个项目做到后期我筛选了 50 个真实场景问题作为黄金测试集覆盖知识库里高频问题、模糊表述、复合问题、边界情况四大类。正确率从最开始的 40% 一路调到了目前的 92% 左右每一步调整都有指标可看而不是靠拍脑袋。具体的评估步骤可以套用这个流程先准备好一个包含 20~50 个问题的黄金集每个问题标注理想答案要点或答案所在文档位置。跑一遍全流程得到每个问题的答案。人工打分二分类回答正确 / 回答错误算出整体正确率。对回答错误的问题逐条分析原因是检索没召回还是召回但不相关或是大模型没正确使用上下文。针对占比最大的问题类型做定向调优重测。这个方法的投入产出比非常高。我每次调整要么改切分参数要么换检索策略要么调 Prompt然后重跑黄金集对比正确率效果一目了然。这比凭感觉试高效得多。5.2 三个高频问题的排查链路我在实际开发和让朋友试用这个项目的过程中遇到的高频问题基本集中在以下三个。每个问题我都记录了自己的排查思路问题一回答像在编造明明知识库里没有的内容也说得头头是道。排查链路先看检索结果——把本次回答对应的检索片段打印出来确认模型参考的上下文是什么。如果检索结果本身就不相关是大模型的忠实度问题说明 Prompt 的约束不够强需要加那句没有答案就承认不知道的兜底如果检索结果相关但仍然回答错误多半是模型能力不足换更强的模型比如从 4o-mini 换到 4o通常能解决。我把这两个环节拆开排查定位根因的速度快很多。问题二换个问法同样的内容就答不出来了。这是 embedding 模型的典型表现纯向量检索对用词差异比较敏感。解决办法按优先级排序先加关键词检索走混合检索再看是否需要换更好的 embedding 模型。我在项目里增加了BM25Retriever做关键词通道和向量检索结果做融合。实测换个问法这类现象混合检索的命中率提升了 30% 左右。问题三知识库更新后旧答案依然被反复召回。这是向量索引更新的问题。Chroma 的持久化目录里旧的向量索引不会自动清除你需要做的是在更新文档后重建索引或按元数据删除旧片段再插入新片段。这个项目里我用collection.delete(where{source: docs/xxx.pdf})这种按来源批量删除的方式比全量重建快得多。记住元数据打标这一步做得好后面增量更新就能省大量功夫。5.3 部署与性能本地跑和线上跑的注意事项系统在 Mac 上本地跑通之后要考虑部署时的性能和稳定性问题。先说 Mac 本地的坑如果你打算完全本地跑embedding 模型用 BGELLM 可以用ollama跑 Qwen这样全程不依赖外网 API体验很顺。但要注意本地模型的启动时间和推理速度都不如 API尤其是流式输出时如果用的是 CPU 推理建议把模型量化到 Q4 或者选用更小的 7B 模型。如果走 API 路线性能和稳定性会更可控但要注意几个问题API 的 token 限制上下文塞入的 top-k 越多Prompt 越长成本和延迟同步上升我需要根据实际效果把 top-k 控制在 4~6 个片段并发控制在线 API 服务都有 RPM 限制多用户同时提问会出现 429 错误我的做法是在检索层加了一个信号量并发队列最多同时放行 5 个请求。Chroma 本身支持并发读但在写索引时会有文件锁冲突这个阶段我选择了串行写。生产部署的一个建议不要直接把 Chroma 的本地目录裸挂在服务器上建议用 Docker 把整个问答服务容器化并把 Chroma 的持久化目录挂载到宿主机卷上。这样迁移、备份、回滚都有余地。这个项目的 Dockerfile 我放在了根目录下基础镜像用python:3.11-slim启动命令只需要docker compose up -d就够日常维护成本很低。6. 再进一步RAG 的瓶颈在哪什么时候考虑换思路这个章节算是回应一下热词里很火的RAG 瓶颈和RAG vs 知识图谱这两个话题。我自己在做了这个项目之后对 RAG 的能力边界有了更实的体感。6.1 RAG 目前最真实的瓶颈复杂推理和全局理解单靠向量检索 大模型生成RAG 在处理多跳问题比如A 方案的缺点会不会影响 B 方案的选用标准这类需要跨文档推理的问题时表现不稳定。原因在于检索的粒度是 chunk每个 chunk 只承载了局部信息即使 top-k 能召回相关片段也缺少把多个片段之间的关联关系显式建模的能力。知识图谱KG和 ontology RAG 这类方案之所以会被反复提起核心就是针对这个问题用实体和关系显式组织知识把检索从找相似文本升级为沿着关系路径找答案。但这个方案成本也不低——构建图谱本身就是一项持续维护的知识工程对工具链和人力要求都很高。做技术选型时我的建议是内容以零散文档为主、需要快速上线的场景选 RAG 没问题内容实体关系强、需要复杂推理的场景比如企业规范、组织架构、法规问答再考虑知识图谱。6.2 图片和表格进了 RAG 吗回答热词里的另一个高频问题RAG 知识库能存储图片吗答案是能但不是把图片直接塞进向量库。常规做法有两条路一条是视觉语言模型路线用支持视觉的 embedding 模型如 CLIP、Piccolo把图片直接向量化检索时将图片本身作为上下文送入多模态大模型。另一条是文本化路线先对图片做 OCR 或者用多模态模型如gpt-4o生成图片的文字描述再把这段描述作为文本 chunk 存进知识库。对工程化落地来说第二条路简单可靠得多。表格的处理思路类似表格用pdfplumber或camelot解析成结构化数据再结合上下文把表格转成 Markdown 格式文本存入 chunk实践效果比直接向量化表格图像好得多。原因很简单文本检索管线已经成熟稳定多模态检索在精度和成本上的优势目前还没有体现出来。这个项目的名字虽然是 langchain-rag-chat但做完之后我最大的体会反而是项目里最难的部分往往不是 LangChain而是数据本身。文档质量参差、格式五花八门、切分边界难定这些都是决定效果的关键变量。在做这个项目之前我一直以为 RAG 的瓶颈在模型和框架做了之后才明白把数据整明白、把评估体系搭起来比换一个更强的模型带来的提升大得多。最后分享一个我自己的习惯每次改完一个环节不急着再随手问一个问题试试而是重新跑一遍黄金测试集用数据说话。这套方法论才是我做 langchain-rag-chat 最大的收获。