RAG工程化 - 解析RAGFlow

  1. 文件解析与清洗: ragflow/rag/flow/parser/parser.py

  2. 文本分块: ragflow/rag/flow/chunker/chunker.py

这两个文件是如何工作的:

1. 文件解析与清洗 (ragflow/rag/flow/parser/parser.py)

这个文件负责将不同格式的原始文件(如PDF, Word, Excel等)解析成统一的、可处理的格式。这个过程可以看作是文件的清洗和结构化提取。

工作原理:

  • Parser 类: 这是核心类,它根据文件类型调用不同的解析方法。它支持多种文件格式,包括 PDF, 表格(Excel, CSV), Word, Markdown, 文本, 图片, 甚至音频。

  • 按文件类型处理:

  • PDF (_pdf方法): 提供了三种解析方式:

  • deepdoc: 一种深度学习模型,用于精确提取文本、表格和图片,并保留其版面布局信息。

  • plain_text: 将PDF转换成纯文本,忽略版面信息。

  • vlm (Vision Language Model): 使用多模态大模型来理解PDF内容。

  • 表格 (_spreadsheet方法): 使用 ExcelParser 将表格文件转换成HTML, JSON或Markdown格式。

  • Word (_word方法): 使用 tika 库来提取Word文档中的文本内容。

  • 图片 (_image方法): 支持两种方式:

  • ocr: 使用光学字符识别技术提取图片中的文字。

  • vlm: 使用多模态大模型来描述图片内容。

  • 音频 (_audio方法): 将音频文件转换成临时文件,然后调用语音转文本模型进行识别。

  • 输出格式: 解析后的内容可以输出为 json (包含文本和位置信息) 或 markdown 等格式,然后传递给下一步的分块模块。

总的来说,parser.py 的作用就是将各种复杂格式的非结构化或半结构化文档,转化成包含文本和必要元数据(如图片、位置信息)的结构化数据,为后续的文本分块做准备。

文本分块 (ragflow/rag/flow/chunker/chunker.py)

当 parser.py 完成文件内容的提取后,chunker.py 负责将这些长文本切分成更小、更易于模型处理的文本块(Chunks)。

工作原理:

  • Chunker 类: 这是分块功能的核心。它定义了多种分块策略,但目前主要实现的是通用的 _general 方法。

  • _general 分块方法:

  • 该方法接收来自 Parser 模块的输出 (text, markdown, html, 或 json)。

  • 它调用 rag.nlp 模块中的 naive_merge 或 naive_merge_with_images 函数来执行分块。

  • 分块过程主要受以下参数控制:

  • chunk_token_size: 每个文本块的目标大小(以token计)。

  • delimiter: 用于切分文本的分隔符,例如换行符 \n。

  • overlapped_percent: 相邻文本块之间的重叠比例,这有助于保持上下文的连续性。

  • 输出: 分块后的结果是一个包含多个文本块(chunk)的列表。每个 chunk 是一个字典,至少包含 "text" 字段,可能还包含图片、位置信息等。

  • 增强功能:

  • 自动生成关键词和问题: 如果配置开启,它还会调用大语言模型(LLM)为每个文本块自动提取关键词 (auto_keywords) 或生成相关问题 (auto_questions),这可以增强后续检索的效果。

总结一下,整个流程是:

  1. Parser 首先介入,像一个“解码器”,把各种格式的文件“翻译”成干净的、程序可读的文本和数据。

  2. 然后 Chunker 像一个“切分器”,把这些长文本按照预设的规则切成大小合适的片段,并能选择性地为这些片段添加额外的元信息(关键词、问题等)。

naive_merge 函数详解 (处理纯文本)

这是最核心的文本分块函数,具体策略如下:

  1. 初始化: 创建一个空的文本块列表 cks,并为第一个块 cks[0] 赋一个空字符串。

  2. 遍历文本片段: 逐一处理从 Parser 模块传入的文本片段(sections)。

  3. 判断片段大小:

  • 小片段直接合并: 如果一个文本片段的Token数量小于设定的 chunk_token_num (块大小),那么它会被直接尝试合并到当前的最后一个文本块中。

  • 大片段需要切分: 如果一个片段本身就超过了 chunk_token_num,则会先用预设的分隔符(delimiter)将其切分成更小的子片段,然后再对这些子片段进行合并处理。默认的分隔符是 \n。;!?,涵盖了换行和中英文的主要句末标点。

  1. 合并逻辑 (add_chunk 函数): 这是策略的关键。当一个新片段(或子片段)需要被添加时,会发生以下情况:
  • 创建新块: 如果当前的最后一个块为空,或者其Token数量已经超过了设定的阈值 chunk_token_num * (1 - overlapped_percent),系统就会创建一个新的文本块。

  • 实现重叠 (Overlap): 在创建新块时,为了保持上下文的连续性,它会从上一个块的尾部取出一部分内容(由 overlapped_percent 控制比例),并将其拼接到新块的开头。

  • 追加到当前块: 如果当前块还有足够空间,新的片段会直接追加到这个块的末尾。

  1. 返回结果: 遍历完所有片段后,函数返回一个由多个文本块组成的列表 cks。

naive_merge_with_images 函数

这个函数是 naive_merge 的一个变体,专门用于处理图文混合的内容。它的策略与前者基本一致,但增加了对图片的处理:

  • 当文本片段被合并时,与这些文本相关联的图片也会被相应地合并。

  • 如果一个文本块是由多个包含图片的片段合并而成的,这些图片会被垂直拼接成一张大图。

