RAG 递归分块和语义分块的区别?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
基础概念:面试官想确认你是不是真搞懂了递归分块(Recursive Chunking)和语义分块(Semantic Chunking)各自的切分原理。很多人就背一句 "一个按长度切,一个按语义切",这肯定不够。
-
实践意识:两种策略各自的优缺点、适用场景、成本差异,你心里有数吗?最好能拿自己项目里的选型经历说事。
-
原理深度:语义分块到底怎么 "感知" 语义?断点阈值又怎么定?这块讲清楚了,水平就出来了。
核心答案
直接给结论:
| 维度 | 递归分块 Recursive Chunking | 语义分块 Semantic Chunking |
|---|---|---|
| 切分依据 | 一组分隔符层次递归(段落 → 句子 → 词) | 句子 Embedding 之间的语义相似度 |
| 切分位置 | 在预定义分隔符处切,目标是凑够 Chunk 大小 | 在语义 "跳变" 处切,按主题边界走 |
| Chunk 大小 | 相对均匀,可控 | 可变长,不固定 |
| 速度 | 快,纯字符串操作 | 慢,每句话都要算 Embedding |
| 成本 | 低 | 高(多次 Embedding 调用) |
| 确定性 | 完全确定 | 取决于 Embedding 模型 |
| 召回率 | 中等,是好基线 | 高(业界实测能到 91%-92%,比简单方法高 ~9%) |
| 适用场景 | 通用兜底方案、快速起步 | 高精度检索、长文档、主题跨度大的内容 |
通俗讲,递归分块是 "按语法结构凑大小",语义分块是 "按语义跳变切主题"。
深度解析
一、递归分块:用分隔符层次递归
递归分块的核心思路很简单——定义一组分隔符优先级,从大粒度往小粒度依次试,把文本切到目标大小以内就行。
典型的分隔符优先级(以 LangChain 的 RecursiveCharacterTextSplitter 为例)是这样的:
["\n\n", "\n", "。", ". ", " ", ""]
它的处理逻辑是:
- 先拿
\n\n(段落分隔)去切,如果切出来的块还是超过目标大小 - 再拿
\n(换行)去切超标的块,还超标 - 继续用句号、空格…… 一路降级到按字符切
上图就是递归分块的降级逻辑。说白了是 "先尽量按自然结构切,切不动了再暴力拆",所以大部分时候切出来的块还能保住句子或段落的完整。
优点:
- 速度快,纯字符串处理,不依赖模型
- 确定性强,同样的输入永远切出同样的结果
- Chunk 大小可控,预估 Token 消耗方便
缺点:
- 分隔符是写死的,碰到不规范排版(比如 PDF 解析出来的乱码换行)容易翻车
- 只看语法结构,不理解内容,偶尔会把一个完整论述切成两半
二、语义分块:用 Embedding 相似度找断点
语义分块的思路就完全不一样了。它不靠预设的分隔符,而是通过算相邻句子之间的语义距离,在 "话题发生跳变" 的地方动刀。
完整流程分 5 步:
走一遍这个流程:
-
第 1 步:切句子。先把文档按句号、问号、感叹号切成一个个独立句子,这是语义分块的最小单元。
-
第 2 步:逐句 Embedding。每个句子都过一次 Embedding 模型,拿到一个向量。这一步就是成本大头——一篇 10000 字的文档可能有几百个句子,一个都跑不掉。
-
第 3 步:计算相邻句子语义距离。把第 i 句和第 i+1 句的 Embedding 算余弦距离,拿到一个表示 "语义跳变程度" 的数值。距离越大,话题转得越猛。
-
第 4 步:确定断点阈值。这步最关键,业界主流有三种做法:
- 百分位法(Percentile):把所有相邻句子的距离排序,取第 95 百分位(默认)当阈值,超了就切。最常用,因为它能自适应不同文档的距离分布。
- 标准差法(Standard Deviation):算所有距离的均值和标准差,超过
均值 + X 倍标准差的地方切一刀。 - 四分位距法(IQR):统计学里找异常值的老办法,超过
Q3 + 1.5 × IQR就是断点。
-
第 5 步:合并句子形成 Chunk。断点之间的连续句子合并成一个 Chunk。
举个直观例子:
假设有一段文字,前面在讲 "Spring AI 的 ETL 流程",中间突然转到 "向量数据库选型",再转到 "Rerank 模型对比"。语义分块会在这三个话题切换的地方各切一刀,得到三个主题集中的 Chunk。而递归分块运气不好时,可能把 "Spring AI 的 ETL 流程" 和 "向量数据库选型" 的开头糊在一个 Chunk 里,检索时这种 Chunk 的信号就糊了。
优点:
- Chunk 内部语义集中,检索精度和召回率都更高
- 自动适应文档的内容结构,不靠固定分隔符
- 业界实测召回率能到 91%-92%,比简单分块高约 9 个百分点
缺点:
- 慢、贵。每句话都要调 Embedding,文档一上来,API 成本和耗时都跟着涨
- 阈值调参有门槛,百分位取 95 还是 90,效果差异明显
- 有不确定性:换一个 Embedding 模型,切分结果可能完全不同
三、Java 框架的代码落地现状
这块得单独说一下。目前 Java 生态对这两种策略的支持是不均衡的,面试时能聊出来,会显得你真动手做过。
Spring AI 的 ETL Pipeline 只内置了一个 TokenTextSplitter,按 Token 数量切,本质是固定长度分块,递归和语义分块都不直接支持。用法大概长这样:
import org.springframework.ai.document.Document;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import java.util.List;
// TokenTextSplitter 默认 chunkSize=800
// 构造参数:目标 token 数、最小 chunk 大小、是否保留 token 数信息等
TokenTextSplitter splitter = new TokenTextSplitter(500, 200, 10, 5000, true);
// 把读取到的文档切分成多个 Chunk
List<Document> chunks = splitter.apply(textReader.get());
Spring AI Alibaba 在这之上多了个 SentenceSplitter,按句子智能拆分,比纯 Token 切分更接近递归分块的思路。
LangChain4j 的 DocumentSplitters 工具类提供了几种基础实现:
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.DocumentSplitter;
import static dev.langchain4j.data.document.DocumentSplitters.*;
// 按段落切,每个 Chunk 最大 300 token,重叠 30 token
DocumentSplitter splitter = documentByParagraph(300, 30);
// 按行切
DocumentSplitter lineSplitter = documentByLine(300, 30);
// 按 token 切
DocumentSplitter tokenSplitter = documentByToken(300, 30);
List<Document> chunks = splitter.split(document);
这里有个实战痛点得吐槽一下:LangChain4j 截至目前还没原生提供 RecursiveCharacterTextSplitter 和 SemanticChunker,社区在 GitHub Issue #1081 里讨论很久了,还停在规划阶段。所以 Java 项目要做真正的语义分块,一般得自己继承 DocumentSplitter 接口实现。
自定义语义分块的核心思路(伪代码示意):
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.DocumentSplitter;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import java.util.*;
public class SemanticDocumentSplitter implements DocumentSplitter {
private final EmbeddingModel embeddingModel;
private final double percentileThreshold; // 断点百分位阈值,例如 0.95
public SemanticDocumentSplitter(EmbeddingModel embeddingModel, double percentileThreshold) {
this.embeddingModel = embeddingModel;
this.percentileThreshold = percentileThreshold;
}
@Override
public List<TextSegment> split(Document document) {
// 1. 按句子切分(用正则或现成的句子切分器)
List<String> sentences = splitToSentences(document.text());
// 2. 逐句 Embedding
List<float[]> embeddings = sentences.stream()
.map(s -> embeddingModel.embed(s).content().vector())
.toList();
// 3. 计算相邻句子的余弦距离
List<Double> distances = new ArrayList<>();
for (int i = 0; i < embeddings.size() - 1; i++) {
distances.add(cosineDistance(embeddings.get(i), embeddings.get(i + 1)));
}
// 4. 取百分位阈值
double threshold = percentile(distances, percentileThreshold);
// 5. 在断点之间合并句子,形成 Chunk
return mergeSentencesByBreakpoints(sentences, distances, threshold);
}
private double cosineDistance(float[] a, float[] b) {
// 余弦距离 = 1 - 余弦相似度
// 实现略
return 0.0;
}
private double percentile(List<Double> values, double p) {
// 取第 p 百分位
// 实现略
return 0.0;
}
// 其他辅助方法...
}
四、实战选型建议
讲了这么多原理,落到工程上到底怎么选?给你几个我踩坑后的经验:
-
起步阶段:用递归分块(或 Spring AI 的
TokenTextSplitter+ 重叠)。够用、够快、可解释,先让 RAG 跑起来再说。 -
效果调优阶段:检索效果不好,且文档主题跨度大、篇幅长(比如一本技术手册、法律法规),再上语义分块。注意预算,Embedding 调用量会暴涨。
-
混合策略:2025 年有篇论文提出了 Recursive Semantic Chunking(RSC),先用递归分块打底保证大小可控,再在块内做语义微调,综合效果最好。
-
别迷信一种策略:生产环境我见过的好案例,大多按文档类型差异化处理。结构化的 Markdown/HTML 走结构化切分,长篇 PDF 走递归或语义分块,QA 知识库干脆一问一答不分块。
面试高频追问
-
递归分块的 Chunk 大小和重叠怎么定?
经验值是 Chunk 200-500 tokens,重叠 50-100 tokens(10%-20% 的重叠率)。重叠是为了不让关键信息被切断在两个 Chunk 之间。具体值得看你用的 Embedding 模型最大输入长度和 LLM 上下文窗口,综合权衡。
-
语义分块的阈值怎么调?
百分位法默认 95,意思是只有语义距离排在前 5% 的位置才切。文档主题切换频繁就调低到 90 甚至 85,主题集中就调高。最好拿一批标注好的测试集跑几组对比,看召回率曲线找最佳点。
-
还有哪些分块策略?
固定长度分块(最简单)、按文档结构分块(Markdown 按标题层级、HTML 按 DOM)、父文档分块(Parent Document:小块检索、大块喂给 LLM)、基于 LLM 的分块(让大模型自己判断切分点,成本最高)。
常见面试变体
- "RAG 的 Chunk 策略有哪些?怎么选?"
- "你在项目里用的是哪种分块方案?为什么?"
- "语义分块的成本你怎么控制?"
- "Chunk 越小越好还是越大越好?"
记忆口诀
递归分块:分隔符排队,从段到字递归降级,凑够大小就收手。
语义分块:句子逐个 Embedding,相邻相似度找断点,百分位阈值一刀切。
总结
递归分块是 RAG 切分里的性价比之选,快、稳、可控,起步和通用场景用就够。语义分块走的是精度路线,靠 Embedding 找语义断点,召回率更高,代价是成本也更高,适合高精度和长文档。面试时把两者原理差异、优缺点讲清楚,再带一句 Java 框架现状(Spring AI / LangChain4j 对语义分块支持还不完善)和自定义实现,分就稳了。
