Featured image of post RAG 分块策略:为什么你的知识库答非所问,问题多半出在这里

RAG 分块策略:为什么你的知识库答非所问,问题多半出在这里

RAG 答非所问,往往从文档分块就已经埋下了问题。

主题 RAGLLM工程实践

你搭了一个知识库问答系统:上传文档、建好索引、跑通 demo——然后 LLM 开始答非所问。

你以为是模型不够聪明。换更强的模型,还是不对。

问题很可能出在一个不起眼的环节:文档入库时的分块。LLM 根本没读到该读的内容,模型再强也白搭。

这篇文章把分块这件事讲透:为什么要分块、五种分块策略分别解决什么问题、生产环境到底怎么选。

为什么要分块?三个绕不过去的原因

先把结论摆出来:解析完的文档,不能整篇扔进检索。

原因有三个,每一个都独立成立。

原因一:上下文窗口是有限的

LLM 的上下文窗口再大也有上限。就算现在有 128K、1M 上下文的模型,检索也不是把整个文档库塞进 Prompt——那样成本直接爆炸。

更重要的是,窗口越大,模型越难从中找到准确的那一段。这就是著名的"大海捞针"问题:上下文拉长,检索精度反而下降。

原因二:检索信噪比

用户问的是文档里的一个小问题,但整篇文档有几千字。

整篇匹配会发生什么?不相关的内容把相关内容的信号稀释了。向量检索返回的 Top-K 结果里,噪音占了大多数,相关的那一小段被埋没。

块太大 → 无关信息稀释信号。这是信噪比的第一面。

原因三:Embedding 对长文本的"平均化"

Embedding 模型擅长表示短文本——一两句话的语义最清晰。

文本一长,向量就被"平均"了:整段的语义混在一起,变成一个模糊的方向。检索时,这个模糊向量既匹配不上问题,也匹配不上文档里的任何精确信息。

Embedding 模型对短文本的语义表示最好,长文本会被"平均"成模糊向量。


三个原因叠加,结论只有一个:必须把文档切成合适大小的块

但怎么切,才是真正的问题。切小了语境不够,切大了检索不精——这就是分块的核心矛盾:检索精度 vs 上下文完整性

分块的两个基础参数

在讨论策略之前,先理解所有分块策略都绕不开的两个参数。

参数含义经验值(中文场景)
chunk_size每块的大小300-800 字(约 500-1200 tokens)
chunk_overlap相邻块之间的重叠量chunk_size 的 10%-20%

为什么需要 overlap?看一个例子。

overlap=0 时,如果"BBCC"恰好是完整语义(比如一个概念的完整描述),它被拦腰切断,两块都不完整。检索时哪块都匹配不精准,LLM 拿到的都是残句。

overlap 就是给跨块语义留的缓冲带。

五种分块策略,逐个拆解

理解了参数,进入正题。分块策略从简单到复杂,正好是一条问题驱动的问题链。

策略一:固定大小分块——最简单,也最粗暴

做法:按预设的字符数或 token 数直接切。

# LangChain
text_splitter = CharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
)

优点只有一个:实现最简单、速度最快。

缺点却很致命:完全无视语义边界。可能在句子中间、甚至词语中间切断。

切出来的块,很多是"半个意思"。检索时匹配到半句话,LLM 看着上下文猜——这正是答非所问的来源。

适合日志、代码等结构弱、语义依赖不强的文本。自然语言文档用它,基本等于摆烂。

策略二:递归分块——默认策略,性价比之王

做法:按优先级尝试分隔符,从大到小逐级切。

# LangChain
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separators=["\n\n", "\n", "。", ",", " ", ""]
)

思路:优先在语义最完整的边界(段落、换行、句号)切断,实在超限才降级到更细的分隔符。

优点:在保持语义边界和控制块大小之间取得最佳平衡,通用场景的默认选择。

缺点:分隔符优先级需要按文档类型微调;对复杂结构文档(表格、代码)力不从心。

策略三:语义分块——让 Embedding 参与决策

递归分块是按"形式"切(分隔符)。语义分块按"意思"切。

做法:

  1. 把文本拆成句子
  2. 计算每个句子的 Embedding 向量
  3. 计算相邻句子的语义相似度
  4. 在相似度"断崖"处切分

核心算法是新颖度检测:

