分块策略对比:五种切法的取舍


文档摘要

分块策略对比:五种切法的取舍 本节摘要:分块(Chunking)决定了检索器能看到什么。边界切错,再强的嵌入模型、再好的重排器、再聪明的 LLM 都无法在下游修复这个损伤。本节从零实现五种分块策略——定长窗口、按句、递归分割、语义聚类、结构化 Markdown 标题——并在一个带黄金答案标注的固定语料上测量 recall@k。你会亲眼看到:散文语料上语义聚类胜出,技术文档上结构化切分胜出,混合语料上递归分割因为自适应而稳住阵脚。读完本节,你能根据文档类型、平均段落长度、是否带显式结构这三个属性,在不跑基准的情况下为新语料选对默认分块器,并识别每种策略会注入的失败模式(孤儿句、符号中断、纯标题块、语义漂移)。 对应原课程:Phase 19 · Lesson 64 · (原英文 )。

分块策略对比:五种切法的取舍

本节摘要:分块(Chunking)决定了检索器能看到什么。边界切错,再强的嵌入模型、再好的重排器、再聪明的 LLM 都无法在下游修复这个损伤。本节从零实现五种分块策略——定长窗口、按句、递归分割、语义聚类、结构化 Markdown 标题——并在一个带黄金答案标注的固定语料上测量 recall@k。你会亲眼看到:散文语料上语义聚类胜出,技术文档上结构化切分胜出,混合语料上递归分割因为自适应而稳住阵脚。读完本节,你能根据文档类型、平均段落长度、是否带显式结构这三个属性,在不跑基准的情况下为新语料选对默认分块器,并识别每种策略会注入的失败模式(孤儿句、符号中断、纯标题块、语义漂移)。

对应原课程:Phase 19 · Lesson 64 · chunking-strategies-advanced(原英文 phases/19-capstone-projects/64-chunking-strategies-advanced/docs/en.md)。本节属第 20 章「毕业项目」的进阶 RAG 赛道(Track F)。

学习目标

阅读完本节,你应当能够:

  1. 从零实现五种分块策略:定长窗口(Fixed-window)按句(Sentence)递归分割(Recursive Split)语义聚类(Semantic Cluster)结构化 Markdown(Structural Markdown)
  2. 在带黄金标注答案区间的固定语料上测量 recall@k,并解释为什么散文和技术文档各有不同的胜出策略。
  3. 读懂分块长度分布,识别每种策略注入的失败模式:孤儿句、符号中断、纯标题块、语义漂移。
  4. 通过检查三个属性(文档类型、平均段落长度、是否带显式结构),在不跑基准的情况下为新语料挑选默认分块器。

一、问题与直觉

每条 RAG 流水线都从「把源文档切成块」开始。块要小到嵌入模型塞得下,又要大到每块承载一个自洽的想法。在哪里下刀,不是超参数,而是检索器能返回什么的上界

一个查询「预算中止阈值长什么样」,只有当承载中止阈值的那个块可达时才能命中。如果定长切分器把阈值数值和它的上下文切开了,嵌入向量就会漂到另一个簇,BM25 分数下降,重排器看到的是噪声,LLM 生成的答案就是错的。2024 年的论文《LongRAG》测出:仅仅因为分块选择的不同,检索召回率就有 35 个百分点的绝对摆动。2025 年关于上下文块头的后续工作缩小了这个差距,但没能关闭它。

本节把五种策略并排构建,跑在同一个带黄金标注的固定语料上,让你自己读召回数字。

recall@k 如何度量边界选择

一条带黄金标注的查询携带答案区间在源文档中的精确字符偏移。分块后,你问:检索器返回的 top-k 块里,有没有任何一块与黄金区间重叠?有,该查询的 recall@k 为 1;没有,为 0。跨查询集求平均。对每种策略跑同一套评估,数值的离散度就告诉你哪种边界策略在你的语料上活得下来。

