21_LangChain多类型文件内容分割

136 阅读13分钟

LangChain多类型文件内容分割

引言

在处理文本数据时,文本分割是一个重要的步骤,尤其是在处理长文本或将文本数据输入到机器学习模型中时。LangChain 是一个用于构建大型语言模型应用程序的库,它提供了多种工具和策略来处理文本分割。本教程将详细介绍LangChain中的各种文本分割方法,以及如何根据不同的需求选择合适的分割策略。

1. 文本分割的重要性

文本分割在大型语言模型应用中的重要性体现在以下几个方面:

  1. 处理长文本:大多数语言模型都有输入长度限制,需要将长文本分割成较小的块
  2. 保持语义完整性:合理的分割可以保持文本的语义完整性,提高下游任务的效果
  3. 优化检索效率:在检索增强生成(RAG)应用中,合适的文本分割可以提高检索的准确性
  4. 减少信息丢失:通过设置块之间的重叠,可以减少分割过程中的信息丢失

2. 递归字符文本分割

递归分割(recursively)是LangChain中用于通用文本的推荐工具。它接受一个字符列表作为参数,按顺序尝试在这些字符上进行分割,直到块足够小。

2.1 基本原理

  1. 文本如何分割:根据字符列表进行分割
  2. 块大小如何衡量:根据字符数量进行衡量

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提供了多种文本分割策略,可以根据不同的需求选择合适的方法:

  1. 递归字符分割:通用且灵活的分割方法,适用于大多数文本
  2. 语义分割:根据语义相似性分割文本,保持语义完整性
  3. Markdown标题分割:尊重文档结构,按标题组织分割
  4. Token分割:精确控制token数量,适用于对上下文长度有严格要求的场景

选择合适的分割策略对于构建高效的语言模型应用至关重要。通过合理的文本分割,可以提高模型的理解能力,减少信息丢失,并优化检索效果。