三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

文档处理流水线详解:PDF解析与文本分块策略最佳实践

文档处理流水线详解:PDF解析与文本分块策略最佳实践

文档处理流水线,PDF解析与分块策略详解

RAG系统的效果好不好,很大程度上取决于知识库的质量。而知识库的质量,第一步就在文档处理。

文档从原始文件,到变成可以检索的向量块,中间要经过好几步。加载、解析、清洗、分块、向量化。每一步没做好,都会影响最终的检索效果。

很多人做RAG,上来就搞向量数据库、搞高级检索算法。结果文档处理没做好,后面再怎么优化都有限。地基打歪了,楼盖得再高也不结实。

这一篇我们讲文档处理的完整流程。每一步做什么,有哪些常见的坑,怎么选择合适的分块策略。


文档处理的完整流程

一个标准的文档处理流水线,大概分这么几步。

第一步,加载。把原始文件读进来,PDF、Word、Excel、网页,各种格式都有。这一步的目标是把不同格式的文件,统一转换成纯文本。

第二步,清洗。原始文本里有很多没用的东西。页眉页脚、页码、目录、水印、广告、导航栏。这些东西要清理掉,不然会混进检索结果里,影响质量。

第三步,分块。把长文本切成一小块一小块的。块太大了搜不准,太小了又丢失上下文。切多大、怎么切,里面有学问。

第四步,向量化。把每个文本块转换成向量,存到向量数据库里。这样后面才能做语义检索。

第五步,元数据。给每个块加上元数据。来源文件、页码、章节、作者、时间。这些信息后面检索的时候能用得上,也方便溯源。

这五步看起来简单,每一步都有不少细节。我们一个一个说。


文档加载

加载是第一步,不同格式有不同的加载器。上一篇文件处理工具里讲过,这里简单回顾一下。

PDF用PyMuPDFLoader或者UnstructuredPDFLoader。效果好的优先选PyMuPDF,有图片和复杂排版的用Unstructured加OCR。

Word用Docx2txtLoader。需要结构信息用python-docx自己解析。

Excel用pandas处理最方便。

网页用BeautifulSoup或者UnstructuredHTMLLoader。

加载阶段最容易出的问题是格式解析不准确。特别是PDF,同样是PDF,用不同工具生成的,解析出来的质量天差地别。

我的建议是,先拿你实际的文档样本测一下。看看解析出来的文本对不对、顺序乱不乱、表格能不能读、图片里的文字要不要OCR。测完了再选合适的工具。

别想当然地觉得所有PDF都能用同一种方式处理。实际项目里,不同来源的PDF可能要写不同的处理逻辑。


文本清洗

加载出来的原始文本,通常是不干净的。有很多噪音需要清理。

常见的噪音有这些。

页眉页脚和页码。几乎所有长文档都有。每页重复出现,不清理的话,会被当成正文内容,影响检索。

目录和索引。文档前面的目录,后面的索引,都是导航用的,不是正文。

重复的版权声明和免责声明。很多文档每页底部都有,重复很多遍。

网页的导航栏、广告、推荐阅读。爬下来的网页,大部分内容都是这些,正文只占一小部分。

乱码和特殊字符。格式转换的时候容易出现。

清理的方法根据不同的情况来。

页眉页脚可以根据位置信息去掉。PyMuPDF能拿到每个文本块的坐标,根据坐标判断是不是页眉页脚。

重复内容可以用相似度检测。连续很多页都出现的相同内容,大概率是页眉页脚或者版权声明。

网页用专门的正文提取库。比如BeautifulSoup配合规则,或者用newspaper3k、trafilatura这类专门的正文提取工具。

清洗这一步很重要。脏数据进,脏数据出。文本不干净,后面检索出来的结果也会乱七八糟。


文本分块

分块是文档处理里最关键的一步。切多大、怎么切,直接影响检索效果。

分块太大有什么问题。一个块里内容太多,可能只有一小部分跟问题相关,但整块都被检索出来了。无关信息太多,会稀释有效内容,影响大模型的判断。也浪费Token。

分块太小又有什么问题。一个完整的意思被切成好几块,单看哪一块都不完整。检索的时候可能只搜到一半,上下文丢了,回答就不准确。

所以块大小要合适。

多大算合适,没有标准答案。跟你的文档类型、用户提问方式、用的Embedding模型都有关系。

常见的经验值是,普通文档200到500个中文字,或者500到1000个英文词。代码文档可以大一点,800到1500个Token。FAQ类的可以小一点,一个问答对就是一块。

这只是经验值。实际项目里,最好的办法是做实验。试几种不同的块大小,看哪种效果最好。


常见的分块方法

固定大小分块。最简单的方法。按字符数或者Token数切,每块固定大小。块之间可以有一些重叠,防止意思被切断。

LangChain里的RecursiveCharacterTextSplitter就是干这个的。它会优先按段落、句子、单词来切,尽量保持语义完整。

fromlangchain.text_splitterimportRecursiveCharacterTextSplitter splitter=RecursiveCharacterTextSplitter(chunk_size=500,chunk_overlap=50,separators=["\n\n","\n","。",","," "],)chunks=splitter.split_text(long_text)

这种方法简单通用,大部分场景都能用。效果也还可以。

按结构分块。根据文档的结构来切。标题、段落、列表、表格,按结构单元来分。一块就是一个相对完整的语义单元。

比如Markdown文档,可以按标题层级来分。一个H2下面的内容作为一块。或者一个H3下面的内容作为一块。

Word和HTML也可以按结构来分。标题、段落、表格,每个元素单独处理。

按结构分块的好处是,语义更完整。一个章节讲的是同一个主题,放在一块很合理。效果通常比固定大小分块好。

缺点是需要解析文档结构。格式不规范的文档,结构解析不准,分出来的块也有问题。

语义分块。更高级的分法。用Embedding来判断每句话的语义相似度,语义变化大的地方就切一刀。这样每一块内部的语义是连贯的。

听起来很美好,实际用起来效果不一定更好。而且速度慢、成本高。除非对分块质量要求特别高,不然不太推荐。


我的建议

说了这么多分块方法,到底用哪个。

我的建议是,先用最简单的。递归字符分割,chunk_size设500,overlap设50。先把系统跑起来,看效果怎么样。

效果不好再分析原因。是检索不到相关内容,还是搜到的内容不完整。是块太大了还是太小了。然后针对性地调。

不要一开始就追求最复杂的方案。很多时候,固定大小分块就够用了。把精力花在更影响效果的地方,比如重排序、Prompt优化、HyDE这些,收益更大。

还有一个经验。块里最好带上上文信息。比如每一块的开头,加上所属的文档标题和章节标题。这样检索的时候,块的上下文更完整,相关性判断也更准。


元数据

每个文本块,除了内容本身,还要附上一些元数据。

常见的元数据有这些。

来源信息。文件名、文件路径、URL。用来溯源。

位置信息。页码、行号、章节。告诉用户答案在文档的什么位置。

分类信息。文档类型、产品分类、部门。用来做过滤。

时间信息。创建时间、更新时间。可以按时间排序,也可以判断新旧。

作者信息。谁写的,谁负责。

元数据的用处很大。检索的时候可以按元数据过滤,只在指定的范围内搜。回答问题的时候可以引用来源,增加可信度。

不要嫌麻烦。加元数据花不了多少时间,后面会很有用。


下一篇我们讲向量数据库。切好的文本块,怎么存进去,怎么搜出来。

← 返回列表