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+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 基础概念:面试官想确认你是不是真搞懂了递归分块(Recursive Chunking)和语义分块(Semantic Chunking)各自的切分原理。很多人就背一句 "一个按长度切,一个按语义切",这肯定不够。

  2. 实践意识:两种策略各自的优缺点、适用场景、成本差异,你心里有数吗?最好能拿自己项目里的选型经历说事。

  3. 原理深度:语义分块到底怎么 "感知" 语义?断点阈值又怎么定?这块讲清楚了,水平就出来了。

核心答案

直接给结论:

维度 递归分块 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 切分更接近递归分块的思路。

LangChain4jDocumentSplitters 工具类提供了几种基础实现:

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 截至目前还没原生提供 RecursiveCharacterTextSplitterSemanticChunker,社区在 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 知识库干脆一问一答不分块。

面试高频追问

  1. 递归分块的 Chunk 大小和重叠怎么定?

    经验值是 Chunk 200-500 tokens,重叠 50-100 tokens(10%-20% 的重叠率)。重叠是为了不让关键信息被切断在两个 Chunk 之间。具体值得看你用的 Embedding 模型最大输入长度和 LLM 上下文窗口,综合权衡。

  2. 语义分块的阈值怎么调?

    百分位法默认 95,意思是只有语义距离排在前 5% 的位置才切。文档主题切换频繁就调低到 90 甚至 85,主题集中就调高。最好拿一批标注好的测试集跑几组对比,看召回率曲线找最佳点。

  3. 还有哪些分块策略?

    固定长度分块(最简单)、按文档结构分块(Markdown 按标题层级、HTML 按 DOM)、父文档分块(Parent Document:小块检索、大块喂给 LLM)、基于 LLM 的分块(让大模型自己判断切分点,成本最高)。

常见面试变体

  • "RAG 的 Chunk 策略有哪些?怎么选?"
  • "你在项目里用的是哪种分块方案?为什么?"
  • "语义分块的成本你怎么控制?"
  • "Chunk 越小越好还是越大越好?"

记忆口诀

递归分块:分隔符排队,从段到字递归降级,凑够大小就收手。

语义分块:句子逐个 Embedding,相邻相似度找断点,百分位阈值一刀切。

总结

递归分块是 RAG 切分里的性价比之选,快、稳、可控,起步和通用场景用就够。语义分块走的是精度路线,靠 Embedding 找语义断点,召回率更高,代价是成本也更高,适合高精度和长文档。面试时把两者原理差异、优缺点讲清楚,再带一句 Java 框架现状(Spring AI / LangChain4j 对语义分块支持还不完善)和自定义实现,分就稳了。