novelty(s_i) = 1 - cosine_sim(embed(s_i), embed(context_window))
当 novelty > 均值 + 0.8 * 标准差 → 切分

优点:语义连贯性最好,检索精度最高。这也是为什么生产环境在"疑难块"上会启用它。

缺点:每个句子都要算一次 Embedding,计算成本翻倍。全量用不划算,适合作为精修手段。

工具:semchunk、LlamaIndex SemanticSplitterNodeParser。官方 benchmark 里,semchunk 的 AI 分块模式在 Legal RAG QA 上正确率 37.7%,比 LangChain 递归分块的 34.8% 高约 2.9 个百分点。

策略四:结构感知分块——利用文档自身的结构

如果文档本身有结构,为什么不直接用?

做法:按文档的固有边界切——Markdown 按标题层级、HTML 按标签、代码按函数类定义、对话按轮次。

关键技巧是注入结构元数据:每个 chunk 附带它在文档中的位置路径。

{
  "content": "聚类算法的核心思想是...",
  "metadata": {
    "header_path": "第三章 > 3.2 无监督学习 > 3.2.1 K-Means",
    "section_level": 3
  }
}

好处很实在:

  • 逻辑清晰,切块天然语义完整
  • 检索结果自带上下文——LLM 不光看到这段内容,还知道它来自哪一章哪一节

缺点:依赖文档格式规范。纯文本、扫描件这类无结构文档,它无从下手。

策略五:父子分块——生产环境的首选

前面的策略都在解决一个问题:块大还是块小。父子分块跳出这个取舍——大小都要

做法:

  • 父块:段落/小节级别(500-1000 tokens),提供完整上下文
  • 子块:句子级别(100-200 tokens),负责精确检索

检索流程:用小的找,用大的答。

优点:检索精度与上下文完整性兼得——化解了分块最核心的矛盾。小粒度保证命中精准,大粒度保证 LLM 拿到完整语境,避免"找到了关键句但缺前因后果"。

缺点:存储开销翻倍(父子块都要存向量)、实现复杂度较高。

工具:LlamaIndex 的 auto-merging retriever / recursive retriever 就是这种思路的官方实现。

生产环境怎么选

五种策略各有适用场景,先看总对比。

策略原理优点缺点适用场景
固定大小按字符/token 数硬切最简单、最快无视语义,可能在句中切断日志、代码等弱语义文本
递归分块按分隔符优先级切语义边界与块大小平衡,通用需调分隔符;复杂结构无力通用默认
语义分块用 Embedding 找语义断崖语义最连贯、精度最高计算成本翻倍高质量问答、疑难块精修
结构感知按文档标题/标签/轮次切语义完整、自带上下文依赖文档格式规范Markdown/HTML/代码文档
父子分块子块检索、父块作答精度与上下文兼得存储翻倍、实现复杂生产环境首选

单看这张表,你可能会问:到底用哪个?

答案是:生产环境用混合分块。单一策略覆盖不了所有文档类型——你的知识库里既有 Markdown 规范文档,也有扫描 PDF,还有代码片段。

混合分块的思路:结构感知先粗切出框架,块大小判断决定后续处理(过小合并、合适直接用、过大递归/语义细分),最后用父子分块建立层级,注入元数据。

落地建议

回到开头的场景——知识库答非所问,怎么排查?

按这个顺序自查,90% 的问题能定位:

  1. 先看分块:打开一个 chunk 看看,句子是不是被拦腰切断?上下文是不是残缺?
  2. 默认从递归分块起步:chunk_size 500-800、overlap 10%-20%,把基线跑通
  3. 结构化文档换结构感知:你的文档有标题层级,就别浪费这个信息
  4. 检索精度不够再上父子分块:让子块找、父块答,通常能显著提升
  5. 疑难块用语义分块精修:只对检索差的高频问题块启用,控制成本

分块不是一个"选一次就完事"的环节。文档类型变了、检索效果变了,分块策略就要跟着调。这是 RAG 系统里最值得花时间的调优点之一——因为它是整个检索质量的起点,也是你最能控制的一环。

本文基于个人对 RAG 工程实践的梳理与官方文档验证(MinerU OmniDocBench v1.6 得分 95.39 来自其官方 README;semchunk 基准数据来自其官方 README 及 isaacus.com 博客)。

留言

欢迎分享你的想法。评论提交后会在审核通过后显示。