dify实现分析-rag-文档切割的实现

1.概述

本文介绍dify中文档切割的几种方式的详细实现,并对这几种方式的实现和关系进行了分析和比较。文本分割是,RAG框架实现中比较关键的一环,分割质量的好坏直接影响RAG回答的效果。dify提供了多种分割方式,可以根据不同的场景来进行适配。

2.函数调用关系

函数调用关系如下:

IndexingRunner.run
IndexingRunner._transform
ParagraphIndexProcessor.transform
QAIndexProcessor.transform
TextSplitter.split_documents
TextSplitter.create_documents
TextSplitter.split_text
RecursiveCharacterTextSplitter._split_text
CharacterTextSplitter.split_text
TokenTextSplitter.split_text
EnhanceRecursiveCharacterTextSplitter.split_text
FixedRecursiveCharacterTextSplitter.split_text

3.递归字符文本分割:RecursiveCharacterTextSplitter

3.1. _split_text函数

​ 该函数的功能是:递归地按照分隔符列表对文本进行分割,确保分割后的文本块大小符合要求。

​ 该函数的实现逻辑如下:

  1. 遍历分隔符列表,若分隔符列表为空,分隔符遍历被设置为"",也就是空分隔符。若分隔符列表不为空,则找到第一个分隔符,并使用第一个分隔符在文本中查找,若找到把第一个分隔符作为本次的分隔符,并把分隔符列表中剩余的分隔符保存到new_separators这个变量中。
  2. 使用在第一步选定的分隔符对文本进行分割,并根据是否要保留分隔符(self._keep_separator变量的值)来对分割后的文本块进行处理。最后得到分割好的文本块列表。
  3. 遍历分割好的文本块列表,并对每个文本块执行以下操作:

​ a. 若当前分块的长度小于self._chunk_size,则把当前分块作为好的分块保存到:_good_splits列表中。

​ b. 若当前分块的长度大于self._chunk_size,则处理_good_splits中的分块,对列表中较小的分块进行合并,合并的操作是在_merge_splits函数中完成。合并完成后的分块大小就是比较合适的分块了。

​ c. 若当前分块的长度大于self._chunk_size,则递归调用_split_text函数继续处理该分块。

  1. 遍历完成后,若_good_splits列表中不为空,则继续对列表中较小的分块进行合并的操作。也就是3.b这个步骤的操作,也就是调用_merge_splits函数来完成合并。
  2. 最后返回
3.2 什么样的分块需要合并

通过以上分析可知,较小的分块太小了,就会变得很多,此时不太容易维护。所以,需要对较小的分块进行合并,合并的操作是在_merge_splits函数中完成的。我们来看一下_merge_splits函数的实现逻辑:

  1. 获取分隔符长度

  2. 遍历 splits 中的每个文本分块,然后进行以下处理:

    1)获取当前小文本块的长度 _len

    2)检查将当前小文本块添加到 current_doc 后是否会超过 self._chunk_size。如果超过,检查 total 是否已经超过 self._chunk_size,如果是则记录警告信息。

    ​ a. 调用 self._join_docs 方法将 current_doc 合并成一个文本块,并添加到 docs 列表中。

    ​ b. 通过循环移除 current_doc 中的前面的小文本块,直到满足重叠 self._chunk_overlap 的要求。

  3. 将当前小文本块添加到 current_doc 中,并更新 totalindex

  4. 处理最后一个文档

    1)调用 self._join_docs 方法将最后一个 current_doc 合并成一个文本块。

    2)如果合并后的文本块不为空,则添加到 docs 列表中。

  5. 返回最终的合并后的文本块列表 docs

