AI 工程基础体系 · 第 86/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。

RAG 切块策略:固定、语义、层次、表格与上下文窗口

检索增强生成(Retrieval-Augmented Generation,RAG)把“模型参数中的知识”与“运行时检索到的外部知识”结合起来。经典 RAG 结构通常包含两个阶段:

  1. 检索:根据用户问题,从文档集合中找出若干相关内容;
  2. 生成:把问题和检索结果放入模型上下文,由生成模型组织答案。

原始 RAG 论文将外部知识表示为可检索的文档集合,并使用神经检索器选择证据,再由生成模型基于这些证据生成回答:Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks

在工程实现中,原始文档通常不能直接作为一个整体参与向量检索。系统会先把文档拆成较小的块(chunk),为每个块建立索引,再根据问题检索块。因此,切块策略并不是简单的数据预处理,而是在改变检索系统的基本单位:

文档切块表示与索引检索上下文组装生成\text{文档} \rightarrow \text{切块} \rightarrow \text{表示与索引} \rightarrow \text{检索} \rightarrow \text{上下文组装} \rightarrow \text{生成}

切得过大,单个块包含多个主题,向量表示变得模糊,检索结果中会夹带大量无关内容;切得过小,关键定义、条件或例外可能被拆开,检索器只能找到半句话。后续生成模型是否能正确回答,往往由这一步是否保留了知识结构决定。


一、先明确切块到底要优化什么

1.1 切块不是为了让文本“平均变短”

一个块至少有四个属性:

  • 内容:实际可用于回答问题的文字、代码或表格;
  • 边界:它从原文哪里开始、在哪里结束;
  • 元数据:标题、章节、页码、产品版本、权限标签等;
  • 索引表示:例如向量、关键词倒排项或混合检索特征。

设原文被切成块集合:

D={c1,c2,,cn}D = \{c_1,c_2,\dots,c_n\}

给定问题 qq,检索器返回前 kk 个块:

R(q)=TopKciDscore(q,ci)R(q)=\operatorname{TopK}_{c_i \in D} \operatorname{score}(q,c_i)

最常见的向量检索得分是余弦相似度:

sim(q,ci)=eqeieqei\operatorname{sim}(q,c_i) = \frac{e_q \cdot e_i} {\|e_q\|\|e_i\|}

其中:

  • eqe_q 是问题的向量;
  • eie_i 是块 cic_i 的向量;
  • 点积衡量方向相似程度;
  • 归一化后得到余弦相似度。

但真正关心的不是“向量相似度最高”,而是检索结果是否包含回答问题所需的完整证据。可以把切块质量抽象成一个多目标问题:

U=αA+βR+γCδNϵTζPU = \alpha A +\beta R +\gamma C -\delta N -\epsilon T -\zeta P

其中:

  • AA可回答性,一个块是否包含完整的定义、条件、动作和例外;
  • RR召回能力,相关证据是否容易被检索到;
  • CC上下文效率,送入模型的 token 中有多少真正有用;
  • NN噪声,无关文本对检索与生成的干扰;
  • TT重复成本,例如 overlap 导致同一内容多次存储和传输;
  • PP权限与版本风险,例如合并块后混入不应暴露的内容;
  • α,β,γ,δ,ϵ,ζ\alpha,\beta,\gamma,\delta,\epsilon,\zeta:由业务目标决定的权重。

这说明不存在脱离数据集和问答任务的“最佳块大小”。技术文档、法律合同、日志、代码和财务表格对上述目标的权重不同。

1.2 一个块至少要满足“问题—证据”关系

例如原文:

退款申请必须在购买后 30 天内提交。数字商品一旦下载,不支持无理由退款。企业合同客户应按照合同中的服务级别协议执行。

如果切成三个很小的块:

  1. “退款申请必须在购买后 30 天内提交。”
  2. “数字商品一旦下载,不支持无理由退款。”
  3. “企业合同客户应按照合同中的服务级别协议执行。”

问题“下载数字商品后还能退款吗?”需要同时理解“30 天期限”和“下载后例外”。只检索第一个块会产生错误答案,即把“30 天内”误认为无条件退款。

更合理的块应保留规则和例外:

退款申请必须在购买后 30 天内提交;但数字商品一旦下载,不支持无理由退款。企业合同客户应按照合同中的服务级别协议执行。

切块的核心不是把字符数量控制在某个范围,而是让块成为一个语义上足够独立、检索后可以被正确解释的证据单元


