你搭了一个知识库问答系统:上传文档、建好索引、跑通 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 参与决策
递归分块是按"形式"切(分隔符)。语义分块按"意思"切。
做法:
- 把文本拆成句子
- 计算每个句子的 Embedding 向量
- 计算相邻句子的语义相似度
- 在相似度"断崖"处切分
核心算法是新颖度检测:
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% 的问题能定位:
- 先看分块:打开一个 chunk 看看,句子是不是被拦腰切断?上下文是不是残缺?
- 默认从递归分块起步:chunk_size 500-800、overlap 10%-20%,把基线跑通
- 结构化文档换结构感知:你的文档有标题层级,就别浪费这个信息
- 检索精度不够再上父子分块:让子块找、父块答,通常能显著提升
- 疑难块用语义分块精修:只对检索差的高频问题块启用,控制成本
分块不是一个"选一次就完事"的环节。文档类型变了、检索效果变了,分块策略就要跟着调。这是 RAG 系统里最值得花时间的调优点之一——因为它是整个检索质量的起点,也是你最能控制的一环。
本文基于个人对 RAG 工程实践的梳理与官方文档验证(MinerU OmniDocBench v1.6 得分 95.39 来自其官方 README;semchunk 基准数据来自其官方 README 及 isaacus.com 博客)。

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