1. 什么是文本分割器? #
文本分割器(Text Splitter)是一种将大型文档拆分为较小、可单独检索的文本片段(chunks)的工具。在自然语言处理和机器学习应用中,文本分割器非常重要,因为:
为什么需要文本分割?
模型上下文窗口限制:大多数语言模型(如 GPT、BERT 等)都有输入长度限制。例如,某些模型只能处理 512 个 token,而另一些可能支持 4096 或更多。当文档超过这个限制时,必须将其分割成更小的片段。
提高检索效率:在 RAG(检索增强生成)系统中,将长文档分割成小块可以提高检索的精确度。用户查询时,系统只需要检索相关的片段,而不是整个文档。
保持语义完整性:合理的分割策略可以确保每个文本块包含完整的语义单元(如段落、句子),避免在句子中间切断,从而保持上下文信息的完整性。
文本分割的核心挑战:
- 如何在不破坏语义的情况下分割文本?
- 如何确定合适的块大小?
- 如何处理不同语言的文本(特别是没有词边界的语言)?
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 基于文本结构的分割 #
核心思想: 利用文本的天然层级结构(段落、句子、词语)进行分割,尽可能保持语义单元的完整性。
工作原理:
- 首先尝试按段落分割(使用
\n\n) - 如果段落仍然太大,按句子分割(使用
\n或.) - 如果句子仍然太大,按词语分割(使用空格)
- 最后才按字符分割
优势:
- 保持语义完整性
- 分割结果更自然
- 适合大多数通用文本
适用场景:
- 普通文档、文章、报告
- 需要保持上下文连贯性的场景
- 大多数 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 基于文档结构的分割 #
核心思想: 利用文档的格式结构(如 Markdown 标题、HTML 标签、JSON 对象)进行分割。
工作原理:
- 识别文档的格式标记(如
#标题、<div>标签) - 按照这些标记进行分割
- 保持文档的逻辑结构
优势:
- 保留文档的逻辑结构
- 每个块内保持上下文
- 对结构化文档效果更好
适用场景:
- Markdown 文档
- HTML 网页
- JSON 数据
- 代码文件
4.3.1 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 #
对于结构化代码(如 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 执行过程 #
第一步:分隔符选择(优先级从高到低)
根据代码,默认分隔符列表是:
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。
但它是由小片段组成的,所以:
- 先按
\n进一步分割这个大块(使用remaining_separators) - 递归调用
_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 要点总结 #
- 先按优先级最高的分隔符(
\n\n)分割成大的文本段 - 对每个大段,如果仍超出
chunk_size,递归用更小的分隔符(如\n)继续分割 - 对每个分割后的小片段列表,使用
_merge_splits合并,并在合并时应用重叠逻辑 - 重叠在
_merge_splits中实现:保存一个块后,保留末尾部分(约chunk_overlap长度),再继续合并后续片段 \n\n表示段落边界,是语义分隔。重叠在同一段落内才有意义:同一段落内相邻块重叠,避免在段落内切分丢失上下文。跨段落不重叠更合理:跨段落边界时,按语义边界断开,不强制重叠。