固定窗口与递归切分

本节目标

你将从最容易验证的固定窗口开始,理解步长、重叠和终止条件;再学习“优先自然边界、最后才硬切”的递归策略。重点不是记住某个库的类名,而是能验证自己的切分器没有漏文、越界或死循环。

固定窗口:先建立确定性基线

设窗口上限为 ,相邻窗口重复 个单位,则步长为:

长度 时,窗口数近似为:

例如 ,stride 为 80,起点是 0、80、160;最后一块只取到文末。若 O=S,步长为 0,循环永远不会前进,所以配置校验不是“可选优化”。

一个可运行的最小实现

下面按 Python 序列索引切分。生产环境中的 units 应来自正确 tokenizer 或解析元素,不要先把 token ID 转回字符串再盲目拼接。

from collections.abc import Sequence  # 导入只读序列接口,使列表和元组都能作为输入。
from typing import TypeVar  # 导入类型变量工具,保留输入元素原本的类型信息。
 
T = TypeVar("T")  # 用 T 表示“任意一种元素类型”,例如 token、句子或解析 element。
 
 
def fixed_windows(  # 定义一个返回“起点、终点、窗口内容”的确定性切分函数。
    units: Sequence[T],  # 待切分的单位序列;这里不假设它一定是字符串。
    *,  # 后面的参数必须写成关键字,避免把 size 与 overlap 的位置写反。
    size: int,  # 每个窗口允许容纳的最大单位数,即 hard max。
    overlap: int,  # 与上一个窗口重复的单位数,用于保留邻接上下文。
) -> list[tuple[int, int, list[T]]]:  # 每项使用半开区间 [start, end) 并携带实际内容。
    if isinstance(size, bool) or not isinstance(size, int) or size <= 0:  # bool 是 int 的子类,需单独拒绝。
        raise ValueError("size 必须是正整数")  # 无法形成正窗口时立即失败,而不是进入循环。
    if (  # 接着验证 overlap 是否既是整数又能保证步长为正。
        isinstance(overlap, bool)  # 同样拒绝 True/False 这类伪整数。
        or not isinstance(overlap, int)  # 拒绝浮点数、字符串等不精确的配置。
        or not 0 <= overlap < size  # overlap 等于 size 会让步长 size-overlap 变成 0。
    ):
        raise ValueError("要求 0 <= overlap < size")  # 用清晰错误提示暴露无法前进的配置。
 
    result: list[tuple[int, int, list[T]]] = []  # 建立输出容器,按原始顺序保存所有窗口。
    start = 0  # 第一个窗口从第 0 个单位开始。
    while start < len(units):  # 只要起点还在输入范围内,就继续生成窗口。
        end = min(start + size, len(units))  # 最后一窗可短于 size,但绝不越过文末。
        result.append((start, end, list(units[start:end])))  # 切出 [start,end),并复制为独立列表。
        if end == len(units):  # 已经覆盖到文末时,不能再生成只含 overlap 的尾窗。
            break  # 立即结束,保证结果没有冗余窗口。
        start += size - overlap  # 以正步长前进,同时留下指定数量的重叠单位。
    return result  # 返回全部窗口;空输入会自然得到空列表。

返回 start/end 而不只返回文本,是为了保留可验证来源 span。空输入返回空列表;最后一块到达文末后立即终止,避免产生只含 overlap 的额外块。

固定窗口能保证什么,不能保证什么

它容易保证:

  • 输出确定;
  • hard max 可预测;
  • 来源单位能全覆盖;
  • overlap 数可精确计算;
  • 适合作为成本与召回基线。

它不能自动保证:

  • 句子、条件和代词完整;
  • 标题与正文不分离;
  • 表头随数据行出现;
  • 函数或 JSON object 不被切断;
  • 相邻块不会混入两个主题。

因此固定窗口是“比较起点”,不是默认生产答案。

递归切分的直觉

递归策略准备一组从强到弱的候选边界,例如:

  1. 已解析的 section/paragraph;
  2. 空行;
  3. 换行;
  4. 语言相关句末;
  5. 空格;
  6. 无可用边界时 hard window。

面对超长片段,先尝试较强边界。若拆出的某个子片段仍超 hard max,就只对它使用更弱边界。最后把相邻小片段按顺序合并,直到加入下一个会超过预算。

split(part, boundary_index):
  if units(part) <= hard_max:
    return [part]
  if no weaker boundary:
    return hard_windows(part)
 
  pieces = split_by_current_boundary(part)
  if boundary made no progress:
    return split(part, boundary_index + 1)
 
  recursively split oversized pieces with weaker boundaries
  merge adjacent complete pieces without crossing hard_max

