在构建 LLM 应用程序时,分块(Chunking)是将大块文本分解成更小的片段的过程。 这是一项重要的技术,一旦我们使用LLM嵌入内容,它有助于优化我们从矢量数据库返回的内容的相关性。 在这篇博文中,我们将探讨它是否以及如何帮助提高LLM相关请求的效率和准确性。
众所周知,我们在 Pinecone 中索引的任何内容都需要首先嵌入。 分块的主要原因是确保我们嵌入的内容尽可能少,但在语义上仍然相关。
例如,在语义搜索中,我们对文档语料库进行索引,每个文档都包含有关特定主题的有价值的信息。 通过应用有效的分块策略,我们可以确保我们的搜索结果准确地捕捉用户查询的本质。 如果我们的块太小或太大,可能会导致搜索结果不精确或错过显示相关内容的机会。 根据经验,如果文本块在没有周围上下文的情况下对人类有意义,那么它对语言模型也有意义。 因此,找到语料库中文档的最佳块大小对于确保搜索结果的准确性和相关性至关重要。
另一个例子是会话代理(Conversational Agent)。 我们使用嵌入的文本块根据知识库为会话代理构建上下文,该知识库使代理基于可信信息。 在这种情况下,对我们的分块策略做出正确的选择很重要,原因有两个:首先,它将确定上下文是否确实与我们的提示相关。 其次,考虑到我们可以为每个请求发送的令牌数量的限制,它将确定我们是否能够在将检索到的文本发送到外部模型提供商(例如 OpenAI)之前将其放入上下文中。 在某些情况下,例如在 32k 上下文窗口中使用 GPT-4 时,拟合块可能不是问题。 尽管如此,我们需要注意何时使用非常大的块,因为这可能会对我们从 Pinecone 返回的结果的相关性产生不利影响。
在这篇文章中,我们将探讨几种分块方法,并讨论在选择分块大小和方法时应考虑的权衡。 最后,我们将提供一些建议,以确定适合你的应用程序的最佳块大小和方法。
在线工具推荐: Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器
当我们嵌入内容时,我们可以根据内容是短(如句子)还是长(如段落或整个文档)来预测不同的行为。
当嵌入一个句子时,所得向量集中于该句子的特定含义。 与其他句子嵌入相比,比较自然会在该级别上进行。 这也意味着嵌入可能会错过段落或文档中更广泛的上下文信息。
当嵌入完整的段落或文档时,嵌入过程会考虑整体上下文以及文本中句子和短语之间的关系。 这可以产生更全面的矢量表示,捕获文本的更广泛的含义和主题。 另一方面,较大的输入文本可能会引入噪音或削弱单个句子或短语的重要性,从而使在查询索引时找到精确匹配变得更加困难。
查询的长度也会影响嵌入之间的相互关系。 较短的查询(例如单个句子或短语)将专注于细节,并且可能更适合与句子级嵌入进行匹配。 跨越多个句子或段落的较长查询可能更适合段落或文档级别的嵌入,因为它可能正在寻找更广泛的上下文或主题。
该索引也可以是非同质的,并且包含不同大小的块的嵌入。 这可能会在查询结果相关性方面带来挑战,但也可能会产生一些积极的后果。 一方面,由于长内容和短内容的语义表示之间的差异,查询结果的相关性可能会波动。 另一方面,非同质索引可能会捕获更广泛的上下文和信息,因为不同的块大小代表文本中不同的粒度级别。 这可以更灵活地适应不同类型的查询。
有几个变量在确定最佳分块策略方面发挥着作用,这些变量根据用例而变化。 以下是需要牢记的一些关键方面:
回答这些问题将使你能够开发一种平衡性能和准确性的分块策略,这反过来又将确保查询结果更相关。
分块的方法有多种,每种方法可能适合不同的情况。 通过检查每种方法的优点和缺点,我们的目标是确定应用它们的正确场景。
针对不同情况,我们介绍4种在LLM应用开发中常见的文本分块策略:按固定大小分块、按语句分块、递归分块和结构化文档分块。
这是最常见、最直接的分块方法:我们只需决定块中的token数量,以及可选地确定它们之间是否应该有重叠。 一般来说,我们希望在块之间保留一些重叠,以确保语义上下文不会在块之间丢失。 在大多数常见情况下,固定大小的分块将是最佳路径。 与其他形式的分块相比,固定大小的分块计算成本低且易于使用,因为它不需要使用任何 NLP 库。
下面是使用 LangChain 执行固定大小分块的示例:
text = "..." # your 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 = "..." # your text
docs = text.split(".")
自然语言工具包 (NLTK) 是一个流行的 Python 库,用于处理人类语言数据。 它提供了一个句子标记器,可以将文本分割成句子,帮助创建更有意义的块。 例如,要将 NLTK 与 LangChain 结合使用,可以执行以下操作:
text = "..." # your text
from langchain.text_splitter import NLTKTextSplitter
text_splitter = NLTKTextSplitter()
docs = text_splitter.split_text(text)
spaCy 是另一个用于 NLP 任务的强大 Python 库。 它提供了复杂的句子分割功能,可以有效地将文本分割成单独的句子,从而在生成的块中更好地保留上下文。 例如,要将 spaCy 与 LangChain 结合使用,可以执行以下操作:
text = "..." # your text
from langchain.text_splitter import SpacyTextSplitter
text_splitter = SpaCyTextSplitter()
docs = text_splitter.split_text(text)
递归分块使用一组分隔符以分层和迭代的方式将输入文本划分为更小的块。 如果分割文本的初始尝试没有生成所需大小或结构的块,则该方法会使用不同的分隔符或标准在生成的块上递归调用自身,直到达到所需的块大小或结构。 这意味着虽然块的大小不会完全相同,但它们仍然“渴望”具有相似的大小。
以下是如何在 LangChain 中使用递归分块的示例:
text = "..." # your text
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
# Set a really small chunk size, just to show.
chunk_size = 256,
chunk_overlap = 20
)
docs = text_splitter.create_documents([text])
Markdown 和 LaTeX 是你可能遇到的结构化和格式化内容的两个示例。 在这些情况下,我们可以使用专门的分块方法在分块过程中保留内容的原始结构。
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 命令和环境,你可以创建尊重内容逻辑组织的块(例如,部分、小节和方程),从而获得更准确且与上下文相关的结果。 例如:
from langchain.text_splitter import LatexTextSplitter
latex_text = "..."
latex_splitter = LatexTextSplitter(chunk_size=100, chunk_overlap=0)
docs = latex_splitter.create_documents([latex_text])
如果常见的分块方法(例如固定分块)无法轻松应用于你的用例,那么这里有一些提示可以帮助你找到最佳的分块大小。
在大多数情况下,对内容进行分块非常简单 - 但当你开始偏离常规时,它可能会带来一些挑战。 没有一种万能的分块解决方案,因此适用于一种用例的方法可能不适用于另一种用例, 希望这篇文章能够帮助你更好地了解如何为LLM应用程序进行分块。
原文链接:LLM应用的分块策略 — BimAnt