def _merge_splits(self, splits: Iterable[str], separator: str, lengths: list[int]) -> list[str]:
        # We now want to combine these smaller pieces into medium size
        # chunks to send to the LLM.
        # 获取分隔符长度
        separator_len = self._length_function(separator)

        docs = []
        current_doc: list[str] = []
        total = 0
        index = 0
        for d in splits:
            # 获取当前分片的长度
            _len = lengths[index]
            # 检查是否超过块大小限制
            if total + _len + (separator_len if len(current_doc) > 0 else 0) > self._chunk_size:
                # 处理超过大小限制的情况
                if total > self._chunk_size:
                    logger.warning(
                        f"Created a chunk of size {total}, which is longer than the specified {self._chunk_size}"
                    )
                # 处理当前累积的文档
                if len(current_doc) > 0:
                    doc = self._join_docs(current_doc, separator)
                    if doc is not None:
                        docs.append(doc)
                    # Keep on popping if:
                    # - we have a larger chunk than in the chunk overlap
                    # - or if we still have any chunks and the length is long
                    # 处理重叠部分:当有一个巨大的check比chunk的重叠部分还大,或则
                    # 任然还有一些chunk的长度太长
                    while total > self._chunk_overlap or (
                        total + _len + (separator_len if len(current_doc) > 0 else 0) > self._chunk_size and total > 0
                    ):
                        #  移除前面的分片直到满足重叠要求
                        total -= self._length_function(current_doc[0]) + (separator_len if len(current_doc) > 1 else 0)
                        current_doc = current_doc[1:]
            # 在遍历结束后,确保所有剩余的文档都被正确地合并并添加到结果列表中。
            current_doc.append(d)
            total += _len + (separator_len if len(current_doc) > 1 else 0)
            index += 1
        # 处理最后一个文档
        doc = self._join_docs(current_doc, separator)
        if doc is not None:
            docs.append(doc)
        return docs
4.CharacterTextSplitter分割

该类提供了文本分割的函数,该函数和其他类不同的是:它会对较小的分块进行合并,但不对长的分块再继续细分。该函数的代码如下:

    def split_text(self, text: str) -> list[str]:
        """Split incoming text and return chunks."""
        # First we naively split the large input into a bunch of smaller ones.
        # 1. 使用正则表达式初步分割文本
        splits = _split_text_with_regex(text, self._separator, self._keep_separator)
        # 确定实际使用的分隔符
        _separator = "" if self._keep_separator else self._separator
        _good_splits_lengths = []  # cache the lengths of the splits
        # 缓存分割后的文本长度
        for split in splits:
            _good_splits_lengths.append(self._length_function(split))
        # 合并分割结果    
        return self._merge_splits(splits, _separator, _good_splits_lengths)
5.FixedRecursiveCharacterTextSplitter固定递归分割

该类的分块实现和前面的实现差不多,只是在选择分隔符上略有不同。该类开始使用的是固定分割符。

    def split_text(self, text: str) -> list[str]:
        """Split incoming text and return chunks."""
        # 使用固定分隔符切割文本
        if self._fixed_separator:
            chunks = text.split(self._fixed_separator)
        else:
            chunks = [text]

        final_chunks = []
        # 遍历所有分块,若分块长度大于设定的长度`_chunk_size`,则递归切割
        for chunk in chunks:
            if self._length_function(chunk) > self._chunk_size:
                final_chunks.extend(self.recursive_split_text(chunk))
            else:
                final_chunks.append(chunk)

        return final_chunks
recursive_split_text函数

该函数的实现逻辑如下:

1.确定合适的分割符

(1) 默认选择最后一个分隔符,作为当前分割符,保存到变量separator 中。

(2) 遍历所有分隔符,若当前分隔符在文本中出现过,则跳过后面的分隔符,选择该分隔符保存到变量separator 中,跳出遍历循环。

2.使用选择的分割符切割文本

(1) 如果 separator 不为空,则使用该分隔符将输入的文本 text 分割成多个块,并将结果存储在 splits 列表中。

(2) 若 separator 为空,则将输入的文本 text 转换为字符列表,并将结果存储在 splits 列表中。

3.遍历所有切割后的分块,处理这些分块

(1) 使用 self._length_function 方法计算当前分块 的长度。

(2) 若当前分块长度小于设定的长度_chunk_size,则将其加入_good_splits中。表示这是一个好的分块。

(3) 如果当前分块长度大于或等于 _chunk_size

​ a. 处理存量的_good_splits小分块的长度,对这些小分块进行判断是否需要合并,若需要合并则进行合并。

​ b. 递归调用自己,对该分块进行递归切割

4.处理最后一部分小的分块(也就是最后一部分_good_splits的内容):对小的分块进行合并判断,对需要合并的分块进行合并。

