导航菜单

  • 1.vector
  • 2.milvus
  • 3.pymilvus
  • 4.rag
  • 5.rag_measure
  • 7.search
  • ragflow
  • heapq
  • HNSW
  • cosine_similarity
  • math
  • typing
  • etcd
  • minio
  • collections
  • jieba
  • random
  • beautifulsoup4
  • chromadb
  • sentence_transformers
  • numpy
  • lxml
  • openpyxl
  • PyMuPDF
  • python-docx
  • requests
  • python-pptx
  • text_splitter
  • all-MiniLM-L6-v2
  • openai
  • llm
  • BPETokenizer
  • Flask
  • RAGAS
  • BagofWords
  • langchain
  • Pydantic
  • abc
  • faiss
  • MMR
  • scikit-learn
  • 1. 什么是文本分割器?
  • 2. 安装必要的库
    • 2.1 Windows 系统安装
    • 2.2 macOS 系统安装
    • 2.3 验证安装
  • 3. 前置知识补充
    • 3.1 什么是 Chunk Size 和 Chunk Overlap?
    • 3.2 分隔符(Separators)的作用
  • 4. 文本分割的三种主要策略
    • 4.1 基于长度的分割
      • 4.1.2 字符分割器(CharacterTextSplitter)
      • 4.1.2 基于 Token 的分割
    • 4.2 基于文本结构的分割
      • 4.2.1. 递归字符文本分割器(推荐使用)
    • 4.3 基于文档结构的分割
      • 4.3.1 Split markdown
      • 4.3.1 Split code
  • 7. 处理无词边界语言
    • 7.1 问题说明
    • 7.2 解决方案:自定义分隔符
    • 7.3 多语言混合文本处理
  • 8. 常见问题与解决方案
    • 8.1 问题 1:分割后的块大小不一致
    • 8.2 问题 2:中文文本分割效果不好
    • 8.3 问题 3:重叠部分导致重复内容
    • 8.4 问题 4:分割速度慢
    • 8.5 问题 5:Token 计数不准确
  • 9.RecursiveCharacterTextSplitter
    • 9.1.简单拆分
      • 9.1.1. main.py
      • 9.1.2. splitters.py
    • 9.2.递归分隔
      • 9.2.1. main.py
      • 9.2.2. splitters.py
    • 9.3.合并小块
      • 9.3.1. main.py
      • 9.3.2. splitters.py
    • 9.4.重叠字符
      • 9.4.1. main.py
      • 9.4.2. splitters.py
    • 9.5 执行过程
      • 9.5.1 第一步:按 \n\n 分割
      • 9.5.2 第二步:处理第一个大块 "段落1\n段落2\n段落3\n段落4"
      • 9.5.3 第三步:_merge_splits 合并第一个大块的小片段
      • 9.5.4 第四步:处理第二个大块 "段落5\n段落6\n段落7\n段落8"
      • 9.5.5 最终结果
      • 9.5.6 要点总结

1. 什么是文本分割器? #

  • text-splitters

文本分割器(Text Splitter)是一种将大型文档拆分为较小、可单独检索的文本片段(chunks)的工具。在自然语言处理和机器学习应用中,文本分割器非常重要,因为:

为什么需要文本分割?

  1. 模型上下文窗口限制:大多数语言模型(如 GPT、BERT 等)都有输入长度限制。例如,某些模型只能处理 512 个 token,而另一些可能支持 4096 或更多。当文档超过这个限制时,必须将其分割成更小的片段。

  2. 提高检索效率:在 RAG(检索增强生成)系统中,将长文档分割成小块可以提高检索的精确度。用户查询时,系统只需要检索相关的片段,而不是整个文档。

  3. 保持语义完整性:合理的分割策略可以确保每个文本块包含完整的语义单元(如段落、句子),避免在句子中间切断,从而保持上下文信息的完整性。

文本分割的核心挑战:

  • 如何在不破坏语义的情况下分割文本?
  • 如何确定合适的块大小?
  • 如何处理不同语言的文本(特别是没有词边界的语言)?

2. 安装必要的库 #

在开始之前,我们需要安装 LangChain 的文本分割器库。LangChain 提供了多种文本分割器实现,是最常用的工具之一。

2.1 Windows 系统安装 #

在 Windows PowerShell 或命令提示符中执行:

# 升级 pip 到最新版本(推荐)
python -m pip install --upgrade pip

# 安装 langchain-text-splitters 库
python -m pip install langchain-text-splitters

注意事项:

  • 如果系统中有多个 Python 版本,可能需要使用 python3 代替 python
  • 如果遇到权限问题,可以添加 --user 参数:python -m pip install --user langchain-text-splitters

2.2 macOS 系统安装 #

在 macOS 终端中执行:

# 升级 pip 到最新版本(推荐)
python3 -m pip install --upgrade pip

# 安装 langchain-text-splitters 库
python3 -m pip install langchain-text-splitters

注意事项:

  • macOS 系统通常需要使用 python3 命令
  • 如果提示找不到 python3,可能需要先安装 Python

2.3 验证安装 #

安装完成后,可以通过以下代码验证是否安装成功:

# 尝试导入文本分割器类
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 如果没有报错,说明安装成功
print("langchain-text-splitters 安装成功!")

3. 前置知识补充 #

3.1 什么是 Chunk Size 和 Chunk Overlap? #

Chunk Size(块大小):

  • 每个文本块的最大长度(可以是字符数或 token 数)
  • 需要根据模型限制和文档类型来设置
  • 太小会丢失上下文,太大会超出模型限制

Chunk Overlap(块重叠):

  • 相邻文本块之间的重叠部分
  • 用于保持上下文连贯性
  • 例如:如果块大小是 100,重叠是 20,那么第一个块是字符 1-100,第二个块是字符 81-180

为什么需要重叠?

  • 避免在句子或段落中间切断
  • 确保重要信息不会因为分割而丢失
  • 提高检索时找到相关内容的概率

3.2 分隔符(Separators)的作用 #

分隔符是用于分割文本的字符或字符串。常见的分隔符包括:

  • \n\n:段落分隔(两个换行符)
  • \n:行分隔(单个换行符)
  • :空格(单词分隔)
  • .:句号(句子分隔)
  • ,:逗号

