0 为什么要拆分文档

当拿到统一的一个Document对象后,接下来需要切分成Chunks。如果不切分,而是考虑作为一个整体的Document对象,会存在两点问题:

  1. 假设提问的Query的答案出现在某一个Document对象中,那么将检索到的整个Document对象 直接放入Prompt中并不是最优的选择,因为其中一定会包含非常多无关的信息,而无效信息越多,对大模型后续的推理影响越大。
  2. 任何一个大模型都存在最大输入的Token限制,如果一个Document非常大,比如一个几百兆的 PDF,那么大模型肯定无法容纳如此多的信息。

基于此,一个有效的解决方案就是将完整的Document对象进行分块处(Chunking) 。无论是在存储还是检索过程中,都将以这些块(chunks) 为基本单位,这样有效地避免内容不相关性问题和超出最大输入限制的问题。
LangChain 中常用的文档切分器有CharacterTextSplitterRecursiveCharacterTextSplTokenTextSplitterSemanticChunker。以下将详细说明其使用。

1 CharacterTextSplitter(字符数分块)

1.1参数情况说明

参数 说明
chunk_size 每个切块的最大字符数,默认值为4000(由长度函数测量)
chunk_overlap 相邻两个块之间的字符重叠数,避免信息在边界处被切断而丢失。默认值为200,通常会设置为chunk_size的10% - 20%
length_function 用于测量给定块字符数的函数,默认值为len函数。len函数在Python中按Unicode字符计数,所以一个汉字、一个英文字母、一个符号都算一个字符
keep_seperator 是否在块中保留分隔符,默认为False
add_start_index 如果为True,则在元数据中包含块的起始索引,默认值为False
strip_whitespace 如果为True,则从每个文档的开始和结束处去除空白字符,默认值为True

1.2代码示例

from langchain_text_splitters import CharacterTextSplitter

# 示例文本
text = """
LangChain 是一个用于开发由语言模型驱动的应用程序的框架的。它提供了一套工具和抽象,使开发者
能够容易地构建复杂的应用程序"""

# 定义字符分割器
splitter = CharacterTextSplitter(
    chunk_size=50, # 每块大小
    chunk_overlap=5, # 块与块之间的重复字符数
    separator="" #为空串表示无需分隔符
)

# 分割文本
split_text = splitter.split_text(text)

# 打印结果
for i,chunk in enumerate(split_text):
    print(f"第{i + 1}块,长度:{len(chunk)}")
    print(chunk)
    print("-" * 50)

1.3运行结果

第一块的长度为49,小于设置的最大长度,因为separator="",不使用分隔符就会导致实际长度略小于chunk_size
请添加图片描述

2 RecursiveCharacterTextSplitter(最常用)

文档切分器中较常用的是RecursiveCharacterTextSplitter(递归字符文本切分器),遇到特定字符时进行分割。默认情况下,它尝试进行切割的字符包括[“\n\n”, “\n”, " ", “”]。

具体为:根据第一个字符进行切块,但如果任何切块太大,则会继续移动到下一个字符继续切块,以此 类推。

此外,还可以考虑添加,。等分割字符。

2.1 特点

保留上下文:优先在自然语言边界(如段落、句子结尾)处分割,减少信息碎片化 。
智能分段:通过递归尝试多种分隔符,将文本分割为大小接近chunk_size的片段。
灵活适配:适用于多种文本类型(代码、Markdown、普通文本等),是LangChain中 最通用的文本拆分器。

2.2 示例代码

from langchain_text_splitters import RecursiveCharacterTextSplitter

# 示例文本
text = """
LangChain是,一个用于开发由语言模型驱动\n的应用程序的框架\n\n的。它提供了一套工具和抽象,使开发者
能够容易地构建复杂的应用程序"""

# 定义字符分割器
splitter = RecursiveCharacterTextSplitter(
    chunk_size=20, # 每块大小
    chunk_overlap=5, # 块与块之间的重复字符数
)

# 分割文本
split_text = splitter.split_text(text)

# 打印结果
for i,chunk in enumerate(split_text):
    print(f"第{i + 1}块,长度:{len(chunk)}")
    print(chunk)
    print("-" * 50)

2.3 运行结果

默认按照[“\n\n”, “\n”, " ", “”]中的分隔符来递归分隔,也可自主定义separators, 但是会有覆盖效果,即只根据自定义的separators中的分隔符来进行分隔
请添加图片描述

