在构建与 LLM(大型语言模型)相关的应用时,「分块」(chunking)是将大段文本拆分成更小片段的过程。这是一种至关重要的技术,它在使用 LLM 嵌入内容并从 向量数据库中获取结果时,有助于优化内容相关性。在这篇博客中,我们将探讨分块是否以及如何改善 LLM 相关应用中的效率和准确性。
如我们所知,在 Pinecone 中索引的所有内容,都需要先进行 嵌入处理。分块的主要目的是确保嵌入的内容片段具有尽可能少的噪声,同时保持语义上的相关性。
例如,在语义搜索中,我们会索引一组文档,每个文档都包含关于特定主题的重要信息。通过应用有效的分块策略,我们能够确保搜索结果准确反映用户查询的核心内容。如果分块过小或过大,可能导致搜索结果不准确或错失展示相关内容的机会。一般而言,如果某段分块的文本在没有周围上下文的情况下对人类而言是有意义的,那么对语言模型来说,同样也是有意义的。因此,为语料库中的文档找到最佳的分块大小,对于确保搜索结果的准确性和相关性至关重要。
另一个例子是对话式代理(我们之前用 Python 和 Javascript 介绍过)。在这种情况下,我们使用嵌入的分块来根据知识库为对话代理构建上下文,使其基于可信信息「扎根」。在这种场景下,选择正确的分块策略非常重要,有两个原因:第一,分块策略将决定上下文是否与我们的提示密切相关;第二,分块策略将决定我们在发送数据到外部模型提供商(例如 OpenAI)之前,是否能够将检索到的文本内容适配到上下文中,这需要考虑每次请求的 token 数量限制。在某些情况下,例如使用具有 32k 上下文窗口的 GPT-4 时,适配分块可能不是问题。然而,当我们使用较大的分块时,需要注意这可能会对从 Pinecone 获取的结果相关性产生负面影响。
在这篇文章中,我们将探讨几种分块方法,并讨论在选择分块大小和方法时应考虑的权衡点。最后,我们将就适合不同应用的最佳分块大小与方法给出一些建议。
开始免费使用 Pinecone
Pinecone 是开发者们的首选 向量数据库,在各种规模下都快速且易于使用。
当我们对内容进行嵌入时,短内容(例如句子)和长内容(例如段落或整个文档)可能会呈现出截然不同的行为。
当嵌入单句时,生成的向量会专注于句子的特定含义。与其他句子嵌入进行对比时,这种嵌入自然会以句子的层级进行比较。然而,这也意味着,对于段落或文档中存在的更广泛的上下文信息,句子嵌入可能无法捕捉。
当嵌入整段或整个文档时,嵌入过程则会考虑文本的整体上下文,以及句子与短语之间的关系。这可以生成更全面的向量表示,捕捉文本的广义内涵和主题。然而,较大的输入文本可能会引入噪声,或者稀释个别句子或短语的重要性,从而使对索引的查询时更难找到精准的匹配。
查询的长度也会影响嵌入之间的关系。较短的查询(比如一个句子或短语)会更专注于具体信息,可能更适合与句子级别的嵌入进行匹配。而较长的查询(超过一个句子或一个段落)可能会与段落或文档级别的嵌入更吻合,因为它很可能在寻找更广泛的上下文或主题。
索引也可能是非同质化的,其中可能包含大小各异的嵌入分块。这可能在查询结果的相关性方面带来挑战,但也可能带来一些积极的作用。一方面,由于长短内容的语义表示存在差异,查询结果的相关性可能会波动。另一方面,非同质化的索引可能捕获更广泛的上下文和信息,因为不同大小的分块代表了文本中不同层级的细粒度。这可以更灵活地适应不同类型的查询。
决定最佳分块策略时,有多种变量需要考虑,而这些变量会随着使用场景而变化。以下是几个需要注意的关键因素:
回答这些问题可以帮助您制定一个在性能与准确性之间找到平衡的分块策略,从而确保查询结果更加相关。
分块有多种方法,每种方法可能适用于不同的场景。通过评估每种方法的优劣,我们的目标是识别合适的应用场景。
这是最常见且最简单的分块方式:我们决定分块中 token 数量,以及是否需要分块间重叠内容。通常,我们希望分块间保留一些重叠部分,以确保语义上下文不会丢失。固定大小分块适用于大多数常见情况。与其他分块形式相比,固定大小分块在计算上开销较小,且使用方式简单,不需要额外使用 NLP 库。
以下是用 LangChain 执行固定大小分块的示例:
text = "..." # 您的文本
from langchain.text_splitter import CharacterTextSplitter
text_splitter = CharacterTextSplitter(
separator = "\n\n",
chunk_size = 256,
chunk_overlap = 20
)
docs = text_splitter.create_documents([text])
此类方法利用内容的特性,应用更复杂的分块策略。以下是一些示例:
如前所述,许多模型对单句内容的嵌入优化效果较好。因此,自然会采用句子分块策略,有多种方法和工具可以实现句子拆分,包括:
text = "..." # 您的文本
docs = text.split(".")
text = "..." # 您的文本
from langchain.text_splitter import NLTKTextSplitter
text_splitter = NLTKTextSplitter()
docs = text_splitter.split_text(text)
text = "..." # 您的文本
from langchain.text_splitter import SpacyTextSplitter
text_splitter = SpacyTextSplitter()
docs = text_splitter.split_text(text)
递归分块通过一组分隔符以层次结构和迭代方式将输入文本分解为更小的分块。如果首次拆分文本未达到预期的分块大小或结构,该方法会使用不同的分隔符或标准递归调用自身,直到达到期望的分块大小或结构。这意味着,这些分块虽然大小不会完全相同,但仍然会「追求」相似的大小。
以下是在 LangChain 中使用递归分块的示例:
text = "..." # 您的文本
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size = 256,
chunk_overlap = 20
)
docs = text_splitter.create_documents([text])
某些情况下,我们会遇到结构化或格式化的内容,例如 Markdown 和 LaTeX。在这种情况下,可以使用专门的分块方法,以在分块过程中保持内容的原始结构。
from langchain.text_splitter import MarkdownTextSplitter
markdown_text = "..."
markdown_splitter = MarkdownTextSplitter(chunk_size=100, chunk_overlap=0)
docs = markdown_splitter.create_documents([markdown_text])
from langchain.text_splitter import LatexTextSplitter
latex_text = "..."
latex_splitter = LatexTextSplitter(chunk_size=100, chunk_overlap=0)
docs = latex_splitter.create_documents([latex_text])
由 Greg Kamradt 首次提出的语义分块提供了一种实验性的分块技术。在他的笔记本中,Kamradt 准确指出,统一的分块大小可能过于笼统,无法考虑文档中段落的语义信息。如果我们使用这种机制,就无法知道是否将相关主题的片段聚合在一起。
幸运的是,如果您正在构建基于 LLM 的应用程序,您很可能已经拥有了生成嵌入的能力——而嵌入可以用来提取数据中存在的语义含义。通过语义分析,您可以创建由包含相同主题或主题的句子组成的分块。
以下是语义分块的工作步骤:
LangChain 基于 Kamradt 的工作实现了 语义分块拆分器。您还可以试试 我们关于 RAG 高级分块方法的笔记本。
以下是一些建议,帮助您确定优化的分块大小,尤其是当固定分块方法不适用时。
在大多数情况下,对内容进行分块非常简单——但当您开始走向更复杂的场景时,分块可能会带来挑战。分块并没有通用的解决方案,因此适合一种使用场景的方法可能不适用于另一种,希望这篇文章能帮助您更好地理解如何为您的应用规划分块策略。