在上一篇文章《LangChain框架入门10:一文带你吃透文档加载器》中详细介绍了文档加载器的用法,在实现RAG功能准备阶段,首先将知识库文档信息加载成Document对象之后,一般就会对文档进行分割处理。
在LangChain中提供了许多开箱即用的、能分割多种格式的文本分割器,本文将会对这些文本分割器进行详细介绍。
文中所有示例代码:github.com/wzycoding/l…
一、什么是文本分割器
在RAG应用中,文档加载器将原始文档转换为Document对象后,通常需要对长文档进行分割处理,这是因为大语言模型的上下文窗口是有限的,如果在RAG检索完成之后,直接将检索到的长文档作为上下文传递给模型,可能会超出模型处理的上下文长度,导致信息丢失或回答质量下降,其中,进行文档分割的组件就是文本分割器。
文本分割器的主要作用有:
- 控制上下文长度:把长文档分割成更小,缩小上下文长度
- 提高检索准确性:小的文本片段能提升文档检索的精确度
- 保持语义完整性:在分割过程中,能尽量保持文本的语义连贯性
LangChain提供了多种文本分割器,常用的有:
| 分割器 | 作用 |
|---|---|
| RecursiveCharacterTextSplitter | 递归按字符分割文本 |
| CharacterTextSplitter | 按指定字符分割文本 |
| MarkdownHeaderTextSplitter | 按Markdown标题分割 |
| PythonCodeTextSplitter | 专门分割Python代码 |
| TokenTextSplitter | 按Token数量分割 |
| HTMLHeaderTextSplitter | 按HTML标题分割 |
大部分文本分割器都继承自TextSplitter基类,该基类定义了分割文本的核心方法:
split_text():将文本字符串分割成字符串列表split_documents():将Document对象列表分割成更小文本片段的Document对象列表create_documents():通过字符串列表创建Document对象
二、递归文本分割器用法
RecursiveCharacterTextSplitter是LangChain中最常用的通用文本分割器,它会根据指定的字符优先级递归分割文本,直到所有片段长度不超过指定上限。
在使用前首先安装依赖:
pip install -qU langchain-text-splitters
执行命令,生成依赖版本快照
pip freeze > requirements.txt
首先介绍一下RecursiveCharacterTextSplitter构造函数几个核心参数:
chunk_size: 每个片段的最大字符数
chunk_overlap:片段之间的重叠字符数
length_function:计算长度函数
is_separator_regex: 分隔符是否为正则表达式
separators:自定义分隔符
2.1 分割文本
首先介绍使用split_text()方法进行文本分割,使用示例如下,其中RecursiveCharacterTextSplitter中指定的块大小为100,片段重叠字符数为30,计算长度的函数使用len。
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 1.分割文本内容
content = ("李白(701年2月28日~762年12月),字太白,号青莲居士,出生于蜀郡绵州昌隆县(今四川省绵阳市江油市青莲镇),一说山东人,一说出生于西域碎叶,祖籍陇西成纪(今甘肃省秦安县)。"
""
"唐代伟大的浪漫主义诗人,被后人誉为“诗仙”,与杜甫并称为“李杜”,为了与另两位诗人李商隐与杜牧即“小李杜”区别,杜甫与李白又合称“大李杜”。"
""
"据《新唐书》记载,李白为兴圣皇帝(凉武昭王李暠)九世孙,与李唐诸王同宗。其人爽朗大方,爱饮酒作诗,喜交友。"
""
"李白深受黄老列庄思想影响,有《李太白集》传世,诗作中多为醉时写就,代表作有《望庐山瀑布》《行路难》《蜀道难》《将进酒》《早发白帝城》等")
# 2.定义递归文本分割器
text_splitter = RecursiveCharacterTextSplitter(chunk_size=100,
chunk_overlap=30,
length_function=len,
)
# 3.分割文本
splitter_texts = text_splitter.split_text(content)
# 4.转换为文档对象
splitter_documents = text_splitter.create_documents(splitter_texts)
print(f"分割文档数量:{len(splitter_documents)}")
for splitter_document in splitter_documents:
print(f"文档片段大小:{len(splitter_document.page_content)}, 文档元数据:{splitter_document.metadata}")
执行结果如下,文本分割器将文本内容分割成了四个文本片段,且内容长度最大为100个字符。
分割文档数量:4
文档片段大小:100, 文档元数据:{}
文档片段大小:100, 文档元数据:{}
文档片段大小:100, 文档元数据:{}
文档片段大小:70, 文档元数据:{}
2.2 分割文档对象
RecursiveCharacterTextSplitter不仅可以分割纯文本,还可以直接分割Document对象,使用示例如下:
from langchain_community.document_loaders import UnstructuredFileLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 1.创建文档加载器,进行文档加载
loader = UnstructuredFileLoader(file_path="李白.txt")
documents = loader.load()
# 2.定义递归文本分割器
text_splitter = RecursiveCharacterTextSplitter(chunk_size=100,
chunk_overlap=30,
length_function=len,
)
# 3.分割文本
splitter_documents = text_splitter.split_documents(documents)
print(f"分割文档数量:{len(splitter_documents)}")
for splitter_document in splitter_documents:
print(f"文档片段大小:{len(splitter_document.page_content)}, 文档元数据:{splitter_document.metadata}")
执行结果如下:
分割文档数量:4
文档片段大小:90, 文档元数据:{'source': '李白.txt'}
文档片段大小:70, 文档元数据:{'source': '李白.txt'}
文档片段大小:53, 文档元数据:{'source': '李白.txt'}
文档片段大小:69, 文档元数据:{'source': '李白.txt'}
2.3 自定义分隔符
RecursiveCharacterTextSplitter默认按照["\n\n", "\n", " ", ""]的优先级进行分割,可以通过separators指定自定义分隔符。
# 2.定义递归文本分割器
text_splitter = RecursiveCharacterTextSplitter(chunk_size=100,
chunk_overlap=30,
length_function=len,
separators=["。", "?", "\n\n", "\n", " ", ""]
)
三、按标题分割Markdown文件
在对Markdown格式的文档进行分割时,一般不能像RecursiveCharacterTextSplitter默认分割规则方式进行分割,通常需要按照标题层次进行分割,LangChain提供了MarkdownHeaderTextSplitter类来实现这个功能。
在对Markdown文件进行分割时,对于那些很长的文档,可以先利用MarkdownHeaderTextSplitter按标题分割,将分割后的文档再使用RecursiveCharacterTextSplitter进行分割,使用示例如下:
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter
# 1.文档加载
loader = TextLoader(file_path="李白.md")
documents = loader.load()
document_text = documents[0].page_content
# 2.定义文本分割器,设置指定要分割的标题
headers_to_split_on = [
("#", "Header 1"),
("##", "Header 2")
]
headers_text_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
# 3.按标题分割文档
headers_splitter_documents = headers_text_splitter.split_text(document_text)
print(f"按标题分割文档数量:{len(headers_splitter_documents)}")
for splitter_document in headers_splitter_documents:
print(f"按标题分割文档片段大小:{len(splitter_document.page_content)}, 文档元数据:{splitter_document.metadata}")
# 4.定义递归文本分割器
text_splitter = RecursiveCharacterTextSplitter(chunk_size=100,
chunk_overlap=30,
length_function=len
)
# 5.递归分割文本
recursive_documents = text_splitter.split_documents(headers_splitter_documents)
print(f"第二次递归文本分割文档数量:{len(recursive_documents)}")
for recursive_document in recursive_documents:
print(
f"第二次递归文本分割文档片段大小:{len(recursive_document.page_content)}, 文档元数据:{recursive_document.metadata}")
执行结果如下,先用MarkdownHeaderTextSplitter将markdown文本内容分割成4个文档,之后在对每一个文档使用RecursiveCharacterTextSplitter进行分割,分割成了11个文档,并且在文档元数据中,还添加了文本片段所属的标题信息。
按标题分割文档数量:4
按标题分割文档片段大小:124, 文档元数据:{'Header 1': '一、李白简介'}
按标题分割文档片段大小:248, 文档元数据:{'Header 1': '二、生平'}
按标题分割文档片段大小:182, 文档元数据:{'Header 1': '三、代表作品'}
按标题分割文档片段大小:200, 文档元数据:{'Header 1': '四、影响与评价'}
第二次递归文本分割文档数量:11
第二次递归文本分割文档片段大小:100, 文档元数据:{'Header 1': '一、李白简介'}
第二次递归文本分割文档片段大小:54, 文档元数据:{'Header 1': '一、李白简介'}
第二次递归文本分割文档片段大小:68, 文档元数据:{'Header 1': '二、生平'}
第二次递归文本分割文档片段大小:76, 文档元数据:{'Header 1': '二、生平'}
第二次递归文本分割文档片段大小:99, 文档元数据:{'Header 1': '二、生平'}
第二次递归文本分割文档片段大小:33, 文档元数据:{'Header 1': '二、生平'}
第二次递归文本分割文档片段大小:92, 文档元数据:{'Header 1': '三、代表作品'}
第二次递归文本分割文档片段大小:89, 文档元数据:{'Header 1': '三、代表作品'}
第二次递归文本分割文档片段大小:88, 文档元数据:{'Header 1': '四、影响与评价'}
第二次递归文本分割文档片段大小:54, 文档元数据:{'Header 1': '四、影响与评价'}
第二次递归文本分割文档片段大小:56, 文档元数据:{'Header 1': '四、影响与评价'}
四、自定义文本分割器
当内置的的文本分割器无法满足业务需求时,可以继承TextSplitter类来实现自定义分割器,不过一般需要自定义文本分割器的情况非常少,
假设我们有如下需求,在对文本分割时,按段落进行分割,并且每个段落只提取第一句话,下面通过实现自定义文本分割器,来实现这个功能,示例如下:
from typing import List
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import TextSplitter
class CustomTextSplitter(TextSplitter):
def split_text(self, text: str) -> List[str]:
text = text.strip()
# 1.按段落进行分割
text_array = text.split("\n\n")
result_texts = []
for text_item in text_array:
strip_text_item = text_item.strip()
if strip_text_item is None:
continue
# 2.按句进行分割
result_texts.append(strip_text_item.split("。")[0])
return result_texts
# 1.文档加载
loader = TextLoader(file_path="李白.md")
documents = loader.load()
document_text = documents[0].page_content
# 2.定义文本分割器
splitter = CustomTextSplitter()
# 3.文本分割
splitter_texts = splitter.split_text(document_text)
for splitter_text in splitter_texts:
print(
f"文本分割片段大小:{len(splitter_text)}, 文本内容:{splitter_text}")
执行结果:
文本分割片段大小:8, 文本内容:# 一、李白简介
文本分割片段大小:43, 文本内容:李白(701年—762年),字太白,号青莲居士,唐代伟大的浪漫主义诗人,被誉为“诗仙”
文本分割片段大小:6, 文本内容:# 二、生平
文本分割片段大小:35, 文本内容:李白出生于绵州昌隆县(今四川江油),自幼聪慧过人,六岁能诵诗,十岁能文
文本分割片段大小:8, 文本内容:# 三、代表作品
文本分割片段大小:25, 文本内容:- 《将进酒》:这是一首最能体现李白豪放性格的诗作
文本分割片段大小:9, 文本内容:# 四、影响与评价
文本分割片段大小:24, 文本内容:李白的诗作气势奔放、意境开阔,对后世文学影响深远
五、总结
本文详细介绍了LangChain中文本分割器的概念和用法。文本分割器是实现RAG的重要组件,它可以将长文本分割成适合模型处理的小片段,同时保持文本的语义完整性。
在本文中,重点介绍了RecursiveCharacterTextSplitter递归文本分割器,它是最常用的通用分割器,能够按照字符优先级进行递归文本分割。对于Markdown格式的文档,MarkdownHeaderTextSplitter能够按照标题层次进行结构化分割,保证文本分割的层次性。
当内置的分割器无法满足特定需求时,我们可以通过继承TextSplitter类来实现自定义分割器,灵活的处理各种文本分割需求。
选择合适的文本分割策略对RAG应用的效果至关重要。在实际项目中,建议根据文档的特点和业务需求来选择或组合使用不同的分割器,来到最佳的文本处理效果。
在下一篇文章中,我们将介绍LangChain中的文本嵌入模型embeddings组件敬请期待。