文章

LLM 应用中的分块策略 [译]

在构建与 LLM 相关的应用的背景下,分块是将大块文本分解为更小段落的过程。这是一项基本技术,可以帮助优化我们从向量数据库获得的内容的相关性,一旦我们使用 LLM 来嵌入内容。在本博客文章中,我们将探讨它是否以及如何帮助提高 LLM 相关应用的效率和准确性。

我们知道,我们在 Pinecone 中索引的任何内容都需要先嵌入。分块的主要原因是确保我们嵌入的内容尽可能少的噪声,但仍然具有语义相关性。

示例

例如,在语义搜索中,我们索引了一系列文档,每个文档都包含了关于特定主题的有价值信息。通过应用有效的分块策略,我们可以确保我们的搜索结果准确捕捉用户查询的本质。如果我们的块太小或太大,可能会导致搜索结果不准确,或错过显示相关内容的机会。按照经验法则,如果一个文本块在没有周围上下文的情况下对人类有意义,那么它对语言模型也是有意义的。因此,找到语料库中文档的最佳块大小对确保搜索结果的准确性和相关性至关重要。

另一个例子是会话代理(我们之前使用 Python 和 Javascript 讨论过)。我们使用嵌入的块来根据知识库构建会话代理的上下文,这个知识库将代理置于可信的信息中。在这种情况下,选择正确的分块策略很重要,有两个原因:首先,它将决定上下文是否真正与我们的提示相关。其次,它将决定我们是否能够在将其发送给外部模型提供商(例如,OpenAI)之前将检索到的文本拟合到上下文中,考虑到我们每次请求可以发送的令牌数量的限制。在某些情况下,比如使用具有 32k 上下文窗口的 GPT-4,拟合块可能不是问题。但是,我们需要注意当我们使用非常大的块时,这可能会对我们从 Pinecone 获取的结果的相关性产生负面影响。

在这篇文章中,我们将探讨几种分块方法,并讨论您在选择分块大小和方法时应考虑的权衡。最后,我们将给出一些建议,帮助您确定适合您的应用程序的最佳分块大小和方法。

嵌入短内容和长内容

当我们嵌入我们的内容时,我们可以预见根据内容是短的(如句子)还是长的(如段落或整个文档),会有不同的行为。

  • 短句子嵌入
    当一个句子被嵌入时,结果向量将专注于句子的特定含义。与其他句子嵌入比较时,比较将自然地在该级别进行。这也意味着嵌入可能会错过段落或文档中找到的更广泛的背景信息。

  • 全段落或文档嵌入
    当嵌入整个段落或文档时,嵌入过程将考虑整体背景和文本内的句子和短语之间的关系。这可能会导致更全面的向量表示,捕捉文本的更广泛的含义和主题。另一方面,较大的输入文本大小可能会引入噪声或稀释单个句子或短语的重要性,使得在查询索引时找到精确匹配更加困难。

查询的长度也会影响嵌入之间的关系。较短的查询,如单个句子或短语,将专注于细节,可能更适合与句子级嵌入进行匹配。一个跨越多个句子或一个段落的更长的查询可能更符合段落或文档级的嵌入,因为它可能在寻找更广泛的背景或主题。

索引也可能是非均质的,并包含不同大小的块的嵌入。这可能在查询结果相关性方面带来挑战,但也可能有一些积极的影响。一方面,由于长短内容的语义表示之间的差异,查询结果的相关性可能会波动。另一方面,非均质索引可能会捕捉更广泛的背景和信息,因为不同的块大小代表了文本中不同级别的粒度。这可以更灵活地适应不同类型的查询。

分块考虑因素

有几个变量在确定最佳分块策略时起作用,这些变量根据用例而异。以下是一些需要记住的关键方面:

  • 内容的性质是什么? 您是在处理长文档,如文章或书籍,还是更短的内容,如推文或即时消息?答案将决定哪种模型更适合您的目标,从而决定应用哪种分块策略。
  • 您正在使用哪种嵌入模型,它在什么样的块大小上表现最好? 例如,句子转换器模型在单个句子上工作良好,但像 text-embedding-ada-002 这样的模型在包含 256 或 512 令牌的块上表现更好。
  • 您对用户查询的长度和复杂度有什么期望? 他们会短而具体,还是长而复杂?这也可能影响您选择分块内容的方式,以便嵌入查询和嵌入块之间有更紧密的相关性。
  • 检索到的结果将如何在您的特定应用程序中使用? 例如,它们将用于语义搜索、问题回答、摘要还是其他目的?例如,如果您的结果需要输入另一个具有令牌限制的 LLM,则您需要考虑这一点,并根据您想要适应 LLM 请求的块数量来限制块的大小。

回答这些问题将允许您开发一个平衡性能和准确性的分块策略,从而确保查询结果更为相关。

分块方法

有不同的分块方法,每种方法可能适用于不同的情况。通过检查每种方法的优点和缺点,我们的目标是确定适合应用它们的正确场景。

固定大小分块

