文档分块(Chunking)不是无关紧要的预处理。它定义了检索系统能够找到、排序、引用、鉴权、更新并发送给模型的基本单元。单元太小,答案可能跨越边界;单元太大,答案会淹没在无关文本中;重叠太多,则会用重复内容耗尽上下文预算。

不存在通用的分块大小(Chunk Size)分块重叠(Chunk Overlap)。多数据集研究显示,简短事实查询与宽上下文问题的最佳 Chunk Size 不同,而且不同 Embedding 模型对长度的敏感度也不同。这正是生产系统应有的认知:分块是根据语料与查询分布验证出来的检索策略,不是代码风格。

本文从证据单元出发,比较现代分块方案,并建立可复现的受控实验。

核心结论

  • 选择 Token 数前,先定义真正承载答案的最小证据单元。
  • 保留便宜的固定 Token 基线;高级方案必须在相同检索 Token 预算下胜出。
  • 结构感知切分与基于 Embedding 的语义边界检测是两种不同技术。
  • Overlap 是实验参数,不是必选项;要同时衡量边界恢复、重复召回和索引膨胀。
  • 父子检索与句子窗口可以把“用于检索的单元”和“用于生成的上下文”分开。
  • Contextual Retrieval、Late Chunking 与 RAPTOR 保存的上下文不同,失败模式也不同。
  • 分别评测证据召回、排名、重复率、引用、回答、延迟、索引和更新成本。

先定义检索契约

实现 Splitter 前,明确五件事:

  1. 语料:政策、API 文档、合同、工单、代码、转录或混合文档;
  2. 查询:精确编号、局部事实、比较、流程、总结或多跳问题;
  3. 证据:真正支持答案的最小来源范围;
  4. 输出:原文引用、带引用回答、摘要、代码还是动作;
  5. 预算:候选数、检索 Token、延迟、索引成本和更新频率。

以员工手册为例:

  • “丧假有几天”可能只需一句话;
  • “比较加州与纽约员工的育儿假”需要两个相距很远的章节;
  • “总结休假期间经理的全部义务”可能需要层级或多阶段检索。

一个 Chunk Size 无法同时优化三种问题。系统可能需要多种粒度或按查询动态扩展。

还要先验证是否真的需要 RAG。小而稳定、能够舒适放入上下文的语料,配合缓存直接输入可能更简单。但仍应实测召回、延迟与权限,而不能因为 Context Window 更大就默认检索已经过时。

首先保留结构与来源

分块对象应该是解析后的文档单元,而不是一条无结构字符串。至少保留:

  • 文档、章节、页、段落、列表、表格、图片和代码边界;
  • 每个单元继承的标题;
  • 指向权威源的字符或字节 Offset;
  • Source URI、版本、Hash、语言、时间和权限范围;
  • 父、子、前一个、后一个关系。

不要在理解 HTML 前一律删标签。导航与广告是噪声,但标题、列表、表格、代码块、链接和强调都可能影响检索。对于 PDF,抽取质量和阅读顺序可能比分块算法更重要。

Chunk 应是权威内容的一种 View。稳定 Offset 与血缘关系支持精确引用、删除传播、解析器升级后重建索引,并避免把模型生成摘要误当事实源。Chunk Metadata 与父子关系不是鉴权边界;检索或扩展 Chunk 时仍需重新检查 Caller、Tenant、Object 和当前保留状态。

策略一:结构单元 + Token 上限

利用 Markdown 标题、HTML Section、段落、表格行、转录轮次或代码 Symbol 等原生边界。相邻小单元可以合并到目标范围,只有超长单元才使用类型相关的降级切分。

这是很强的生产基线:

  • 确定、可解释;
  • 构建和更新成本低;
  • 容易生成精确引用;
  • 不容易破坏列表、代码块和定义。

它叫结构感知切分,不等于语义分块。Markdown 标题是作者提供的结构,Embedding 相似度下降才是模型推断的语义边界。

结构也可能很差:一个章节可能长达 30 页,或者源文档根本没有有效标题。仍需实验,而不是假设保留章节必然提升检索。

策略二:固定 Token 窗口

固定 Token 窗口是受控基线,并能保证在特定 Tokenizer 下不超过上限。使用时必须记录 Tokenizer 与版本。

Overlap 可以保护人工切点,却带来:

  • 更多 Chunk 与 Embedding 调用;
  • Top-K 中出现重复结果;
  • 固定生成预算下唯一证据减少;
  • 分数解释和引用去重更复杂。

把 Overlap 做成消融实验,例如 0、32、64、128 Token,同时比较边界敏感查询的召回和唯一证据密度。百分比隐藏了真实重复 Token 数,在不同 Chunk Size 下不可直接比较。

策略三:语义边界检测

基于 Embedding 的语义切分通常:

  1. 先切为句子或小单元;
  2. 为每个单元生成向量;
  3. 计算相邻单元的相似度或变化;
  4. 超过阈值时切断;
  5. 强制最小和最大 Chunk Size。