分隔符的选择直接影响分割质量。通常按照从粗粒度到细粒度的顺序使用分隔符。

4. 文本分割的三种主要策略 #

根据不同的应用场景和文档类型,文本分割可以采用三种主要策略。每种策略都有其独特的优势和适用场景。

4.1 基于长度的分割 #

核心思想: 严格按照指定的长度(字符数或 token 数)进行分割,确保每个块大小一致。

工作原理:

  • 按固定长度切分文本
  • 不考虑语义边界
  • 可以基于字符数或 token 数

优势:

  • 实现简单直接
  • 块大小一致,便于管理
  • 可以精确控制块大小

适用场景:

  • 对块大小有严格要求
  • 不需要考虑语义边界
  • 处理结构化数据

4.1.2 字符分割器(CharacterTextSplitter) #

字符分割器按照字符数进行分割,是最简单的分割方法。

# 导入字符文本分割器类
from langchain_text_splitters import CharacterTextSplitter

# 创建字符分割器实例,设置每个块最大长度为100个字符、不重叠、使用空字符串作为分隔符(即按字符切分)
text_splitter = CharacterTextSplitter(
    chunk_size=100, # 每个块最大长度为100个字符
    chunk_overlap=0, # 块之间不重叠
    separator="" # 使用空字符串作为分隔符(即按字符切分)
)

# 构造一个需要分割的长文本,这里由100个'1'、100个'2'和100个'3'拼接组成
document = f"""{"1"*100}{"2"*100}{"3"*100}"""

# 使用分割器的split_text方法,将原始文本切分为若干个子块
texts = text_splitter.split_text(document)

# 打印原始文本的长度(字符数)
print(f"原文长度:{len(document)} 字符")
# 打印分割后块的数量
print(f"分割为 {len(texts)} 个块")
# 打印前3个分块的内容(如果存在多于3个块)
print("\n前 3 个块:")
for i, text in enumerate(texts[:3], 1):
    # 打印每个块的编号、该块的字符长度和内容
    print(f"\n块 {i}({len(text)} 字符):{repr(text)}")

4.1.2 基于 Token 的分割 #

对于语言模型应用,按 token 数分割通常更准确,因为模型限制通常以 token 数表示。

# 从 langchain_text_splitters 导入 RecursiveCharacterTextSplitter,用于递归字符/Token分割
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 使用 tiktoken 编码器创建递归字符分割器
# 这里的编码名称为 "cl100k_base",适用于 GPT-4
# chunk_size 设置为 100,表示每块最多 100 个 token
# chunk_overlap 为 0,表示不同块之间没有重叠
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
    encoding_name="cl100k_base",  # 编码名称(GPT-4 使用的编码)
    chunk_size=100,               # 每个块最多 100 个 token
    chunk_overlap=0               # 块之间不重叠
)

# 准备需要分割的长文本,由 100 个 "hello "、100 个 "world " 和 100 个 "python " 组成
document = f"""{"hello " * 100}{"world " * 100}{"python " * 100}"""

# 调用分割器的 split_text 方法,将长文本按 token 拆分成多个块
texts = text_splitter.split_text(document)

# 打印分割后的块数量
print(f"分割为 {len(texts)} 个块")
# 遍历分割后的每一个块,并打印块编号和具体内容
for i, text in enumerate(texts, 1):
    print(f"\n块 {i}:{repr(text)}")

注意事项:

  • 使用 from_tiktoken_encoder 需要安装 tiktoken 库:pip install tiktoken
  • cl100k_base 是 GPT-4 使用的编码,其他模型可能使用不同的编码
  • Token 数量通常小于字符数,因为一个 token 可能包含多个字符

4.2 基于文本结构的分割 #

核心思想: 利用文本的天然层级结构(段落、句子、词语)进行分割,尽可能保持语义单元的完整性。

工作原理:

  1. 首先尝试按段落分割(使用 \n\n)
  2. 如果段落仍然太大,按句子分割(使用 \n 或 .)
  3. 如果句子仍然太大,按词语分割(使用空格)
  4. 最后才按字符分割

优势:

  • 保持语义完整性
  • 分割结果更自然
  • 适合大多数通用文本

适用场景:

  • 普通文档、文章、报告
  • 需要保持上下文连贯性的场景
  • 大多数 RAG 应用

4.2.1. 递归字符文本分割器(推荐使用) #

RecursiveCharacterTextSplitter 是 LangChain 中最常用的文本分割器,它实现了基于文本结构的分割策略。对于大多数应用场景,这是推荐的默认选择。

为什么推荐?

  • 在保持上下文完整性和管理块大小之间取得了良好的平衡
  • 开箱即用,默认配置就能很好地工作
  • 只有在需要针对特定应用进行微调时才需要调整参数

下面的示例演示如何使用 RecursiveCharacterTextSplitter 分割文本。

# 从 langchain_text_splitters 模块导入递归字符文本分割器类
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 创建递归字符文本分割器对象,指定参数
# chunk_size 表示每块最大允许的字符数为 100
# chunk_overlap 表示块与块之间没有重叠(重叠字符数为 0)
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=100,
    chunk_overlap=0
)

# 构造待分割的文本,包含多段各自为 99 或 100 个相同字符以及换行符
document = f"""{"1"*100}\n{"2"*99}\n\n{"3"*99}\n{"4"*99}"""

# 使用文本分割器的 split_text 方法将 document 分割为多个字符串块
texts = text_splitter.split_text(document)

# 打印一共分割出的块数
print(f"共分割为 {len(texts)} 个块:")

# 枚举输出每个块的序号及块内容(使用 repr 格式便于查看特殊字符)
for i, text in enumerate(texts, 1):
    print(i, repr(text))

运行说明:

  • 保存代码为 splitter_demo.py
  • 在终端运行:python splitter_demo.py(Windows)或 python3 splitter_demo.py(macOS)
  • 查看分割结果

RecursiveCharacterTextSplitter 的主要参数:

chunk_size(块大小):

  • 每个块的最大大小
  • 大小由 length_function 决定(默认是字符数)
  • 建议值:200-1000 字符,取决于你的模型限制