这是最常见和最简单的分块方法:我们简单地决定我们的块中的令牌数量,以及它们之间是否应该有任何重叠。通常,我们会希望块之间保持一些重叠,以确保语义上下文不会在块之间丢失。在大多数常见情况下,固定大小分块将是最佳路径。与其他形式的分块相比,固定大小分块计算成本低,使用简单,因为它不需要使用任何 NLP 库。

这是一个使用 LangChain 进行固定大小分块的示例:

1
2
3
4
5
6
7
8
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])

「内容感知」分块

这些是一套方法,可以利用我们正在分块的内容的性质,并对其应用更复杂的分块。以下是一些示例:

句子分割

正如我们之前提到的,许多模型都是为嵌入句子级内容而优化的。自然,我们会使用句子分块,有几种方法和工具可以做到这一点,包括:

  • 朴素分割
    最朴素的方法是通过句点(.)和换行符来分割句子。虽然这可能快速简单,但这种方法不会考虑所有可能的边缘情况。以下是一个非常简单的示例:
    1
    2
    
    text = "..." 
    docs = text.split(".")
    
  • NLTK
    Natural Language Toolkit(NLTK)是一个用于处理人类语言数据的流行 Python 库。它提供了一个句子分词器,可以将文本分割成句子,帮助创建更有意义的块。例如,要使用 LangChain 和 NLTK,您可以执行以下操作:
    1
    2
    3
    4
    
    text = "..." 
    from langchain.text_splitter import NLTKTextSplitter
    text_splitter = NLTKTextSplitter()
    docs = text_splitter.split_text(text)
    
  • spaCy
    spaCy 是另一个强大的用于 NLP 任务的 Python 库。它提供了一个复杂的句子分割功能,可以高效地将文本分割成单独的句子,使得在生成的块中更好地保留上下文。例如,要使用 LangChain 和 spaCy,您可以执行以下操作:
    1
    2
    3
    4
    
    text = "..." 
    from langchain.text_splitter import SpacyTextSplitter
    text_splitter = SpaCyTextSplitter()
    docs = text_splitter.split_text(text)
    

递归分块

递归分块使用一组分隔符以层次和迭代的方式将输入文本分割成更小的块。如果初次尝试分割文本没有产生所需的大小或结构的块,该方法将递归地在结果块上调用自身,使用不同的分隔符或条件,直到达到所需的块大小或结构。这意味着,虽然块不会完全相同大小,但它们仍然「渴望」具有类似的大小。

以下是如何使用 LangChain 进行递归分块的示例:

1
2
3
4
5
6
7
8
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 语法(例如,标题、列表和代码块),您可以根据其结构和层次智能地划分内容,从而得到更具语义连贯性的块。例如:
    1
    2
    3
    4
    
    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 命令和环境,您可以创建尊重内容逻辑组织的块(例如,节、小节和方程式),从而得到更准确和上下文相关的结果。例如:
    1
    2
    3
    4
    
    from langchain.text_splitter import LatexTextSplitter
    latex_text = "..."
    latex_splitter = LatexTextSplitter(chunk_size=100, chunk_overlap=0)
    docs = latex_splitter.create_documents([latex_text])
    

确定您应用程序的最佳块大小

如果常见的分块方法,如固定分块,不容易适用于您的用例,以下是一些提示,可以帮助您找出最佳的块大小。

  1. 预处理您的数据 - 在确定您的应用程序的最佳块大小之前,您需要首先预处理您的数据以确保质量。例如,如果您的数据是从网上检索的,您可能需要删除 HTML 标签或仅添加噪声的特定元素。
  2. 选择一系列块大小 - 一旦您的数据被预处理,下一步是选择一系列潜在的块大小进行测试。如前所述,选择应考虑内容的性质(例如,短消息或冗长的文档)、您将使用的嵌入模型及其功能(例如,令牌限制)。目标是找到一个在保留上下文和保持准确性之间找到平衡的点。从探索一系列块大小开始,包括较小的块(例如,128 或 256 令牌)以捕捉更细粒度的语义信息,以及较大的块(例如,512 或 1024 令牌)以保留更多的上下文。
  3. 评估每个块大小的性能 - 为了测试各种块大小,您可以使用多个索引或一个带有多个命名空间的单个索引。使用代表性的数据集,为您想要测试的块大小创建嵌入,并将它们保存在您的索引(或索引)中。然后,您可以运行一系列可以评估质量的查询,并比较各种块大小的性能。这很可能是一个迭代的过程,您可以测试不同的块大小针对不同的查询,直到您可以确定您的内容和预期查询的最佳性能块大小。

结论

在大多数情况下,分块您的内容是相当简单的 - 但当您开始偏离常规路径时,它可能会带来一些挑战。分块没有一刀切的解决方案,所以适用于一个用例的方法可能不适用于另一个。希望本文能帮助您更好地了解如何为您的应用程序选择分块方法。



原文作者:Roie Schwaber-Cohen
原文链接:https://www.pinecone.io/learn/chunking-strategies

本文由作者按照 CC BY 4.0 进行授权