RAG 核心技术解析:分块的奥秘、策略与最佳实践

117 阅读7分钟

摘要:检索增强生成的核心在于“检索”,而检索的基石则是“分块”。本文将深入探讨 RAG 中分块的概念、必要性、多种策略、代码实现以及最佳实践,帮助你构建更高效、更准确的 RAG 系统。

1. 什么是分块?

在 RAG 系统中,分块 是指将原始的、冗长的文档数据(如 PDF、Word、TXT 等)分割成更小、更易于管理的片段的过程。这些片段被称为 “块”

一个简单的比喻: 想象一下你正在准备一场开卷考试,考试允许你带一本教科书。你不会把整本书一页不差地直接塞进脑子里去答题,而是会根据问题,快速翻到相关的章节、段落或页面来寻找答案。在这里,整本书就是你的知识库,而章节、段落就是“块”。分块的过程,就是为这本书创建一个精细的“目录”或“索引”。

在技术实现上,分块通常发生在文档被向量化之前。原始文本被分割成块后,每个块都会被单独编码成向量,并存入向量数据库。

2. 为什么需要分块?

分块并非可选项,而是构建高效 RAG 系统的关键前提。其主要原因有以下几点:

  1. 突破模型上下文窗口限制

    • 所有大型语言模型都有一个固定的上下文窗口(例如 4K, 8K, 16K, 128K tokens)。这意味着模型在一次交互中,能够“看到”的文本量是有限的。
    • 如果我们试图将一本几百页的书整个塞给模型,不仅会远超其处理能力,即使能处理,成本也极高。分块确保了每个检索到的信息块都能被轻松地放入模型的上下文窗口中。
  2. 提升检索精度与相关性

    • 检索的目标是找到与用户问题最相关的信息。如果一个文档块包含了太多不相关的主题,那么即使用户问题只与其中一小部分相关,这个“大杂烩”块也可能因为包含了一些关键词而被检索出来,导致答案噪声大、不精准。
    • 小而专注的块能更精确地匹配查询的意图。例如,对于问题“Python 中如何连接字符串?”,检索到一个专门讲解 + 号和 join() 方法的块,远比检索到一个从变量定义讲到面向对象的庞大章节要精准得多。
  3. 改善生成答案的质量

    • 提供给模型的上下文信息越集中、越相关,模型就越容易生成准确、切题且简洁的答案。它不需要从一大段文本中费力地“挑出”关键信息,从而减少了产生幻觉或答非所问的概率。
  4. 平衡计算效率与成本

    • 更小的块意味着更短的向量序列,这使得向量数据库的相似性搜索更快、更高效。
    • 同时,在生成阶段,处理更短的上下文也意味着更低的计算成本和更快的响应速度。

为了更直观地理解分块在 RAG 流程中的位置,我们来看下面的流程图:

deepseek_mermaid_20251024_c51ed2.png

3. 分块的核心策略与技术

分块看似简单,但“如何分”却是一门艺术,直接决定了 RAG 系统的性能。以下是几种主流的策略:

3.1. 固定大小分块

这是最简单、最常用的方法。它通过设置一个固定的块大小(如 token 数或字符数)和一个可选的块重叠大小来分割文本。

  • 优点:实现简单,计算高效。
  • 缺点:可能生硬地切断完整的句子或段落,破坏语义完整性。

代码示例(使用 LangChain):

from langchain.text_splitter import CharacterTextSplitter

# 加载原始文档
with open(‘example.txt‘, ‘r‘, encoding=‘utf-8‘) as f:
    text = f.read()

# 创建固定大小的文本分割器
# chunk_size: 每个块的字符数
# chunk_overlap: 块与块之间的重叠字符数。这有助于保持上下文的连贯性。
text_splitter = CharacterTextSplitter(
    separator = “\n\n”,  # 可选的分隔符,优先按双换行分
    chunk_size = 500,
    chunk_overlap = 50
)

# 执行分块
chunks = text_splitter.split_text(text)

# 打印结果
for i, chunk in enumerate(chunks):
    print(f“--- Chunk {i} (Length: {len(chunk)}) ---”)
    print(chunk)
    print(“\n”)
3.2. 递归分块/按分隔符分块

这是一种更先进的方法,它尝试利用文本中固有的结构(如段落、标题等)进行分块。它使用一个分隔符层次结构(例如,先按 \n\n 分,再按 \n 分,再按 . 分),递归地将文本分割成越来越小的块,直到块的大小达到预设值。

  • 优点:能更好地保留文本的语义边界,生成质量更高的块。
  • 缺点:实现比固定大小分块稍复杂。

代码示例(使用 LangChain 的 RecursiveCharacterTextSplitter):

from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    # 设置一个理想的目标块大小
    chunk_size = 500,
    chunk_overlap = 50# 分隔符优先级列表
    separators = [“\n\n”, “\n”, “。”, “!”, “?”, “, ”, “ “, “”] 
)

chunks = text_splitter.split_text(text)
3.3. 语义分块

这是最复杂但也可能最有效的方法。它不仅仅依赖于表面形式,而是试图在语义发生明显转变的地方进行分割。例如,它可能会使用 NLP 模型来检测主题的变化,并在主题边界处进行分块。

  • 优点:理论上能产生语义最连贯的块。
  • 缺点:计算成本高,实现复杂,依赖于额外的模型。
3.4. 专用分块器

对于特定格式的文档,有更优的分块方案:

  • 代码分块器:能够理解编程语言的语法(如 Python、JavaScript),按函数、类等结构进行分块。
  • Markdown 分块器:能够解析 Markdown 标题和结构,生成具有层次结构的块。

4. 分块的最佳实践与挑战

最佳实践:

  1. 块大小是黄金参数:没有放之四海而皆准的大小。需要根据你的文档类型、查询性质和使用场景进行实验。
    • 小块(128-512 tokens):适用于事实性问答、搜索,精度高。
    • 中块(512-1024 tokens):通用性较好。
    • 大块(1024+ tokens):适用于需要大量上下文的任务,如摘要、复杂推理。
  2. 使用重叠:重叠是解决“断头台”效应的有效手段。通常设置为块大小的 10%-20%。
  3. Know Your Data:了解你的数据特性。是技术文档?是小说?是法律合同?针对不同数据选择最合适的分隔符和策略。
  4. 多索引实验:对于生产系统,可以尝试用不同的块大小对同一份知识库建立多个索引,让路由逻辑根据查询类型选择最合适的索引。

常见挑战:

  • 上下文碎片化:一个完整的观点或故事被强行分割到不同的块中,导致模型无法看到全貌。
  • 丢失全局结构:过于细粒度的分块可能丢失文档的章节结构、目录等信息。
  • 边界问题:即使在重叠的帮助下,在块的开头或结尾处切断关键信息的情况仍可能发生。

5. 总结

分块是 RAG 管道中一个看似简单却至关重要的组件。它深刻地影响着后续检索和生成的质量。一个精心设计的分块策略,是构建高性能 RAG 应用与一个效果平平的“玩具”项目之间的关键区别。

在实践中,我们建议从 递归分块器 开始,将其作为默认选择,然后根据具体的评估指标(如答案准确性、引用精度)反复迭代和优化你的分块策略。

希望这篇详细的解析能帮助你在 RAG 的道路上走得更稳、更远!欢迎在评论区留言讨论。