def recursive_split_text(self, text: str) -> list[str]:
        """Split incoming text and return chunks."""
        final_chunks = []
        # Get appropriate separator to use
        # 默认选择最后一个分隔符
        separator = self._separators[-1]
        # 遍历所有分隔符,若当前分隔符在文本中出现过,则跳过后面的分隔符,选择该分隔符。
        for _s in self._separators:
            if _s == "":
                separator = _s
                break
            if _s in text:
                separator = _s
                break
        # Now that we have the separator, split the text
        # 使用选择的分隔符切割文本
        if separator:
            splits = text.split(separator)
        else:
            splits = list(text)
        # Now go merging things, recursively splitting longer texts.
        # 递归切割长文本
        _good_splits = []
        _good_splits_lengths = []  # cache the lengths of the splits
        # 遍历所有切割后的分块
        for s in splits:
            s_len = self._length_function(s)
            # 若当前分块长度小于设定的长度`_chunk_size`,则将其加入_good_splits中
            if s_len < self._chunk_size:
                _good_splits.append(s)
                _good_splits_lengths.append(s_len)
            else:
                # 处理现有_good_splits中的小分块,对其中太小的分块进行合并(_merge_splits)
                if _good_splits:
                    merged_text = self._merge_splits(_good_splits, separator, _good_splits_lengths)
                    final_chunks.extend(merged_text)
                    _good_splits = []
                    _good_splits_lengths = []
                # 若分片长度大于设定的长度`_chunk_size`,则递归切割
                other_info = self.recursive_split_text(s)
                final_chunks.extend(other_info)
        # 处理_good_splits中最后一部分分块,对其中太小的分块进行合并
        if _good_splits:
            merged_text = self._merge_splits(_good_splits, separator, _good_splits_lengths)
            final_chunks.extend(merged_text)
        return final_chunks
EnhanceRecursiveCharacterTextSplitter:增强递归字符分割

在该类中,其他的逻辑和父类RecursiveCharacterTextSplitter的相同。只不过,计算分片长度的函数的计算方式不同,该类中会通过嵌入模型先把文本分块转换成token,然后计算token的长度作为分块的长度。

        def _token_encoder(text: str) -> int:
            if not text:
                return 0

            if embedding_model_instance:
                return embedding_model_instance.get_text_embedding_num_tokens(texts=[text])
            else:
                return GPT2Tokenizer.get_num_tokens(text)

在构建类实体时,把该函数作为计算分块长度的函数:

        return cls(length_function=_token_encoder, **kwargs)

几种文本分割方式的区别

RecursiveCharacterTextSplitter

这种方式是一种基础的文本分割方式,该方式直接使用递归策略,从分隔符列表中依次尝试,并对每个文本块都进行分隔符检查的选择。

这种方式的特点是:通过递归对文本进行分割,处理开销较大,是一种基础的文本分割方式。

EnhanceRecursiveCharacterTextSplitter

这种方式继承了基础分割器的功能,只是增加了自定义token计数支持,提供模型特定的分词功能,并支持GPT2分词器作为后备处理器。

FixedRecursiveCharacterTextSplitter

这种方式把分割过程分成2个阶段:首先使用固定分隔符进行快速分割,然后分割过程中仅对超长块再进行递归处理。这种方式性能较优。

主要区别

特性基础分割器增强分割器固定分割器
分割策略纯递归纯递归+自定义计数固定+递归
token计算基础模型特定模型特定
性能较低中等较高
适用场景通用文本特定模型文本结构化文本
内存占用

总结

本文介绍了dify中文本分割的几种方式。并对这几种方式的实现进行了分析和比较。文本分割是,RAG框架实现中比较关键的一环,分割质量的好坏直接影响RAG回答的效果。dify中考虑到了文本的长度和语义的连贯性,会对小的分块进行合并,而且又会保证了文本有一定的重叠,保证了语义的连贯性。

另外,在其他的RAG框架中也可以使用tiktoken包来对文本块进行token化,然后再对token进行分割。这种方式相对比较简介,在后续文章中会继续分析这种实现方式的原理。

