大语言模型 (LLM) 技术的飞速发展,为我们带来了前所未有的应用可能性。然而,LLM 的应用并非毫无限制。其中一个关键的挑战在于 LLM 的上下文窗口限制 (Token Limits)。这意味着 LLM 能够处理的文本长度是有限的。为了有效利用 LLM 处理海量信息,例如来自 PDF 文档、Notion 数据库或 Markdown 文件的数据,文档分割 (Document Splitting) 成为一项至关重要的技术。本文将深入探讨在 LangChain 中如何进行文档分割,以及各种分割器的使用方法,帮助你更好地利用 LLM 的强大能力。
为什么文档分割至关重要?
正如开篇所言,LLM 的上下文窗口存在限制。这意味着我们需要将大型文档分解成更小的、可管理的块,才能将其输入 LLM 进行处理。如果没有 文档分割,直接将大型文档输入 LLM 可能会导致以下问题:
- 超出上下文窗口限制: LLM 会截断超出部分,导致信息丢失。
- 处理效率降低: 超长文本会增加 LLM 的处理时间,降低效率。
- 上下文理解不足: LLM 难以理解整个文档的上下文,影响生成结果的质量。
文档分割 的目的在于:
- 将相关内容保留在一起: 确保分割后的文本块包含完整的语义信息。
- 允许 LLM 进行高效处理: 使 LLM 能够在有限的上下文窗口内进行高效推理。
- 保留文档结构: 尽量保持文档的原始结构,例如章节、段落等。
例如,假设你需要使用 LLM 从一份 500 页的 PDF 报告中提取关键信息。如果不进行 文档分割,直接将整个报告输入 LLM,很可能会超出其上下文窗口限制,导致信息丢失或处理失败。而通过 文档分割,将报告分割成若干个章节或段落,再分别输入 LLM,就可以有效解决这个问题,并提升处理效率。
LangChain 中的文档分割器:一览
LangChain 提供了多种 文档分割器,用于将文本分割成不同的块。不同的分割器采用不同的分割策略,适用于不同的文档类型和应用场景。以下是 LangChain 中常见的文档分割器:
- CharacterTextSplitter: 基于特定字符进行分割,例如空格、换行符等。
- RecursiveCharacterTextSplitter: 递归地尝试不同的字符进行分割,直到文本块的大小符合要求。
- TokenTextSplitter: 基于 Token (词元) 进行分割,确保每个文本块包含一定数量的 Token。
- MarkdownHeaderTextSplitter: 专门用于 Markdown 文件的分割器,可以根据标题进行分割。
选择合适的 文档分割器 取决于你的具体需求。例如,如果你的文档是 Markdown 文件,并且你想根据标题进行分割,那么 MarkdownHeaderTextSplitter
可能是最佳选择。如果你的文档是纯文本文件,并且你希望尽可能保留文本的原始结构,那么 RecursiveCharacterTextSplitter
可能会更适合。
CharacterTextSplitter:基于字符的简单分割
CharacterTextSplitter
是最简单的 文档分割器 之一。它基于指定的字符将文本分割成块。你可以指定分割符 ( separator
)、块大小 ( chunk_size
) 和块重叠 ( chunk_overlap
)。
separator
:用于分割文本的字符。例如,空格、换行符等。chunk_size
:每个文本块的最大长度。chunk_overlap
:相邻文本块之间的重叠长度,用于保持上下文的连贯性。
以下是一个使用 CharacterTextSplitter
的示例代码:
from langchain.text_splitter import CharacterTextSplitter
text = "This is a long document. It has many sentences. We want to split it into smaller chunks."
text_splitter = CharacterTextSplitter(
separator="\n",
chunk_size=100,
chunk_overlap=0,
length_function=len,
)
chunks = text_splitter.split_text(text)
print(chunks)
在这个例子中,我们使用换行符 ( \n
) 作为分隔符,将文本分割成最大长度为 100 个字符的块。chunk_overlap
设置为 0,表示相邻文本块之间没有重叠。
CharacterTextSplitter
适用于简单的文本分割场景,例如将纯文本文件分割成段落。但是,它可能无法很好地处理复杂的文档结构,例如 Markdown 文件或 HTML 文件。
RecursiveCharacterTextSplitter:递归分割,保留结构
RecursiveCharacterTextSplitter
是一种更智能的 文档分割器。它递归地尝试不同的字符进行分割,直到文本块的大小符合要求。这意味着它可以更好地保留文档的原始结构,例如章节、段落等。
RecursiveCharacterTextSplitter
使用一个字符列表作为分隔符,并按照列表的顺序依次尝试分割。默认的字符列表如下:
["\n\n", "\n", " ", ""]
这意味着它首先尝试使用两个换行符 ( \n\n
) 进行分割,如果分割后的文本块仍然太大,则尝试使用一个换行符 ( \n
) 进行分割,以此类推,直到文本块的大小符合要求。
以下是一个使用 RecursiveCharacterTextSplitter
的示例代码:
from langchain.text_splitter import RecursiveCharacterTextSplitter
text = """
This is a long document.
It has many sentences.
We want to split it into smaller chunks.
"""
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=100,
chunk_overlap=0,
length_function=len,
)
chunks = text_splitter.split_text(text)
print(chunks)
在这个例子中,我们没有指定分隔符,因此 RecursiveCharacterTextSplitter
使用默认的字符列表进行分割。由于文本中使用了两个换行符 ( \n\n
) 分隔段落,RecursiveCharacterTextSplitter
会优先使用这两个换行符进行分割,从而保留了文档的段落结构。
RecursiveCharacterTextSplitter
适用于需要保留文档结构的文本分割场景,例如分割 Markdown 文件或 HTML 文件。
TokenTextSplitter:基于 Token 的精确控制
TokenTextSplitter
基于 Token (词元) 进行分割,而不是字符。这使得它可以更精确地控制文本块的大小,确保每个文本块包含一定数量的 Token。
Token 是 LLM 处理文本的基本单位。不同的 LLM 使用不同的 Tokenizer (分词器),将文本转换成 Token。因此,在使用 TokenTextSplitter
时,你需要指定 LLM 使用的 Tokenizer。
以下是一个使用 TokenTextSplitter
的示例代码:
from langchain.text_splitter import TokenTextSplitter
text = "This is a long document. It has many sentences. We want to split it into smaller chunks."
text_splitter = TokenTextSplitter(
chunk_size=100,
chunk_overlap=0,
)
chunks = text_splitter.split_text(text)
print(chunks)
在这个例子中,我们没有指定 Tokenizer,因此 TokenTextSplitter
使用默认的 Tokenizer。chunk_size
设置为 100,表示每个文本块最多包含 100 个 Token。
TokenTextSplitter
适用于需要精确控制文本块大小的文本分割场景,例如在微调 LLM 时,需要将训练数据分割成固定大小的 Token 序列。
MarkdownHeaderTextSplitter:针对 Markdown 文件的智能分割
MarkdownHeaderTextSplitter
专门用于 Markdown 文件的分割。它可以根据标题进行分割,将 Markdown 文件分割成若干个章节或段落。
MarkdownHeaderTextSplitter
会识别 Markdown 文件中的标题,并根据标题的级别 (例如 <h1>
, <h2>
, <h3>
等) 进行分割。你可以指定要分割的标题级别。
以下是一个使用 MarkdownHeaderTextSplitter
的示例代码:
from langchain.text_splitter import MarkdownHeaderTextSplitter
markdown_text = """
# Title 1
This is the content of title 1.
## Title 2
This is the content of title 2.
### Title 3
This is the content of title 3.
"""
headers_to_split_on = [
("#", "Header 1"),
("##", "Header 2"),
("###", "Header 3"),
]
text_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on
)
md_header_splits = text_splitter.split_text(markdown_text)
print(md_header_splits)
在这个例子中,我们指定了要分割的标题级别,包括 #
, ##
, ###
。MarkdownHeaderTextSplitter
会根据这些标题将 Markdown 文件分割成若干个章节或段落。
MarkdownHeaderTextSplitter
适用于需要根据标题进行分割的 Markdown 文件。它可以有效地保留 Markdown 文件的结构,并方便后续的处理。
文档分割的实践应用
文档分割 技术在 LLM 应用中有着广泛的应用场景。以下是一些实际的例子:
- 问答系统: 将大型文档分割成块,然后使用 LLM 从相关的文本块中提取答案。例如,你可以将一份法律文件分割成若干个条款,然后使用 LLM 回答用户关于特定条款的问题。
- 文档摘要: 将大型文档分割成块,然后使用 LLM 对每个文本块进行摘要,最后将所有摘要合并成一个总体的摘要。例如,你可以将一篇新闻报道分割成若干个段落,然后使用 LLM 对每个段落进行摘要,最后将所有摘要合并成一个新闻概要。
- 知识图谱构建: 将大型文档分割成块,然后使用 LLM 从每个文本块中提取实体和关系,构建知识图谱。例如,你可以将一份公司年报分割成若干个章节,然后使用 LLM 从每个章节中提取公司名称、产品名称、财务数据等信息,构建公司知识图谱。
- 代码生成: 将大型代码库分割成块,然后使用 LLM 理解代码的结构和功能,并生成新的代码。例如,你可以将一个大型的 Python 项目分割成若干个模块,然后使用 LLM 理解每个模块的功能,并生成新的模块或函数。
优化文档分割策略:提升 LLM 性能的关键
仅仅进行文档分割是不够的,选择合适的分割策略并进行优化,才能真正提升 LLM 的性能。以下是一些优化 文档分割 策略的建议:
- 选择合适的分割器: 根据文档类型和应用场景选择合适的分割器。例如,对于 Markdown 文件,
MarkdownHeaderTextSplitter
可能是最佳选择。对于纯文本文件,RecursiveCharacterTextSplitter
可能会更适合。 - 调整分割参数: 调整分割器的参数,例如
chunk_size
和chunk_overlap
,以获得最佳的分割效果。chunk_size
应该根据 LLM 的上下文窗口大小和文档的复杂度进行调整。chunk_overlap
可以帮助保持上下文的连贯性,但也需要注意不要设置过大,以免造成信息冗余。 - 考虑语义信息: 在分割文档时,尽量考虑语义信息,例如句子、段落、章节等。避免将一个句子或段落分割成两部分,以免影响 LLM 的理解。
- 预处理文档: 在分割文档之前,可以对文档进行预处理,例如去除 HTML 标签、纠正拼写错误、标准化文本格式等。这可以提高分割的准确性和效率。
- 评估分割效果: 在实际应用中,需要评估分割效果,并根据评估结果调整分割策略。例如,你可以使用 LLM 对分割后的文本块进行处理,然后评估 LLM 的性能,并根据评估结果调整
chunk_size
和chunk_overlap
。
总结:文档分割,释放 LLM 的无限可能
文档分割 是使用 LLM 处理大型文档的关键技术。通过将大型文档分割成更小的、可管理的块,我们可以克服 LLM 的上下文窗口限制,并有效地利用 LLM 的强大能力。 LangChain 提供了多种 文档分割器,可以满足不同的需求。选择合适的 文档分割器 并进行优化,可以显著提升 LLM 的性能,并解锁 LLM 的无限可能。 掌握 文档分割 技术,意味着你掌握了打开 LLM 应用大门的钥匙,能够在诸如问答系统、文档摘要、知识图谱构建等众多领域取得突破。 记住,文档分割 不仅仅是一种技术,更是一种思维方式,它引导我们思考如何更有效地利用 LLM 处理海量信息,从而创造更大的价值。