递归分块和语义分块的区别?

#RAG#Agent#面试八股

递归分块和语义分块的区别?

结论:递归分块(Recursive Splitting)是规则驱动——按分隔符优先级逐级降级,切到长度达标为止,结果确定、零额外成本; 语义分块(Semantic Chunking)是数据驱动——先算相邻句子的向量相似度,在"话题突变"处下刀,更贴合内容,但要多付一轮 embedding,且对阈值敏感。 工程默认选递归,只在文档话题跳跃明显、且愿意为评估投入时才上语义分块。

1. 递归分块:按优先级降级

目标 500 字,分隔符优先级:["\n\n", "\n", "。", ";", ","]
  step1 用 "\n\n" 切 → 每段 800 字,仍超长
  step2 对这些段用 "\n" 切 → 得 500 字左右
  step3 还超长的用 "。" 切 → 得句子
  step4 句子拼到接近 500 字,装不下就开新块
  最后:把过短的尾块合并回上一块
层级分隔符保住的东西
1空行段落边界,最接近"一个意思"
2换行列表项、短行
3句号/问号/感叹号完整句子
4分号/逗号并列成分,最后手段
5空串硬切,仅用于无标点的长串

它的确定性是最大优点:同样输入永远同样输出,便于回归测试。

2. 语义分块:找相似度的"谷底"

句子:   S1     S2     S3     S4     S5     S6
相似度:   0.81   0.79   0.42   0.77   0.35   ← 相邻句余弦
                        ↑切点          ↑切点
结果: [S1 S2 S3] [S4 S5] [S6 ...]

做法:按句切分 → 每句向量化 → 计算相邻句余弦 → 低于阈值(如全部相似度的第 20 百分位,或均值减一个标准差)就切。 阈值取百分位而不是固定值,是因为不同文档的整体相似度水平差异很大。

参数调大调小
百分位阈值切得少,块变大切得多,块碎成一句一块
最小块长抑制碎块容易出现单句块
最大块长—必须设上限,否则会切出超长块

3. 正面对比

维度递归分块语义分块
额外成本0每句一次 embedding(批量后仍可观)
结果稳定性完全确定依赖阈值与模型版本,会漂移
结构感知强,天然保留标题与段落弱,不看 ##,可能跨章节合并
话题转折看不出来,可能把两个主题塞进一块正是它的强项
典型失败在论述中途断开;无关段落被合并切出大量单句块,丢失上下文;相似度噪声导致切点乱跳
适用手册、规范、FAQ、代码文档访谈稿、会议纪要、长篇叙事

4. 代码:两种实现放一起看

import numpy as np

def recursive_split(text: str, target: int = 500,
                    seps: tuple[str, ...] = ("\n\n", "\n", "。", ";", ",", "")) -> list[str]:
    """按分隔符优先级降级,直到每块 <= target 的 1.5 倍。"""
    if len(text) <= target * 1.5 or not seps:
        return [text]

    sep, rest = seps[0], seps[1:]
    if sep == "":
        return [text[i:i + target] for i in range(0, len(text), target)]

    pieces, buf = [], ""
    for part in text.split(sep):
        piece = part + (sep if sep != "\n\n" else "\n\n")
        if len(buf) + len(piece) <= target:
            buf += piece
        else:
            if buf:
                pieces.append(buf)
            # 单段本身还超长,继续用更细的分隔符拆
            pieces.extend(recursive_split(piece, target, rest) if len(piece) > target * 1.5 else [piece])
            buf = ""
    if buf:
        pieces.append(buf)
    return [p.strip() for p in pieces if p.strip()]

def semantic_split(sents: list[str], vecs: np.ndarray, pct: float = 20) -> list[list[int]]:
    """相似度低于第 pct 百分位处断开,返回句子下标分组。"""
    sims = np.array([vecs[i] @ vecs[i + 1] for i in range(len(vecs) - 1)])
    cuts = set(np.where(sims < np.percentile(sims, pct))[0] + 1)
    groups, start = [], 0
    for i in range(1, len(sents) + 1):
        if i in cuts or i == len(sents):
            groups.append(list(range(start, i)))
            start = i
    return groups

5. 实际常用的混合策略

组合方式做法好处
结构骨架 + 语义断点先按 ## 切章节,章节内再跑语义切分不跨主题,也不在论述中途断
递归兜底语义切出的块超过上限,再递归切防止单块撑爆上下文
父子结构子块细(语义),父块粗(章节)检索用子块,喂模型用父块

6. 常见追问

Q:语义分块一定要调 LLM 吗? 不一定,句子向量就够。也有用 LLM 判"这里是不是话题转折"的做法,准确但成本高一个数量级,只适合离线处理少量高价值文档。

Q:为什么语义分块有时候效果更差? 因为它只优化"块内一致性",不优化"可检索性"。切成一两句一块后,块里没有主语和前提,检索到了也答不了。

Q:中文句子切分要注意什么? 别只按 。,还有 !?; 和省略号;小数点、编号(3.2 节)会误切,正则要把数字前后的点排除掉。

小结

递归分块是"工程默认值",语义分块是"特定文档的优化项"。先把结构切分做对,再考虑用相似度找断点——顺序颠倒往往白忙。

笔记加载中…