1. 为什么 Loader 和 Splitter 决定了 RAG 的上限1.1 RAG 管线中 Loader 与 Splitter 的位置我先说一个可能有点反直觉的判断:真正把 RAG(检索增强生成)项目做砸的,往往不是模型选得不好,也不是向量数据库不够快,而是最前面的数据入口出了问题。Loader 负责把 PDF、Word、网页、数据库里的内容变成程序能读的文本,Splitter 负责把这段文本切成合适大小的片段。这两步一旦做得粗糙,后面无论怎么调 embedding、怎么换 prompt,都是在垃圾数据上做精装修。很多人在搭 RAG 的时候,第一反应是选模型、配向量库,因为这些东西容易出效果、容易展示。但你可以回想一下自己踩过的坑:文档加载出来全是乱码,明明有表格却丢失了结构,内容太长导致召回精准度惨不忍睹,或者问题稍微绕一点就答非所问。这些症状追根溯源,绝大多数都发生在 Loader 和 Splitter 这两个环节。所以我把这两个组件叫作 RAG 的“地基”。它们不产生炫酷的演示效果,但决定整个系统能走多远。一个结构混乱、切分随意的知识库,即使搭配了最先进的推理模型,也只会输出更流畅的胡话。1.2 知识库质量:链式问题而非单点问题有人会觉得,Loader 和 Splitter 不过是数据预处理,随便写几行代码就能搞定。实际工作中,你面对的不再是教程里的干净示例文本,而是扫描版 PDF、带复杂排版的政府公告、几百页的技术手册、结构混乱的网页正文。这些真实文档会在加载阶段就带来大量意外,而 Splitter 又是建立在加载结果之上的,所以错误的链条会一直传导下去。我习惯把知识库搭建拆成一条链:原始文档形态决定 Loader 选择,Loader 输出质量决定 Splitter 策略,Splitter 结果决定 embedding 质量和检索效果,最后才轮到模型生成答案。这条链上每一个环节都会向下游累积误差。如果你跳过了对前两个环节的审视,后面出了问题也往往无从下手。从项目管理的角度看,Loader 和 Splitter 也意味着最高性价比的优化点。调 embedding 换模型,效果提升可能有限;但你把 PDF 加载问题解决了,把 chunk 从 500 改成 300,检索相关性立刻会有肉眼可见的变化。因为这两步决定了系统看到的文本颗粒度和信息保真度。1.3 从目标的视角看两个组件的匹配Loader 和 Splitter 不是独立的,它们必须放在一起考虑。举个例子,如果你加载的是一份 Markdown 格式的技术文档,Loader 保留了标题结构,那么 Splitter 就可以利用结构信息做更合理的切分;如果 Loader 输出的是纯文本抹掉了所有层级,Splitter 就只能靠固定长度硬切,效果自然是两个水平。同样,如果你用 OCR 处理扫描件,得到的文本往往有识别错误、段落断裂,这时候盲目用固定大小拆分,会把语义完整的段落切成碎片。所以我建议先想清楚最终希望系统回答什么样的问题、召回到什么层级的片段,再倒推 Loader 和 Splitter 应该怎么配置。这不是一条固定流程,而是一个需要根据文档形态和目标反复调整的过程。2. Loader:把异构文档变成可处理文本2.1 常用 Loader 类型与适用场景Loader 做的事情很简单:把一种文件格式转换成文本内容。但不同格式之间差异极大,我按接入来源把常用的 Loader 分成了几类。文档类:包括 PDF、Word、PPT。PDF 是最麻烦的,后面单独说。Word 可以用 Docx2txtLoader,PPT 可以用 Unstructured 系列加载器。这类 Loader 的共同挑战是排版复杂度,文本可能在表格里、在批注里、在图片里,加载策略稍有不同结果就不一样。网页类:支持从 URL 直接抓取正文。常见做法是配合 Selenium 处理 JavaScript 渲染的页面,或者用 Readability 算法抽取正文,去掉导航和广告。网页加载的关键是区分正文内容和页面框架,否则你会把导航菜单也存进知识库。数据库与 API 类:通过数据库连接器把结构化数据按行读取,再转化为文本描述。结构化的数据转成自然语言时要设计好模板,比如“设备编号 A 的状态是运行中,温度 45 度”,这种模板化的文本后续拆分和检索都比较稳定。代码仓库类:加载整个 Git 仓库里的源码文件。和普通文本不同,代码的语义边界是函数、类、模块,所以拆代码需要专门的工具,比如按语法树切分,这一点我会在 Splitter 部分展开。选 Loader 时,我的经验是先看文档占比最高的格式,保证主线文档的加载质量,再考虑长尾格式。不要一开始就追求支持所有格式,真实项目里 PDF 和网页往往能覆盖 80% 以上的需求。2.2 Loader 解析背后的真实挑战:PDF 解析案例PDF 几乎是 RAG 项目里最让人头疼的格式,因为 PDF 本质上是一种排版格式,并不是为了让文本方便提取而设计的。我实际试过多种解析方式,差异非常大。先说常见的 PyPDFLoader 和 PDFPlumberLoader。这两种工具适合文本型 PDF,也就是可以直接复制文字的 PDF。它们的区别在于,PyPDFLoader 速度快,但对复杂排版容易丢字或乱序;PDFPlumber 基于坐标解析,处理表格、多栏排版会更稳定,代价是速度慢。如果你的 PDF 是单栏纯文本,用 PyPDFLoader 就够了;如果带表格或者双栏排版,直接上 PDFPlumber 会更省心。扫描型 PDF 必须走 OCR。这需要先渲染出清晰的图像,再用 OCR 引擎识别文字。中文扫描件建议用专门做过中文预训练的 OCR 模型,识别准确率会显著好于通用引擎。OCR 输出的文本通常带坐标信息,可以结合版面分析还原阅读顺序,这对后续拆分极其重要。还需要注意的一个坑是 PDF 里的表格。表格一旦被转换成纯文本,行列关系就丢失了。我建议在加载时单独判断当前区域是否为表格,如果是,就保留一定的分隔符号,比如用竖线或者 tab 来维持结构,这样 Splitter 切出来的片段,模型还能勉强理解数字的对应关系。2.3 元数据:Loader 最容易被忽视的价值很多人用 Loader 只取“文本”这个字段,把元数据丢得一干二净。这是非常可惜的。元数据指来源文件、页码、章节标题、作者、时间等等。它至少有两个重要用途。第一,元数据可以辅助检索过滤。比如用户只想知道某个版本的手册内容,你就可以根据版本号过滤;用户需要引用出处时,回答后面可以附上来源文件的页码。没有元数据的知识库就像一个没有目录的图书馆,书籍内容全都堆在一起,查得到内容却无法追溯。第二,元数据可以辅助 Splitter 做结构感知切分。如果你保留了章节标题,Splitter 可以按标题层级做切分,而不是机械地按字符数硬切。很多加载器提供了自动填充元数据的功能,比如 Unstructured 可以自动识别文档标题层级。你只需要在接入管线时把元数据传给下游的 Document 对象,成本极低,回报却很高。我实际项目里就吃过不保留元数据的亏。当时做了一套内部知识库,文档内容答得出来,但用户追问“这是哪份文件里的原文”,系统答不上来。后来重新设计元数据字段,才把引用溯源补上。别看这一步不起眼,在正式交付时往往是客户最看重的功能。2.4 网页与数据库加载的注意点网页加载最常见的问题是抽取正文时带了大量噪音。我的处理方式是两层过滤:先根据 HTML 标签剔除导航、页脚、侧栏,再用正文密度算法抽取核心内容。处理动态页面时,需要考虑渲染等待时间和懒加载问题,不然抓下来的内容会少一截。这里有个小技巧:对单页应用,先在无头浏览器里滚动页面触发懒加载,再抽取 HTML 快照,可以拿到完整正文。数据库加载则是另一个极端,数据格式通常很规整,难的是如何把结构化数据变成适合检索的语义化描述。如果直接把每一行原始数据丢进知识库,检索时用户输入的自然语言很难跟字段名对上。我习惯在加载时用一个描述模板,把字段名翻译成自然语言,比如把 created_at 转成“创建时间是 2024 年 3 月 5 日”,这样后续检索会更友好。3. Splitter:决定检索颗粒度的拆分策略3.1 简单 vs 递归拆分Splitter 的核心任务是决定向量库里单个片段的大小和边界。我见过不少项目直接把一整篇文档塞进一个向量,检索时效果非常差。因为 embedding 模型只能处理有限长度的文本,超长文本会被截断,或者被压缩成模糊的语义,用户的细节问题根本匹配不上。所以拆分是必须的,问题只在于怎么拆。最简单的方式是按固定字符数切。这个方法实现起来很快,但缺点很致命:很可能从句子中间硬切一刀。乍一看没什么大不了,实际上检索到的片段语义不完整,模型回答时缺少上下文。所以我基本不会单独用固定字符切分。递归字符拆分(RecursiveCharacterTextSplitter)是我用得最多的默认方案。它的逻辑是先按比较大的分隔符切,比如段落级别的换行符;如果切出来的片段还是太长,再换成句子级别的分隔符,比如句号、感叹号;还不行就继续降级到更小的分隔符。这样做的结果是,切出来的块尽量保留了完整语义单元,只有在迫不得已时才硬切。用生活类比来解释:固定字符切分就像把一块蛋糕从中间一刀切成整齐的大小,完全不管奶油草莓在哪;递归拆分则更像顺着蛋糕天然的纹路去切,尽可能不破坏奶油草莓的完整性,只有在蛋糕太大时才补一刀。3.2 长度参数背后的计算与权衡选择 chunk_size 和 chunk_overlap 时,很多新手会照抄网上的“最佳实践”,比如 500 配 50。但实际参数应该由你的文档类型、embedding 模型的上下文长度、以及用户提问的粒度共同决定。我建议这样思考 chunk_size:它表示每个片段包含的字符或 token 数量。这个值越小,片段越聚焦,召回精度通常越高,但上下文信息越少;这个值越大,片段包含的背景越完整,但向量化的语义越模糊,而且容易引入无关内容。实际项目中,我做过一个简单的对照实验,用相同的问答集分别测 200、400、800 三种 chunk_size,结果 400 的综合评分最高,200 的精确匹配更好但漏召回不少,800 的回答更完整但答非所问的比例明显上升。chunk_overlap 的作用是让相邻片段之间保留一段重叠,避免关键信息恰好落在切割边界上。比如一句话被切成两半,前半段的结尾和后后半段的开头会丢失语义,overlap 正是为了把边界处的上下文多保留一点。重叠比例我一般取 chunk_size 的 10% 到 20%,太低起不到缓冲作用,太高会让每个 token 被重复编码多次,增加向量数据库的存储和检索压力。这里你还要考虑 token 计数的方式。用字符数会导致中文文本被过度切分,因为一个中文字符在 LLM 里可能需要两个 token 甚至更多。更稳妥的做法是让 Splitter 使用模型的 tokenizer 来切分,以 token 数作为长度单位,保证每个片段在模型上下文中真的够用。3.3 Token 感知拆分的必要性很多人第一次看到“token 感知”这个词会有点懵,我用大白话解释一下。embedding 模型和生成模型处理文本时,并不是按用户看到的“字”为单位,而是把文本拆成 token 序列。一个 token 可能是半个词、一个词或者几个字符,中英文的比例差别很大。如果你按字符数切分,实际送入模型的 token 数可能远超预期,导致片段被截断,或者检索效果变差。我用过一个实际案例对比:一个 512 token 的片段,按英文字符估算大约是 2000 字符,但按中文字符估算只需要约 800 字符。如果 Splitter 设置的是字符上限,那么中文片段可能会包含比预期多一倍以上的 token。所以只要条件允许,我都会选择支持 token 计数器的 Splitter,比如 langchain 里传入对应模型的 tokenizer,或者直接用 OpenAI 的 tiktoken 做长度核算。这部分的实操并不复杂,写代码时预留一个参数切换即可。但如果你忽视它,线上系统换模型时,容易出现知识库里全是超长片段的情况,检索和回答质量双双下降,排查起来还很隐蔽。4. 实操案例:从 PDF 到向量库的完整管线4.1 环境准备与组件选择下面我用一个实际演练来串起 Loader 和 Splitter 的组合方式。假设场景是:把一份几十页的产品技术手册做成 RAG 问答系统,文档类型以文字为主,有少量表格,没有复杂双栏排版。我给出的工具链是 langchain 框架搭配 PDFPlumberLoader、RecursiveCharacterTextSplitter 和一种常用的向量化方案。选 PDFPlumberLoader 的原因是它的表格识别能力明显好于 PyPDFLoader,可以省掉很多手工清理。Splitter 选择递归字符拆分,同时在重载方法里传入模型的 tokenizer,避免中文长度偏差。环境准备只需要 Python 和对应依赖包。项目目录里建一个 data 文件夹放 PDF 原文,代码文件可以分成 load、split、store、query 四个模块,便于后期单独调参和排错。不要把所有逻辑塞在一个脚本里,否则后面改参数要来回拖拽,非常痛苦。4.2 加载并检查 PDF 的实际效果加载 PDF 之后,我强烈建议先打印前几页的文本,人工检查一遍。这一步很多人偷懒跳过了,结果把问题一路带到了线上。检查的重点有三个:有没有乱码或丢字、表格是否还能看出结构、标题层级是否保留。实操时可以先输出每个 Document 的文本长度和第一行内容,确认加载器没有把整份 PDF 合成一个巨型文本。接着抽查表格所在页,确认表格行没有被拆得乱七八糟。如果有特殊符号乱码,比如全角引号变成菱形,说明加载器对字体编码处理有问题,需要更换加载方案或先预处理 PDF。还可以顺手检查 metadata 里的页码是否准确。如果页码错乱,后续引用出处就会误导用户。这一步是纯人工的,但值得花十分钟,因为它是后面一切工作的输入。4.3 搭配拆分策略并入库加载完成后,我会把得到的 Document 列表传给 Splitter。这里要注意,不要直接把整个长篇文本一次性传给 Splitter,应该先按章节或页把文档切成若干段落,再在每个段落内部分 chunk。这样能避免一个大标题下面的内容全被塞进一个片段,也避免两个不相关章节被硬拼在一起。参数上,我先把 chunk_size 设为 400 token,chunk_overlap 设为 50 token。这个组合在多数中等复杂度文档上表现稳定。如果你发现某个片段内部包含了完全无关的两个主题,就把 chunk_size 调小;如果发现片段内容上下文不足,模型答得干巴巴,就适当调大。入库时,每个 chunk 的 metadata 应该包含原始文件名、页码、章节标题。向量化后存入向量库,再建立索引。入库完成后可以先做一轮简单检索,拿几个典型用户问题测试召回,不要等到整个系统搭完再验证,那是把风险后置了。4.4 检索验证与调参我在检索阶段会同时观察两个指标:召回的相关性,以及片段内容的可读性。相关性可以看 top-k 结果里是否包含正确答案,可读性则直接阅读片段文本,检查有没有句子被切断、语义是否完整。这两个指标能快速暴露 Loader 或 Splitter 的问题。比如检索结果里出现“第 3 页的内容混进第 4 页中间”,说明 Loader 的页面边界处理有问题;比如片段读到一半句子没有结尾,说明 Splitter 的字符边界设置不合理。找到具体问题后,回对应环节修改参数,重新入库即可。调参不要一次改多个变量,否则很难定位是哪一步引起的改善或恶化。5. 高频问题与排查实录5.1 为什么加载出来的内容是乱序的这是 PDF 加载里最常见的问题。原因多半是 PDF 内部文本流的顺序和视觉阅读顺序不一致,尤其是多栏排版或带有浮动物体的文档。解决方案有两个思路:一是更换能感知版面顺序的加载器,比如做了版面分析的工具;二是在加载后做后处理排序,根据坐标把文本块按从上到下、从左到右重排。排查时,你可以把加载出的文本按页打印,看是否有“栏 A 的内容和栏 B 的内容交替出现”的典型症状。如果只是少数页有问题,也可以直接在预处理阶段把 PDF 重新排版导出,比如先转成图片再 OCR,或者用工具把多栏改成单栏,再加载。这并不是偷懒,而是利用下游工具补齐 Loader 的短板。5.2 chunk_size 越大越好吗不是。我见过不少团队把 chunk_size 调到很大,理由是“上下文越多,模型理解越好”。实际上,大 chunk 会稀释 embedding 的语义焦点。举个我测试过的例子,关于设备故障代码的说明,chunk_size 从 400 调到 1200 之后,简单的故障码查询反而答不出来,因为向量里混入了太多无关的背景描述。大 chunk 还有个隐蔽问题:检索出的片段如果包含多段内容,模型在回答时可能引用到错误段落,造成幻觉式回答。所以我的默认策略是从小参数开始调,比如 300 到 500,只有明确发现上下文不足时再往上加。不要盲目追求大片段。5.3 中文语境下的特殊问题中文文档拆分的痛点主要在“没有标准分词边界”。英文按空格切词很干净,中文句子里的词连在一起,字符切分很容易从语义中间切开。两种解决手段配合使用:第一,使用递归字符拆分,把句号、感叹号、问号作为句子级分隔符,优先保证整句完整;第二,使用带中文 NLP 能力的 splitter,部分工具能利用分词结果挑选更自然的断点。还有一个小问题是中英文混排时字符长度与 token 长度的换算很不规律。我遇到过一个知识库,中英文不同来源的文档混在一起,用固定字符数拆分后,中文片段明显比英文片段“臃肿”。后来我把长度单位统一改成 token,问题基本消失。如果你在做多语言知识库,务必重视这一点。5.4 结构型文档怎么拆分更合理对于 Markdown、HTML、代码这类强结构化内容,固定字符拆分是最差的选择。我有三个推荐做法。按标题层级切:先按 Markdown 标题识别章节,每个独立章节作为候选块,如果章节内容过长,再在该章节内部递归切分。这样每个片段都有清晰的标题上下文。按语义段落切:利用空行和列表结构作为天然边界,让拆出来的片段尽量对应一个完整的话题。按代码语法树切:处理代码文件时,按函数、类、导入段作为边界,避免把函数定义和注释拆散。如果你做的是企业知识库,这些结构化文档占比往往不低。我的经验是,在 Splitter 之前先写一个简单的“结构识别”步骤,输出文档的层级树,再基于层级树决定切分边界。这套方案比通用 splitter 要稳定很多,虽然多写一些代码,但效果值得。6. 我个人的调参心得与建议最后分享几个我在多个项目中反复印证的经验,希望能帮你少走弯路。第一,Loader 和 Splitter 的调试顺序一定是“先保证加载正确,再优化拆分”。如果加载出的文本本身就乱,调 Splitter 是白费功夫。我在每个新项目里都会先人工抽查不少于十页加载结果,确认没问题才开始后续操作。第二,真实文档永远比示例文档复杂。我建议准备一套“脏数据测试集”,包括扫描 PDF、带复杂表格的文档、双栏论文、繁体中文手册等等,每换一种 Loader 或 Splitter 配置,就拿来测一遍。这套测试集会帮你提前发现很多线上才会爆的问题。第三,演示效果好不等于生产可交付。演示时通常只有几页文档,你可以手动调参让效果看起来很流畅;但生产环境有成千上万页内容,参数必须靠系统化的实验去定,而不是靠手感。我建议构建一个包含几十个真实问题的评测集,每次调参后跑一遍,用召回率和答案正确性做量化对比。第四,不要迷信“最佳参数”。不同的 embedding 模型、不同的文档风格、不同的问题粒度,最优的 chunk_size 可能相差很大。你需要的不是网上的标准答案,而是一套自己的调参流程和验证方法。我踩过很多次坑之后,最大的体会是:RAG 系统的上限由数据入口决定,而不是由模型决定。把 Loader 和 Splitter 这一关把住了,后面的路会顺很多。希望这篇内容能帮你把地基打牢。
1. 为什么 Loader 和 Splitter 决定了 RAG 的上限1.1 RAG 管线中 Loader 与 Splitter 的位置我先说一个可能有点反直觉的判断:真正把 RAG(检索增强生成)项目做砸的,往往不是模型选得不好,也不是向量数据库不够快,而是最前面的数据入口出了问题。Loader 负责把 PDF、Word、网页、数据库里的内容变成程序能读的文本,Splitter 负责把这段文本切成合适大小的片段。这两步一旦做得粗糙,后面无论怎么调 embedding、怎么换 prompt,都是在垃圾数据上做精装修。很多人在搭 RAG 的时候,第一反应是选模型、配向量库,因为这些东西容易出效果、容易展示。但你可以回想一下自己踩过的坑:文档加载出来全是乱码,明明有表格却丢失了结构,内容太长导致召回精准度惨不忍睹,或者问题稍微绕一点就答非所问。这些症状追根溯源,绝大多数都发生在 Loader 和 Splitter 这两个环节。所以我把这两个组件叫作 RAG 的“地基”。它们不产生炫酷的演示效果,但决定整个系统能走多远。一个结构混乱、切分随意的知识库,即使搭配了最先进的推理模型,也只会输出更流畅的胡话。1.2 知识库质量:链式问题而非单点问题有人会觉得,Loader 和 Splitter 不过是数据预处理,随便写几行代码就能搞定。实际工作中,你面对的不再是教程里的干净示例文本,而是扫描版 PDF、带复杂排版的政府公告、几百页的技术手册、结构混乱的网页正文。这些真实文档会在加载阶段就带来大量意外,而 Splitter 又是建立在加载结果之上的,所以错误的链条会一直传导下去。我习惯把知识库搭建拆成一条链:原始文档形态决定 Loader 选择,Loader 输出质量决定 Splitter 策略,Splitter 结果决定 embedding 质量和检索效果,最后才轮到模型生成答案。这条链上每一个环节都会向下游累积误差。如果你跳过了对前两个环节的审视,后面出了问题也往往无从下手。从项目管理的角度看,Loader 和 Splitter 也意味着最高性价比的优化点。调 embedding 换模型,效果提升可能有限;但你把 PDF 加载问题解决了,把 chunk 从 500 改成 300,检索相关性立刻会有肉眼可见的变化。因为这两步决定了系统看到的文本颗粒度和信息保真度。1.3 从目标的视角看两个组件的匹配Loader 和 Splitter 不是独立的,它们必须放在一起考虑。举个例子,如果你加载的是一份 Markdown 格式的技术文档,Loader 保留了标题结构,那么 Splitter 就可以利用结构信息做更合理的切分;如果 Loader 输出的是纯文本抹掉了所有层级,Splitter 就只能靠固定长度硬切,效果自然是两个水平。同样,如果你用 OCR 处理扫描件,得到的文本往往有识别错误、段落断裂,这时候盲目用固定大小拆分,会把语义完整的段落切成碎片。所以我建议先想清楚最终希望系统回答什么样的问题、召回到什么层级的片段,再倒推 Loader 和 Splitter 应该怎么配置。这不是一条固定流程,而是一个需要根据文档形态和目标反复调整的过程。2. Loader:把异构文档变成可处理文本2.1 常用 Loader 类型与适用场景Loader 做的事情很简单:把一种文件格式转换成文本内容。但不同格式之间差异极大,我按接入来源把常用的 Loader 分成了几类。文档类:包括 PDF、Word、PPT。PDF 是最麻烦的,后面单独说。Word 可以用 Docx2txtLoader,PPT 可以用 Unstructured 系列加载器。这类 Loader 的共同挑战是排版复杂度,文本可能在表格里、在批注里、在图片里,加载策略稍有不同结果就不一样。网页类:支持从 URL 直接抓取正文。常见做法是配合 Selenium 处理 JavaScript 渲染的页面,或者用 Readability 算法抽取正文,去掉导航和广告。网页加载的关键是区分正文内容和页面框架,否则你会把导航菜单也存进知识库。数据库与 API 类:通过数据库连接器把结构化数据按行读取,再转化为文本描述。结构化的数据转成自然语言时要设计好模板,比如“设备编号 A 的状态是运行中,温度 45 度”,这种模板化的文本后续拆分和检索都比较稳定。代码仓库类:加载整个 Git 仓库里的源码文件。和普通文本不同,代码的语义边界是函数、类、模块,所以拆代码需要专门的工具,比如按语法树切分,这一点我会在 Splitter 部分展开。选 Loader 时,我的经验是先看文档占比最高的格式,保证主线文档的加载质量,再考虑长尾格式。不要一开始就追求支持所有格式,真实项目里 PDF 和网页往往能覆盖 80% 以上的需求。2.2 Loader 解析背后的真实挑战:PDF 解析案例PDF 几乎是 RAG 项目里最让人头疼的格式,因为 PDF 本质上是一种排版格式,并不是为了让文本方便提取而设计的。我实际试过多种解析方式,差异非常大。先说常见的 PyPDFLoader 和 PDFPlumberLoader。这两种工具适合文本型 PDF,也就是可以直接复制文字的 PDF。它们的区别在于,PyPDFLoader 速度快,但对复杂排版容易丢字或乱序;PDFPlumber 基于坐标解析,处理表格、多栏排版会更稳定,代价是速度慢。如果你的 PDF 是单栏纯文本,用 PyPDFLoader 就够了;如果带表格或者双栏排版,直接上 PDFPlumber 会更省心。扫描型 PDF 必须走 OCR。这需要先渲染出清晰的图像,再用 OCR 引擎识别文字。中文扫描件建议用专门做过中文预训练的 OCR 模型,识别准确率会显著好于通用引擎。OCR 输出的文本通常带坐标信息,可以结合版面分析还原阅读顺序,这对后续拆分极其重要。还需要注意的一个坑是 PDF 里的表格。表格一旦被转换成纯文本,行列关系就丢失了。我建议在加载时单独判断当前区域是否为表格,如果是,就保留一定的分隔符号,比如用竖线或者 tab 来维持结构,这样 Splitter 切出来的片段,模型还能勉强理解数字的对应关系。2.3 元数据:Loader 最容易被忽视的价值很多人用 Loader 只取“文本”这个字段,把元数据丢得一干二净。这是非常可惜的。元数据指来源文件、页码、章节标题、作者、时间等等。它至少有两个重要用途。第一,元数据可以辅助检索过滤。比如用户只想知道某个版本的手册内容,你就可以根据版本号过滤;用户需要引用出处时,回答后面可以附上来源文件的页码。没有元数据的知识库就像一个没有目录的图书馆,书籍内容全都堆在一起,查得到内容却无法追溯。第二,元数据可以辅助 Splitter 做结构感知切分。如果你保留了章节标题,Splitter 可以按标题层级做切分,而不是机械地按字符数硬切。很多加载器提供了自动填充元数据的功能,比如 Unstructured 可以自动识别文档标题层级。你只需要在接入管线时把元数据传给下游的 Document 对象,成本极低,回报却很高。我实际项目里就吃过不保留元数据的亏。当时做了一套内部知识库,文档内容答得出来,但用户追问“这是哪份文件里的原文”,系统答不上来。后来重新设计元数据字段,才把引用溯源补上。别看这一步不起眼,在正式交付时往往是客户最看重的功能。2.4 网页与数据库加载的注意点网页加载最常见的问题是抽取正文时带了大量噪音。我的处理方式是两层过滤:先根据 HTML 标签剔除导航、页脚、侧栏,再用正文密度算法抽取核心内容。处理动态页面时,需要考虑渲染等待时间和懒加载问题,不然抓下来的内容会少一截。这里有个小技巧:对单页应用,先在无头浏览器里滚动页面触发懒加载,再抽取 HTML 快照,可以拿到完整正文。数据库加载则是另一个极端,数据格式通常很规整,难的是如何把结构化数据变成适合检索的语义化描述。如果直接把每一行原始数据丢进知识库,检索时用户输入的自然语言很难跟字段名对上。我习惯在加载时用一个描述模板,把字段名翻译成自然语言,比如把 created_at 转成“创建时间是 2024 年 3 月 5 日”,这样后续检索会更友好。3. Splitter:决定检索颗粒度的拆分策略3.1 简单 vs 递归拆分Splitter 的核心任务是决定向量库里单个片段的大小和边界。我见过不少项目直接把一整篇文档塞进一个向量,检索时效果非常差。因为 embedding 模型只能处理有限长度的文本,超长文本会被截断,或者被压缩成模糊的语义,用户的细节问题根本匹配不上。所以拆分是必须的,问题只在于怎么拆。最简单的方式是按固定字符数切。这个方法实现起来很快,但缺点很致命:很可能从句子中间硬切一刀。乍一看没什么大不了,实际上检索到的片段语义不完整,模型回答时缺少上下文。所以我基本不会单独用固定字符切分。递归字符拆分(RecursiveCharacterTextSplitter)是我用得最多的默认方案。它的逻辑是先按比较大的分隔符切,比如段落级别的换行符;如果切出来的片段还是太长,再换成句子级别的分隔符,比如句号、感叹号;还不行就继续降级到更小的分隔符。这样做的结果是,切出来的块尽量保留了完整语义单元,只有在迫不得已时才硬切。用生活类比来解释:固定字符切分就像把一块蛋糕从中间一刀切成整齐的大小,完全不管奶油草莓在哪;递归拆分则更像顺着蛋糕天然的纹路去切,尽可能不破坏奶油草莓的完整性,只有在蛋糕太大时才补一刀。3.2 长度参数背后的计算与权衡选择 chunk_size 和 chunk_overlap 时,很多新手会照抄网上的“最佳实践”,比如 500 配 50。但实际参数应该由你的文档类型、embedding 模型的上下文长度、以及用户提问的粒度共同决定。我建议这样思考 chunk_size:它表示每个片段包含的字符或 token 数量。这个值越小,片段越聚焦,召回精度通常越高,但上下文信息越少;这个值越大,片段包含的背景越完整,但向量化的语义越模糊,而且容易引入无关内容。实际项目中,我做过一个简单的对照实验,用相同的问答集分别测 200、400、800 三种 chunk_size,结果 400 的综合评分最高,200 的精确匹配更好但漏召回不少,800 的回答更完整但答非所问的比例明显上升。chunk_overlap 的作用是让相邻片段之间保留一段重叠,避免关键信息恰好落在切割边界上。比如一句话被切成两半,前半段的结尾和后后半段的开头会丢失语义,overlap 正是为了把边界处的上下文多保留一点。重叠比例我一般取 chunk_size 的 10% 到 20%,太低起不到缓冲作用,太高会让每个 token 被重复编码多次,增加向量数据库的存储和检索压力。这里你还要考虑 token 计数的方式。用字符数会导致中文文本被过度切分,因为一个中文字符在 LLM 里可能需要两个 token 甚至更多。更稳妥的做法是让 Splitter 使用模型的 tokenizer 来切分,以 token 数作为长度单位,保证每个片段在模型上下文中真的够用。3.3 Token 感知拆分的必要性很多人第一次看到“token 感知”这个词会有点懵,我用大白话解释一下。embedding 模型和生成模型处理文本时,并不是按用户看到的“字”为单位,而是把文本拆成 token 序列。一个 token 可能是半个词、一个词或者几个字符,中英文的比例差别很大。如果你按字符数切分,实际送入模型的 token 数可能远超预期,导致片段被截断,或者检索效果变差。我用过一个实际案例对比:一个 512 token 的片段,按英文字符估算大约是 2000 字符,但按中文字符估算只需要约 800 字符。如果 Splitter 设置的是字符上限,那么中文片段可能会包含比预期多一倍以上的 token。所以只要条件允许,我都会选择支持 token 计数器的 Splitter,比如 langchain 里传入对应模型的 tokenizer,或者直接用 OpenAI 的 tiktoken 做长度核算。这部分的实操并不复杂,写代码时预留一个参数切换即可。但如果你忽视它,线上系统换模型时,容易出现知识库里全是超长片段的情况,检索和回答质量双双下降,排查起来还很隐蔽。4. 实操案例:从 PDF 到向量库的完整管线4.1 环境准备与组件选择下面我用一个实际演练来串起 Loader 和 Splitter 的组合方式。假设场景是:把一份几十页的产品技术手册做成 RAG 问答系统,文档类型以文字为主,有少量表格,没有复杂双栏排版。我给出的工具链是 langchain 框架搭配 PDFPlumberLoader、RecursiveCharacterTextSplitter 和一种常用的向量化方案。选 PDFPlumberLoader 的原因是它的表格识别能力明显好于 PyPDFLoader,可以省掉很多手工清理。Splitter 选择递归字符拆分,同时在重载方法里传入模型的 tokenizer,避免中文长度偏差。环境准备只需要 Python 和对应依赖包。项目目录里建一个 data 文件夹放 PDF 原文,代码文件可以分成 load、split、store、query 四个模块,便于后期单独调参和排错。不要把所有逻辑塞在一个脚本里,否则后面改参数要来回拖拽,非常痛苦。4.2 加载并检查 PDF 的实际效果加载 PDF 之后,我强烈建议先打印前几页的文本,人工检查一遍。这一步很多人偷懒跳过了,结果把问题一路带到了线上。检查的重点有三个:有没有乱码或丢字、表格是否还能看出结构、标题层级是否保留。实操时可以先输出每个 Document 的文本长度和第一行内容,确认加载器没有把整份 PDF 合成一个巨型文本。接着抽查表格所在页,确认表格行没有被拆得乱七八糟。如果有特殊符号乱码,比如全角引号变成菱形,说明加载器对字体编码处理有问题,需要更换加载方案或先预处理 PDF。还可以顺手检查 metadata 里的页码是否准确。如果页码错乱,后续引用出处就会误导用户。这一步是纯人工的,但值得花十分钟,因为它是后面一切工作的输入。4.3 搭配拆分策略并入库加载完成后,我会把得到的 Document 列表传给 Splitter。这里要注意,不要直接把整个长篇文本一次性传给 Splitter,应该先按章节或页把文档切成若干段落,再在每个段落内部分 chunk。这样能避免一个大标题下面的内容全被塞进一个片段,也避免两个不相关章节被硬拼在一起。参数上,我先把 chunk_size 设为 400 token,chunk_overlap 设为 50 token。这个组合在多数中等复杂度文档上表现稳定。如果你发现某个片段内部包含了完全无关的两个主题,就把 chunk_size 调小;如果发现片段内容上下文不足,模型答得干巴巴,就适当调大。入库时,每个 chunk 的 metadata 应该包含原始文件名、页码、章节标题。向量化后存入向量库,再建立索引。入库完成后可以先做一轮简单检索,拿几个典型用户问题测试召回,不要等到整个系统搭完再验证,那是把风险后置了。4.4 检索验证与调参我在检索阶段会同时观察两个指标:召回的相关性,以及片段内容的可读性。相关性可以看 top-k 结果里是否包含正确答案,可读性则直接阅读片段文本,检查有没有句子被切断、语义是否完整。这两个指标能快速暴露 Loader 或 Splitter 的问题。比如检索结果里出现“第 3 页的内容混进第 4 页中间”,说明 Loader 的页面边界处理有问题;比如片段读到一半句子没有结尾,说明 Splitter 的字符边界设置不合理。找到具体问题后,回对应环节修改参数,重新入库即可。调参不要一次改多个变量,否则很难定位是哪一步引起的改善或恶化。5. 高频问题与排查实录5.1 为什么加载出来的内容是乱序的这是 PDF 加载里最常见的问题。原因多半是 PDF 内部文本流的顺序和视觉阅读顺序不一致,尤其是多栏排版或带有浮动物体的文档。解决方案有两个思路:一是更换能感知版面顺序的加载器,比如做了版面分析的工具;二是在加载后做后处理排序,根据坐标把文本块按从上到下、从左到右重排。排查时,你可以把加载出的文本按页打印,看是否有“栏 A 的内容和栏 B 的内容交替出现”的典型症状。如果只是少数页有问题,也可以直接在预处理阶段把 PDF 重新排版导出,比如先转成图片再 OCR,或者用工具把多栏改成单栏,再加载。这并不是偷懒,而是利用下游工具补齐 Loader 的短板。5.2 chunk_size 越大越好吗不是。我见过不少团队把 chunk_size 调到很大,理由是“上下文越多,模型理解越好”。实际上,大 chunk 会稀释 embedding 的语义焦点。举个我测试过的例子,关于设备故障代码的说明,chunk_size 从 400 调到 1200 之后,简单的故障码查询反而答不出来,因为向量里混入了太多无关的背景描述。大 chunk 还有个隐蔽问题:检索出的片段如果包含多段内容,模型在回答时可能引用到错误段落,造成幻觉式回答。所以我的默认策略是从小参数开始调,比如 300 到 500,只有明确发现上下文不足时再往上加。不要盲目追求大片段。5.3 中文语境下的特殊问题中文文档拆分的痛点主要在“没有标准分词边界”。英文按空格切词很干净,中文句子里的词连在一起,字符切分很容易从语义中间切开。两种解决手段配合使用:第一,使用递归字符拆分,把句号、感叹号、问号作为句子级分隔符,优先保证整句完整;第二,使用带中文 NLP 能力的 splitter,部分工具能利用分词结果挑选更自然的断点。还有一个小问题是中英文混排时字符长度与 token 长度的换算很不规律。我遇到过一个知识库,中英文不同来源的文档混在一起,用固定字符数拆分后,中文片段明显比英文片段“臃肿”。后来我把长度单位统一改成 token,问题基本消失。如果你在做多语言知识库,务必重视这一点。5.4 结构型文档怎么拆分更合理对于 Markdown、HTML、代码这类强结构化内容,固定字符拆分是最差的选择。我有三个推荐做法。按标题层级切:先按 Markdown 标题识别章节,每个独立章节作为候选块,如果章节内容过长,再在该章节内部递归切分。这样每个片段都有清晰的标题上下文。按语义段落切:利用空行和列表结构作为天然边界,让拆出来的片段尽量对应一个完整的话题。按代码语法树切:处理代码文件时,按函数、类、导入段作为边界,避免把函数定义和注释拆散。如果你做的是企业知识库,这些结构化文档占比往往不低。我的经验是,在 Splitter 之前先写一个简单的“结构识别”步骤,输出文档的层级树,再基于层级树决定切分边界。这套方案比通用 splitter 要稳定很多,虽然多写一些代码,但效果值得。6. 我个人的调参心得与建议最后分享几个我在多个项目中反复印证的经验,希望能帮你少走弯路。第一,Loader 和 Splitter 的调试顺序一定是“先保证加载正确,再优化拆分”。如果加载出的文本本身就乱,调 Splitter 是白费功夫。我在每个新项目里都会先人工抽查不少于十页加载结果,确认没问题才开始后续操作。第二,真实文档永远比示例文档复杂。我建议准备一套“脏数据测试集”,包括扫描 PDF、带复杂表格的文档、双栏论文、繁体中文手册等等,每换一种 Loader 或 Splitter 配置,就拿来测一遍。这套测试集会帮你提前发现很多线上才会爆的问题。第三,演示效果好不等于生产可交付。演示时通常只有几页文档,你可以手动调参让效果看起来很流畅;但生产环境有成千上万页内容,参数必须靠系统化的实验去定,而不是靠手感。我建议构建一个包含几十个真实问题的评测集,每次调参后跑一遍,用召回率和答案正确性做量化对比。第四,不要迷信“最佳参数”。不同的 embedding 模型、不同的文档风格、不同的问题粒度,最优的 chunk_size 可能相差很大。你需要的不是网上的标准答案,而是一套自己的调参流程和验证方法。我踩过很多次坑之后,最大的体会是:RAG 系统的上限由数据入口决定,而不是由模型决定。把 Loader 和 Splitter 这一关把住了,后面的路会顺很多。希望这篇内容能帮你把地基打牢。