二、从零实现

code/main.py 实现了以下组件(完整代码见原课程,这里给出关键骨架):

  • fixed_window(text, size, overlap) —— 基线。
  • sentence_chunks(text, target) —— 简单的句子打包器。
  • recursive_split(text, separators, target) —— 分层递归。
  • semantic_chunks(text, similarity_threshold) —— 基于质心的聚类,跑在一个确定性 mock 嵌入之上。
  • structural_markdown(text) —— 标题感知的切分器。
  • mock_embed(text, dim) —— 基于哈希的嵌入,让整个回路离线运行。
  • DenseIndex —— 与第 65 节混合检索同形的稠密索引。
  • eval_recall(strategy, corpus, queries, k) —— 对比回路。
  • 一个 main(),在固定语料上跑每种策略,打印 recall@k 表。

递归分割的核心骨架

递归分割是「最强的单策略基线」,其精髓在于按最强分隔符优先尝试,失败就回退:

def recursive_split(text, separators, target): """按分隔符优先级递归切分,直到每块 <= target 字符。""" if len(text) <= target: return [text] sep = separators[0] if sep == "": # 兜底:按字符硬切 return [text[i:i+target] for i in range(0, len(text), target)] pieces = text.split(sep) chunks, buf = [], "" for piece in pieces: candidate = (buf + sep + piece) if buf else piece if len(candidate) > target and buf: chunks.append(buf) buf = piece else: buf = candidate if buf: chunks.append(buf) # 若单块仍超长,用下一级分隔符再切 result = [] for chunk in chunks: if len(chunk) > target and len(separators) > 1: result.extend(recursive_split(chunk, separators[1:], target)) else: result.append(chunk) return result

设计要点:分隔符列表通常是 ["\n\n", "\n", "。", " ", ""]——从段落、到行、到句号、到空格、到字符。这种自适应让递归分割在结构不一致的文档上仍然稳住。

语义聚类的核心骨架

def semantic_chunks(text, similarity_threshold=0.6): """嵌入每句,按质心相似度聚类相邻句子。""" sentences = split_sentences(text) embs = [mock_embed(s) for s in sentences] chunks, cur, centroid = [], [0], embs[0] for i in range(1, len(sentences)): sim = cosine(centroid, embs[i]) if sim < similarity_threshold: chunks.append(" ".join(sentences[c] for c in cur)) cur, centroid = [], embs[i] cur.append(i) centroid = running_mean(centroid, embs[i]) if cur: chunks.append(" ".join(sentences[c] for c in cur)) return chunks

评估回路

def eval_recall(strategy, corpus, queries, k=5): index = DenseIndex() for doc in corpus: for chunk in strategy(doc["text"]): index.add(chunk, doc_id=doc["id"]) hits = 0 for q in queries: results = index.search(q["text"], k) # 任一 top-k 块的字符区间与黄金区间重叠即命中 if any(overlap(r.span, q["gold_span"]) for r in results): hits += 1 return hits / len(queries)

运行:

python3 code/main.py

输出是一张小表,每行一种策略,每列一个 k。在结构化语料上按句策略会输;在 Markdown 语料上结构化切分会赢;在混合语料上递归分割因自适应而稳住;在散文语料上语义聚类因为没有有用结构线索而胜出。

三、框架对比

把五种策略横向对比,关键是它们各自牺牲什么:

策略 切分依据 强项 弱项 适用语料
定长窗口 字符数 快、确定 边界极差 仅作对照基线
按句 句号/正则 不切断单词 切断段落与章节 无其他结构的散文
递归分割 分隔符层级 自适应 实现略复杂 结构不一致的混合文档
语义聚类 嵌入质心漂移 边界反映语义 慢、依赖嵌入模型 段内换主题的散文
结构化 Markdown H1/H2/H3 每主题最小块 仅限良构文档 Markdown/RFC/API 文档

