1. 从模型对话到检索增强为什么这块值得单独拎出来讲如果你已经跟着这个系列用 LangChain 搭过几次基本对话链大概率会产生一个疑惑模型生成能力都这么强了上下文窗口也越做越大为什么还要搞一套检索与文档的东西我一开始也这么想直到真正把一个内部知识库问答项目从玩具做成可用状态才明白这背后的差距有多大。先说个最直观的场景。假设要做一个问答机器人它的使命是回答企业内部的规章制度问题比如年假没休完的离职时能折算工资吗。这种问题如果你直接丢给大模型模型八成会参考它训练数据里某地某公司的规定给出一个模棱两可的回答。这个回答本身可能没错但错在与你仓库里那一版经过法务确认的制度文件不一致。这时候你需要的不是更强的模型而是让模型在回答之前先去你的文档库里找到关于年假折算的那一段原始描述然后只依据这段描述来回答。这就是检索增强生成RAG的核心理念也是本篇文章标题里检索与文档这四个字的含义。整套流程拆开看其实是一条非常朴素的生产线把散落在 PDF、Word、网页、数据库里的内容收集起来切成方便处理的小块转成向量存进向量库用户提问时把问题也转成向量去库里捞最相关的若干块内容再连同问题一起交给大模型组织答案。LangChain 在这条生产线上提供的不是某个杀手级组件而是一套完整的、可替换的积木。文档加载器负责收集文本分割器负责切块向量存储负责存放和召回检索器负责把召回哪些块这件事做得更聪明。这篇文章要解决的正是这四个环节里那些文档里不会写明白的实际问题格式五花八门的文档怎么装进统一的结构切块参数到底怎么定才既能保住上下文又不超模型上限embedding 模型和向量库怎么选才不花冤枉钱以及最容易被忽略的——同一套检索代码在不同数据规模和数据形态下表现能差出十倍。我会沿着生产线顺序一步步讲每一环都会给出可复现的参数、代码和踩坑记录。2. 文档装载文件格式才是第一个坑2.1 合不合规先看加载器怎么解析原始文件文档加载器在 LangChain 里的定位就是把各种格式的原始文件变成统一的Document对象。这个对象内部就两个核心字段page_content存文本内容metadata存关于这段文本的附加信息。听起来很直接但实际使用时几乎所有人的第一个坑都出在加载器对原始文件的解析能力上。以最常用的 PDF 为例。PDF 本身是一个排版格式不是文本格式里面的文字可能有物理顺序和阅读顺序不一致的情况更不要说那些由扫描图片组成的 PDF压根没有文字层。PyPDFLoader这类基于文本提取的加载器处理带文字层的 PDF 没问题但碰上扫描版 PDF提取出来全是空白或者乱码。早年我接过一个历史档案数字化的项目几百份 PDF 里大概有三分之一是扫描件用PyPDFLoader加载完检索质量差到没法看后来换了PyMuPDFLoader稍微好一点但遇到复杂的表格排版提取出来的文本顺序依然是乱的。这里给你一个选型参考优先选PyMuPDFLoader它对文本层 PDF 的提取速度和准确度兼顾得很好扫描版先走 OCR把识别后的文本落成带文字层的 PDF 或者直接存成文本文件再交给加载器。LangChain 里不是没有 OCR 类加载器但这类任务更稳定的做法是先离线做好 OCR再进入检索流水线否则每次加载文档都跑一遍 OCR时间和成本都扛不住。2.2 非结构化文档的隐藏信息来源再说一个很多人没注意的点metadata不是可填可不填的附加项它在检索阶段是过滤和追溯的依据。我见过太多项目从头到尾metadata都是空的等到文档库大了以后检索结果里混进一堆来源不明的段落整个系统直接变得不可信。实际项目中加载器通常能从文件名、目录结构、页面信息里自动带出部分metadata但更重要的信息得自己补。比如一份 PDF文件名是员工手册_2024_v3.pdf加载器默认只会把文件名塞进metadata但你在业务里真正需要的可能是版本号、生效日期、所属部门。这些信息最好在入库前统一整理并注入metadata。我当时写了一个简单的预处理脚本把所有文档按目录映射到公司内部的知识分类体系再把分类编号写进metadata后面检索时用分类过滤效果立竿见影。2.3 带格式的内容别只当纯文本处理如果你处理的文档里包含 Markdown 或 HTML建议用对应的专用加载器比如MarkdownHeaderSplitter这类工具会保留标题层级作为切分边界。这背后有个朴素道理类似第3章 请假制度下的3.1 年假下的段落和3.1.2 折算标准下的段落虽然从纯文本看都是各自独立的一小段但它们之间的语义关联是靠标题层级维系的。如果加载阶段就把标题揉进正文当纯文本处理切分阶段就失去了按层级组织内容的能力检索质量会受影响。代码文件的加载也有讲究。源码文件里注释、函数定义、字符串常量混在一起如果整个文件当一段文本塞进去会被切得很碎。LangChain 有按代码语法结构切分的加载器会把函数、类作为切块边界。我在处理一批开源项目的文档站点源代码时用这类加载器后检索命中率明显提高因为问题通常指向某个具体函数的行为而不是整个文件。3. 文本分割决定回答质量的分水岭3.1 硬切分和语义切分到底差在哪把文档切成块这件事看起来是所有步骤里最简单的实际上对检索质量的影响比 embedding 模型还大。切得太粗一块里混杂多个主题向量化后语义被稀释检索时容易擦边命中切得太细上下文被截断模型回答时缺少背景信息容易答非所问。最常见的做法是按固定字符数切分设置一个chunk_size和一个chunk_overlap。这个方案简单直接但问题在于它不考虑文本自身的语义边界很可能一句话被拦腰截断后半句丢了主语向量表示的语义就已经变了。比如公司规定员工累计工作已满1年不满10年的年休假5天已满10年不满20年的年休假10天这一段如果切分点恰好落在分号前面左右两边各剩半句话检索工作十年能休几天时命中的块里只有年休假5天那半句回答自然是错的。LangChain 里的RecursiveCharacterTextSplitter比硬切分好一些它会按一组分隔符迭代尝试切分优先在段落、换行、句号这些天然边界处切断尽量保住语义完整性。我的经验是先用这个加载器做通用切分处理不了的特殊格式再单独写逻辑。通用的东西已经覆盖了八成场景别一开始就陷在我要做一个完美的切分器里面。3.2 chunk_size 和 overlap 的参数推演参数定多少很多人拍脑袋定个 500 和 50也不管自己的文档长什么样。实际上这两个值应该由文档特性和下游模型能力共同决定。第一约束是模型的上下文长度。你打算把检索出来的块拼进 prompt 里再把 prompt 发给模型所以块的长度加问题长度加系统提示词长度不能超过模型上下文上限否则要么截断要么报错。假设上下文是 8K tokens系统提示词和问题占用 2K剩下给检索内容的就只有 6K。如果你每轮要召回 4 个块那每块最多 1.5K tokens而 1.5K tokens 大致对应中文 1500 到 3000 字英文 3000 到 4500 字左右。第二约束是语义密度。制度条文、操作手册这类文档语义密度很高500 到 800 字一个块是合理区间而市场分析报告、行业资讯这类冗余度高的内容块可以放宽到 1200 字因为语义信息分散太短反而捞不到要点。chunk_overlap的作用是避免切到关键句边界时把完整信息切散。一般设成chunk_size的 10% 到 20%但不要机械地当默认值。如果你的文档里存在大量跨页段落比如表格被拆分、领导讲话跨了两页适当加大 overlap 能明显改善召回质量。代价是存储体积变大、检索耗时略增数据量在百万级以下时完全可接受。3.3 表格和 Markdown 要单独处理表格是自动切分最容易搞砸的内容。一个单元格里的数字、日期、文本被切成分块后向量化出来的语义是残缺的。一个保险做法是在加载阶段就把表格转成行内描述文本比如一行数据转成张三部门研发部入职日期2022年3月职级P6再参与切分。虽然检索时会把整段描述召回回答时却能从中提取到准确信息。前端项目里常见的 Markdown 文档要用MarkdownHeaderSplitter之类的工具按标题层级切分能够把标题级别内容块作为一条记录存进metadata而不是单纯按字符数切。这样检索时还可以根据标题做针对性过滤比如用户只想查配置说明部分就直接排除其他标题下的内容。这个做法在处理文档站点、产品手册这种长文档矩阵时尤其有用。4. 向量化与存储选型背后的成本账4.1 Embedding 模型维度不是越大越好文本切好块后下一步是把每个块转成向量。这里的关键选型是 embedding 模型。如果你只跑个 Demo随便用一个开源小模型甚至直接调云服务商的 embedding API 都行但一旦要进入生产就要面对三个现实问题维度、成本、语言适配。维度这个指标很有意思。很多人的直觉是维度越高代表模型越强但维度直接决定向量库的存储大小和检索耗时。以 OpenAI 的 text-embedding-3-large 为例支持 3072 维而小模型只有 1024 维上下的差异存储和计算开销是成倍的关系。中文场景下一些专门针对中文训练的 embedding 模型在相同维度下对中文语义的编码质量往往比通用英文模型更好。我的经验是优先选在中文语料上做过针对性优化的模型不要一味追高维。成本这块容易踩坑的是 API 计费方式。embedding 是按 token 数计费的千万别拿文库总字数去估算成本。一个文档切块之后token 数大约等于原始字数的 1.5 到 2 倍因为切块会产生大量重复的前缀和分隔符这些都会计入 token。我第一次估算时没算 overlap结果费用翻了一倍还多倒不是绝对金额多大而是这种意外让人觉得很不专业。4.2 向量库选型的真实取舍向量库的选型是一个被过度营销的话题。真实项目里按数据量分级选择就可以了十万级以下用 Chroma、FAISS 这类轻量级嵌入式向量库足够。Chroma 的优点是部署简单代码里直接调用适合原型和中小业务FAISS 更偏底层只做索引和检索没有完整的数据管理能力适合对代码驾驭力强、不想被框架绑死的团队。百万级到千万级要开始考虑 Milvus、Qdrant 这类独立向量数据库服务。数据量上来以后分布式索引、增删改查、并发控制这些能力就不是一个嵌入式库能解决的了。超大规模这时候你面临的不只是向量数据库选型还有分片策略、混合检索架构、缓存设计属于基础设施建设范畴一般团队很少走到这一步。一个容易犯的错误是项目刚起步就上重型向量数据库结果运维成本拖垮了迭代速度。向量库这种东西晚点换成本并不高因为数据本身是可重新索引的迁移一个索引比迁移业务系统简单得多。4.3 自建的隐形开销不少团队喜欢自建向量库以省成本但这里的坑在于向量检索不只是存储和暴力扫描它依赖索引算法HNSW、IVF 这类。没有靠谱的索引数据量一旦上万单次检索就会明显卡顿。而自己调索引参数、处理索引更新、做故障恢复每一项都是隐性人力成本。我的判断标准很简单如果项目核心价值不在向量检索引擎本身那就用现成的。工程上最贵的从来不是许可证费用而是团队花在基础设施上的时间。这时间应该省下来做检索质量的调优那才是决定用户体验的部分。5. 检索器实践Top-K 之外的能力边界5.1 朴素相似度检索为什么不够用向量库搭好之后最朴素的检索方式就是把用户问题转成向量在库里找最相似的 K 个块拼进 prompt。这个方案在内容形态简单、话题集中时能跑通但真实文档库往往不是这样的。问题出在相似度并不等于相关性。用户问年终奖在离职时还发吗库里可能有一段讲年终奖的发放对象是基于考核截止日出勤状态的员工字面上高度相似语义上却是在讲另一件事。还有一类是多义性问题同一个词在不同部门文档里指的不是同一个东西备份在运维文档里指数据备份在行政文档里指值班人员储备纯向量相似度会把两类内容同时召回再让模型从中挑答案很容易被误导。5.2 MMR 和 Multi-Query 的实际收益LangChain 里有两个检索增强手段值得一用MMR和MultiQueryRetriever。MMR最大边际相关性解决的是多样性问题。它不只看候选块与问题的相似度还看候选块之间的差异性避免召回的 K 个块全在讲同一句话。我在处理规章制度问答时用 MMR 之后明显发现同一个问题召回的块覆盖了制度条款、配套解释、历史变更三个不同侧面回答的完整度提升了一大截。MultiQueryRetriever 的思路更有意思它不是拿原始问题去检索一次而是先让大模型把问题改写生成多个不同表述拿每个改写后的问题去检索再汇总去重。比如离职时年假没休完怎么办大模型可能改写为年假未休的离职补偿规则、剩余年休假如何处理、离职时未使用的年假是否折算工资这几个表述分别去库中检索命中的内容差异很大。这个方案额外花几次模型调用但提升效果非常明显尤其在用户问题表述口语化、而库里文档用词正式的场景里。5.3 元数据过滤一个常被忽视的杠杆很多检索器默认是在整个文档库中做全局检索但实际业务里文档往往有明确的结构不同产品线的文档、不同年份的制度、不同部门的规范。元数据过滤就是在检索前先缩小候选范围让向量相似度计算只发生在有意义的子空间内。举个实际例子。我维护过一个包含多个产品线文档的知识库最初检索时不区分产品线用户问这个接口怎么鉴权库里不同产品线的接口文档互相干扰返回结果经常串产品线。后来我在metadata里加了product_line字段检索时先用用户问题里识别出的产品线信息过滤命中率立刻上升。更细节的做法是在过滤时不做硬性过滤而是把元数据匹配作为打分项的一部分防止误识别导致完全召回不到内容。5.4 父子文档检索PDF 场景里的救命方案最后一招对长文档特别有用叫父子文档检索。基本思路是索引时同一个文档做成两层父层是大块比如整个章节子层是小块比如段落检索时在小块里找相似度最高的段落但最终把命中小块所属的父块整体提交给模型。原因在于检索单元和生成单元的最优粒度往往不一致。检索需要小块来精准定位生成需要大块来提供上下文。PDF 里的一个管理规定第 3.2 节可能整体才是请假流程的完整上下文单独一个小段落到答案生成时缺乏上下文的支撑。父子文档结构同时满足了精准召回和完整上下文两个需求是处理长文档问答最值得尝试的进阶方案之一。6. 让检索系统真正跑进业务里聊完检索的生产线最后想给几个偏工程的经验。这些东西不在 LangChain 的官方文档里却是我在真实项目里反复踩过坑后沉淀下来的。第一评估一个检索系统不能只看准不准要看找不找得到。检索质量要从两个维度看召回率即正确答案有没有进到被召回的块里精确率即召回结果里有多少是真正相关的。这两者往往此消彼长调整切分、检索参数时要同时看两个指标不能只看 Top-K 结果顺不顺眼。第二索引更新要纳入日常运维。文档库不是静态的制度会修订、产品文档会更新如果不做版本管理和增量索引迟早出现模型回答的是旧版规定的情况。这一块没有统一方案我在实践中的做法是每次导入文档时都从文件名里解析出版本号和生效日期存入metadata检索结果里带上这些信息至少让模型在回答时能感知到自己依据的是哪个版本。第三善用可观测性。LangChain 的链式结构很适合做中间过程的可视化。把用户问题→改写后的问题→召回内容的元数据→最终答案这一路记录下来线上出问题时你能快速定位是检索环节漏了还是生成环节错了。否则面对一个错误的回答你不知道该优化哪端只能靠猜。第四给自己留退路。检索方案无论多完善都有召回不到正确内容的可能。在生产环境里不如明确告诉用户我找到的依据可能不全面请参考原文并附上来源链接比让模型硬编一个答案要诚实得多也更经得起业务方的追问。这不仅是技术设计也是对用户负责的态度。这个系列聊到第四篇检索与文档已经覆盖了从数据接入到答案生成的完整链路。下一步可以聊的是如何把这套能力收拢成可靠的服务比如做一层缓存来降低高频问题的重复检索开销或者把评价反馈接进来变成检索策略的持续优化闭环。这些内容等后续有项目实践了再继续写。