分块单位不是长度而是「可答性」:太小丢失表头等上下文导致答非所问,太大使embedding稀释、检索对具体查询失效。应优先按文档天然边界分块,长度作为上限而非规则。
每篇教程都会选一个数字——512 tokens、1000 个字符、50% 重叠——但没有一篇能告诉你为什么。这个数字取决于文档的某个属性,一旦知道是哪个属性,这个数字几乎是不言自明的。
"What chunk size should I use"这个问题假设了单位是长度。但单位其实是可答性:一个 chunk 的大小合适,当且仅当它包含了回答被检索问题所需的全部信息,且不包含任何会稀释其 embedding 的内容。
两种失败方向在生产环境中都真实存在,且表现各异。太小的 chunk 是一个碎片——表格缺少表头、从句缺少定义、函数体缺少导入其类型的语句。检索成功了但答案仍然是错的,这是更难察觉的 bug。太大了则 embedding 是多个主题的平均值,因此对所有查询都排名中等,没有一个排名靠前;检索对特定查询悄悄失效,但在模糊查询下看起来还好。
文档本身就包含了你想要的边界。先利用它们,让长度只是一个上限,而非规则:
Overlap 是对你无法选好边界的一种补救。如果你随意分割,一些跨越切分点的句子会被破坏,而在每个 chunk 末尾重复 10–20% 的内容放到下一个 chunk 的开头,可以增加至少一份完整保留的概率。
这意味着 overlap 的成本与你的边界质量成正比。结构性分割可以将它降到零;随意分割则要多付出 15% 的存储、15% 的 embedding 成本,以及——人们容易忘记的部分——引入近乎重复的 chunk,在排名中相互竞争,把真正不同的结果挤出局。如果你使用了 overlap,在组装 prompt 之前先去重。
一个值得养成的好习惯:读一读你的 chunks。不是抽样看 embedding,而是读三十个随机选取 chunk 的实际文本。这只需要十分钟,但它能暴露出任何指标都发现不了的问题——一个把每个表格都切成两半的分词器、一个把两列交叉混合的 PDF 提取器、页眉页脚被重复到每个 chunk 中、导航菜单占据了语料库三分之一的空间。Chunking 的 bug 对人类来说通常显而易见,但对 recall@k 却是隐形的,因为一个系统性被破坏的语料库对评估集同样是被破坏的。
Chunking 最深层的问题在于,chunk 被从赋予其意义的文档中撕裂出来。"The rate increased by 3% in the second quarter"是无法被检索的:它没有指明是哪家公司、哪一年、哪种比率。
Anthropic 在 2024 年 9 月发表了一种方法,名为 Contextual Retrieval:在 embedding 之前,给每个 chunk 前面追加一句简短的、由模型生成的、将 chunk 定位在其父文档中的句子。他们报告称,上下文 embedding 使其评估语料库上的 top-20 检索失败率降低了约三分之一,而将上下文 embedding 与上下文 BM25 索引结合则使该比率降低约一半。这些是他们在他们语料库上的数字,迁移过去的是方法本身,而非数字。
同一思路的廉价版本完全不需要调用模型:将文档标题和标题路径作为字面文本追加到每个 chunk 前面。每个 chunk 只花几个 tokens,就能恢复分割时破坏的大部分命名信息。另一个值得了解的相近模式是小—大模式:用小 chunk 做 embedding 以保证检索精度,但在放入 prompt 之前扩展到其所属的父 section,这样模型读到的是连贯的内容。
没有人能告诉你 chunk size 应该是多少,但产生这个数字的实验规模很小。它需要一个标注集,这也是唯一真正繁琐的部分:
# 1. 五十个真实问题,每个标注了回答它的那段文本。
# 从支持工单或搜索日志中写,而不是凭想象。
gold = [("How do I rotate an API key?", "docs/keys.md#rotation"), ...]
# 2. 遍历你实际在选择的配置组合。
for size in (256, 512, 1024, 2048):
for overlap in (0, 0.1, 0.2):
index = build_index(corpus, size=size, overlap=overlap)
hits = sum(any(src in r.source for r in index.search(q, k=5))
for q, src in gold)
print(size, overlap, "recall@5 =", hits / len(gold))
# 3. 然后,分别在最好的两个设置上评估端到端答案。
# Recall@5 和答案质量不一定在同一个配置上达到峰值。
第三步是人们容易跳过的那一步,而惊喜就藏在那里:检索 recall 最好的配置不一定能产出最好的答案,因为一个稍大一点的 chunk 虽然排名略差,却携带了模型所需的上下文。测量你实际交付的那个东西。
从一开始就内置到系统中的,而不是事后补救的:每个 chunk 的元数据。源路径、文档标题、标题路径、版本和日期,至少这些。存储成本几乎为零,它是使引用成为可能的东西,它让你可以在向量搜索运行之前按产品版本或日期范围过滤查询,它也让你可以不重建整个索引而只重索引单个文档。只有一个 embedding 和一段文字的 chunk 是一个无法操作的 chunk。