在构建与 LLM(大型语言模型)相关的应用时,「分块」(chunking)是将大段文本拆分成更小片段的过程。这是一种至关重要的技术,它在使用 LLM 嵌入内容并从 向量数据库中获取结果时,有助于优化内容相关性。在这篇博客中,我们将探讨分块是否以及如何改善 LLM 相关应用中的效率和准确性。
如我们所知,在 Pinecone 中索引的所有内容,都需要先进行 嵌入处理。分块的主要目的是确保嵌入的内容片段具有尽可能少的噪声,同时保持语义上的相关性。
例如,在语义搜索中,我们会索引一组文档,每个文档都包含关于特定主题的重要信息。通过应用有效的分块策略,我们能够确保搜索结果准确反映用户查询的核心内容。如果分块过小或过大,可能导致搜索结果不准确或错失展示相关内容的机会。一般而言,如果某段分块的文本在没有周围上下文的情况下对人类而言是有意义的,那么对语言模型来说,同样也是有意义的。因此,为语料库中的文档找到最佳的分块大小,对于确保搜索结果的准确性和相关性至关重要。
另一个例子是对话式代理(我们之前用 Python 和 Javascript 介绍过)。在这种情况下,我们使用嵌入的分块来根据知识库为对话代理构建上下文,使其基于可信信息「扎根」。在这种场景下,选择正确的分块策略非常重要,有两个原因:第一,分块策略将决定上下文是否与我们的提示密切相关;第二,分块策略将决定我们在发送数据到外部模型提供商(例如 OpenAI)之前,是否能够将检索到的文本内容适配到上下文中,这需要考虑每次请求的 token 数量限制。在某些情况下,例如使用具有 32k 上下文窗口的 GPT-4 时,适配分块可能不是问题。然而,当我们使用较大的分块时,需要注意这可能会对从 Pinecone 获取的结果相关性产生负面影响。
在这篇文章中,我们将探讨几种分块方法,并讨论在选择分块大小和方法时应考虑的权衡点。最后,我们将就适合不同应用的最佳分块大小与方法给出一些建议。
开始免费使用 Pinecone
Pinecone 是开发者们的首选 向量数据库,在各种规模下都快速且易于使用。
嵌入短文本与长文本
当我们对内容进行嵌入时,短内容(例如句子)和长内容(例如段落或整个文档)可能会呈现出截然不同的行为。
当嵌入单句时,生成的向量会专注于句子的特定含义。与其他句子嵌入进行对比时,这种嵌入自然会以句子的层级进行比较。然而,这也意味着,对于段落或文档中存在的更广泛的上下文信息,句子嵌入可能无法捕捉。
当嵌入整段或整个文档时,嵌入过程则会考虑文本的整体上下文,以及句子与短语之间的关系。这可以生成更全面的向量表示,捕捉文本的广义内涵和主题。然而,较大的输入文本可能会引入噪声,或者稀释个别句子或短语的重要性,从而使对索引的查询时更难找到精准的匹配。
查询的长度也会影响嵌入之间的关系。较短的查询(比如一个句子或短语)会更专注于具体信息,可能更适合与句子级别的嵌入进行匹配。而较长的查询(超过一个句子或一个段落)可能会与段落或文档级别的嵌入更吻合,因为它很可能在寻找更广泛的上下文或主题。
索引也可能是非同质化的,其中可能包含大小各异的嵌入分块。这可能在查询结果的相关性方面带来挑战,但也可能带来一些积极的作用。一方面,由于长短内容的语义表示存在差异,查询结果的相关性可能会波动。另一方面,非同质化的索引可能捕获更广泛的上下文和信息,因为不同大小的分块代表了文本中不同层级的细粒度。这可以更灵活地适应不同类型的查询。
分块的注意事项
决定最佳分块策略时,有多种变量需要考虑,而这些变量会随着使用场景而变化。以下是几个需要注意的关键因素:
- 被索引内容的性质是什么? 您是在处理长文档,比如文章或书籍,还是较短内容,例如推文或即时消息?答案会决定更适合的模型目标以及相应应采用的分块策略。
- 使用的嵌入模型是什么,它在哪些分块大小范围内表现最佳? 比如,sentence-transformer 模型在单句级别表现良好,而像 text-embedding-ada-002 这样的模型更适合包含 256 或 512 个 tokens 的分块。
- 对用户查询的期望长度和复杂度是什么? 查询是简短而具体的,还是更长且复杂的?这可能影响分块方式的选择,从而更紧密地匹配嵌入的查询与分块。
- 检索到的结果会以何种方式在特定应用中被使用? 比如,这些结果是否用于语义搜索、问答、摘要生成或其他用途。例如,如果结果需要进一步以 token 对数限制导入另一个 LLM,则需要考虑分块的大小,以确保可以将所需分块适配到请求中。
回答这些问题可以帮助您制定一个在性能与准确性之间找到平衡的分块策略,从而确保查询结果更加相关。
分块的方法
分块有多种方法,每种方法可能适用于不同的场景。通过评估每种方法的优劣,我们的目标是识别合适的应用场景。
固定大小分块
这是最常见且最简单的分块方式:我们决定分块中 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(".")
- NLTK:自然语言工具包(NLTK)是一个流行的用于处理人类语言数据的 Python 库。它提供了一种句子标记化工具,可以将文本拆分为句子,帮助创建更具意义的分块。例如,与 LangChain 配合使用:
text = "..." # 您的文本
from langchain.text_splitter import NLTKTextSplitter
text_splitter = NLTKTextSplitter()
docs = text_splitter.split_text(text)
- spaCy:spaCy 是另一个强大的 Python NLP 库。它提供高级的句子分割功能,可以高效地将文本划分为独立的句子,从而在生成的分块中更好地保持语境。例如,与 LangChain 配合使用:
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。在这种情况下,可以使用专门的分块方法,以在分块过程中保持内容的原始结构。
- Markdown:Markdown 是一种常用的轻量级标记语言。通过识别 Markdown 语法(如标题、列表和代码块),您可以根据层级划分内容,从而生成更有语义连贯性的分块。例如:
from langchain.text_splitter import MarkdownTextSplitter
markdown_text = "..."
markdown_splitter = MarkdownTextSplitter(chunk_size=100, chunk_overlap=0)
docs = markdown_splitter.create_documents([markdown_text])
- LaTex:LaTeX 是一种文档准备系统和标记语言,通常用于学术论文和技术文档。通过解析 LaTeX 命令和环境,您可以根据内容的逻辑组织(如章节、子章节和方程式)创建分块,从而生成更准确且语境相关的结果。例如:
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 高级分块方法的笔记本。
为您的应用选择最佳分块大小
以下是一些建议,帮助您确定优化的分块大小,尤其是当固定分块方法不适用时。
- 预处理您的数据: 首先需要对数据进行预处理,以确保数据质量后再确定最佳分块大小。例如,如果您的数据是从网络爬取的,可能需要移除 HTML 标签或特定的噪音元素。
- 选择分块大小的范围: 在预处理完成后,下一步是选择一系列可能的分块大小加以测试。如前所述,选择应该考虑内容的性质(例如短消息或长文档)、所用嵌入模型及其能力(例如 token 限制)。目标是在保留上下文和保持准确性之间找到平衡。可以从探索各种分块大小开始,包括较小(如 128 或 256 tokens)以捕获更多细粒度语义信息的分块,以及较大(如 512 或 1024 tokens)以保留更多上下文的分块。
- 评估每种分块大小的表现: 要测试不同分块大小,可以使用多个索引,或单一索引与多个 命名空间相结合。在代表性数据集上,为要测试的分块大小创建嵌入,并将其保存到索引中。然后运行一系列可以评估质量的查询,对比不同分块大小的性能。这很可能是一个迭代过程,需要针对不同的查询测试不同分块大小,直到找到适合内容和预期查询的最佳分块表现。
总结
在大多数情况下,对内容进行分块非常简单——但当您开始走向更复杂的场景时,分块可能会带来挑战。分块并没有通用的解决方案,因此适合一种使用场景的方法可能不适用于另一种,希望这篇文章能帮助您更好地理解如何为您的应用规划分块策略。