💡 业界主流库(LangChain 的 RecursiveCharacterTextSplitter、LlamaIndex 的 SentenceSplitter、Anthropic 的 Contextual Retrieval)本质上都是这五种策略的组合与增强。Anthropic 2024 年的 Contextual Retrieval 给每块加一句「上下文摘要」前置,能进一步缩小策略间差距,但仍无法关闭它——边界选择仍是上界。

四、表格藏不住的失败模式

孤儿句(Orphan sentences)。 按句打包会产生漏掉主题句的块,嵌入随后指向错误的簇。

符号中断(Mid-symbol cuts)。 在代码或 YAML 内部用定长窗口,会把一个标识符切成两半,两半都嵌入成噪声。

纯标题块(Header-only chunks)。 结构化 Markdown 会吐出只含 ## 标题 的块。要过滤掉,或把下一块的首段附上来。

语义漂移(Semantic drift)。 当语料整体都在一个主题上时,语义聚类会欠切,一个 5000 字符的块把许多具体答案压进一个弥散的嵌入。要把语义聚类和一个硬性字符上限组合使用。

嵌入陈旧(Stale embeddings)。 语义聚类用了一个嵌入模型。换模型,块也跟着变。把分块模型与检索模型分别锁版本,或一起重建索引。

五、可复用产物

  • eval_recall 回路:本节的评估函数返回的形状,被第 68 节「RAG 评估」直接复用来在生产中给策略选择打分。
  • DenseIndex:与第 65 节「混合检索」共用同一形状,保证块在不同课之间可比。
  • 策略选择表:下表的三个属性可在不跑基准时为任何新语料挑默认分块器。

不跑基准时的默认选择

属性 取值 默认策略
文档类型 无结构的散文 递归分割,target 800
文档类型 Markdown/RFC/API 文档 结构化 Markdown
文档类型 代码 AST 感知(超出本节范围,见第 02 节)
段落长度 长、单一主题 按句,target 500
段落长度 短、混合主题 语义聚类,阈值 0.6

拿不准时选递归分割——它是最强的单策略基线。

生产模式:

  • 上线新流水线前先跑评估,别轻信你用的库的默认策略。
  • 每当换嵌入模型或语料构成时重跑评估——胜者是语料相关的。
  • 在每个块的元数据里持久化策略名,以便日后追溯回归。

六、练习

  1. 加第六种策略:用 tiktoken 做 token 窗口而非字符计数,在同一固定语料上对比定长窗口。
  2. 注入代码块:把 30% 的代码块混进散文语料,重跑表格,解释为什么除结构化 Markdown 外每种策略都掉召回。
  3. 换真实嵌入:把确定性嵌入换成你项目真实提供商的模型,测量语义聚类的召回差值,报告策略间离散度是变宽还是变窄。
  4. 加摘要字段:给每块加一句「质心描述」摘要,把摘要追加到块体后重跑评估,测量召回提升。

本节要点回顾

  1. 分块不是超参数,是召回上界——边界切错,下游任何模型都无法修复。
  2. 五种策略:定长窗口(基线)、按句、递归分割(最强单策略)、语义聚类(按意义切)、结构化 Markdown(按标题切)。
  3. recall@k 度量边界:top-k 块是否与黄金答案区间重叠,跨查询求平均。
  4. 胜者是语料相关的:散文归语义聚类,技术文档归结构化切分,混合归递归分割。
  5. 五大失败模式:孤儿句、符号中断、纯标题块、语义漂移、嵌入陈旧。
  6. 三属性选默认:文档类型、段落长度、是否带显式结构;拿不准就选递归分割。
  7. 工程化:把策略名写进块元数据,换模型/语料就重跑评估。

下一节,我们将进入「混合检索 BM25 与稠密」——本节切出的块在这里被词法与语义两路索引,再用倒数排名融合合并成一份胜过任何单路的 top-k。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U