chunk_overlap(块重叠):

  • 相邻块之间的重叠大小
  • 有助于在上下文被分割到不同块时减轻信息丢失
  • 建议值:chunk_size 的 10-20%

length_function(长度函数):

  • 用于确定块大小的函数
  • 默认是 len(字符数)
  • 也可以使用 token 计数函数

is_separator_regex(分隔符是否为正则表达式):

  • 默认为 False
  • 如果设置为 True,分隔符列表会被解释为正则表达式

separators(分隔符列表):

  • 默认是 ["\n\n", "\n", " ", ""]
  • 按照从粗粒度到细粒度的顺序使用
  • 可以自定义分隔符列表

4.3 基于文档结构的分割 #

  • Text splitters

核心思想: 利用文档的格式结构(如 Markdown 标题、HTML 标签、JSON 对象)进行分割。

工作原理:

  • 识别文档的格式标记(如 # 标题、<div> 标签)
  • 按照这些标记进行分割
  • 保持文档的逻辑结构

优势:

  • 保留文档的逻辑结构
  • 每个块内保持上下文
  • 对结构化文档效果更好

适用场景:

  • Markdown 文档
  • HTML 网页
  • JSON 数据
  • 代码文件

4.3.1 Split markdown #

  • Split markdown

在实际工程中,许多文档(如 Markdown、HTML、代码文件)都具有明显的结构性。针对这类结构化文本,直接用通用分割器可能导致优雅的段落、标题等被切割,影响后续检索和上下文理解。因此,LangChain 提供了专门的结构化文本分割器(如 MarkdownHeaderTextSplitter),能够基于文档的结构标签有层次地拆分文本。

以 Markdown 为例,MarkdownHeaderTextSplitter 支持根据不同级别的标题(如 #、##、### 等)对文本进行递归分割,每个分割块能够保持所属标题信息,便于后续内容聚合、检索与问答等应用。

下面的例子展示了如何使用 MarkdownHeaderTextSplitter 按 Markdown 结构拆分文档:

  • 自定义需要拆分的标题等级(如一级标题、二级标题、三级标题)
  • 拆分后,每个文本块可以关联上层的标题,保持了文档的层级组织
  • 便于后续实现“只检索特定章节内容”以及“溯源原始结构”
from langchain_text_splitters import MarkdownHeaderTextSplitter
markdown_document = f"""# Foo
Hi this is Bob

## Bar

Hi this is Alice

### Boo 

Hi this is Lance"""   

headers_to_split_on = [
    ("#", "Header 1"),
    ("##", "Header 2"),
    ("###", "Header 3"),
]

markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on,strip_headers=False)
md_header_splits = markdown_splitter.split_text(markdown_document)
for i, text in enumerate(md_header_splits, 1): 
    print(i, repr(text.page_content))

4.3.1 Split code #

  • Splitting code

对于结构化代码(如 Python、JavaScript、HTML、JSON 等),直接基于字符数或简单的分隔符进行切分往往会破坏代码的结构,造成函数、类、逻辑块等被断裂。LangChain 针对不同的主流编程语言提供了结构感知的分割器(如 RecursiveCharacterTextSplitter.from_language),能够更好地按照语言语法、结构边界(如函数、类、注释分隔)合理拆分代码块。

这种做法的好处包括:

  • 每个分割块尽量保持语法和语义完整,便于后续实现代码段检索、问答和溯源定位。
  • 可自定义 chunk 大小,自动处理代码块重叠,避免关键上下文信息缺失。
  • 支持多种常见编程语言(如 Python、JavaScript、Java、C++、Go、HTML、JSON 等),无需手动实现分割规则。

实际应用中,如果要处理大段代码文本,可以优先选择结构化的分割器,提升检索和问答质量。同时,对于数据格式(如 Markdown、JSON 等),建议结合专用分割器和通用分割器,满足不同应用场景的需求。

# 从langchain_text_splitters模块导入Language和RecursiveCharacterTextSplitter
from langchain_text_splitters import (
    Language,
    RecursiveCharacterTextSplitter,
)

# 定义一个包含Python代码的多行字符串
PYTHON_CODE = """
def hello_world():
    print("Hello, World!")

# Call the function
hello_world()
"""

# 创建一个适用于Python语言的递归字符文本分割器,设置分块大小为50,无重叠
python_splitter = RecursiveCharacterTextSplitter.from_language(
    language=Language.PYTHON, chunk_size=50, chunk_overlap=0
)

# 使用分割器将Python代码分割为文档
python_docs = python_splitter.create_documents([PYTHON_CODE])

# 输出分割得到的文档对象
for i, doc in enumerate(python_docs, 1):
    print(i, doc.page_content)

7. 处理无词边界语言 #

某些语言(如中文、日文、泰文)没有明显的词边界,使用默认分隔符可能导致单词被拆分。本节介绍如何处理这些语言。

7.1 问题说明 #

什么是无词边界语言?

  • 中文、日文、泰文等语言在书写时词与词之间没有空格
  • 例如:"我喜欢编程" 中,"我"、"喜欢"、"编程" 之间没有分隔符
  • 使用默认分隔符 ["\n\n", "\n", " ", ""] 可能在不合适的位置分割

为什么需要特殊处理?

  • 默认分隔符主要针对英文设计
  • 对于中文等语言,需要添加额外的标点符号作为分隔符
  • 这样可以更好地保持语义完整性

7.2 解决方案:自定义分隔符 #

下面的代码演示如何为中文文本添加合适的分隔符。

# 导入递归字符文本分割器类
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 创建递归字符文本分割器对象,专为中文文本优化
text_splitter = RecursiveCharacterTextSplitter(
    # 指定用于分割的分隔符列表,包含常见中英文标点
    separators=[
        "\n\n",    # 两个换行符,分隔段落
        "\n",      # 单个换行符,分隔行
        " ",       # 空格,分隔英文单词
        ".",       # 英文句号
        "。",      # 中文句号
        ",",      # 中文逗号(全角)
        "、",      # 中文顿号
        "\u200b",  # 零宽空格(部分亚洲语种分词用)
        "\uff0c",  # Unicode 全角逗号
        "\uff0e",  # Unicode 全角句号
        "\u3002",  # Unicode 中文句号
        "",        # 最后按字符强制分割
    ],
    # 设置每个块的最大字符数为 100
    chunk_size=100,
    # 设置相邻块之间的重叠字符数为 20
    chunk_overlap=20
)

# 构造待分割的中文文本示例
chinese_text = """
人工智能是计算机科学的一个分支。它试图理解智能的实质,并生产出一种新的能以人类智能相似的方式做出反应的智能机器。

机器学习是人工智能的一个子领域。它使计算机能够在没有明确编程的情况下学习和改进。

深度学习是机器学习的一个子集。它使用神经网络来模拟人脑的工作方式。
"""

# 使用分割器的 split_text 方法对中文文本进行分块
texts = text_splitter.split_text(chinese_text)

# 打印原文字符总长度
print(f"原文长度:{len(chinese_text)} 字符")
# 打印分割后得到的块数
print(f"分割为 {len(texts)} 个块\n")

# 遍历每个分块,依次打印其编号、长度及内容
for i, text in enumerate(texts, 1):
    print(f"块 {i}({len(text)} 字符):")
    print(text)
    # 打印分隔线便于区分
    print("-" * 50)

运行说明:

  • 运行后会看到中文文本被按照句号、逗号等标点符号合理分割
  • 可以尝试不同的分隔符组合,找到最适合你文本的分割方式

7.3 多语言混合文本处理 #

如果你的文档包含多种语言(如中英文混合),可以使用更全面的分隔符列表。

# 导入递归字符文本分割器类
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 创建一个递归字符文本分割器对象,支持多语言分隔符
text_splitter = RecursiveCharacterTextSplitter(
    # 指定用于分割的分隔符列表,包含中英文常用符号
    separators=[
        "\n\n",    # 段落分隔符
        "\n",      # 行分隔符
        "。",      # 中文句号
        ".",       # 英文句号
        "!",      # 中文感叹号
        "!",       # 英文感叹号
        "?",      # 中文问号
        "?",       # 英文问号
        ",",      # 中文逗号
        ",",       # 英文逗号
        ";",      # 中文分号
        ";",       # 英文分号
        " ",       # 空格
        "",        # 最后按字符进行分割
    ],
    # 设置每个块的最大字符数为 300
    chunk_size=300,
    # 相邻块之间重叠 50 个字符
    chunk_overlap=50
)

# 定义一段中英文混合的示例文本
mixed_text = """
Python is a popular programming language. 它简单易学,功能强大。

You can use Python for web development, data analysis, and machine learning. 
你可以用它来开发网站、分析数据和进行机器学习。

The syntax is clean and readable. 语法简洁易读。
"""

# 使用文本分割器将文本分割为多个块
texts = text_splitter.split_text(mixed_text)

# 打印分割后块的总数
print(f"分割为 {len(texts)} 个块\n")
# 遍历每一个分割出来的块,逐块打印其编号和内容
for i, text in enumerate(texts, 1):
    print(f"块 {i}:")
    print(text)
    print("-" * 50)

8. 常见问题与解决方案 #

在使用文本分割器的过程中,可能会遇到一些问题。本节列出常见问题及解决方法。

8.1 问题 1:分割后的块大小不一致 #

问题描述: 设置了 chunk_size=500,但实际块的大小可能小于 500。

原因:

  • 递归分割器会尽量在语义边界(如段落、句子)处分割
  • 如果段落或句子小于 500 字符,块就会小于设定值
  • 这是正常行为,有助于保持语义完整性

解决方案:

  • 如果确实需要固定大小,使用 CharacterTextSplitter 而不是 RecursiveCharacterTextSplitter
  • 或者接受较小的块,因为保持语义完整性通常更重要

8.2 问题 2:中文文本分割效果不好 #

问题描述: 中文文本被不合理地分割,可能在句子中间切断。

原因:

  • 默认分隔符主要针对英文设计
  • 中文没有词边界,需要添加中文标点符号作为分隔符

解决方案:

  • 添加中文标点符号到分隔符列表
  • 使用 ["\n\n", "\n", "。", ",", " ", ""] 这样的分隔符列表

8.3 问题 3:重叠部分导致重复内容 #

问题描述: 设置了 chunk_overlap,但发现相邻块有重复内容。

原因:

  • 这是 chunk_overlap 的正常行为
  • 重叠是为了保持上下文连贯性

解决方案:

  • 如果不需要重叠,设置 chunk_overlap=0
  • 如果觉得重叠太多,减小 chunk_overlap 的值
  • 在检索时,可以使用去重逻辑处理重复内容

8.4 问题 4:分割速度慢 #

问题描述: 处理大文件时,分割速度很慢。

原因:

  • 递归分割需要多次尝试不同的分隔符
  • 大文件需要处理更多内容

解决方案:

  • 减少分隔符数量
  • 使用更简单的分割器(如 CharacterTextSplitter)
  • 考虑并行处理多个文件

8.5 问题 5:Token 计数不准确 #

问题描述: 使用 token 分割时,实际 token 数与预期不符。

原因:

  • 不同模型使用不同的 tokenizer
  • tiktoken 的编码可能与你的模型不匹配

解决方案:

  • 确认使用的编码名称与你的模型匹配
  • GPT-4 使用 cl100k_base
  • GPT-3.5 使用 cl100k_base
  • 其他模型可能需要不同的编码

9.RecursiveCharacterTextSplitter #

9.1.简单拆分 #

9.1.1. main.py #

main.py

# 导入RecursiveCharacterTextSplitter类用于文本分割
from langchain_text_splitters import RecursiveCharacterTextSplitter
# from splitters import RecursiveCharacterTextSplitter  # 可以选用自定义的splitters模块

# 创建一个文本分割器对象,设置分块大小为4,分块重叠为0
text_splitter = RecursiveCharacterTextSplitter(chunk_size=4,chunk_overlap=0)

# 定义一个包含三个段落的文档字符串
document = f"""段落1\n段落2\n段落3"""

# 使用分割器对文档内容进行分割,返回分块列表
texts = text_splitter.split_text(document)

# 遍历每个分块,输出分块编号、长度和内容
for i, text in enumerate(texts):
    print(f"分块 {i+1}: 长度={len(text)} 内容: {repr(text)}")

9.1.2. splitters.py #

splitters.py

# 定义递归字符分割器类
class RecursiveCharacterTextSplitter:
    # 构造方法,初始化参数:分块大小、重叠长度和分隔符列表
    def __init__(
        self,
        chunk_size: int = 1000,              # 默认分块大小为1000
        chunk_overlap: int = 0,              # 默认块间重叠为0
        separators: list[str] | None = None, # 分隔符列表,默认为None
    ):
        # 赋值分块大小
        self.chunk_size = chunk_size
        # 赋值分块重叠长度
        self.chunk_overlap = chunk_overlap
        # 如果未传入分隔符,则使用默认分隔符列表
        self.separators = separators or ["\n\n", "\n", " ", ""]

    # 内部方法:根据分隔符递归分割文本
    def _split_text(self, text: str, separators: list[str]) -> list[str]:
        # 初始化选择的分隔符为最后一个
        chosen_separator = separators[-1]
        # 遍历所有分隔符
        for i, sep in enumerate(separators):
            # 如果分隔符为空,直接选中并跳出
            if not sep:
                chosen_separator = sep
                break
            # 如果分隔符存在于文本中,则选中该分隔符并跳出
            if sep in text:
                chosen_separator = sep
                break
        # 如果有可用分隔符则按其切分文本
        if chosen_separator:
            text_pieces = text.split(chosen_separator)
        # 如果没有合适分隔符,则逐字符切分
        else:
            text_pieces = list(text)
        # 返回分割后的文本块列表
        return text_pieces

    # 公共方法:对文本进行分割
    def split_text(self, text: str) -> list[str]:
        # 调用内部方法进行分割并返回分割结果
        return self._split_text(text, self.separators)

9.2.递归分隔 #

9.2.1. main.py #

main.py

# 导入RecursiveCharacterTextSplitter类用于文本分割
+#from langchain_text_splitters import RecursiveCharacterTextSplitter
+from splitters import RecursiveCharacterTextSplitter  # 可以选用自定义的splitters模块

# 创建一个文本分割器对象,设置分块大小为4,分块重叠为0
text_splitter = RecursiveCharacterTextSplitter(chunk_size=4,chunk_overlap=0)

# 定义一个包含三个段落的文档字符串
+document = f"""段落1\n段落2\n\n段落3\n段落4"""

# 使用分割器对文档内容进行分割,返回分块列表
texts = text_splitter.split_text(document)

# 遍历每个分块,输出分块编号、长度和内容
for i, text in enumerate(texts):
    print(f"分块 {i+1}: 长度={len(text)} 内容: {repr(text)}")

9.2.2. splitters.py #

splitters.py

# 定义递归字符分割器类
class RecursiveCharacterTextSplitter:
    # 构造方法,初始化参数:分块大小、重叠长度和分隔符列表
    def __init__(
        self,
        chunk_size: int = 1000,              # 默认分块大小为1000
        chunk_overlap: int = 0,              # 默认块间重叠为0
        separators: list[str] | None = None, # 分隔符列表,默认为None
    ):
        # 赋值分块大小
        self.chunk_size = chunk_size
        # 赋值分块重叠长度
        self.chunk_overlap = chunk_overlap
        # 如果未传入分隔符,则使用默认分隔符列表
        self.separators = separators or ["\n\n", "\n", " ", ""]

    # 内部方法:根据分隔符递归分割文本
    def _split_text(self, text: str, separators: list[str]) -> list[str]:
+       # 最终结果:存放所有切好的文本块
+       final_chunks = []
        # 初始化选择的分隔符为最后一个
        chosen_separator = separators[-1]
+       # 记录还没用过的"刀"(如果当前这把刀不够用,就用更小的刀)
+       remaining_separators = []
        # 遍历所有分隔符
        for i, sep in enumerate(separators):
            # 如果分隔符为空,直接选中并跳出
            if not sep:
                chosen_separator = sep
                break
            # 如果分隔符存在于文本中,则选中该分隔符并跳出
            if sep in text:
                chosen_separator = sep
+               # 记录剩余的分隔符(如果切出来的块还是太大,就用这些更小的分隔符继续切)
+               remaining_separators = separators[i + 1 :]
                break
        # 如果有可用分隔符则按其切分文本
        if chosen_separator:
            text_pieces = text.split(chosen_separator)
        # 如果没有合适分隔符,则逐字符切分
        else:
            text_pieces = list(text)
+       # 遍历每个切出来的片段
+       for piece in text_pieces:
+          # 如果片段长度小于等于最大分块大小,直接加入结果
+          if len(piece) <= self.chunk_size:
+              final_chunks.append(piece)
+          else:
+              # 如果还有更小的分隔符可用,递归分割这个大块
+              if remaining_separators:
+                  sub_chunks = self._split_text(piece, remaining_separators)
+                  final_chunks.extend(sub_chunks)
+              else:
+                  # 如果没有更小的分隔符了,就按固定大小机械切分
+                  for i in range(0, len(piece), self.chunk_size):
+                      final_chunks.append(piece[i : i + self.chunk_size])
+       return final_chunks

    # 公共方法:对文本进行分割
    def split_text(self, text: str) -> list[str]:
        # 调用内部方法进行分割并返回分割结果
        return self._split_text(text, self.separators)

9.3.合并小块 #

9.3.1. main.py #

main.py

# 导入RecursiveCharacterTextSplitter类用于文本分割
#from langchain_text_splitters import RecursiveCharacterTextSplitter
+from splitters import RecursiveCharacterTextSplitter

# 创建一个文本分割器对象,设置分块大小为4,分块重叠为0
+text_splitter = RecursiveCharacterTextSplitter(chunk_size=17,chunk_overlap=0)

+document = f"""段落1\n段落2\n段落3\n段落4\n\n段落5\n段落6\n段落7\n段落8"""

# 使用分割器对文档内容进行分割,返回分块列表
texts = text_splitter.split_text(document)

# 遍历每个分块,输出分块编号、长度和内容
for i, text in enumerate(texts):
    print(f"分块 {i+1}: 长度={len(text)} 内容: {repr(text)}")

9.3.2. splitters.py #

splitters.py

# 定义递归字符分割器类
class RecursiveCharacterTextSplitter:
    # 构造方法,初始化参数:分块大小、重叠长度和分隔符列表
    def __init__(
        self,
        chunk_size: int = 1000,              # 默认分块大小为1000
        chunk_overlap: int = 0,              # 默认块间重叠为0
        separators: list[str] | None = None, # 分隔符列表,默认为None
    ):
        # 赋值分块大小
        self.chunk_size = chunk_size
        # 赋值分块重叠长度
        self.chunk_overlap = chunk_overlap
        # 如果未传入分隔符,则使用默认分隔符列表
        self.separators = separators or ["\n\n", "\n", " ", ""]

    # 内部方法:根据分隔符递归分割文本
    def _split_text(self, text: str, separators: list[str]) -> list[str]:
        # 最终结果:存放所有切好的文本块
        final_chunks = []
        # 初始化选择的分隔符为最后一个
        chosen_separator = separators[-1]
        # 记录还没用过的"刀"(如果当前这把刀不够用,就用更小的刀)
        remaining_separators = []
        # 遍历所有分隔符
        for i, sep in enumerate(separators):
            # 如果分隔符为空,直接选中并跳出
            if not sep:
                chosen_separator = sep
                break
            # 如果分隔符存在于文本中,则选中该分隔符并跳出
            if sep in text:
                chosen_separator = sep
                # 记录剩余的分隔符(如果切出来的块还是太大,就用这些更小的分隔符继续切)
                remaining_separators = separators[i + 1 :]
                break
        # 如果有可用分隔符则按其切分文本
        if chosen_separator:
            text_pieces = text.split(chosen_separator)
        # 如果没有合适分隔符,则逐字符切分
        else:
            text_pieces = list(text)
+       # 收集所有"小块"(长度小于 chunk_size 的片段),准备合并
+       small_pieces = []
+       # 合并时使用的分隔符(把小块粘在一起时用的"胶水")
+       glue = chosen_separator if chosen_separator else ""        
        # 遍历每个切出来的片段
        for piece in text_pieces:
+          # 如果这个片段很小(小于 chunk_size),就收集起来准备合并
           if len(piece) <= self.chunk_size:
+              small_pieces.append(piece)
           else:
               # 如果还有更小的分隔符可用,递归分割这个大块
               if remaining_separators:
                   sub_chunks = self._split_text(piece, remaining_separators)
                   final_chunks.extend(sub_chunks)
               else:
                   # 如果没有更小的分隔符了,就按固定大小机械切分
                   for i in range(0, len(piece), self.chunk_size):
                       final_chunks.append(piece[i : i + self.chunk_size])
+       # ========== 第四步:处理最后收集的小块 ==========
+       # 如果最后还有一些小块没合并,现在合并它们
+       if small_pieces:
+          merged_chunks = self._merge_splits(small_pieces, glue)
+          final_chunks.extend(merged_chunks)
+       # 返回所有切好的文本块
        return final_chunks
+   # 定义一个方法,用于将较小的片段拼接成不超过最大长度的块
+   def _merge_splits(self, small_pieces: list[str], separator: str) -> list[str]:
+       # 如果输入的小片段列表为空,直接返回空列表
+       if not small_pieces:
+           return []
+       
+       # 计算分隔符的长度(合并片段时用于占位)
+       separator_len = len(separator)
+       
+       # 用于存储最终合并好的文本块
+       merged_chunks = []
+       
+       # 当前正在合并的片段组
+       current_chunk_pieces = []
+       # 当前合并片段组的长度
+       current_chunk_len = 0
+       
+       # 遍历每一个小片段
+       for piece in small_pieces:
+           # 当前小片段的长度
+           piece_len = len(piece)
+           
+           # 计算如果加入当前片段,合并块的总长度会是多少
+           # 如果已有片段,则还需加上分隔符长度
+           if current_chunk_pieces:
+               total_len = current_chunk_len + separator_len + piece_len
+           else:
+               # 如果这是第一个片段,只考虑片段自身长度
+               total_len = piece_len
+           
+           # 如果加上该片段后总长度超过最大长度,且当前块里已有内容
+           if total_len > self.chunk_size and current_chunk_pieces:
+               # 用分隔符拼接当前片段组,作为一个完整的块加入结果列表
+               chunk = separator.join(current_chunk_pieces)
+               merged_chunks.append(chunk)
+               
+               # 重置,准备下一组片段的合并
+               current_chunk_pieces = []
+               current_chunk_len = 0
+           
+           # 当前片段加入当前块
+           current_chunk_pieces.append(piece)
+           # 更新当前块的长度
+           current_chunk_len = len(separator.join(current_chunk_pieces))
+       
+       # 合并并加入最后一组未保存的片段(如果有)
+       if current_chunk_pieces:
+           chunk = separator.join(current_chunk_pieces)
+           merged_chunks.append(chunk)
+       
+       # 返回所有合并好的文本块
+       return merged_chunks   
    # 公共方法:对文本进行分割
    def split_text(self, text: str) -> list[str]:
        # 调用内部方法进行分割并返回分割结果
        return self._split_text(text, self.separators)

9.4.重叠字符 #

9.4.1. main.py #

main.py

# 导入RecursiveCharacterTextSplitter类用于文本分割
#from langchain_text_splitters import RecursiveCharacterTextSplitter
from splitters import RecursiveCharacterTextSplitter

# 创建一个文本分割器对象,设置分块大小为4,分块重叠为0
+text_splitter = RecursiveCharacterTextSplitter(chunk_size=10, chunk_overlap=4)

document = f"""段落1\n段落2\n段落3\n段落4\n\n段落5\n段落6\n段落7\n段落8"""

# 使用分割器对文档内容进行分割,返回分块列表
texts = text_splitter.split_text(document)

# 遍历每个分块,输出分块编号、长度和内容
for i, text in enumerate(texts):
    print(f"分块 {i+1}: 长度={len(text)} 内容: {repr(text)}")

9.4.2. splitters.py #

splitters.py

# 定义递归字符分割器类
class RecursiveCharacterTextSplitter:
    # 构造方法,初始化参数:分块大小、重叠长度和分隔符列表
    def __init__(
        self,
        chunk_size: int = 1000,              # 默认分块大小为1000
        chunk_overlap: int = 0,              # 默认块间重叠为0
        separators: list[str] | None = None, # 分隔符列表,默认为None
    ):
        # 赋值分块大小
        self.chunk_size = chunk_size
        # 赋值分块重叠长度
        self.chunk_overlap = chunk_overlap
        # 如果未传入分隔符,则使用默认分隔符列表
        self.separators = separators or ["\n\n", "\n", " ", ""]

    # 内部方法:根据分隔符递归分割文本
    def _split_text(self, text: str, separators: list[str]) -> list[str]:
        # 最终结果:存放所有切好的文本块
        final_chunks = []
        # 初始化选择的分隔符为最后一个
        chosen_separator = separators[-1]
        # 记录还没用过的"刀"(如果当前这把刀不够用,就用更小的刀)
        remaining_separators = []
        # 遍历所有分隔符
        for i, sep in enumerate(separators):
            # 如果分隔符为空,直接选中并跳出
            if not sep:
                chosen_separator = sep
                break
            # 如果分隔符存在于文本中,则选中该分隔符并跳出
            if sep in text:
                chosen_separator = sep
                # 记录剩余的分隔符(如果切出来的块还是太大,就用这些更小的分隔符继续切)
                remaining_separators = separators[i + 1 :]
                break
        # 如果有可用分隔符则按其切分文本
        if chosen_separator:
            text_pieces = text.split(chosen_separator)
        # 如果没有合适分隔符,则逐字符切分
        else:
            text_pieces = list(text)
        # 收集所有"小块"(长度小于 chunk_size 的片段),准备合并
        small_pieces = []
        # 合并时使用的分隔符(把小块粘在一起时用的"胶水")
        glue = chosen_separator if chosen_separator else ""        
        # 遍历每个切出来的片段
        for piece in text_pieces:
           # 如果这个片段很小(小于 chunk_size),就收集起来准备合并
           if len(piece) <= self.chunk_size:
               small_pieces.append(piece)
           else:
               # 如果还有更小的分隔符可用,递归分割这个大块
               if remaining_separators:
                   sub_chunks = self._split_text(piece, remaining_separators)
                   final_chunks.extend(sub_chunks)
               else:
                   # 如果没有更小的分隔符了,就按固定大小机械切分
                   for i in range(0, len(piece), self.chunk_size):
                       final_chunks.append(piece[i : i + self.chunk_size])
               # ========== 第四步:处理最后收集的小块 ==========
        # 如果最后还有一些小块没合并,现在合并它们
        if small_pieces:
           merged_chunks = self._merge_splits(small_pieces, glue)
           final_chunks.extend(merged_chunks)
        # 返回所有切好的文本块
        return final_chunks
    # 定义一个方法,用于将较小的片段拼接成不超过最大长度的块
    def _merge_splits(self, small_pieces: list[str], separator: str) -> list[str]:
        # 如果输入的小片段列表为空,直接返回空列表
        if not small_pieces:
            return []

        # 计算分隔符的长度(合并片段时用于占位)
        separator_len = len(separator)

        # 用于存储最终合并好的文本块
        merged_chunks = []

        # 当前正在合并的片段组
        current_chunk_pieces = []
+       # 当前合并片段组的总长度
+       total = 0

        # 遍历每一个小片段
        for piece in small_pieces:
            # 当前小片段的长度
            piece_len = len(piece)

            # 计算如果加入当前片段,合并块的总长度会是多少
            # 如果已有片段,则还需加上分隔符长度
            if current_chunk_pieces:
+               total_len = total + separator_len + piece_len
            else:
                # 如果这是第一个片段,只考虑片段自身长度
                total_len = piece_len

            # 如果加上该片段后总长度超过最大长度,且当前块里已有内容
            if total_len > self.chunk_size and current_chunk_pieces:
                # 用分隔符拼接当前片段组,作为一个完整的块加入结果列表
                chunk = separator.join(current_chunk_pieces)
                merged_chunks.append(chunk)

+               # 为了支持重叠,从前面移除元素直到总长度小于等于chunk_overlap
+               # 或者当前长度加上新元素长度仍然超过chunk_size
+               while total > self.chunk_overlap or (
+                   total_len > self.chunk_size and total > 0
+               ):
+                   # 从前面移除第一个片段
+                   if current_chunk_pieces:
+                       removed_piece = current_chunk_pieces[0]
+                       removed_len = len(removed_piece)
+                       total -= removed_len
+                       # 如果还有多个片段,需要减去一个分隔符的长度
+                       if len(current_chunk_pieces) > 1:
+                           total -= separator_len
+                       current_chunk_pieces = current_chunk_pieces[1:]
+                       # 重新计算总长度
+                       if current_chunk_pieces:
+                           total_len = total + separator_len + piece_len
+                       else:
+                           total_len = piece_len
+                   else:
+                       break

            # 当前片段加入当前块
            current_chunk_pieces.append(piece)
+           # 更新总长度
+           if len(current_chunk_pieces) > 1:
+               total = len(separator.join(current_chunk_pieces))
+           else:
+               total = piece_len

        # 合并并加入最后一组未保存的片段(如果有)
        if current_chunk_pieces:
            chunk = separator.join(current_chunk_pieces)
            merged_chunks.append(chunk)

        # 返回所有合并好的文本块
        return merged_chunks   
    # 公共方法:对文本进行分割
    def split_text(self, text: str) -> list[str]:
        # 调用内部方法进行分割并返回分割结果
        return self._split_text(text, self.separators)

9.5 执行过程 #

graph TD A[开始分割文本] --> B{遍历分隔符列表}; B --> C{找到合适分隔符?}; C -->|是| D[按该分隔符切分文本]; C -->|否| E[按字符切分文本]; D --> F; E --> F; subgraph F [处理每个片段] F1[遍历所有片段] --> F2{片段长度 ≤ chunk_size?}; F2 -->|是| F3[收集到small_pieces]; F2 -->|否| F4{还有剩余分隔符?}; F4 -->|是| F5[递归分割]; F4 -->|否| F6[机械切分]; F5 --> F7[添加到final_chunks]; F6 --> F7; end F --> G{small_pieces不为空?}; G -->|是| H[调用_merge_splits合并]; G -->|否| I[返回final_chunks]; H --> I; subgraph J [_merge_splits流程] K[初始化当前块] --> L{遍历所有小片段}; L --> M{加入后超长且当前块不为空?}; M -->|是| N[保存当前块]; N --> O[移除前面片段直到满足重叠要求]; O --> P[添加当前片段到当前块]; M -->|否| P; P --> Q{还有更多片段?}; Q -->|是| L; Q -->|否| R[保存最后一块]; R --> S[返回合并后的块列表]; end

第一步:分隔符选择(优先级从高到低)

根据代码,默认分隔符列表是:

separators = ["\n\n", "\n", " ", ""]

算法会按顺序检查文本中是否存在这些分隔符,找到第一个存在的就用它分割。

当前文档:

"段落1\n段落2\n段落3\n段落4\n\n段落5\n段落6\n段落7\n段落8"

文档中包含 \n\n(在"段落4"和"段落5"之间),所以先按 \n\n 分割。

正确的流程

9.5.1 第一步:按 \n\n 分割 #

原文:段落1\n段落2\n段落3\n段落4\n\n段落5\n段落6\n段落7\n段落8
                                          ↑ 这里有两个换行

分割后:
text_pieces = [
    "段落1\n段落2\n段落3\n段落4",  # 第一个大块
    "段落5\n段落6\n段落7\n段落8"   # 第二个大块
]
chosen_separator = "\n\n"
glue = "\n\n"  (用于合并时粘合片段)
remaining_separators = ["\n", " ", ""]  (剩余更小的分隔符)

9.5.2 第二步:处理第一个大块 "段落1\n段落2\n段落3\n段落4" #

这个块的长度是 3+1+3+1+3+1+3 = 15 字符,大于 chunk_size=10。

但它是由小片段组成的,所以:

  1. 先按 \n 进一步分割这个大块(使用 remaining_separators)
  2. 递归调用 _split_text(piece, ["\n", " ", ""])

递归处理 "段落1\n段落2\n段落3\n段落4":

  • 按 \n 分割得到:["段落1", "段落2", "段落3", "段落4"]
  • 每个片段长度 ≤ 10,都是"小块"
  • 收集到 small_pieces = ["段落1", "段落2", "段落3", "段落4"]
  • 然后用 _merge_splits 合并,使用分隔符 "\n"

9.5.3 第三步:_merge_splits 合并第一个大块的小片段 #

现在详细跟踪 _merge_splits(["段落1", "段落2", "段落3", "段落4"], "\n"):

初始状态:
current_chunk_pieces = []
total = 0
separator = "\n" (长度=1)
chunk_size = 10
chunk_overlap = 4

处理 "段落1" (长度=3):

total_len = 3 ≤ 10 ✅
current_chunk_pieces = ["段落1"]
total = 3

处理 "段落2" (长度=3):

total_len = 3 + 1(分隔符) + 3 = 7 ≤ 10 ✅
current_chunk_pieces = ["段落1", "段落2"]
total = len("段落1\n段落2") = 7

处理 "段落3" (长度=3):

total_len = 7 + 1 + 3 = 11 > 10 ❌ 超出限制!

1. 保存当前块:
   chunk = "段落1\n段落2"  (长度=7)
   merged_chunks.append(chunk)
   merged_chunks = ["段落1\n段落2"]

2. 应用重叠逻辑(关键步骤):
   当前 total = 7, chunk_overlap = 4
   循环条件:total > chunk_overlap → 7 > 4 ✅ 进入循环

   移除第一个片段 "段落1" (长度=3):
   total = 7 - 3 - 1(分隔符) = 3
   current_chunk_pieces = ["段落2"]

   检查:total = 3 ≤ 4 ✅ 满足条件,退出循环

3. 加入新片段 "段落3":
   current_chunk_pieces = ["段落2", "段落3"]
   total = len("段落2\n段落3") = 7

处理 "段落4" (长度=3):

total_len = 7 + 1 + 3 = 11 > 10 ❌ 超出限制!

1. 保存当前块:
   chunk = "段落2\n段落3"  (长度=7)
   merged_chunks.append(chunk)
   merged_chunks = ["段落1\n段落2", "段落2\n段落3"]

2. 应用重叠逻辑:
   total = 7, chunk_overlap = 4
   移除 "段落2" (长度=3):
   total = 7 - 3 - 1 = 3 ≤ 4 ✅
   current_chunk_pieces = ["段落3"]

3. 加入 "段落4":
   current_chunk_pieces = ["段落3", "段落4"]
   total = len("段落3\n段落4") = 7

最后保存剩余块:

chunk = "段落3\n段落4"  (长度=7)
merged_chunks.append(chunk)
merged_chunks = ["段落1\n段落2", "段落2\n段落3", "段落3\n段落4"]

9.5.4 第四步:处理第二个大块 "段落5\n段落6\n段落7\n段落8" #

同样递归处理,最终得到:

merged_chunks = ["段落5\n段落6", "段落6\n段落7", "段落7\n段落8"]

9.5.5 最终结果 #

分块 1: 长度=7 内容: '段落1\n段落2'
分块 2: 长度=7 内容: '段落2\n段落3'    ← "段落2"重叠
分块 3: 长度=7 内容: '段落3\n段落4'    ← "段落3"重叠
分块 4: 长度=7 内容: '段落5\n段落6'
分块 5: 长度=7 内容: '段落6\n段落7'    ← "段落6"重叠
分块 6: 长度=7 内容: '段落7\n段落8'    ← "段落7"重叠

9.5.6 要点总结 #

  1. 先按优先级最高的分隔符(\n\n)分割成大的文本段
  2. 对每个大段,如果仍超出 chunk_size,递归用更小的分隔符(如 \n)继续分割
  3. 对每个分割后的小片段列表,使用 _merge_splits 合并,并在合并时应用重叠逻辑
  4. 重叠在 _merge_splits 中实现:保存一个块后,保留末尾部分(约 chunk_overlap 长度),再继续合并后续片段
  5. \n\n 表示段落边界,是语义分隔。重叠在同一段落内才有意义:同一段落内相邻块重叠,避免在段落内切分丢失上下文。跨段落不重叠更合理:跨段落边界时,按语义边界断开,不强制重叠。

访问验证

请输入访问令牌

Token不正确,请重新输入