LangChain框架入门11:文本分割器实战指南

222 阅读10分钟

在上一篇文章《LangChain框架入门10:一文带你吃透文档加载器》中详细介绍了文档加载器的用法,在实现RAG功能准备阶段,首先将知识库文档信息加载成Document对象之后,一般就会对文档进行分割处理。

在LangChain中提供了许多开箱即用的、能分割多种格式的文本分割器,本文将会对这些文本分割器进行详细介绍。

文中所有示例代码:github.com/wzycoding/l…

一、什么是文本分割器

在RAG应用中,文档加载器将原始文档转换为Document对象后,通常需要对长文档进行分割处理,这是因为大语言模型的上下文窗口是有限的,如果在RAG检索完成之后,直接将检索到的长文档作为上下文传递给模型,可能会超出模型处理的上下文长度,导致信息丢失或回答质量下降,其中,进行文档分割的组件就是文本分割器

文本分割器的主要作用有:

  1. 控制上下文长度:把长文档分割成更小,缩小上下文长度
  2. 提高检索准确性:小的文本片段能提升文档检索的精确度
  3. 保持语义完整性:在分割过程中,能尽量保持文本的语义连贯性

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组件敬请期待。