它能发现格式中不存在的话题变化,但并不天然更好:

  • 可能需要为每个句子做 Embedding;
  • 阈值依赖语料与模型;
  • 渐进话题变化没有明显边界;
  • 短引用可能与定义分离;
  • 模型或阈值变化会增加重建成本。

应同时评估边界质量和检索结果。语义连贯的长块仍可能是糟糕的搜索单元。

策略四:Small-to-Big Retrieval

索引小 Child 以获得精确匹配,命中后扩展到更大上下文:

  • 父子检索:子段落 -> 所属章节;
  • 句子窗口:命中句 -> 邻近句;
  • 表格扩展:单元格 -> 行、表头和 Caption;
  • 代码扩展:语句或 Symbol -> 所属函数/类。

扩展范围取决于问题和文档。固定前后两句只适合作为基线。应扩展到证据能够独立理解或 Token 预算耗尽,并对重叠父块去重。

它分离了精准搜索与充分上下文,但也可能因一个很小的命中 Child 拉入巨大且噪声很多的 Parent。

策略五:上下文化 Chunk

局部 Chunk 可能有歧义:

text
公司在第四季度将其提高了 3 个百分点。

Contextual Retrieval 会在建立词法与 Dense 表示前,为 Chunk 生成或附加一小段文档级上下文,明确主题、实体、时间或章节。Anthropic 在其测试语料上报告,Contextual Embedding、Contextual BM25 和 Rerank 组合降低了检索失败率;这些数字属于厂商实验,不是普遍保证。

生成上下文可能补全召回,也可能产生幻觉或加入原文不存在的词。应把它与原文分字段存储,记录模型与 Prompt 版本,最终仍返回原文作为证据。

策略六:Late Chunking

传统 Dense Retrieval 先切块,再独立为每块生成向量。Late Chunking 使用长上下文 Embedding 模型先计算整篇文档的 Token 表示,之后才按边界对 Token Embedding Pooling。

目标是让短 Chunk 向量保留周边上下文,又无需生成文本前缀。它要求:

  • 模型/API 暴露合适的 Token Embedding 或原生能力;
  • 源文档不超过模型有效上下文;
  • Pooling 与边界精确对齐;
  • 在论文数据集之外重新评测。

Late Chunking 是值得实验的研究方案,但其 ICLR 2025 投稿被拒,评审关注点包括下游任务评测不足。不能把它包装成行业默认答案。

策略七:层级检索

层级结构服务不同抽象层的问题。RAPTOR 递归地对叶子 Chunk 做 Embedding、聚类和摘要,建立树状索引,并从原始叶子与高层摘要中检索。

它可能改善没有单个连续叶子能够回答的主题性和多跳问题,但会增加:

  • 基于 LLM 的离线索引成本;
  • 摘要漂移和无依据抽象;
  • 增量更新复杂度;
  • 更多索引节点和检索策略;
  • 回到原始叶子生成引用的要求。

如果文档本身已有可靠层级,应优先复用。只有评测证明跨章节查询存在明显缺口时,才引入生成式层级。

如何做受控分块实验

比较方案时应固定检索 Token 预算,而不只是固定 top_k。5 个 200 Token Chunk 与 5 个 1000 Token Chunk 给生成模型的信息量与成本完全不同。

为每个评测查询标注一个或多个权威证据 Span,然后衡量:

  • 给定 Token 预算内的证据 Span Recall;
  • 有分级相关性时的 nDCG 或 MRR;
  • 唯一证据 Token / 检索 Token;
  • 边界切断失败;
  • 引用正确率与完整率;
  • 下游答案准确率或业务 Rubric;
  • 索引体积、构建/更新耗时、检索延迟和生成成本。

下面只依赖 Python 标准库,按权威源 Offset 计算证据覆盖率:

python
from dataclasses import dataclass


@dataclass(frozen=True)
class Span:
    start: int
    end: int

    def __post_init__(self) -> None:
        if self.start < 0 or self.end <= self.start:
            raise ValueError("span must satisfy 0 <= start < end")


def intersection_length(left: Span, right: Span) -> int:
    return max(0, min(left.end, right.end) - max(left.start, right.start))


def merge_spans(spans: list[Span]) -> list[Span]:
    merged: list[Span] = []
    for span in sorted(spans, key=lambda item: (item.start, item.end)):
        if merged and span.start <= merged[-1].end:
            previous = merged[-1]
            merged[-1] = Span(previous.start, max(previous.end, span.end))
        else:
            merged.append(span)
    return merged


def evidence_recall(evidence: list[Span], retrieved: list[Span]) -> float:
    if not evidence:
        raise ValueError("at least one evidence span is required")

    evidence = merge_spans(evidence)
    retrieved = merge_spans(retrieved)
    covered = 0
    total = sum(span.end - span.start for span in evidence)
    for target in evidence:
        boundaries = {target.start, target.end}
        for chunk in retrieved:
            boundaries.add(max(target.start, min(target.end, chunk.start)))
            boundaries.add(max(target.start, min(target.end, chunk.end)))
        points = sorted(boundaries)
        for start, end in zip(points, points[1:]):
            probe = Span(start, end)
            if any(intersection_length(probe, chunk) == end - start for chunk in retrieved):
                covered += end - start
    return covered / total