3 TokenTextSplitter(Token分块)

当我们将文本拆分为块时,除了字符以外,还可以: 按Token的数量分割 (而非字符或单词数),将长文本切分成多个小块。
特点:

  • 核心依据:Token数量 + 自然边界。(TokenTextSplitter 严格按照 token 数量进行分割,但同时 会优先在自然边界(如句尾)处切断,以尽量保证语义的完整性。)
  • 优点:与LLM的Token计数逻辑一致,能尽量保持语义完整 缺点:对非英语或特定领域文本,Token化效果可能不佳
  • 典型场景:需要精确控制Token数输入LLM的场景

3.1 代码示例

from langchain_text_splitters import TokenTextSplitter

# 示例文本
text = """
LangChain是,一个用于开发由语言模型驱动的应用程序的框架的。它提供了一套工具和抽象,使开发者
能够容易地构建复杂的应用程序"""

# 定义字符分割器
splitter = TokenTextSplitter(
    chunk_size=20, # 每块大小
    chunk_overlap=0, # 块与块之间的重复字符数
    encoding_name="cl100k_base"
)

# 分割文本
split_text = splitter.split_text(text)

# 打印结果
for i,chunk in enumerate(split_text):
    print(f"第{i + 1}块,长度:{len(chunk)}")
    print(chunk)
    print("-" * 50)

3.2 运行结果

我们发现第一块的长度就超出了我们所设定的20,这是因为字符长度并不等于Token数量,我们这里设置的chunk_size为token数量,而一个Token可能是由几个字符组成的。
请添加图片描述

4 SemanticChunker(语义分块)

SemanticChunking(语义分块)是 LangChain 中一种更高级的文本分割方法,它超越了传统的基于字符或固定大小的分块方式,而是根据文本的语义结构进行智能分块,使每个分块保持语义完整性 ,从而提高检索增强生成(RAG)等应用的效果。

4.1 语义分割和普通分割的对比

特性 语义分割(SemanticChunker) 传统字符分割(RecursiveCharacterTextSplitter)
分割依据 嵌入向量相似度 固定字符/换行符以及最大字符数
语义完整性 连贯完整 句子逻辑可能不完整
计算成本 高(需要嵌入模型计算)
适用场景 需要高语义一致性的任务 简单文本处理

语义分割是需要使用嵌入模型进行计算的,是需要消耗算力的,所以使用成本比普通的分割要高,所以适用于对语义完整性高的场景。

4.2 示例代码

尝试了很久始终无法正常切割文本,始终只能得到一整块文本,使用了Qwen系列的文本嵌入模型和本地部署的nomic-embed-text模型都不能解决,只能是放弃了,也许使用OpenAi的模型有用,但是我的账户没有余额就不尝试了

import dotenv
from langchain_experimental.text_splitter import SemanticChunker
from langchain_ollama import OllamaEmbeddings

dotenv.load_dotenv()

with open("split_text", encoding="utf-8") as f:
    split_text = f.read()

embeddings_model = OllamaEmbeddings(
    model="nomic-embed-text:latest",
)

text_splitter = SemanticChunker(
    embeddings=embeddings_model,
    breakpoint_threshold_type="percentile",
    breakpoint_threshold_amount=1,
)

docs = text_splitter.create_documents(texts=[split_text])

print(f"📊 总块数: {len(docs)}\n")

for i, doc in enumerate(docs, 1):
    content = doc.page_content
    print(f"⚡️ Chunk {i} ({len(content)} 字符):")
    print("-" * 60)
    if len(content) > 250:
        print(f"{content[:150]}...\n...{content[-100:]}")
    else:
        print(content)
    print()



4.3 输出结果

大模型没有很好地按语义切割文本,可能是使用的模型问题。
请添加图片描述

LangChain官网的对话助手询问了文本切割方法,ai只回答了上述三种常用的切割方法,并没有提到基于语义的方式,接着继续提问,回答说这个SemanticChunker目前还属于实验性方法,不太稳定,并不是很推荐使用。最推荐的还是RecursiveCharacterTextSplitter方法。
请添加图片描述

5 小结

尝试了很久SemanticChunker,一直都不能成功,总是只切割为一份,最后放弃了,有个了解就行了,重点掌握RecursiveCharacterTextSplitter的使用就好了。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