正如本章引言所述,大规模检索增强生成 (RAG)系统的有效性在很大程度上取决于您如何准备和组织数据。原始信息,无论其形式和数量,都必须经过细致处理和分割成可理解的、上下文 (context)丰富的单元(即“块”),才能被嵌入 (embedding)并索引以便检索。本节讨论文档分块和预处理的方法,这些方法行之有效,旨在在庞大的分布式数据集上高效运行。妥善处理这一阶段非常重要;不理想的分块可能导致上下文支离破碎、检索到的段落不相关,并最终降低大语言模型 (LLM)生成内容的质量。
挑战的体量:大规模分块与预处理
当处理太字节或拍字节的数据时,这些数据来源于不同,且持续更新,传统的预处理和分块方法常常失效。挑战包含多个方面:
- 数据量与速度: 庞大的数据量要求采用分布式处理。摄取和重新处理大量语料,特别是在数据频繁变动的情况下,需要能够处理高吞吐量 (throughput)和低延迟的管道。
- 数据多样性: 企业数据很少是统一的。您会遇到各种数据,从结构化PDF、复杂HTML页面到纯文本、源代码和扫描文档。每种类型都需要量身定制的解析和清理逻辑。
- 计算开销: 解析、清理,尤其是对数十亿文档应用复杂的分块算法,会产生巨大的计算成本。针对分布式环境优化这些操作极为重要。
- 确定“理想”数据块: 理想的数据块大小和分段策略仍是一个持续存在的问题。数据块必须足够小以实现高效嵌入 (embedding)和检索,但也要足够大以保留足够的语义上下文 (context)。在处理大规模、长度和结构各异的文档时,这种平衡变得更加复杂。
- 元数据完整性与传播: 数据块如果没有其关联的元数据(例如,源文档ID、原始结构、时间戳),其作用就会很有限。确保这些元数据被准确捕获、在预处理过程中得到保留并与每个数据块关联起来,是分布式系统中一项重要的工程任务。
- 幂等性与重处理: 管道必须设计为幂等。如果解析模块中发现错误或引入新的预处理步骤,您需要能够高效地重新处理受影响的文档或整个语料库,避免冗余工作或数据损坏。
分布式环境中的核心预处理操作
文档分块之前,它们必须经过几个预处理步骤。在大规模场景下,这些操作本身需要是分布式的且具有弹性。
分布式文档摄取与解析
第一步是获取和解析原始文档。这通常包括:
- 可伸缩获取: 从分布式文件系统(如HDFS或S3)、消息队列(Kafka)或大规模网络爬取中读取。
- 解析: 使用能处理各种格式的库(例如,用于多种类型的Apache Tika,用于PDF的
PyMuPDF,用于HTML的Beautiful Soup)。您的系统必须能够妥善处理格式错误的文档或解析失败,记录问题而不中断整个管道。
- 并行处理: 运用Apache Spark或Dask等框架,在集群中并行加载和解析数百万文档。例如,一个Spark任务可能会将一个函数映射到分布式文档URI集合上,以并行加载和解析它们。
大规模文本清洗与标准化
文本提取后,需要进行清洗:
- 样板内容移除: 清除网页内容中的页眉、页脚、导航栏、广告,或结构化文档中不相关的部分。
- 文本标准化: 通过统一大小写(通常为小写)、处理Unicode标准化(例如NFC、NFKC)以及移除或替换特殊字符或控制代码来标准化文本。
- 语言识别: 对于多语言语料库,识别每个文档或片段的语言对应用特定语言的清洗规则、分词 (tokenization)器 (tokenizer)或分块策略很重要。
- 个人身份信息(PII)检测与修订: 在许多应用中,检测和修订或匿名化个人身份信息(PII)是合规性要求。在大规模场景下准确执行此操作需要高效的算法和细致的工作流设计。
进阶结构分析
对于多种文档类型,特别是PDF、Word文档或科学论文,理解文档的布局和结构可以显著提升分块质量。
- 布局感知模型: LayoutLM、LlamaParse等工具和模型,或商业解决方案,可以识别标题、段落、表格、图表和列表等元素。
- 逻辑分段: 利用这些结构信息将文档分段为逻辑单元,在细粒度分块前可以保持连贯性。例如,书中的一个章节或报告中的一个独立部分。
- 表格与图表提取: 从表格和图表中提取内容并将其转换为大语言模型 (LLM)可以理解的文本表示是一项专门任务。这可能涉及将表格行线性化或生成图表的文本描述。
可伸缩文档分块方法
有了预处理过的文本和可能的结构信息,下一步是分块。策略的选择及其实现必须考虑操作的规模。
从原始文档到可嵌入 (embedding)数据块的文档处理管道,其中突出了各种分块策略。
固定大小分块
这是最直接的方法:将文本分割成预设长度(例如N个字符或N个token)的片段,并可选择是否重叠。
- 优点: 实现简单,易于并行化。数据块大小可预测。
- 缺点: 常常在句中或段落中分割文本,破坏语义上下文 (context)。理想大小N难以确定,并且可能在整个语料库中有所不同。
- 伸缩性: 易于分布式处理。主要考虑是高效的字符串操作和在分布式工作节点间正确管理重叠。重叠有助于减轻边界处的上下文丢失,但会增加存储和处理开销。常见的重叠是数据块大小的10-20%。
内容感知(语义)分块
这些方法旨在在自然的语义边界处分割文本。
- 句子分割: 使用自然语言处理库(例如spaCy、NLTK)将文本分割成单个句子。每个句子或一组句子可以构成一个数据块。这比固定大小分块的计算量更大,但通常会生成更连贯的数据块。
- 示例:
doc = nlp(text); chunks = [sent.text for sent in doc.sents]
- 段落分割: 将每个段落视为一个数据块。这通常与文本中的逻辑分隔点很好地对齐 (alignment)。需要可靠的段落边界检测。
- 嵌入驱动的边界检测: 一种更先进的技术是使用句子嵌入来识别语义转换。计算句子(或小段句子)的嵌入,并在连续句子嵌入之间的余弦距离超过某个阈值时进行分割。这可以适应不同的内容密度,但增加了初始嵌入步骤的开销。
- 递归分块: 从较大的语义单元(例如通过布局分析识别的部分)开始,如果它们超出目标大小,则使用内容感知方法递归分割它们。反之,合并非常小的相邻语义数据块。
- 伸缩性: 分布式处理自然语言处理模型及其处理可能具有挑战性,因为模型大小和状态的限制。Spark等框架可以通过将自然语言处理函数(UDF)应用于文档分区来分发此任务。需要仔细批处理以优化推理 (inference)。
布局感知分块
对于具有丰富结构信息(例如PDF、HTML、Markdown)的文档,利用此结构可以生成高度相关的数据块。
- 策略: 使用提取结构元素(标题、章节、列表项、表格单元格)的解析器。根据这些元素定义数据块。例如,H2标题下的每个小节都可以是一个数据块。
- 优点: 数据块与文档的预期组织对齐,保留了逻辑上下文。
- 缺点: 严重依赖结构解析的质量。可能不适用于纯文本文档。
- 伸缩性: 解析步骤(例如运行布局感知模型)是瓶颈。一旦结构作为元数据被提取,分块逻辑本身就可以分布式处理。
针对大语言模型 (LLM)的Token限制感知分块
鉴于检索到的数据块会送入具有固定上下文窗口大小(例如4096、8192或128k token)的大语言模型,分块理想情况下应遵循这些限制。
- 策略: 使用您的大语言模型所用的相同分词 (tokenization)器 (tokenizer)(例如来自Hugging Face Transformers的)来计算token。分割文本,使每个数据块的token数低于大语言模型的token限制,减去提示或系统消息所需的任何token。
- 改进:
- 即使接近token限制,也尝试在句子边界处分割。
- 实施token层面的策略性重叠,而不仅仅是字符层面的,以确保上下文连续性。
- 考虑大语言模型可使用的“有效”上下文可能小于其理论最大值,特别是对于“中间丢失”问题。
- 伸缩性: 分词相对较快,但对数百万个数据块重复分词和调整边界需要高效的实现。
根据语义连贯性和计算成本比较分块策略。实际性能会因数据和实现而异。
混合策略
通常,没有哪一种策略是普遍理想的。专业系统常采用混合方法:
- 多阶段分块: 从布局感知分块开始,获得粗粒度分段。然后,在这些分段内部应用语义或token感知分块。
- 自适应选择: 对不同文档类型使用不同的策略。例如,对PDF使用布局感知分块,对纯文本电子邮件使用语义分块,对代码使用专门的解析器/分块器。
- 基于规则的覆盖: 实施规则以处理特定的已知文档结构或边缘情况。
分布式系统中的实现模式
在大规模环境下执行这些策略需要利用分布式计算范式。
- 诸如Apache Spark、Apache Beam或Dask等框架: 它们允许您编写可以自动在集群中并行化的预处理和分块逻辑。
- 在Spark中,您可能拥有文档的RDD或DataFrame。您将应用一系列
map或flatMap转换来进行解析、清洗和分块。
- 用户自定义函数(UDF)常用于封装复杂的逻辑,如自然语言处理或自定义解析规则。
- 幂等性与高效重新计算: 设计您的转换使其幂等。如果作业失败并重新启动,或者您需要重新处理,它应该生成相同的输出而没有副作用。对于重新计算,利用中间结果的缓存(例如Spark的
cache()或persist()),并设计管道以仅重新处理已更改或受影响的数据。这就是后续讨论的数据变更捕获(CDC)对触发部分更新变得重要的地方。
- 批处理与流处理:
- 批处理: 适用于大量静态语料的初始摄取。作业按计划或按需运行。
- 流处理(例如Spark Streaming、Flink、Kafka Streams): 适用于持续到达的文档。预处理和分块逻辑应用于小批量或单个事件,使得您的RAG系统知识库能够近实时更新。
通过分块保留和使用元数据
简单系统中常常被忽视的一个重要方面是对每个数据块关联元数据的细致管理。
- 基本元数据: 每个数据块至少应存储:
- 数据块自身的唯一ID。
- 源文档的ID。
- 位置信息(例如页码、字节偏移、章节路径)。
- 时间戳(创建时间、源的最后修改时间)。
- 结构元数据(例如“此数据块来自3.2.1节”,“此数据块是表格标题的一部分”)。
- 传播: 确保这些元数据在所有预处理和分块阶段与数据块的文本内容一起正确传播。
- 存储: 将这些元数据直接与数据块文本一起存储(例如,作为嵌入 (embedding)的JSON对象),或在向量 (vector)数据库中与嵌入一起存储,或在通过数据块ID链接的单独元数据存储中。
- 作用: 丰富的元数据对于以下方面非常有用:
- 过滤: 允许对检索进行范围限定(例如,“仅查找上周修改的文档中的信息”)。
- 加权: 对来自特定章节或文档类型的数据块赋予更高的权重 (weight)。
- 引用: 使大语言模型 (LLM)能够准确引用来源。
- 调试: 追溯检索到的数据块的来源。
进阶与专用分块技术
对于处理高度复杂信息需求或数据类型的专家级RAG系统,可能需要更复杂的分块方法。
- 命题式分块: 这涉及将文档分解为单独的事实陈述或命题。每个命题理想情况下代表一个单一的、不可分割的信息片段。
- 方法: 通常需要大语言模型 (LLM)从较大的文本片段生成这些命题。
- 优点: 可以实现高度细粒度的检索,可能提高基于事实查询的准确性。
- 挑战: 在大规模场景下生成高质量的命题计算成本高昂,并可能导致大量小数据块,需要复杂精密的检索和合成策略。
- 分层分块 / 父文档策略: 存储更小、详细的数据块以进行精确检索,但同时也将它们链接到提供更广泛上下文 (context)的较大“父”数据块或摘要。
- 示例: 一个小数据块可能是一个单独的段落。它的父级可以是其所属的整个部分,或该部分的由大语言模型生成的摘要。
- 检索: 检索小数据块以获取细节,然后获取它们的父数据块,以便在生成前为大语言模型提供更多上下文。LangChain的
ParentDocumentRetriever是这种模式的一个示例。
- 处理复杂数据类型:
- 表格: 不要仅仅将表格线性化为文本。考虑专门的表格解析(提取行、列、标题),将其转换为Markdown,甚至嵌入 (embedding)表格结构的表示。一些模型正在训练以直接理解表格数据。
- 图表: 提取图表标题。使用多模态 (multimodal)模型生成图像或图表的文本描述,然后可以将其分块和嵌入。
- 源代码: 代码分块需要理解其语法和结构(函数、类、方法、注释块)。存在专门的代码分块器,它们能遵循这些边界。
大规模分块策略评估
选择和优化您的分块策略是一个迭代过程。您需要方法来评估它们的有效性,特别是在大规模分布式RAG系统的背景下。
- 数据块计数的指标:
- 语义连贯性: 数据块是否代表完整的思想或想法?这可以通过定性评估或使用基于嵌入 (embedding)的指标(例如,数据块内平均句子相似度)进行近似。
- 边界正确性: 句子、段落或逻辑单元被不当分割的频率如何?
- 上下文 (context)保留与特异性: 每个数据块中是否有足够的上下文?数据块是否过长,导致检索噪音?
- 对下游RAG指标的影响: 最终的测试是分块如何影响您的RAG系统的端到端性能。使用以下指标进行评估:
- 检索准确率和召回率(例如,
命中率、MRR)。
- 生成答案的忠实性和相关性(例如,使用RAGAs或其他评估框架)。
- A/B测试: 实施机制对您数据或用户流量的子集进行不同分块策略的A/B测试。监控性能指标(KPI)以确定哪些策略为您的特定使用场景和数据带来了更好的结果。
- 计算成本与延迟: 始终考虑与不同分块策略相关的处理时间、资源消耗和索引延迟。理论上“完美”但对于您的生产环境来说过慢或过于昂贵的分块器是不切实际的。
高效地预处理和分块数据是任何高性能、大规模分布式RAG系统不可或缺但重要的基础。这里讨论的方法为应对这一挑战提供了工具集,但请记住,最适合的方法将取决于您的具体数据特点、系统要求以及您的RAG系统旨在解决的信息检索任务的性质。持续的评估和优化是构建和维护专家级RAG方案的一部分。