总的来说,ragflow 采用的是一种相当实用且灵活的分块方法:

  • 语义边界切分: 它优先使用用户定义的分隔符(默认为句末标点和换行符)来切分文本,这在很大程度上能保证在语义边界上进行切分,避免将一句完整的话从中间断开。

  • 大小控制: 严格控制每个分块的Token数量上限,确保其不会超过模型的处理能力。

  • 上下文保持: 通过块间重叠(Overlap)的机制,有效地保留了上下文信息,使得每个独立的文本块都包含了部分上一块的结尾和下一块的开头,有助于模型更好地理解语境。

  • 图文处理: 能够智能地处理图文混合内容,将相关的文本和图片绑定在同一个块中。

这种策略在保证了分块大小的同时,也尽可能地维持了文本的语义完整性和上下文连续性,是一种在RAG应用中非常常见且有效的分块方法。

Hierarchical_merge

hierarchical_merge 的工作原理:

它采用的是一种基于文档结构的分层分块策略,这天然地创造了父子关系。

  1. 识别结构: 它使用一组复杂的正则表达式 BULLET_PATTERN 来识别文档中的标题层级。
  • 代码体现:

    python

            # rag/nlp/init.py

            BULLET_PATTERN = [[

                r"第[零一二三四五六七八九十百0-9]+(分?编|部分)",

                r"第[零一二三四五六七八九十百0-9]+章",

                r"第[零一二三四五六七八九十百0-9]+节",

                # ... and so on for different languages and formats

            ]]

  • 它能识别出像“第一章”、“Section 1.2”、“(一)”这样的标题,并为它们分配不同的层级。

  1. 构建层级地图: 函数会遍历所有文本片段,根据它们是否匹配上标题模式,为文档的所有内容构建一个层级地图。

  2. 自下而上聚合: 最关键的一步,它会从最细粒度的内容(普通段落,即最低层级)开始,向上查找它所属的父标题。它会将一个段落和它的直接父标题(甚至爷标题)合并成一个逻辑上的块。

. 如何利用这种父子关系进行检索 (Small-to-Large Retrieval策略)

hierarchical_merge 这样的分块方法,是实现一种称为 “Small-to-Large” 或 “父文档检索 (Parent Document Retriever)” 策略的基础。这种策略能极大地提升RAG的准确性。

流程如下:

  1. 分块: 你会创建两种类型的块:
  • 子块 (Child Chunks): 这些是小的、语义精确的文本块,比如独立的段落。这些块非常适合进行向量相似度搜索,因为它们的主题单一、明确。

  • 父块 (Parent Chunks): 这些是大的、包含丰富上下文的块,比如整个章节。它可能包含多个子块。

  1. 检索 (Retrieval):
  • 用户的查询首先与所有子块进行向量相似度匹配。因为子块小而精,所以很容易精确命中与问题最相关的几句话。

  • 你得到了最匹配的那个“子块”。

  1. 追溯 (Expansion):
  • 此时,你不是把这个小小的“子块”直接扔给LLM。因为这个子块可能缺少必要的上下文。

  • 相反,你通过预先建立的父子关系,找到这个“子块”所属的那个完整的“父块”。

  1. 生成 (Generation):
  • 你将这个内容更完整、上下文更丰富的“父块”作为最终的上下文,提供给LLM来生成答案。

为什么这个策略能提升准确性?

它完美结合了两种块的优点:

  • 用“小块”保证检索的精确度:避免了在充满无关信息的大块中稀释语义。

  • 用“大块”保证生成的质量:为LLM提供了充分的上下文,让它能做出更全面、准确的回答。

检索阶段 (The Retrieval Phase)

当用户发起一个查询时,神奇的事情发生了。

步骤 1: 在“子块”中进行精确搜索

  • 用户的查询(例如:“如何更换电池?”)被转换成一个查询向量。

  • 系统在“子块”的向量索引中执行相似度搜索。

  • 因为“子块”都是些精准的段落,系统很可能会以高分命中 child_chunk_001,因为它的内容与查询直接相关。

步骤 2: 追溯到“父块”以扩展上下文

  • 系统获取到排名靠前的“子块”列表,比如我们命中了 child_chunk_001。

  • 系统并不会直接将 "要更换电池..." 这段孤零零的话发给LLM。

  • 相反,它会读取这个子块的元数据,找到 parent_chunk_id: "parent_chunk_ABC"。

  • 然后,系统使用这个ID去文档库中取出完整的“父块”。

步骤 3: 将完整的上下文提供给LLM

  • 最终,被放入Prompt、作为上下文提供给LLM的,是那个内容丰富的“父块”:

"## 3.2 电池维护\n\n警告:在进行任何维护前,请断开电源。\n\n要更换电池,首先请确保设备已关机..."

  • LLM现在不仅知道具体步骤,还看到了标题(知道这是关于电池维护的),看到了重要的警告信息。这使得它能够生成一个更安全、更全面、更准确的答案。

总结:hierarchical_merge 的核心价值

hierarchical_merge 通过其对文档层级结构的理解,完美地赋能了“Small-to-Large Retrieval”策略。它解决了传统RAG的一个核心矛盾:

  • 用于检索的块(Chunks for Retrieval) 应该小而精,以保证搜索精度。

  • 用于生成的块(Chunks for Generation) 应该大而全,以保证答案质量。