有效切分源文档,即“分块”,是构建高性能RAG系统的基础步骤,直接影响检索准确性。分块的具体做法在处理各类数据格式时会显得尤为必要。简单地按固定字符或标记 (token)数来分割文档,常会破坏信息的语义完整性,给生成器带来不完整或误导性的上下文 (context)。本文介绍进阶策略,以使分块方法适应各类数据源的特殊性,确保每个块都代表一个连贯且有意义的信息单元。
统一分块的问题
想象一下,你的RAG系统需要处理研究论文、源代码文件、财务电子表格和会议记录。对这些不同的格式应用一种简单的、固定大小的分块策略(例如,每500个字符分割一次)会产生不理想的结果:
- 含义被割裂: 研究论文中的句子可能会被一分为二,使其变得不合逻辑。代码文件中的函数定义可能会被分开,导致函数签名与其主体分离。电子表格中的一行,代表一个单独的数据记录,可能会被分成多个块。
- 上下文 (context)丢失: 建立文档不同部分之间关系的重要信息可能会丢失,如果这些部分最终位于单独、不重叠的块中。例如,表头可能在一个块中,而其对应的数据行在另一个块中,使得数据难以理解。
- 无关信息稀释或过分集中: 大的固定大小块可能包含太多无关信息,稀释了核心内容的关联性。反之,非常小的块可能缺乏足够的上下文,导致检索模型难以理解其重要性。
智能分块的主要目的是将文档分割成尽可能自含、语义连贯且大小适合你的嵌入 (embedding)模型和下游LLM的部分。
根据数据结构定制分块
不同数据类型具有独特的结构属性,这些属性可以并且应该指导你的分块策略。
大量文本的文档(文章、报告、书籍)
对于主要由散文构成的文档,可以采用几种方法:
- 基于句子的分块: 使用自然语言处理(NLP)库,如NLTK或spaCy,可以将文本分成独立的句子。每个句子,或一小群连续的句子,可以形成一个块。这通常能很好地保持语义单元。然而,句子长度可能差异很大,可能导致块大小不一致。
- 基于段落的分块: 段落通常代表不同的想法或子主题。通过段落分隔符(例如,两个换行符)进行分割是一种直接且通常有效的方法。
- 递归字符文本分割: 这是LangChain等框架推广的一种灵活方法。你定义一个分隔符的层次结构(例如,
\n\n 用于段落,然后 \n 用于行,然后 . 用于句子,然后 用于单词)。文本使用列表中的第一个分隔符进行分割。如果任何产生的块仍然太大,它们将使用层次结构中的下一个分隔符进行递归分割,依此类推,直到所有块都满足指定的尺寸限制。这种方法很适合各种文本结构。
- 结构感知分块(Markdown,HTML): 如果你的文档是Markdown或HTML等格式,你可以使用解析器来识别标题、列表和表格等结构元素。然后可以根据这些元素执行分块,例如,将标题下的每个部分视为一个潜在的块。这通常会产生高度相关且上下文 (context)丰富的块。
源代码
对源代码进行分块需要遵从其语法和逻辑结构:
- 基于函数或类的分块: 最常见的方法是将每个函数、方法或类定义视为一个单独的块。这与开发人员组织和理解代码的方式非常吻合。
- 使用抽象语法树(AST): 为了更精细地控制,你可以将代码解析成AST,然后根据特定的节点类型或代码块定义分块边界。像
tree-sitter 这样的库在这里会很有用。
- 上下文补充: 将相关上下文(如导入语句或紧邻函数的重要注释)包含在该函数的块中可能是有益的。
表格数据(CSV、电子表格、数据库摘录)
原始的表格数据通常不适合直接嵌入 (embedding)。策略包括:
- 面向行的分块: 每行,或一小组相关的行,可以被视为一个块。如果每行代表一个独立记录,这种方法很适合。
- 表格到文本的转换: 一种有效方法是将表格或表格的某些部分在分块前转换成自然语言描述或类似Markdown的结构化文本格式。例如,一行
ProductID: 101, ProductName: "Laptop", Price: 1200 可以转换为句子:“产品ID 101,名为'笔记本电脑',价格为1200美元。”这使得信息更容易被标准文本嵌入模型处理。
- 包含标题: 务必确保列标题与其在块中的相应数据关联,或作为上下文提供。
PDF文档
PDF文档因其侧重于视觉呈现而非语义结构而出了名的难以处理。
- 布局感知解析:
PyMuPDF、pdfplumber 或 unstructured 等工具旨在分析PDF页面的布局。它们尝试识别不同的文本块、段落、表格和图片,并确定逻辑阅读顺序。这些识别出的块可以作为分块的基础。
- 表格提取与转换: PDF中的表格应特别提取,如果可能,在分块或作为文本处理前转换成更结构化的格式(如CSV或Markdown)。
- 处理扫描PDF: 如果处理扫描(基于图像)的PDF,光学字符识别(OCR)是前提条件。OCR输出的质量将对后续的分块和检索产生明显影响。
演示文稿(例如,PowerPoint、Google Slides)
演示文稿幻灯片通常包含文本、图片和讲者备注的混合内容。
- 幻灯片作为块: 最简单的方法是将每张幻灯片视为一个单独的块。
- 基于组件的分块: 更精细的方法是提取每张幻灯片中的独立文本框、标题、项目符号和讲者备注,并可能创建更小、更集中的块,或智能地组合它们。
- 图片内容: 如果幻灯片包含带有文本的重要图片,可以应用OCR。对于图表,如果可行,可以考虑生成文本描述。
下图说明了文档在简单分块策略与内容感知分块策略下的切分方式。
比较说明了简单的固定大小分块如何随意切分内容,而内容感知分块旨在从包含散文、代码和表格的文档中保留逻辑单元。
进阶分块策略
更精巧的技巧可以进一步提升分块质量。
语义分块
语义分块旨在根据文本段落的含义和关联性进行分组,而不是仅仅依赖于语法边界或固定大小。这通常涉及在分块过程中使用嵌入 (embedding)模型。
一种常见方法包括:
- 初始切分: 将文档分成小而易于处理的单元(例如,单个句子或小群句子)。
- 嵌入生成: 为每个初始单元生成向量 (vector)嵌入。
- 基于相似性的分组: 遍历这些单元。比较当前单元(或正在形成的块)与下一个单元的嵌入。
- 如果语义相似性(例如,嵌入之间的余弦相似性)高(高于定义的阈值),则将这些单元合并到同一块中。
- 如果相似性明显下降,或达到最大块大小,则开始一个新的块。
- 目标是找到文本中自然的“语义断点”。
另外,也可以嵌入句子,然后应用聚类算法。同一聚类中的句子可以分组到块中,尽管保持句子的原始顺序很重要。
优点: 语义分块可以生成高度连贯的块,即使这意味着块大小会显著变化。
挑战: 它比简单方法计算量更大。质量在很大程度上取决于用于相似性度量的嵌入模型。调整相似性阈值或聚类参数 (parameter)需要进行实验。
考虑多模态 (multimodal)内容
对于本质上混合了文本与其他模态(例如,文档中的图片,带有关键帧图片的视频转录)的数据源,分块策略应理想地考虑这一点。虽然完整的多模态RAG是一个更广的话题,但在分块阶段,你可以:
- 为图片生成文本描述或说明(使用图片到文本模型),并将这些描述作为单独的块与附近的文本块链接,或者将它们整合到文本块中。
- 对于音频/视频,使用转录文本作为主要文本,并可能为块创建元数据标签,指示相应的时间戳或视觉元素。
优化分块大小和重叠
分块大小或重叠没有一个通用的最佳数值。这些参数 (parameter)需要仔细考虑,并且通常需要通过实验调整。
- 分块大小:
- 嵌入 (embedding)模型限制: 嵌入模型有输入标记 (token)限制。块必须遵守这些限制。此外,某些模型在特定长度范围的输入下表现最佳。
- 信息密度: 对于密集的技术文档,较小的块可能更适合保持专注。对于叙事文本,可能需要更大的块来捕捉一个完整的想法。
- 查询特异性: 如果你预期查询非常具体,更小、更细粒度的块可能更有益。对于更宽泛的查询,提供更多上下文 (context)的较大块可能更好。
- 生成器模型的上下文窗口: 提供给生成器的最终上下文(包括检索到的块)也有其自身限制。
- 分块重叠:
- 目的: 重叠块(一个块的末尾在下一个块的开头重复)有助于在块边界处保持上下文连续性。这对于散文特别有用,可避免句子或想法在查询中被突然截断或丢失,特别是当查询内容接近边界时。
- 程度: 常见的重叠大小是块大小的10-20%。例如,一个500标记的块可能有50-100标记的重叠。
- 何时使用: 对想法在句子和段落之间流动的文本最有效。对于代码函数或独立表格行等高度结构化的数据,重叠可能不那么必要,如果管理不当,甚至可能引入无用的冗余。
- 实现: 通常以“滑动窗口”方式实现。
以下图表说明了不同分块策略的语义连贯性得分。“内容感知”策略,即尊重数据结构的策略,通常会带来更好的连贯性。
不同的分块策略对生成块语义连贯性的影响。适应内容结构的策略往往表现更好。
元数据的必要作用
每个块都应附带丰富的元数据。这些元数据通常不与块的内容一起嵌入 (embedding),而是与向量 (vector)一同存储,在检索和生成过程中非常有用。重要的元数据包括:
- 来源标识符:
document_id(文档ID)、file_name(文件名)、url。
- 位置信息:
page_number(页码,适用于PDF)、slide_number(幻灯片编号,适用于演示文稿)、section_title(章节标题)、paragraph_id(段落ID)、line_numbers(行号,适用于代码)。
- 数据类型: 一个标签,指示块内容的性质(例如,
prose(散文)、python_code(Python代码)、table_markdown(表格Markdown)、slide_notes(幻灯片备注))。
- 时间戳: 源数据的
creation_date(创建日期)、last_modified_date(最后修改日期)。
- 原始块边界: 原始文档中的起始和结束字符偏移量。
丰富元数据的好处:
- 过滤检索: 允许对搜索结果进行预过滤或后过滤(例如,“仅从'annual_report_2023.pdf'中检索信息”)。
- 为LLM提供上下文 (context): 元数据可以与块内容一同传递给生成器LLM,提供关于信息来源和性质的有价值的上下文。
- 改进引用和事实核查: 使追溯生成信息到其来源变得更容易。
- 调试和分析: 有助于理解为何检索到某些块而未检索到其他块。
实施和评估的考量
- 工具:
- 像 LangChain 和 LlamaIndex 这样的框架提供了大量预构建的文本分割器和文档加载器,它们处理各种格式和分块策略(例如,
RecursiveCharacterTextSplitter、MarkdownTextSplitter、PythonCodeTextSplitter)。
- 像 NLTK 和 spaCy 这样的NLP库对于句子分词 (tokenization)和其他语言处理来说十分重要。
- 用于PDF处理的专业库(PyMuPDF、pdfplumber、unstructured.io)对于处理复杂的PDF布局必不可少。
- 评估循环:
- 直接衡量分块策略的“优劣”本身是困难的。最有效的评估通常是间接的,通过其对端到端RAG系统性能的影响来进行。
- 在具有代表性的一组查询上监控检索指标(例如,命中率、平均倒数排名(MRR)、NDCG)。
- 使用LLM辅助评估或人工审查来评估生成质量指标(例如,忠实性、相关性、幻觉 (hallucination)的缺乏)。
- 定性分析: 定期检查你的策略生成的一批块样本。它们有意义吗?重要的想法是否被不自然地分割了?是否存在太多无关信息或信息过少?
- 考虑创建一个“黄金数据集”,其中你手动确定理想的源文本段落。然后,通过检查这些段落是否被捕获在单一、连贯的块中,来评估你的分块策略的匹配程度。
优化针对各类数据源的分块是一个迭代过程。它需要理解你的数据,尝试不同的策略和参数 (parameter),并严格评估其对RAG系统整体有效性的影响。虽然它增加了数据摄取流程的复杂性,但在提升检索相关性和生成质量方面,对于生产级别的RAG应用来说,回报是可观的。