LangChain多类型文件内容分割
引言
在处理文本数据时,文本分割是一个重要的步骤,尤其是在处理长文本或将文本数据输入到机器学习模型中时。LangChain 是一个用于构建大型语言模型应用程序的库,它提供了多种工具和策略来处理文本分割。本教程将详细介绍LangChain中的各种文本分割方法,以及如何根据不同的需求选择合适的分割策略。
1. 文本分割的重要性
文本分割在大型语言模型应用中的重要性体现在以下几个方面:
- 处理长文本:大多数语言模型都有输入长度限制,需要将长文本分割成较小的块
- 保持语义完整性:合理的分割可以保持文本的语义完整性,提高下游任务的效果
- 优化检索效率:在检索增强生成(RAG)应用中,合适的文本分割可以提高检索的准确性
- 减少信息丢失:通过设置块之间的重叠,可以减少分割过程中的信息丢失
2. 递归字符文本分割
递归分割(recursively)是LangChain中用于通用文本的推荐工具。它接受一个字符列表作为参数,按顺序尝试在这些字符上进行分割,直到块足够小。
2.1 基本原理
- 文本如何分割:根据字符列表进行分割
- 块大小如何衡量:根据字符数量进行衡量
2.2 使用示例
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 示例文本
text = """
LangChain是一个用于开发由语言模型驱动的应用程序的框架。
它可以帮助应用程序更具有以下特点:
- 上下文感知:连接语言模型与上下文源(提示指令、少样本示例、内容来源等)
- 推理:依靠语言模型推理(选择要使用的工具、提取结构化信息等)
"""
# 创建分割器
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=100,
chunk_overlap=20,
length_function=len,
is_separator_regex=False,
)
# 直接获取字符串内容
chunks = text_splitter.split_text(text)
print(f"分割后的块数: {len(chunks)}")
for i, chunk in enumerate(chunks):
print(f"块 {i+1}: {chunk}")
print("-" * 50)
# 创建LangChain Document对象
documents = text_splitter.create_documents([text])
print(f"创建的文档数: {len(documents)}")
print(f"第一个文档的内容: {documents[0].page_content}")
2.3 参数详解
RecursiveCharacterTextSplitter的主要参数包括:
- chunk_size:块的最大大小,由length_function决定
- chunk_overlap:块之间的目标重叠,有助于减少信息丢失
- length_function:确定块大小的函数,默认为len
- is_separator_regex:分隔符列表是否应被解释为正则表达式
- separators:用于分割文本的字符列表,默认为
["\n\n", "\n", " ", ""]
3. 处理没有词边界的语言
一些书写系统没有词边界,例如中文、日文和泰文。使用默认分隔符列表["\n\n", "\n", " ", ""]分割文本可能会导致单词被分割在不同块之间。
3.1 自定义分隔符
为了保持单词在一起,可以覆盖分隔符列表,包括额外的标点符号:
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 示例中文文本
chinese_text = """
自然语言处理是人工智能的一个分支,它关注计算机与人类语言之间的交互。
这个领域结合了计算机科学、人工智能和语言学。
深度学习方法在近年来显著提高了自然语言处理系统的性能。
"""
# 为中文文本自定义分隔符
chinese_splitter = RecursiveCharacterTextSplitter(
chunk_size=100,
chunk_overlap=20,
separators=[
"\n\n", # 段落
"\n", # 行
"。", # 中文句号
"!", # 中文感叹号
"?", # 中文问号
",", # 中文逗号
";", # 中文分号
":", # 中文冒号
" ", # 空格
"" # 单字符
]
)
# 分割中文文本
chinese_chunks = chinese_splitter.split_text(chinese_text)
print(f"分割后的中文文本块数: {len(chinese_chunks)}")
for i, chunk in enumerate(chinese_chunks):
print(f"块 {i+1}: {chunk}")
print("-" * 50)
3.2 推荐的分隔符
对于不同的语言,可以添加以下分隔符:
- 中文:添加ASCII句号".",Unicode全角句号".",以及表意句号"。"
- 日文:添加表意句号"。"和零宽空格
- 泰文、缅甸文、高棉文:添加零宽空格
- 通用标点:添加ASCII逗号",",Unicode全角逗号",",以及Unicode表意逗号"、"
4. 按照语义块分割文本
有时候,我们希望根据语义相似性来拆分文本块,而不仅仅是根据字符或标记。
4.1 语义分块原理
语义分块会将文本拆分成句子,然后根据嵌入空间中的相似性将句子分组。如果嵌入足够远,文本块将被拆分。
4.2 安装依赖项
pip install langchain-experimental
4.3 使用示例
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
import os
from dotenv import load_dotenv
# 加载环境变量
load_dotenv()
# 示例文本
text = """
人工智能(AI)是计算机科学的一个分支,它致力于创建能够执行通常需要人类智能的任务的系统。
这些任务包括视觉感知、语音识别、决策制定和语言翻译。
机器学习是AI的一个子集,它使用统计方法使计算机系统能够从数据中"学习",而无需明确编程。
深度学习是机器学习的一个子集,它使用神经网络进行学习。
自然语言处理(NLP)是AI的另一个分支,专注于计算机理解和生成人类语言的能力。
强化学习是一种机器学习方法,其中代理通过与环境交互并接收反馈来学习。
"""
# 创建语义分块器
embeddings = OpenAIEmbeddings()
semantic_splitter = SemanticChunker(embeddings)
# 创建文档
documents = semantic_splitter.create_documents([text])
# 查看分块结果
print(f"语义分块后的文档数: {len(documents)}")
for i, doc in enumerate(documents):
print(f"文档 {i+1}: {doc.page_content}")
print("-" * 50)
4.4 断点设置
语义分块器的工作原理是确定何时"断开"句子。这是通过查找任意两个句子之间的嵌入差异来完成的。当该差异超过某个阈值时,它们就会被拆分。
有几种方法可以确定该阈值,这由breakpoint_threshold_type关键字参数控制:
4.4.1 百分位数方法
拆分的默认方式是基于百分位数。在此方法中,计算所有句子之间的差异,然后任何大于X百分位数的差异都会被拆分。
# 使用百分位数方法的语义分块器
semantic_splitter_percentile = SemanticChunker(
embeddings,
breakpoint_threshold_type="percentile",
breakpoint_threshold=90.0 # 90th百分位数
)
# 创建文档
documents_percentile = semantic_splitter_percentile.create_documents([text])
# 查看分块结果
print(f"使用百分位数方法的语义分块后的文档数: {len(documents_percentile)}")
4.4.2 标准差方法
另一种方法是基于标准差。在此方法中,计算所有句子之间差异的平均值和标准差,然后任何大于平均值加上X倍标准差的差异都会被拆分。
# 使用标准差方法的语义分块器
semantic_splitter_std = SemanticChunker(
embeddings,
breakpoint_threshold_type="standard_deviation",
breakpoint_threshold=2.0 # 平均值 + 2倍标准差
)
# 创建文档
documents_std = semantic_splitter_std.create_documents([text])
# 查看分块结果
print(f"使用标准差方法的语义分块后的文档数: {len(documents_std)}")
5. 按标题拆分Markdown
在处理Markdown文档时,我们可能希望特别尊重文档本身的结构。Markdown文件是按标题组织的,在特定标题组中创建分块是一个直观的想法。
5.1 动机
正如Pinecone的笔记中提到的,分块通常旨在将具有共同上下文的文本保持在一起。考虑到这一点,我们可以使用MarkdownHeaderTextSplitter来根据指定的一组标题来拆分Markdown文件。
5.2 使用示例
from langchain_text_splitters import MarkdownHeaderTextSplitter
# 示例Markdown文本
markdown_text = """# LangChain简介
LangChain是一个强大的框架,用于开发由语言模型驱动的应用程序。
## 核心概念
LangChain提供了多种组件,可以单独使用,也可以链接在一起。
### 提示模板
提示模板是创建语言模型提示的工具。
### 链
链是将组件连接在一起以完成特定任务的方式。
## 应用场景
LangChain可以应用于各种场景。
### 问答系统
可以构建基于文档的问答系统。
### 聊天机器人
可以创建具有记忆功能的聊天机器人。
"""
# 定义标题分割器
headers_to_split_on = [
("#", "标题1"),
("##", "标题2"),
("###", "标题3"),
]
# 创建Markdown标题分割器
markdown_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on,
strip_headers=True # 是否从输出块的内容中删除标题
)
# 分割Markdown文本
markdown_docs = markdown_splitter.split_text(markdown_text)
# 查看分割结果
print(f"Markdown分割后的文档数: {len(markdown_docs)}")
for i, doc in enumerate(markdown_docs):
print(f"文档 {i+1}:")
print(f"内容: {doc.page_content}")
print(f"元数据: {doc.metadata}")
print("-" * 50)
5.3 保留标题
默认情况下,MarkdownHeaderTextSplitter会从输出块的内容中删除正在拆分的标题。可以通过设置strip_headers = False来禁用此功能:
# 创建保留标题的Markdown分割器
markdown_splitter_with_headers = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on,
strip_headers=False # 保留标题
)
# 分割Markdown文本
markdown_docs_with_headers = markdown_splitter_with_headers.split_text(markdown_text)
# 查看分割结果
print(f"保留标题的Markdown分割后的文档数: {len(markdown_docs_with_headers)}")
for i, doc in enumerate(markdown_docs_with_headers):
print(f"文档 {i+1}:")
print(f"内容: {doc.page_content}")
print(f"元数据: {doc.metadata}")
print("-" * 50)
6. 按token来分割文本
语言模型有一个标记限制,在分割文本时,最好计算标记数。有许多标记器,在计算文本中的标记数时,应使用与语言模型中使用的相同的标记器。
6.1 使用tiktoken
tiktoken是由OpenAI创建的快速BPE标记器。我们可以使用tiktoken来估算使用的标记数,对于OpenAI模型,这可能会更准确。
6.1.1 安装依赖
pip install tiktoken
6.1.2 使用CharacterTextSplitter与tiktoken
from langchain_text_splitters import CharacterTextSplitter
# 示例文本
text = """
LangChain是一个用于开发由语言模型驱动的应用程序的框架。
它可以帮助应用程序更具有以下特点:
- 上下文感知:连接语言模型与上下文源(提示指令、少样本示例、内容来源等)
- 推理:依靠语言模型推理(选择要使用的工具、提取结构化信息等)
"""
# 使用tiktoken创建分割器
splitter_tiktoken = CharacterTextSplitter.from_tiktoken_encoder(
encoding_name="cl100k_base", # 或者使用model_name="gpt-4"
chunk_size=50, # 以token为单位的块大小
chunk_overlap=10, # 以token为单位的重叠大小
separators=["\n\n", "\n", " ", ""] # 分隔符列表
)
# 分割文本
chunks_tiktoken = splitter_tiktoken.split_text(text)
# 查看分割结果
print(f"使用tiktoken分割后的块数: {len(chunks_tiktoken)}")
for i, chunk in enumerate(chunks_tiktoken):
print(f"块 {i+1}: {chunk}")
print("-" * 50)
6.1.3 使用RecursiveCharacterTextSplitter与tiktoken
要对块大小实施硬约束,我们可以使用RecursiveCharacterTextSplitter.from_tiktoken_encoder,如果块大小较大,则会递归分割每个块:
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 使用tiktoken创建递归分割器
recursive_splitter_tiktoken = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
encoding_name="cl100k_base",
chunk_size=50,
chunk_overlap=10
)
# 分割文本
recursive_chunks_tiktoken = recursive_splitter_tiktoken.split_text(text)
# 查看分割结果
print(f"使用递归tiktoken分割后的块数: {len(recursive_chunks_tiktoken)}")
for i, chunk in enumerate(recursive_chunks_tiktoken):
print(f"块 {i+1}: {chunk}")
print("-" * 50)
6.1.4 使用TokenTextSplitter
我们还可以加载一个TokenTextSplitter分割器,它直接与tiktoken一起使用,并确保每个分割块都比块大小小:
from langchain_text_splitters import TokenTextSplitter
# 创建TokenTextSplitter
token_splitter = TokenTextSplitter(
encoding_name="cl100k_base",
chunk_size=50,
chunk_overlap=10
)
# 分割文本
token_chunks = token_splitter.split_text(text)
# 查看分割结果
print(f"使用TokenTextSplitter分割后的块数: {len(token_chunks)}")
for i, chunk in enumerate(token_chunks):
print(f"块 {i+1}: {chunk}")
print("-" * 50)
6.2 处理多字节字符
一些书面语言(例如中文和日文)的字符编码为2个或更多个标记。直接使用TokenTextSplitter可能会导致字符的标记在两个块之间分割,从而导致不正确的Unicode字符。请使用RecursiveCharacterTextSplitter.from_tiktoken_encoder或CharacterTextSplitter.from_tiktoken_encoder来确保块包含有效的Unicode字符。
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 示例中文文本
chinese_text = """
自然语言处理是人工智能的一个分支,它关注计算机与人类语言之间的交互。
这个领域结合了计算机科学、人工智能和语言学。
深度学习方法在近年来显著提高了自然语言处理系统的性能。
"""
# 使用tiktoken创建递归分割器,处理中文文本
chinese_splitter_tiktoken = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
encoding_name="cl100k_base",
chunk_size=50,
chunk_overlap=10,
separators=[
"\n\n", # 段落
"\n", # 行
"。", # 中文句号
"!", # 中文感叹号
"?", # 中文问号
",", # 中文逗号
";", # 中文分号
":", # 中文冒号
" ", # 空格
"" # 单字符
]
)
# 分割中文文本
chinese_chunks_tiktoken = chinese_splitter_tiktoken.split_text(chinese_text)
# 查看分割结果
print(f"使用tiktoken分割中文文本后的块数: {len(chinese_chunks_tiktoken)}")
for i, chunk in enumerate(chinese_chunks_tiktoken):
print(f"块 {i+1}: {chunk}")
print("-" * 50)
7. 综合应用:处理不同类型的文档
在实际应用中,我们可能需要处理不同类型的文档,并为每种文档选择合适的分割策略。以下是一个综合示例:
import os
from langchain_text_splitters import (
RecursiveCharacterTextSplitter,
MarkdownHeaderTextSplitter,
TokenTextSplitter
)
from langchain_community.document_loaders import (
TextLoader,
PyPDFLoader,
UnstructuredMarkdownLoader
)
from langchain.schema import Document
def split_document(file_path):
"""根据文件类型选择合适的分割策略"""
_, file_extension = os.path.splitext(file_path)
# 加载文档
if file_extension.lower() == '.pdf':
loader = PyPDFLoader(file_path)
docs = loader.load()
# 使用tiktoken分割PDF文档
splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
encoding_name="cl100k_base",
chunk_size=1000,
chunk_overlap=200
)
return splitter.split_documents(docs)
elif file_extension.lower() == '.md':
# 先使用Markdown标题分割
with open(file_path, 'r', encoding='utf-8') as f:
markdown_text = f.read()
headers_to_split_on = [
("#", "标题1"),
("##", "标题2"),
("###", "标题3"),
]
markdown_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on
)
md_docs = markdown_splitter.split_text(markdown_text)
# 然后对每个部分使用递归字符分割
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
result = []
for doc in md_docs:
smaller_docs = text_splitter.split_documents([doc])
result.extend(smaller_docs)
return result
elif file_extension.lower() in ['.txt', '.py', '.java', '.js', '.html']:
loader = TextLoader(file_path)
docs = loader.load()
# 使用递归字符分割
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
return splitter.split_documents(docs)
else:
# 对于未知类型,使用通用分割策略
try:
with open(file_path, 'r', encoding='utf-8') as f:
text = f.read()
doc = Document(page_content=text, metadata={"source": file_path})
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
return splitter.split_documents([doc])
except Exception as e:
print(f"无法处理文件 {file_path}: {str(e)}")
return []
# 使用示例
def process_directory(directory_path):
"""处理目录中的所有文档"""
all_chunks = []
for root, _, files in os.walk(directory_path):
for file in files:
file_path = os.path.join(root, file)
chunks = split_document(file_path)
all_chunks.extend(chunks)
print(f"处理文件 {file_path}, 生成了 {len(chunks)} 个文本块")
return all_chunks
# 示例调用
# chunks = process_directory("./data")
# print(f"总共生成了 {len(chunks)} 个文本块")
8. 最佳实践与注意事项
8.1 选择合适的分割策略
- 通用文本:使用RecursiveCharacterTextSplitter
- Markdown文档:使用MarkdownHeaderTextSplitter
- 需要精确token计数:使用tiktoken相关的分割器
- 保持语义完整性:考虑使用SemanticChunker
8.2 设置合适的块大小和重叠
- 块大小:根据模型的上下文窗口大小设置,通常为模型最大token数的1/3到1/2
- 块重叠:通常设置为块大小的10%-20%,以确保上下文连续性
8.3 处理特殊字符和格式
- 对于代码、表格等特殊格式,可能需要自定义分隔符
- 对于多语言文本,确保使用适合该语言的分隔符
8.4 性能考虑
- 对于大型文档集合,考虑批处理和并行处理
- 使用缓存避免重复计算嵌入
9. 总结
LangChain提供了多种文本分割策略,可以根据不同的需求选择合适的方法:
- 递归字符分割:通用且灵活的分割方法,适用于大多数文本
- 语义分割:根据语义相似性分割文本,保持语义完整性
- Markdown标题分割:尊重文档结构,按标题组织分割
- Token分割:精确控制token数量,适用于对上下文长度有严格要求的场景
选择合适的分割策略对于构建高效的语言模型应用至关重要。通过合理的文本分割,可以提高模型的理解能力,减少信息丢失,并优化检索效果。