二、切块之前:先定义文档、单位和 token

2.1 字符、词和 token 不是同一个单位

模型上下文窗口通常以 token 计量,而不是字符数或自然语言词数。token 是模型分词器产生的离散单元:

Tokenize(x)=(t1,t2,,tm)\operatorname{Tokenize}(x) = (t_1,t_2,\dots,t_m)

同一段文本在不同分词器下可能产生不同 token 数量。中文、英文、代码、URL、JSON 和表格的 token 密度也不同:

  • 中文通常按字、词片段或其组合切分;
  • 英文长词可能被拆成多个子词;
  • 代码中的标点、缩进和标识符可能占用大量 token;
  • URL、哈希、日志 ID 通常很难压缩;
  • Markdown 表格中的分隔符也会占用上下文。

因此,“每块 500 个字符”并不等价于“每块 500 个 token”。生产系统应使用与嵌入模型或生成模型相匹配的分词器进行测量。若只是做原理验证,可以使用字符数或空白分词,但必须明确这只是近似。

2.2 切块前还必须保留文档结构

原始文档通常不是纯文本,而是带有结构的对象:

文档
├── 标题
├── 章节
│   ├── 小节
│   │   ├── 段落
│   │   ├── 列表
│   │   └── 表格
│   └── 小节
└── 附录

如果先把 PDF、HTML 或 Markdown 展平成一大段字符串,再按长度切分,可能发生:

  • 章节标题与正文分离;
  • 页眉、页脚混入正文;
  • 表格列顺序丢失;
  • 代码块在中间断开;
  • 脚注与主文脱离;
  • 文档版本和权限标签被丢弃。

一个实际块通常应携带类似以下元数据:

{
  "document_id": "policy-2025-01",
  "chunk_id": "policy-2025-01#refund#02",
  "title_path": ["退款政策", "数字商品"],
  "content": "数字商品一旦下载,不支持无理由退款。",
  "page": 3,
  "version": "2025-01",
  "acl": ["customer", "support"]
}

这些字段不一定全部拼接进向量文本,但应保存在索引元数据中,用于过滤、引用、权限检查和结果解释。


三、固定切块:简单、稳定,但不理解语义

3.1 定义

**固定切块(fixed-size chunking)**按照预定长度切分文本。长度可以按字符、词或 token 计算,通常还会设置重叠区间(overlap)。

设 token 序列为:

T=(t1,t2,,tn)T=(t_1,t_2,\dots,t_n)

块长度为 LL,步长为 SS,且 S=LOS=L-O,其中 OO 是重叠长度。第 jj 个块为:

cj=(tjS+1,,tjS+L)c_j=(t_{jS+1},\dots,t_{jS+L})

例如:

  • 块长度 L=8L=8
  • 重叠 O=2O=2
  • 步长 S=6S=6

对于序列:

t1 t2 t3 t4 t5 t6 t7 t8 t9 t10 t11 t12 t13 t14

切分结果为:

块 1: t1  t2  t3  t4  t5  t6  t7  t8
块 2: t7  t8  t9  t10 t11 t12 t13 t14

重叠的目的,是降低关键信息恰好落在边界两侧的概率。

3.2 一个可运行的固定切块示例

下面代码使用空白分词模拟 token,仅用于说明算法。生产环境应替换为目标模型对应的 tokenizer。

from dataclasses import dataclass
from typing import List


@dataclass
class Chunk:
    index: int
    tokens: List[str]
    text: str


def fixed_chunk(text: str, size: int = 8, overlap: int = 2) -> List[Chunk]:
    if size <= 0:
        raise ValueError("size 必须大于 0")
    if overlap < 0 or overlap >= size:
        raise ValueError("overlap 必须满足 0 <= overlap < size")

    tokens = text.split()
    step = size - overlap
    chunks = []

    for index, start in enumerate(range(0, len(tokens), step)):
        part = tokens[start:start + size]
        if not part:
            break

        chunks.append(
            Chunk(
                index=index,
                tokens=part,
                text=" ".join(part),
            )
        )

        if start + size >= len(tokens):
            break

    return chunks


if __name__ == "__main__":
    text = "退款申请 必须 在 购买 后 30 天 内 提交 数字商品 一旦 下载 不支持 无理由退款"
    for chunk in fixed_chunk(text, size=8, overlap=2):
        print(f"{chunk.index}: {chunk.text}")

