深入分析 RAG 应用中分块(chunking)的各种策略、权衡和最佳实践,这是影响 RAG 系统效果的核心环节。
[编者按:岁末假期期间,我们会稍作休整,并为明年做好准备。因此,我们将重新发布本年度阅读量最高的十篇文章。希望你喜欢我们今年最喜爱的作品,2025 年再见。]
随着你在构建基于 LLM 的应用这条道路上不断深入,可能会发现自己需要让 LLM 的回答以源数据为依据。使用自定义数据对 LLM 进行微调,或许能得到一个理解特定领域的生成式 AI 模型,但它仍可能出现事实错误和幻觉。这促使许多组织开始研究检索增强生成(retrieval-augmented generation,RAG),用特定数据来支撑 LLM 的回答,并提供相应的信息来源。
在 RAG 中,你会为想要引用和检索的数据片段创建文本 embedding。这样就能将源文本片段放入 LLM 用来生成回答的语义空间中。同时,RAG 系统也可以返回源文本,使 LLM 的回答能够以人类创作的文本为依据,并附带引用。
构建 RAG 系统时,你需要格外注意各个数据片段的大小。如何拆分数据被称为 chunking,它比直接对整篇文档进行 embedding 更加复杂。本文将介绍目前业界对于 RAG 系统数据 chunking 的一些思考。
分块后数据的大小,会极大地影响搜索能够找到哪些信息。当你对一段数据进行 embedding 时,整段内容都会被转换成一个向量。如果一个 chunk 中包含的内容太多,向量就会失去准确表达其中任何一个具体主题的能力;如果包含的内容太少,又会丢失数据的上下文。
不要只听我们的一面之词。我们曾在播客中采访 Pinecone 的 Staff Developer Advocate Roie Schwaber-Cohen,讨论了 RAG 和 chunking 的方方面面。Pinecone 是向量数据库领域的领先公司之一。
Schwaber-Cohen 表示:“之所以需要开始思考如何把内容拆分成更小的 chunk,是为了在检索时真正命中正确的内容。你会取得用户的查询,并对它进行 embedding。然后再将其与内容的 embedding 进行比较。如果被 embedding 的内容大小与用户查询的大小相差悬殊,就更有可能得到较低的相似度分数。”
简而言之,大小很重要。
但你必须同时考虑查询和回答的大小。正如 Schwaber-Cohen 所说,你需要将文本 chunk 的向量与查询向量进行匹配。不过,你也需要考虑用来生成回答的 chunk 大小。“假设我 embedding 的是完整一章内容,而不只是一页或一个段落,那么向量数据库会在查询与这一章之间找到某种语义相似性。但整章内容都相关吗?很可能不是。更重要的是,LLM 能否结合检索出的内容和用户的查询,生成相关的回答?有可能可以,也有可能不行。这些内容中或许存在干扰因素,也或许不存在。最终还是取决于具体的 use case。”
如果 chunking 是一个非黑即白的问题,业界应该早就迅速形成统一标准了。但事实上,最佳 chunking 策略取决于具体的 use case。幸运的是,你并不是把数据分块、向量化之后就只能听天由命——你还有 metadata。metadata 可以是指向原始 chunk 或文档更大部分的链接,也可以是分类、标签、文本,实际上可以是任何内容。Schwaber-Cohen 表示:“它有点像一个 JSON blob,你可以用它过滤内容。如果只查找数据中的某个特定子集,就能显著缩小搜索空间;还可以利用 metadata,将回答中使用的内容链接回原始内容。”
围绕这些问题,业界逐渐形成了几种常见的 chunking 策略。最基本的策略是将文本切分成固定大小的 chunk。这种方法适合相对同质化的数据集,其中内容的格式和大小都比较接近,例如新闻文章或博客文章。从所需计算量来看,这是成本最低的方法,但它不会考虑被分块内容的上下文。这一点对你的 use case 可能无关紧要,也可能最终产生巨大影响。
如果数据集并不均匀,包含多种文档类型,也可以使用随机大小的 chunk。这种方法不依赖任何特定文档类型的惯例,因此可能捕获更丰富的语义上下文和主题。不过,随机 chunk 本质上是一场赌博:内容可能会从句子或段落中间被切断,最终产生毫无意义的文本片段。
对于上述两种方式,都可以通过滑动窗口应用 chunking 方法。也就是说,新的 chunk 不从上一个 chunk 的结尾处开始,而是与上一个 chunk 的内容重叠,并包含其中一部分。这种方式可以更好地捕获每个 chunk 边缘位置的上下文,提升整个系统的语义相关性。代价是需要更多存储空间,而且可能保存重复信息,从而导致搜索时需要额外处理,也让 RAG 系统更难高效地找到正确的信息来源。
这种方法并不适用于某些内容。Schwaber-Cohen 表示:“我不应该为了让内容有意义而不得不重新组合 chunk,那些确实需要放在一起的部分就应该保持完整。例如代码示例。如果直接把一段代码 Markdown 交给 recursive text chunker,得到的将是残缺的代码。”
一种略微复杂的方法会关注内容本身,尽管采用的方式相对朴素。上下文感知的 chunking 会根据句号、逗号、分段等标点和边界来拆分文档;如果内容中包含 Markdown 或 HTML 标签,也可以利用这些标签进行拆分。大多数文本都包含这类语义标记,用来指示哪些字符共同构成了有意义的片段,因此使用它们非常合理。你可以递归地把文档切分成更小、彼此重叠的片段。这样,一章内容会被向量化并建立链接,而其中包含的每一页、每个段落和每个句子也会得到同样的处理。
例如,在 Stack Overflow 实现语义搜索时,我们对 embedding pipeline 进行了配置,将问题、回答和评论视为相互独立的语义 chunk。我们的问答页面具有高度结构化的特点,页面结构本身就承载了大量信息。任何使用 Stack Overflow for Teams 的用户,都可以通过这种语义丰富的结构来组织自己的数据。
虽然上下文感知的 chunking 可以取得不错的效果,但它需要额外的预处理来切分文本。这会增加计算需求,拖慢 chunking 过程。如果你只需要一次性处理一批文档,然后长期从中检索内容,那就不成问题。但如果数据集中的文档会随时间变化,这部分资源开销就可能不断累积。
接下来是自适应 chunking,它将上下文感知的方法提升到了新的层次。它会根据每篇文档的内容进行分块。许多自适应 chunking 技术本身也会使用机器学习,判断每个 chunk 的最佳大小以及 chunk 之间应该在哪里重叠。显然,在这里额外增加一层 ML,会让这种方法产生密集的计算需求,但它也能生成高度定制、充分感知上下文的语义单元。
不过总体而言,Schwaber-Cohen 建议使用更小的 chunk:“我们发现,在大多数情况下,如果能够创建更小、语义连贯,并且与潜在用户查询相对应的单元,通常会获得更好的结果。”
可选的 chunking 策略有很多,因此,要找到最适合自身 use case 的方案,需要进行一些实践。有些人认为,应该为处理的每一篇文档定制 chunking 策略。你可以同时使用多种策略,也可以在一篇文档上递归应用这些策略。但归根结底,目标都是以一种 LLM 能够根据查询字符串进行检索的方式,保存文档及其组成部分的语义。
测试 chunking 方法时,应使用示例查询来检验 RAG 系统的结果。可以通过人工审核和 LLM evaluator 对结果进行评分。确定哪种方法能够持续取得更好的表现之后,还可以根据余弦相似度分数过滤结果,进一步提升效果。
无论最终采用哪种方法,chunking 都只是生成式 AI 技术拼图中的一块。要让 AI 项目取得成功,你还需要 LLM、向量数据库和存储系统。最重要的是,你必须有一个明确的目标,否则你的 GenAI 功能将永远无法走出实验阶段。