<think>好的,用户遇到了Dify添加文件建立知识库失败的问题,我需要帮他们解决。首先,我应该考虑可能的原因,并逐步分析。常见的失败原因可能包括文件格式不支持、文件大小限制、解析错误、网络问题、权限不足、系统兼容性或软件本身的Bug。 首先,文件格式方面,Dify可能支持PDF、DOCX、TXT等常见格式,但用户可能上传了不支持的格式,比如图片或EPUB。需要确认用户使用的文件类型,并提醒他们检查。 接下来是文件大小限制。如果文件太大,超过系统设定的上限,上传就会失败。应该建议用户查看官方文档中的限制,并尝试压缩或分割文件。 然后是文件内容解析问题。特别是PDF文件,有时提取文本会遇到格式错误或加密问题。可能需要用户检查文件是否加密,或者尝试将PDF转换为TXT再上传。 网络连接不稳定也可能导致上传中断,建议用户检查网络状况,或者换个时间重试。权限问题方面,用户可能没有写入权限,尤其是在自托管的情况下,需要检查目录权限。 系统兼容性方面,某些特殊字符或长文件名可能导致问题,建议用户重命名文件,去掉特殊字符,缩短名称长度。另外,软件版本过旧也可能存在Bug,升级到最新版本可能解决。 如果以上步骤都尝试过仍然失败,就需要联系Dify的技术支持,提供错误日志和详细描述,以便进一步排查。可能需要用户查看浏览器控制台或服务器日志,寻找具体的错误信息。 在组织回答时,要结构清晰,分点列出可能的原因和对应的解决方法,使用用户容易理解的语言,避免技术术语过多。同时,确保每个步骤都简单可行,用户可以自行操作。最后,提醒用户记录错误信息,方便后续支持人员协助解决。</think>当你在Dify中添加文件建立知识库失败时,可能是由以下原因导致。请按照以下步骤逐步排查: --- ### **一、常见失败原因分析** 1. **文件格式不支持** - ✅ 检查文件类型:Dify通常支持 `PDF`、`DOCX`、`TXT`、`Markdown` 等格式,但某些特殊格式(如加密PDF、EPUB、图片等)可能无法解析。 - ✅ 解决方法:将文件转换为支持的格式(例如PDF转DOCX)。 2. **文件大小超出限制** - ✅ 检查文件体积:Dify可能有默认文件大小限制(如100MB),超过限制会导致上传失败。 - ✅ 解决方法:分割大文件为多个小文件,或压缩文件内容。 3. **文件内容解析错误** - ✅ 特殊字符或编码问题:如果文件包含非常规字符(如乱码、特殊符号),可能导致解析失败。 - ✅ 解决方法:用文本编辑器打开文件,检查内容是否完整,删除异常字符。 4. **网络或服务器问题** - ✅ 上传中断:网络不稳定或服务器响应超时。 - ✅ 解决方法:尝试更换网络环境,或稍后重试。 --- ### **二、进阶排查步骤** #### 1. **检查文件完整性** - 对于PDF文件: - 确保未加密或受密码保护。 - 使用工具(如Adobe Acrobat)验证是否能正常提取文本- 对于DOCX文件: - 用Microsoft Word打开,检查是否存在损坏提示。 #### 2. **查看错误日志** - 如果使用**自托管版Dify**,检查服务器日志(通常在 `docker logs` 或应用日志目录)。 - 如果是**云服务版**,留意浏览器控制台报错(按 `F12` 打开开发者工具,查看Console或Network标签)。 #### 3. **权限问题** - 自托管环境下,确保存储目录(如 `storage` 文件夹)有写入权限: ```bash chmod -R 755 /path/to/dify/storage ``` --- ### **三、临时解决方案** 1. **分批次上传** 如果文件数量过多,尝试分批上传(每次5-10个文件)。 2. **简化文件名** 避免文件名包含特殊符号(如`#`、`空格`、`中文`),建议使用英文短名称。 3. **手动提取文本** 对于复杂格式文件(如扫描版PDF),手动复制文本到TXT文件中再上传。 --- ### **四、联系技术支持** 如果以上方法无效,请提供以下信息联系Dify官方: 1. 使用的Dify版本(社区版/企业版)。 2. 错误截图或日志内容。 3. 文件样本(如不涉密)。 --- 通过逐步排查,通常可以定位到具体原因。建议优先检查文件格式、大小和内容完整性,这是最常见的失败根源。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值