预期输出:

0: 退款申请 必须 在 购买 后 30 天 内
1: 天 内 提交 数字商品 一旦 下载 不支持
2: 下载 不支持 无理由退款

这里的第 0 块和第 1 块重叠了“天 内”,第 1 块和第 2 块重叠了“下载 不支持”。重叠可以帮助检索器在边界处保留上下文,但也带来两个代价:

  1. 索引块数量增加;
  2. 检索后多个块可能包含大量重复内容,挤占生成上下文。

如果 NN 是原文 token 数,固定长度为 LL,重叠为 OO,步长为 S=LOS=L-O,块数量近似为:

MNLS+1M \approx \left\lceil \frac{N-L}{S} \right\rceil + 1

OO 增大时,SS 变小,块数量和索引成本都会增加。

3.3 固定切块的优势

固定切块适合以下情况:

  • 文档结构很弱,例如日志、聊天记录、转储文本;
  • 需要快速建立第一版基线;
  • 内容长度相对均匀;
  • 评测集足够大,可以直接调节块长度和 overlap;
  • 系统需要稳定、可预测的处理成本。

它的主要优点是实现简单、吞吐量高、行为确定。相同输入和参数通常可以得到相同的块边界,便于增量索引和问题定位。

3.4 固定切块的反例

假设原文是:

3. 备份策略

每日 02:00 执行增量备份。

4. 恢复策略

发生主库故障时,先提升最近一次完整备份,再应用增量日志。

若按固定长度切分,可能得到:

块 1: 每日 02:00 执行增量备份。4. 恢复策略
块 2: 发生主库故障时,先提升最近一次完整备份,再应用增量日志。

问题“主库故障时如何恢复?”能够命中块 2;但问题“备份何时执行?”可能命中块 1,其中混入了恢复章节标题。若块长度更小,标题和对应正文又可能分离,导致检索结果只含“4. 恢复策略”,却没有恢复步骤。

固定策略并非一定错误,而是没有利用“章节标题是正文语义的一部分”这一事实。


四、语义切块:让边界跟随主题变化

4.1 定义

**语义切块(semantic chunking)**不预先固定每个块的长度,而是试图在主题发生明显变化的位置切分。

一个常见流程是:

  1. 按段落、句子或自然结构得到初始单元;
  2. 为每个单元生成向量;
  3. 比较相邻单元之间的语义相似度;
  4. 当相似度显著下降时建立边界;
  5. 将相邻且主题相近的单元合并,同时设置最大长度。

设相邻句子或段落的向量为 ei,ei+1e_i,e_{i+1},相似度为:

si=cos(ei,ei+1)s_i=\cos(e_i,e_{i+1})

如果 sis_i 很低,说明相邻单元可能属于不同主题。可以定义语义距离:

di=1sid_i=1-s_i

did_i 大于阈值 τ\tau 时切分:

boundary(i)={1,di>τ0,diτ\operatorname{boundary}(i)= \begin{cases} 1,&d_i>\tau\\ 0,&d_i\leq\tau \end{cases}

实际系统通常不会只使用阈值,还会施加最大块长度约束:

cjLmax|c_j| \leq L_{\max}

否则一篇长篇且主题连续的章节可能永远不切,最终形成过大的块。

4.2 语义相似度下降不一定意味着应该切分

考虑下面的段落:

用户可以在控制台创建备份任务。
创建任务后,系统会在每日 02:00 执行增量备份。
如果主库发生故障,管理员需要先恢复最近一次完整备份。
随后,系统会按时间顺序应用增量日志。

“备份任务创建”和“恢复流程”是两个主题,应该切开。但下面这一组句子虽然词汇不同,仍属于一个完整流程:

提交退款申请后,系统会校验订单状态。
对于已下载的数字商品,校验结果会标记为不可退款。
如果订单属于企业合同客户,系统会转入人工审核。

相邻句子的词面和向量相似度可能不高,但它们共同描述一个决策流程。单纯根据相似度下降切分,可能把“条件”和“动作”分开,降低可回答性。

因此语义切块需要同时考虑:

  • 段落和标题结构;
  • 句子之间的条件—结论关系;
  • 列表项之间的共同主题;
  • 最大和最小块长度;
  • 代码、表格、公式等不可随意拆分的结构。

4.3 语义切块的典型边界错误

错误一:定义和限定条件被拆开

所有 API 请求默认限制为每分钟 600 次。
对于批量导入接口,限制为每分钟 60 次。

若只命中第二句,答案可以较准确;若问题是“API 请求的默认限制是多少”,命中第一句即可。但问题“批量导入接口的限制是否和普通 API 一样”,需要两句一起出现。语义切块应识别“对于……”是前一句规则的例外,而不是独立主题。

错误二:标题向量压过正文向量

标题“权限配置”很短,向量表示可能集中在“权限”“配置”等词上。问题“如何限制财务数据的访问”可能同时命中多个只包含标题的块。解决方式不是盲目增大块,而是把标题路径拼接到正文:

章节:权限配置 > 财务数据

财务数据仅允许 finance-admin 和 auditor 角色访问。

这样标题提供主题定位,正文提供实际证据。

4.4 语义切块的工程边界

语义切块通常比固定切块需要更多计算:

  • 需要先进行句子或段落解析;
  • 需要为中间单元生成 embedding;
  • 需要执行相似度计算和边界决策;
  • 重新切块会导致大范围重新嵌入。

当原文频繁更新时,固定切块可能更容易做增量处理。语义切块并不自动提高召回率;如果嵌入模型不适合领域术语、文档解析错误或阈值不稳定,结果可能比固定基线更难诊断。


五、层次切块:同时保存局部证据和父级上下文

5.1 定义

**层次切块(hierarchical chunking)**把文档表示为多个层级:

文档
└── 章节
    └── 小节
        └── 段落或细粒度子块

它通常包含两类对象:

  • 父块(parent chunk):较大、语义完整的章节或小节;
  • 子块(child chunk):较小、适合精确检索的段落或句子组。

检索时可以对小子块建立向量索引,但返回结果时带回对应父块:

retrieve(q){childi}{parent(childi)}\operatorname{retrieve}(q) \rightarrow \{child_i\} \rightarrow \{parent(child_i)\}

这解决了两个相互冲突的目标:

  • 子块小,便于精准匹配;
  • 父块大,便于保留定义、条件和例外。

5.2 为什么层次结构能解决“检索粒度”和“生成粒度”的冲突

假设小节内容如下:

4.2 批量导入

批量导入接口最多接受 10,000 行记录。
客户端必须使用异步接口。
导入失败时,系统会返回任务 ID,客户端应查询任务状态。

如果整个小节作为一个块,问题“批量导入最多接受多少行?”的向量可能被“异步接口”“任务状态”等词稀释。

如果每句话单独作为块,问题“批量导入失败后如何处理?”可能只命中“返回任务 ID”,而没有“查询任务状态”。

层次结构可以这样建立:

父块 P1:
4.2 批量导入
批量导入接口最多接受 10,000 行记录。
客户端必须使用异步接口。
导入失败时,系统会返回任务 ID,客户端应查询任务状态。

子块 C1:
批量导入接口最多接受 10,000 行记录。

子块 C2:
客户端必须使用异步接口。

子块 C3:
导入失败时,系统会返回任务 ID,客户端应查询任务状态。

问题先通过 C1 或 C3 精确定位,再根据配置选择:

  • 只返回命中的子块;
  • 返回子块加前后邻居;
  • 返回整个父块;
  • 返回父块摘要加子块原文。

5.3 层次检索的状态变化

一次查询可以按以下步骤执行:

flowchart LR
    Q[用户问题] --> E[问题向量]
    E --> I[子块向量索引]
    I --> C[候选子块]
    C --> F[权限与版本过滤]
    F --> P[映射到父块]
    P --> D[去重与上下文预算]
    D --> G[生成模型]

关键路径是:

  1. 用户问题只产生一个查询向量;
  2. 子块索引返回候选子块;
  3. 权限和版本过滤必须在生成前执行;
  4. 子块映射到父块后需要去重;
  5. 父块不能无限扩张,否则会超过上下文预算;
  6. 最终送入模型的内容可能不是原始命中的子块,而是“子块 + 父级标题 + 有限邻域”。

5.4 层次切块的风险

父块回填(parent expansion)容易造成上下文膨胀。假设检索命中同一父块中的 5 个子块,如果不去重,可能把同一段父块重复拼接 5 次。

应使用父块 ID 去重,并明确扩展规则:

def expand_to_parents(child_hits, parent_store, max_parent_chars=4000):
    result = []
    seen = set()

    for hit in child_hits:
        parent_id = hit["parent_id"]
        if parent_id in seen:
            continue

        parent = parent_store[parent_id]
        content = parent["content"][:max_parent_chars]

        result.append({
            "parent_id": parent_id,
            "title_path": parent["title_path"],
            "content": content,
        })
        seen.add(parent_id)

    return result

这段代码只演示去重和单父块上限。生产系统还应在截断前使用 token 计数,而不是字符数;还要考虑父块截断是否把例外条件留在了被丢弃的位置。


六、表格切块:不能把表格当普通段落处理

6.1 表格的语义不是线性的

表格通常包含:

  • 列标题;
  • 行标题;
  • 单元格值;
  • 合并单元格;
  • 单位;
  • 脚注;
  • 行与列之间的对应关系。

例如:

方案 月费 并发数 数据保留
基础版 99 元 10 7 天
专业版 499 元 100 30 天

如果按普通文本展开为:

方案 月费 并发数 数据保留 基础版 99 元 10 7 天 专业版 499 元 100 30 天

问题“专业版的并发数是多少?”仍可能得到正确结果,但问题“499 元对应哪个方案?”或“基础版和专业版的数据保留分别是多少?”对行列对应关系更敏感。PDF 抽取还可能把列顺序打乱,形成:

基础版 专业版
99 元 499 元
10 100
7 天 30 天

这类文本表面上包含所有值,实际上已经丢失结构。

6.2 表格的三种表示方式

方式一:保留 Markdown 表格

| 方案 | 月费 | 并发数 | 数据保留 |
|---|---:|---:|---:|
| 基础版 | 99 元 | 10 | 7 天 |
| 专业版 | 499 元 | 100 | 30 天 |

优点是可读性好,适合生成模型;缺点是长表格会迅速消耗 token。

方式二:行记录序列化

方案=基础版;月费=99 元;并发数=10;数据保留=7 天
方案=专业版;月费=499 元;并发数=100;数据保留=30 天

每一行都重复列名,token 成本较高,但行级检索和过滤通常更稳定。

方式三:结构化 JSON

[
  {"方案": "基础版", "月费": "99 元", "并发数": 10, "数据保留": "7 天"},
  {"方案": "专业版", "月费": "499 元", "并发数": 100, "数据保留": "30 天"}
]

适合程序化处理、字段过滤和后续计算,但生成模型能否正确使用它仍取决于格式、提示和任务类型。

6.3 表格切块的基本单位

表格切块通常不应只按字符长度切。更合理的层次是:

表格标题 + 单位 + 列标题
├── 行 1
├── 行 2
└── 行 3

如果表格很长,可以按行分块,但每个块都要重复列标题和单位:

表格:套餐限制,金额单位:人民币/月
列:方案 | 月费 | 并发数 | 数据保留

行:基础版 | 99 元 | 10 | 7 天
行:专业版 | 499 元 | 100 | 30 天

不能只存:

基础版 | 99 元 | 10 | 7 天
专业版 | 499 元 | 100 | 30 天

因为检索到这段后,模型不知道第三列是“并发数”还是“用户数”。

6.4 表格问答的边界

向量检索适合找到相关表格行,但不一定适合执行精确计算。例如问题:

哪个方案的月费最低且并发数至少为 100?

这包含过滤和比较:

argminr{月费(r)并发数(r)100}\operatorname{argmin}_{r} \{\text{月费}(r)\mid \text{并发数}(r)\ge100\}

更可靠的流程是:

  1. 检索表格或相关行;
  2. 解析为结构化记录;
  3. 使用程序执行过滤、排序和计算;
  4. 把计算结果和原始行交给模型解释。

如果只让生成模型阅读自然语言表格,它可能在数值比较、单位换算或缺失值处理上出错。表格切块解决的是“找到正确行列”,不是替代数据库查询或确定性计算。


七、上下文窗口:切块大小必须放进完整预算中计算

7.1 定义

**上下文窗口(context window)**是模型一次请求能够处理的最大 token 范围。它通常包括:

  • 系统提示词;
  • 用户问题;
  • 对话历史;
  • 检索到的文档;
  • 工具调用结果;
  • 要求模型生成的输出空间。

设模型上下文上限为 WW,则一次请求必须满足:

Tsystem+Thistory+Tquery+Tretrieved+Ttools+ToutputWT_{\text{system}} +T_{\text{history}} +T_{\text{query}} +T_{\text{retrieved}} +T_{\text{tools}} +T_{\text{output}} \leq W

其中 TxT_x 表示对应部分的 token 数量。

注意,检索块能放进上下文,不代表答案质量会提高。上下文中存在大量无关内容时,会产生:

  • 相关证据被噪声淹没;
  • 多个版本的同一规则同时出现;
  • 模型引用错误章节;
  • 输入成本增加;
  • 输出延迟增加;
  • 接近窗口上限时请求失败或输出被截断。

7.2 一个具体预算例子

假设某模型的上下文上限为:

W=16,000W=16{,}000

系统提示和工具说明占 1,5001{,}500 token,对话历史占 2,0002{,}000,用户问题占 300300,预留输出 2,0002{,}000。检索内容预算为:

Tretrieved16,0001,5002,0003002,000=10,200T_{\text{retrieved}} \leq 16{,}000-1{,}500-2{,}000-300-2{,}000 =10{,}200

如果每个检索块约 1,200 token,最多只能放入:

10,2001,200=8\left\lfloor \frac{10{,}200}{1{,}200}\right\rfloor=8

但如果层次检索将每个子块扩展为约 3,000 token 的父块,最多只有:

10,2003,000=3\left\lfloor \frac{10{,}200}{3{,}000}\right\rfloor=3

这就是为什么“父块越完整越好”并不成立。父块完整性和可放入的证据数量之间存在直接的预算冲突。

7.3 上下文组装不只是取 Top-K

最简单的策略是取相似度最高的 kk 个块:

Ck=TopK(R(q))C_k = \operatorname{TopK}(R(q))

但这种方法有两个问题:

  1. Top-K 结果可能全部来自同一段落,覆盖面很低;
  2. 每个块长度不同,token 总量不可控。

更实用的组装过程是:

  1. 先按检索分数排序;
  2. 对相同父块、相邻块进行去重或合并;
  3. 删除权限不匹配、版本不匹配的结果;
  4. 根据 token 预算逐个放入;
  5. 必要时保留标题、证据片段和引用位置;
  6. 对超长父块进行结构化截取,而不是盲目从尾部截断。

可将其视为一个受容量约束的选择问题:

maxSciSv(q,ci)s.t.ciStokens(ci)B\max_{S} \sum_{c_i\in S} v(q,c_i) \quad \text{s.t.} \quad \sum_{c_i\in S}\operatorname{tokens}(c_i) \leq B

其中:

  • v(q,ci)v(q,c_i) 是块对问题的价值;
  • BB 是检索内容的 token 预算;
  • SS 是最终送入模型的块集合。

如果还要避免重复,可以给相似块增加冗余惩罚:

v(ci)=v(ci)λmaxcjSsim(ci,cj)v'(c_i) = v(c_i) -\lambda \max_{c_j\in S}\operatorname{sim}(c_i,c_j)

这类思想对应常见的多样性重排方法。它的目的不是追求数学上的最优,而是避免 10 个几乎相同的块占满上下文。


八、固定、语义、层次和表格策略如何组合

这些策略不是互斥选项。生产系统常用组合方式:

8.1 固定长度 + 结构边界

先按标题、段落、列表和代码块切出自然单元,再把相邻单元合并到最大 token 长度。这样既保留结构,又避免单个章节过长。

流程可以是:

解析文档结构
→ 保护代码块、表格和列表
→ 以段落为初始单元
→ 合并相邻单元
→ 超过最大 token 数时切分
→ 为块添加标题路径和元数据

这通常比直接按字符截断更稳定。

8.2 语义边界 + 最大长度

语义相似度用于决定“在哪里更适合切”,最大 token 长度用于决定“无论如何不能超过多少”。完整条件应同时满足:

切分点=语义变化点    长度超过上限\text{切分点} = \text{语义变化点} \;\lor\; \text{长度超过上限}

还可以设置最小块长度,避免产生只包含一个短句的碎片:

LminciLmaxL_{\min}\leq |c_i|\leq L_{\max}

但最小长度不能机械执行。例如一个独立的安全警告虽然只有一句话,也可能是必须单独检索和展示的证据。

8.3 层次检索 + 语义子块

一种常见架构是:

  • 父块按章节或小节划分;
  • 子块按段落或语义边界划分;
  • 子块建立向量索引;
  • 命中子块后,根据问题复杂度决定是否回填父块。

简单事实问题可以只返回子块:

默认并发限制是多少?

复杂流程问题可以返回父块:

主库故障后完整恢复流程是什么?

这要求检索层记录子块与父块的稳定关系,不能在每次查询时临时猜测父级边界。

8.4 表格与层次结构组合

表格可作为一个不可拆分的父对象,行作为子对象:

父块:2025 年套餐限制表
子块:基础版行
子块:专业版行
子块:企业版行

检索具体套餐时返回对应行;问题涉及多个套餐比较时,回填列标题和多个行。若表格含单位、脚注或适用范围,应把它们视为表格父级上下文,而不是普通邻近文本。


九、常见误解与失败表现

9.1 “块越小,召回越精准”

块变小通常会减少无关文本,但也会降低证据完整性。失败表现包括:

  • 只检索到条件,没有结论;
  • 只检索到结论,没有例外;
  • 只检索到表格值,没有列名;
  • 只检索到代码调用,没有参数定义;
  • 模型开始根据常识补全缺失信息。

诊断方法是对每个问答样本标记“最小充分证据集合”。如果正确答案需要三个相邻块,而系统平均只召回其中一个,问题不是生成提示词,而是切块或召回粒度不合适。

9.2 “块越大,模型理解越完整”

块变大可能提升局部完整性,却会降低检索区分度。一个同时包含“安装”“升级”“卸载”“故障排查”的章节,对任何一个主题都可能有中等相似度,却不一定成为对应问题的最高结果。

诊断时可观察:

  • 相关块在候选集中的排名;
  • 块内非相关 token 占比;
  • 前 K 个块之间的重复率;
  • 生成答案引用的实际句子是否集中在块的某一小部分。

如果大块的有效证据只占 5%,说明需要更细的子块或二阶段重排,而不是继续扩大块。

9.3 “增加 overlap 一定能修复边界问题”

overlap 只复制相邻内容,不能修复:

  • 章节解析错误;
  • 表格列错位;
  • 文档版本混合;
  • 权限标签丢失;
  • 定义和跨章节引用关系缺失。

而且 overlap 过大可能让多个结果高度重复,造成上下文浪费。边界问题首先应通过结构化切分和父子关系解决,overlap 只是辅助措施。

9.4 “向量相似度高就代表证据正确”

向量相似度衡量语义接近,不保证:

  • 时间版本正确;
  • 权限允许访问;
  • 数值和单位一致;
  • 例外条件被包含;
  • 结论适用于当前用户。

例如问题询问 2025 年政策,最相似的块可能来自 2023 年文档。版本过滤必须在检索和上下文组装阶段执行,不能依赖生成模型自己判断。

9.5 “把所有检索结果交给模型,让模型自己筛选”

这会把检索错误转化为生成错误。尤其是以下情况风险很高:

旧政策:退款期限为 30 天。
新政策:退款期限为 14 天。

如果两个版本都进入上下文,模型可能折中回答“14 到 30 天”,或者引用旧政策。正确流程应先根据生效时间、租户、产品和权限过滤,再进行重排和生成。


十、评测切块质量:不要只看最终答案

切块策略应使用分层评测,而不是只看生成模型的最终准确率。

10.1 检索层指标

给定问题和标注证据集合 GqG_q,可以计算 Recall@K:

Recall@K=TopK(q)GqGq\operatorname{Recall@K} = \frac{ |\text{TopK}(q)\cap G_q| }{ |G_q| }

它回答的是:前 K 个结果是否覆盖了所需证据。

如果一个问题需要多个证据块,还应评估证据集合召回率,而不只是“是否命中任意一个相关块”。

10.2 块完整性指标

对每个问题标记:

  • 必需定义;
  • 适用条件;
  • 操作步骤;
  • 例外条件;
  • 数值和单位;
  • 版本或生效范围。

然后检查命中的块是否保留了这些要素。一个块即使和问题高度相似,只要缺少例外条件,也不能视为完整证据。

10.3 生成层指标

最终答案应检查:

  • 是否回答了问题;
  • 是否只使用检索证据;
  • 是否正确处理冲突版本;
  • 是否保留数值、单位和条件;
  • 是否在证据不足时拒答或说明不确定;
  • 引用位置是否对应实际支持结论的句子。

一个切块策略可能提高 Recall@10,却降低答案准确率,因为它带入了更多相互冲突的内容。因此需要同时观察检索和生成指标。

10.4 成本与延迟指标

切块策略也影响:

  • 索引阶段 embedding 调用次数;
  • 向量数据库存储量;
  • 查询阶段检索数量;
  • 重排输入 token 数;
  • 生成输入 token 数;
  • 端到端延迟;
  • 每次请求成本。

同一数据集应至少比较几种基线,例如:

固定长度,无 overlap
固定长度,有 overlap
结构化固定长度
语义边界 + 最大长度
层次子块检索 + 父块回填

在相同嵌入模型、相同 Top-K、相同生成模型和相同评测问题下比较,才能判断收益来自切块策略还是其他变量。


十一、权限、版本和更新必须与切块一起设计

切块会复制标题、邻接内容和 overlap,因此权限和版本字段必须在复制时一并继承。每个块都应能回答:

  1. 它来自哪个原始文档;
  2. 原始文档的版本是什么;
  3. 何时生效;
  4. 哪些用户或租户可以访问;
  5. 原文位置在哪里;
  6. 文档更新后如何失效或重建。

一个安全的数据流应当是:

原始文档
→ 解析与切块
→ 继承 ACL、租户、版本、生效时间
→ 建立索引
→ 查询时过滤
→ 重排
→ 上下文组装
→ 生成

不能只在生成后过滤答案,因为敏感内容已经进入了模型上下文。也不能只在文档级过滤:一个文档内部可能存在不同段落级权限。

文档更新时,旧块也必须处理。常见做法是:

  • 为块生成稳定的内容哈希;
  • 记录 document_versionchunk_version
  • 新版本建立新索引;
  • 查询时只允许命中当前生效版本;
  • 验证新索引完成后再切换别名或索引指针;
  • 出现解析错误时保留旧版本,避免索引被部分覆盖。

切块边界变化会使大量块的 ID 变化,因此更新策略应避免把“整库重建”误认为唯一方案。


十二、如何选择策略

可以按数据特征和问题类型做判断:

数据特征或问题 优先考虑
日志、聊天转储、无明确章节 固定切块,较小 overlap
结构良好的技术文档 标题/段落边界 + 最大 token 长度
长篇章节、流程和条件较多 层次切块
主题转换明显、段落质量较高 语义切块 + 最大长度
价格、配额、规格、对照关系 表格结构化表示
需要跨多个段落总结 子块检索 + 父块或邻域回填
需要精确计算和过滤 检索表格后交给 SQL、代码或专用工具
权限、版本和租户敏感 所有块继承元数据,并在生成前过滤

一个稳妥的开发顺序是先建立结构化固定切块基线,再根据失败样本引入复杂策略:

  1. 先确认文档解析正确;
  2. 用标题、段落和最大 token 长度切块;
  3. 评估证据召回和上下文成本;
  4. 对失败的长文档引入层次结构;
  5. 对主题变化明显的文档尝试语义边界;
  6. 对表格单独建立结构化管道;
  7. 根据问题类型决定返回子块、父块还是表格行。

这样可以把“切块问题”与“解析、嵌入、重排、权限和生成问题”区分开,而不是一次性引入多个不可解释的变化。


结语:切块是证据建模,不是长度调参

固定切块解决的是可预测性和实现成本;语义切块解决的是主题边界;层次切块解决的是精确检索与完整上下文之间的冲突;表格切块解决的是行列关系和结构化数值;上下文窗口则决定这些证据最终能否以可控成本进入模型。

真正可靠的切块结果应同时满足:

可检索可解释证据完整上下文可容纳权限与版本正确\text{可检索} \land \text{可解释} \land \text{证据完整} \land \text{上下文可容纳} \land \text{权限与版本正确}

如果评测发现答案错误,应先追问错误发生在哪一层:

  • 相关证据是否被切成不可独立理解的碎片;
  • 检索器是否找到了正确块;
  • 重排是否丢弃了必要条件;
  • 父块回填是否引入了冲突版本;
  • 上下文预算是否导致证据被截断;
  • 表格结构是否在解析时已经丢失;
  • 模型是否在证据不足时进行了无依据补全。

只有把这些状态分开,才能知道应该调整块大小、语义边界、层次回填、表格表示、召回数量,还是数据权限和版本管道。切块策略的目标不是产生形式上整齐的文本片段,而是建立一组在检索后仍然能够支持正确推理的证据单元。


系列导航与关联阅读

官方资料

本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。