gold = [Span(100, 160), Span(900, 940)]
retrieved = [Span(80, 140), Span(880, 950)]
print(evidence_recall(gold, retrieved))  # 0.8

Offset 可以表示字符、字节、Token、表格单元格或时间范围,但必须统一权威坐标系。该函数会先合并重叠 Span,避免重复召回虚增 Recall;它仍只衡量覆盖率,还需配合排名与重复上下文指标。

实验矩阵

变量 候选值
边界策略 Token、段落、标题、语义
目标大小 由语料定义的一组网格
Overlap 0 与多个绝对 Token 数
检索单元 Child、Parent、动态窗口
表示 原文、上下文前缀、Late Embedding
Retriever 词法、Dense、Hybrid
Reranker 无、Cross-Encoder、模型
预算 固定唯一检索 Token

每次比较固定无关变量,并按精确编号、局部事实、流程、比较、主题、多跳和对抗查询切片报告结果。生成存在随机性时,应提供置信区间或多次运行方差。

分块本体指标可以在检索前暴露缺陷。Adaptive Chunking 提出了引用完整性、块内凝聚度、文档上下文连贯性、结构块完整性与大小合规性,可作为发现引用断裂、主题混杂、文档流丢失、结构破坏和尺寸离群的候选诊断。但这些指标不能替代证据 Span Recall 与下游评测:一个 Chunk 即使本体分数良好,也可能无法匹配真实查询分布。论文报告的增益仅适用于其语料、Chunker 与评测条件。

推荐分阶段推进:

  1. 验证解析质量与证据血缘;
  2. 基于证据标注优化检索;
  3. 增加 Rerank 与上下文扩展;
  4. 评估生成与引用;
  5. 测试延迟、成本、更新、鉴权和删除。

这样可以避免用分块参数去补偿解析器、Retriever 或生成 Prompt 的问题。

常见失败模式

  • **只优化最终回答:**生成噪声会掩盖检索退化。
  • **用相同 Top-K 比较:**大 Chunk 获得不公平的 Token 优势。
  • **把重复 Overlap 计入 Recall:**重复文本占用预算,却没有增加证据。
  • **把 Metadata 混进源文本:**生成标题可能主导向量,应分开保存原始与派生字段。
  • **扩展 Parent 时丢失权限:**命中 Child 不代表用户能读取 Parent。
  • **把表格当普通段落:**应保留表头、行、单位、Caption 和坐标。
  • **按字符切代码:**尽量使用真实 Parser,同时测试语法不完整和预处理复杂的代码。
  • **重建索引不记录版本:**保存 Parser、Tokenizer、Embedding、上下文化 Prompt 和 Splitter 版本。
  • **认为检索能消灭幻觉:**检索只提供证据,生成模型仍可能忽略或误述。

生产检查清单

  • [ ] 已从真实查询抽样并标注权威证据;
  • [ ] 所有变换都保留权威 Offset 与来源;
  • [ ] 固定 Token 和结构感知基线已建立;
  • [ ] Chunk Size 与 Overlap 来自实验;
  • [ ] 方案在相同唯一 Token 预算下比较;
  • [ ] 重复召回和 Parent 扩展已量化;
  • [ ] 精确、语义、局部、主题、多跳查询分别报告;
  • [ ] 引用能够回到原始内容;
  • [ ] 权限和删除会传播到 Parent 与摘要;
  • [ ] 索引、更新、延迟、存储和回答成本满足预算;
  • [ ] 分块变更经过检索与端到端回归门禁。

常见问题

事实查询一定适合更小的 Chunk 吗?

不一定。小块通常更容易定位,但可能丢失限定条件、标题和关系。多数据集研究发现,部分简短答案数据集偏好小块,宽上下文任务偏好大块,且结果随 Embedding 模型变化。

按标题切分后还需要 Overlap 吗?

不一定。标题边界可能已经足够完整。应专门测试证据跨章节的查询,确认 Overlap 增加的是唯一召回,而不是重复结果。

长上下文 Embedding 能消除分块吗?

不能。模型能够接受长输入,不代表单个向量能保留所有事实。它支持 Late Chunking 等方案,但检索粒度与引用单元仍然重要。

语义分块值得额外索引成本吗?

只有它在你的语料上改善证据召回或下游结果时才值得。应与递归结构基线同时比较构建和更新成本。

总结

最佳分块策略不是一个数字,而是源结构、答案局部性、查询分布、Embedding 行为、检索策略和生成上下文预算之间经过测量的契约。

从透明基线开始,保留权威证据,在相同预算下比较候选方案。只有标注评测集证明价值时,才增加上下文化或层级复杂度。

一手资料