LangChain 当前文档把递归字符切分列为常见通用起点;这是一种实用默认值,不是它在所有语料上最优的实验证明。其默认按字符计数,候选分隔符为 ['\n\n', '\n', ' ', ''],更贴近以空格分词的语言。中文、日文或泰文应按实际文本补充句末/全角标点或零宽空格等边界,并用 fixture 验证不会破坏语义。即使把长度函数换成 tokenizer 计数,分隔符仍只定义候选边界;hard max 仍须由目标模型的实际 token 限制验收。Unstructured 则先基于解析元素组合,仅在单元素过长时继续切分。两者输入抽象不同,不能只比较类名。

合并比“切开”更容易出错

只递归拆开而不重新合并,会产生孤立标题和一句话碎块。合并器应满足:

  • 保持原始顺序;
  • separator 的语义不能静默消失,避免 foobar 式粘连;
  • 同 source、revision、ACL 和允许的结构边界内才合并;
  • 候选超过 hard max 时先关闭当前块;
  • 单个元素本身超过 hard max 时进入兜底窗口;
  • 空白片段不生成 chunk,但来源偏移仍以规范化文本为准。

若先 strip() 再计算字符偏移,位置会漂移。更稳妥的顺序是:规范化换行和 Unicode,记录单位的字符区间,再从原规范文本切片。

语言、代码与结构边界

自然语言分句并不简单:

  • 中文句号、问号与引号组合需要测试;
  • 英文缩写、小数和 URL 会误触发句点规则;
  • 没有空格的长串仍需 hard fallback;
  • emoji、组合字符和 Unicode 规范化会影响字符偏移。

代码优先使用函数、类或语法树边界;Markdown fenced code 不应被普通段落分隔符拆散;表格优先按完整行切,并把表头作为检索上下文。上述规则将在 [[Knowledge/AI Agent Engineer/docs-CN/Chunking策略/03-结构化与语义切分|结构化与语义切分]]继续展开。

必测性质

用示例测试只能证明几个例子;切分器更适合检查不变量:

性质失败意味着什么
所有来源单位至少被覆盖一次内容丢失
每块长度 <= hard max下游可能截断或拒绝
ordinal 连续、顺序稳定引用与增量更新不可预测
每次循环都前进某些配置会死循环
overlap 只出现在预期窗口重复成本失控
不跨 source/revision/ACL来源或权限污染
相同输入与版本生成相同结果无法缓存、比较或重放
内容哈希与实际正文一致向量/引用可能来自旧文本

边界用例至少覆盖:空文本、一个单位、恰好 hard max、hard max+1、没有任何 separator 的长串、全空白、多语言、超长代码块和超长表格行。

常见错误与排查

  • 分隔符被删掉:检查合并后相邻词是否粘连,或将 separator 归属前一片/后一片。
  • 短尾巴重复两次:到达文末后没有立即 break。
  • 最后若干单位漏掉:窗口数量公式或 end 取值错误。
  • 递归没有进展:当前 separator 根本不存在,却仍对同一输入再次递归。
  • 先切字符串后猜 token span:引用偏移和 tokenizer 长度对不上。
  • 单元素越界被“允许”:hard max 已不再是 hard max。

练习

  1. 手算 的所有半开区间 [start,end) 与实际重复量。
  2. 给上面的 fixed_windows 添加 6 个 unittest:空输入、恰好一窗、尾窗、overlap、非法配置和确定性。
  3. 设计一个 separator 优先级,使 Markdown 标题、代码 fence、段落与句子按强到弱排列;写出代码 fence 内出现空行时的处理规则。
  4. 解释为何“把切出的字符串重新拼起来等于原文”在有 overlap 时不能直接成立;提出去重后的验证办法。

掌握检查

  • 我会推导 stride 与窗口数量。
  • 我能证明循环前进并在文末终止。
  • 我把 hard max 视为不可突破的契约。
  • 我知道递归策略是“强边界优先 + 超长兜底 + 小片段合并”。
  • 我会检查覆盖、顺序、边界隔离、哈希和确定性。
  • 我不会把一个语言的分句正则推广到全部语料。

小结与下一步

固定窗口提供可复现下限,递归切分尽量保留自然边界;真正可靠的生产输入通常还带有解析结构。下一步:[[Knowledge/AI Agent Engineer/docs-CN/Chunking策略/03-结构化与语义切分|结构化与语义切分]]。

参考资料

来源获取日期:2026-07-22。返回 [[Knowledge/AI Agent Engineer/docs-CN/Chunking策略/00-目录|Chunking 策略目录]]。