Appearance
第6章 Chunking:固定分块、结构化分块与语义分块
"A chunk is not a span of characters. It is a unit of retrievable meaning." — Chunking 里的第一原理
本章要点
- Chunk 不是字符切片,是可检索的语义单元——分块策略决定了下游能不能把某段知识作为整体召回
- 三大策略各有边界:固定长度(最简单、覆盖场景最广)、结构化(保留文档层级、适合带结构的源)、语义(按内容相似度切分、对话/叙事类最佳)
- 三条工程参数贯穿所有策略:chunk_size(token 上限)、overlap(重叠 token 数)、split_boundary(优先切分点)
- 2024 年出现的 Contextual Chunk(Anthropic)把每个 chunk 前加一段 LLM 生成的文档级 context——显著抬升了检索准确率
- Chunk 质量不能靠感觉评估——必须用端到端召回率(是否命中 gold chunk)量化
6.1 为什么 chunk 不是字符切片
第 5 章把原始文档变成了 blocks + metadata 的 IR。本章要回答的问题:每个 block 应该作为一个整体送给下游 Embedding,还是该继续切细?
朴素答案是"按固定 token 数切"。但这会破坏语义。看一个真实例子:
「新版企业版套餐中,私有化部署是包含的增值服务之一,但 SSO 功能需要额外购买企业增强包。企业增强包的定价为每年 20,000 元,包含以下能力:SAML 2.0 集成、OIDC 支持、LDAP 同步、审计日志。」
这段 150 字的文本按"每 80 字一 chunk、无重叠"切会变成两个 chunk:
- chunk 1:「新版企业版套餐中,私有化部署是包含的增值服务之一,但 SSO 功能需要额外购买企业增强包。企业增强包」
- chunk 2:「的定价为每年 20,000 元,包含以下能力:SAML 2.0 集成、OIDC 支持、LDAP 同步、审计日志。」
用户问"企业增强包包含什么能力"——chunk 2 是匹配的,但 chunk 2 本身没提"企业增强包"四个字(被切到 chunk 1 末尾了)。Embedding 模型拿 chunk 2 的文本算向量,query "企业增强包 能力" 的相似度可能很低。这就是语义完整性被分块切断的典型失败,对应第 3 章 §3.3 子模式 2。
chunk 的好坏,本质是回答一个问题:当这个 chunk 作为唯一上下文交给 LLM 时,LLM 能不能独立理解它、并从中得到该有的信息?——如果不能,chunk 就有缺陷,需要更好的切法、或更大的 overlap、或额外 context 注入。
6.2 三大策略全景
三种策略并非互斥——生产系统通常混用:结构化为主、固定长度作 fallback、语义分块用在叙事/对话类。选型参考:
| 策略 | 适用源 | 优点 | 缺点 |
|---|---|---|---|
| 固定长度 | 非结构化文本、任意 | 简单、可控、延迟低 | 容易切断语义 |
| 结构化 | Markdown、HTML、代码、技术文档 | 保留文档层级 | 依赖解析质量、长节需再切 |
| 语义 | 对话记录、叙事文本、客服工单 | 切点和意义对齐 | 需要 embedding 推理、慢 |
6.3 固定长度分块:三个关键参数
最朴素也最实用。三个参数决定效果:
chunk_size
单 chunk 的 token 数上限。典型取值 200-1000 token,与 Embedding 模型的训练语料长度对齐:
- bge-small / text-embedding-3-small: 训练时多数样本 < 512 token,chunk 200-400 最佳
- bge-m3 / text-embedding-3-large: 支持 8192 token 输入,chunk 可放大到 800-1500
- Contextual / ColBERT / E5: 类似 512 token
太小:单 chunk 信息不足,检索时很多相关 chunk 但都不完整。太大:单 chunk 主题发散,Embedding 向量"均值化"——每个方向的相似度都平淡,难以精准命中。
实操起点:先选 400-600 token 作为默认,然后按本章 §6.8 的评估方法用 gold set 调优。
overlap
相邻 chunk 的重叠 token 数。典型 10%-20% 的 chunk_size(即 50-150 token)。作用是弥补固定切断造成的边界损失——关键句子被切到下一 chunk 开头时,前一 chunk 的结尾有相同文本,保证至少一个 chunk 语义完整。
overlap 的代价:索引膨胀 —— 100 万字原文用 500 token chunk + 100 overlap,总 chunk 数约是无 overlap 的 1.25 倍。Embedding、存储、检索成本都相应增加。生产实操取 10-15% 是甜点区。
split_boundary
"优先切分点"——切到 chunk_size 上限时,往前/往后找最近的自然边界再切,而不是从字符中间硬切。优先级从高到低:
- 段落分隔(
\n\n) - 句号(
。.!?) - 分号、逗号(次优,中文里逗号很弱)
- 空格(不得已时)
- 字符边界(终极兜底)
Python 生态的 RecursiveCharacterTextSplitter(LangChain)和 SentenceSplitter(LlamaIndex)都实现了这套优先级。自己写也不难——20 行代码。
一份配置样例
python
# LangChain 风格
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=80,
separators=["\n\n", "\n", "。", "!", "?", ";", " ", ""],
length_function=count_tokens, # 按 tokenizer 计,不是字符数
)注意 length_function——必须用下游 Embedding 模型的 tokenizer 计算长度,不能按字符数。中英混文本里同样 500 字符,token 数可能从 300(纯中文)到 800(纯英文)差很多。按字符数配 chunk_size 会导致英文 chunk 远超 embedding 限长。
6.4 结构化分块:让文档的层级说话
Markdown、HTML、代码等带结构的源,最好按结构切而不是按字符切。好处:
- 切点天然语义完整(小节、列表、代码块是完整单元)
- chunk metadata 可以继承结构(section_path="产品规格 > SSO 配置")
- 下游引用时能精准定位到"哪一节的哪一段"
按 heading 分块
Markdown / HTML 的 H1-H6 标题构成层级树。结构化分块的基本策略:
- 叶子节点(最深层 heading 下的内容)作为独立 chunk
- 若某个叶子内容 < chunk_size,合并到上一级 heading 下
- 若某个叶子内容 > chunk_size,用固定长度再切(fallback)
每个 chunk 带 section_path=["产品规格", "功能列表", "身份认证"] 作为 metadata。检索时可以按 section_path 前缀过滤("只看功能列表相关的 chunk")、也可以让 LLM 在 prompt 里显式引用。
表格和代码块的特殊处理
表格和代码块不能按行切——破坏语义的典型场景。策略:
- 小表格(< chunk_size):整个表格 + caption + 相邻段落作为一个 chunk
- 大表格(> chunk_size):保留表头 + 部分行作为一个 chunk,下一个 chunk 复制表头 + 接下来的行。表头复制不是 overlap,是为了让每个 chunk 独立可读
- 代码块:优先作为一个整体 chunk。超长代码用 AST(第 5 章 §5.6)按函数/类切,不按行切
一个常见反模式:表格被硬切成两段后,第二段失去表头"产品 | 价格 | 版本"的上下文,embedding 看到的就是一堆数字。用户问"企业版价格"——第二段命中但模型看不懂、还得靠第一段补。
列表的处理
长列表(20+ 项)切分时保留列表标识:
text
原文:
- SAML 2.0 集成
- OIDC 支持
- LDAP 同步
- ... (20 项)
- 审计日志导出
chunk 1:
以下是企业增强包包含的能力(第 1-10 项):
- SAML 2.0 集成
- OIDC 支持
...
chunk 2:
企业增强包能力(接上,第 11-20 项):
- ...
- 审计日志导出每个 chunk 都自解释——告诉读者"这是某个列表的一部分"。这样检索到 chunk 2 时模型知道这是完整列表的后半段、而不是孤立信息。
6.5 语义分块:用相似度找切点
结构化分块依赖文档有明确结构。对话记录、会议纪要、访谈实录这类长段连续叙事没有 heading,固定长度又可能切断话题切换——需要语义分块。
基本思路:把相邻句子的 Embedding 距离作为"话题相似度"信号,距离突然变大的地方就是切点。LlamaIndex 的 SemanticSplitter 实现了这套逻辑:
python
# 伪代码
sentences = split_to_sentences(text)
embeddings = [embed(s) for s in sentences]
distances = [1 - cosine(embeddings[i], embeddings[i+1]) for i in range(len(sentences)-1)]
threshold = percentile(distances, 95) # 取分布的 95 分位作为切点阈值
boundaries = [i for i, d in enumerate(distances) if d > threshold]
chunks = split_by_boundaries(sentences, boundaries)语义分块的实操问题
- 延迟高:每句都要 embedding,10 万字文档要几千次推理。离线解析时可接受,实时索引不行
- 阈值漂移:不同 domain 的"话题切换"幅度不同,固定阈值不通用。需要按 domain 校准或用自适应阈值
- 过度敏感:短文本、口语化对话可能每句话都话题漂移,切出一堆碎片 chunk
实操建议:语义分块只用在值得的场景——访谈、会议纪要、用户与 Agent 的长对话。普通文档用结构化 + 固定长度组合更划算。
LLM 分块(2024 年以后的新路径)
用 LLM 直接判断分块点是近年新兴方案。Prompt 例如:
以下是一段文档。请在语义转折处插入
<SPLIT>标记,让每段成为独立可检索的单元。每段 200-400 字。
优点:切点质量极高、对长文档和专业领域有效。缺点:成本(每段文档一次 LLM call)、延迟(几秒到几十秒)、一致性(同一段文档跑两次结果可能不同)。
常见妥协:用小模型(Llama 3.1 8B、Qwen 2.5 7B)本地推理做 LLM 分块,成本可接受。SOTA 的 RAG 系统在值得精加工的核心知识库上会用这种策略。
6.6 Contextual Chunk:2024 年的关键进展
Anthropic 在 Contextual Retrieval 中提出的思路:每个 chunk 在 embedding 前,先用 LLM 生成一段文档级 context 加到 chunk 前面。
例子:
- 原 chunk:「企业增强包的定价为每年 20,000 元...」
- Contextual chunk:「这段文字来自《企业版产品规格 V3》的"定价"章节,描述增强包定价。具体内容:企业增强包的定价为每年 20,000 元...」
再做 embedding。效果(Anthropic 公开数据):
- 纯向量 retrieval,top-20 错误率 5.7%
- 加 Contextual Chunk 后 top-20 错误率 3.7%(下降 35%)
- 再加 BM25 混合 + Rerank,综合错误率 1.9%
Context 由 LLM 基于"完整文档 + 当前 chunk"生成。Anthropic 把这一步集成进 prompt cache——整份文档只读一次进 cache,每个 chunk 查询时只算 delta,显著降本。
实操要点
Contextual Chunk 的三个工程要点:
- 模型选型:Claude Haiku / GPT-4o-mini 这类小模型足够——context 生成是低复杂度任务
- prompt cache:完整文档作为 cacheable prefix,每个 chunk 用这个 cache。成本可降 80%+
- 增量更新:文档变了,所有 chunk 的 context 理论上要重生成。实际上如果变动集中在局部,只重建被影响的 chunk 更划算
第 14 章 Rerank 会讨论 Contextual Chunk + Rerank 的组合效果。
为什么 Contextual Chunk 管用
从第一性原理看,Contextual Chunk 解决的是一个信息损失补偿问题。原 chunk 在分块时丢失了两类信息:
- 结构信息:这是哪份文档、哪一节、上下文是什么
- 指代信息:chunk 里的"它""这个""上述"指的是什么
向量检索阶段,模型只看 chunk 文本。丢失的结构和指代信息让 embedding 表达不准确——"它"在不同文档里意思完全不同,但 embedding 分不清。加上文档级 context 后,embedding 能把"这段 chunk 讨论的是 X 产品的 Y 功能"这一锚定信息编码进去。
这也解释了为什么 BM25(只看字面)加 Contextual Chunk 效果同样好——context 里显式出现"企业版""SSO"等关键词时,BM25 能通过这些词命中更相关的 chunk。
Contextual Chunk 的失败场景
不是所有场景都适合:
- context 生成耗时:实时索引场景(流式处理用户上传的文档)不能等 LLM 生成 context
- 长文档成本高:几百页的书,每个 chunk 都要看全文生成 context,即使有 cache 也有成本
- 语言覆盖:多数 LLM 对小语种(阿拉伯语、泰语)的指代解析还不稳定
对这些场景可以用轻量 context替代——用规则抽文档 title + section path 拼到 chunk 前,不走 LLM。效果不如 full Contextual Chunk,但接近 60-70% 的收益、0 成本。
6.7 常见分块反模式
在给具体策略打磨参数之前,先列清楚哪些做法直接扣分——它们不是"可以调参数解决的次优方案",而是"无论怎么调都不如别的"。
反模式 1:按字符数切中文。中文的 token 数和字符数不是 1:1 关系,不同 tokenizer 差异也大。按字符数切可能把 chunk_size 定到远高于 embedding 模型限长。正确做法:先 tokenize,按 token 切。
反模式 2:切完不再验证。跑完 chunker 直接送下游,没人检查有没有 chunk 只有一个单词、没有 chunk 是纯 metadata、没有 chunk 被切成 2 个字符。生产 chunker 的输出必须过 validator:chunk 长度必须在 [min_len, max_len] 区间、文本必须非空、token 数必须合规。
反模式 3:全局一套参数。知识库里 50% 是技术文档、30% 是客服 FAQ、20% 是合同——用同一个 chunk_size=500 处理所有文档肯定不是最优。按 doc_type 配置不同分块策略。
反模式 4:分块后丢弃结构。解析阶段辛苦抽出的 section_path、table_caption、list_context,分块后全成了纯文本 chunk 的一部分,metadata 里没保留。结构化信息在分块环节必须传递下去——它是 retrieval 和引用的重要输入。
反模式 5:sentence-level 分块。有人把每句话作为 chunk,embed 后检索。句子太短——上下文缺失导致 embedding 语义漂移。除非语料极特殊(格言、FAQ 问答 Q),否则单句 chunk 效果远不如段落 chunk。
反模式 6:overlap 过大。有人把 overlap 设到 50% 甚至更多,以为"重叠越多越保险"。代价是索引膨胀 2 倍、检索时一个问题召回同内容的多个 chunk、上下文冗余。10-20% 是经验值,超过 30% 一定错。
6.8 重叠、去重和 metadata 继承
Overlap 的两种形态
固定长度分块的 overlap 是"尾 - 头复制"。结构化分块有两种 overlap:
- sliding overlap:相邻叶子 chunk 各自复制对方一部分——成本高但语义连续性最好
- header overlap:每个 chunk 前面加上文档 title + 上级 section path——模拟"你现在在读哪一节"。对检索帮助大、成本低
推荐默认策略:结构化分块 + header overlap + 语义边界偏好。
去重:防止同内容 chunk
知识库里常有同一段文字出现在多份文档里(模板条款、公司介绍段落、重复的 FAQ)。索引时若不去重,检索会返回多份相同内容的 chunk,上下文浪费。
去重策略:
- 内容 hash 去重:
sha256(chunk.text)相同的只保留一份、其他用 reference 指回 - 近似去重:simhash 或 minhash,相似度 > 0.95 的 chunk 合并
- 保留来源链接:去重后保留被合并 chunk 的
source_ids列表——检索时能还原所有来源
metadata 继承
每个 chunk 从 IR metadata 继承基础字段(doc_id、publish_date、access_level、language)、再加上自己的结构字段(chunk_id、section_path、start_token、end_token、overlap_prev、overlap_next)。完整 chunk schema:
json
{
"chunk_id": "prod-spec-v3#7#a1b2",
"doc_id": "prod-spec-v3",
"text": "企业增强包的定价...",
"context": "这段文字来自《...",
"metadata": {
"section_path": ["产品规格", "定价", "增强包"],
"publish_date": "2026-03-15",
"access_level": "internal",
"language": "zh",
"start_char": 4200,
"end_char": 4750,
"overlap_prev": "prod-spec-v3#6#c3d4",
"overlap_next": "prod-spec-v3#8#e5f6",
"token_count": 480,
"source_ids": ["prod-spec-v3", "prod-spec-v2"]
}
}6.9 怎么评估分块策略
chunk_size、overlap、strategy 的选择不能靠感觉——需要端到端指标。两条路线:
Retrieval-only 评估
构造一批 QA pair(约 50-200 条),每条标注应该命中哪个 chunk(gold chunk)。不同分块策略跑一遍,对比:
- recall@k:gold chunk 是否在 top-k 召回
- MRR:gold chunk 的倒数排名平均
- gold coverage:gold chunk 是否至少完整(没被切断)
注意 gold chunk 会随分块策略变化——不同 chunk_size 下 gold chunk 的边界不同。评估时要动态确定 gold chunk:找到包含答案文本的最小 chunk 集合。
End-to-end 评估
跑完整 RAG 链路,看最终答案的 faithfulness、准确率。用 ragas / trulens 等自动评估框架。这条路线更真实,但慢且受生成模型质量影响。
实操顺序:先 retrieval-only 快速筛掉明显差的策略,再对 top-3 候选做 end-to-end 验证。每次策略变更都跑一次——chunking 是 RAG 质量的强杠杆,任何改动都要 A/B 验证。
实测参数扫描的典型结果
下面是一个真实企业 FAQ 知识库上做参数扫描的典型数据(约 5000 篇 FAQ,gold set 200 条 QA):
| chunk_size | overlap | strategy | recall@10 | MRR | 索引 chunk 数 |
|---|---|---|---|---|---|
| 200 | 0 | fixed | 0.68 | 0.42 | 18k |
| 400 | 60 | fixed | 0.78 | 0.55 | 12k |
| 600 | 100 | fixed | 0.81 | 0.58 | 8k |
| 1000 | 150 | fixed | 0.75 | 0.51 | 5k |
| 400 | 60 | structured | 0.85 | 0.62 | 10k |
| 400 | 60 | structured + Contextual | 0.91 | 0.71 | 10k |
三个观察:
- chunk_size=600 是 fixed 策略的峰值——小于它上下文不够、大于它主题发散
- 结构化分块一步跳升 5-7 个点——保留 section 结构就是便宜的收益
- Contextual Chunk 在 structured 基础上再加 6 个点——Anthropic 的数据在企业场景可复现
数据因 domain 而异——法律、代码、新闻的甜点区不同。但跑一次参数扫描的成本远低于猜一个参数踩半年坑的成本。每次接一个新知识库都做这件事。
Gold set 怎么构造
Gold set 的质量决定评估的可信度。构造要点:
- 多样性:覆盖短问题、长问题、简单事实、复合推理、否定问、冷僻查询。30% 短 + 50% 中 + 20% 长 是经验分布
- 真问题:从真实用户日志采样 >> 人工编造。后者会带编写者的偏见
- 标注 gold chunk 而非 gold answer:问题是"正确答案在哪段知识库"而不是"正确答案是什么"。前者可用于 retrieval 评估、后者只能做 end-to-end
- 定期刷新:业务和知识库都在变,gold set 每季度 review 一次、补 50 条新样本、去掉过时的
一个 200 条的 gold set 是 MVP、500-1000 条是生产标配。超过 1000 条后边际收益递减,把精力放在分层(按场景分桶)更有价值。
6.10 真实参数的经验区间
在若干公开和实际项目里观察到的典型参数:
| 场景 | chunk_size | overlap | 主策略 |
|---|---|---|---|
| 通用企业知识库 | 400-600 token | 15% | 结构化 + 固定 fallback |
| 代码检索 | 按函数/类 | 0(AST 边界) | AST 结构化 |
| 法律/合同 | 800-1200 token | 10% | 结构化按条款 |
| 学术论文 | 按 section | 0-10% | 结构化按章节 |
| 对话/工单 | 300-500 token | 20% | 语义分块 |
| 新闻/博客 | 400-800 token | 15% | 固定长度 |
这些是起点,不是终点。每个系统都要用 gold set 微调。
多 embedding 模型的分块差异
同一份文档、同样的分块策略,送给不同 embedding 模型,效果可能差异很大。原因是每个模型对 chunk 长度的敏感度不同:
- bge-small-zh-v1.5:最佳 chunk_size 在 200-400 token。> 500 显著下降
- bge-m3:长文本友好,500-1500 token 都表现稳定
- text-embedding-3-small:8192 限长但 sweet spot 在 300-600
- voyage-2:超长文本(> 2000 token)上表现比多数模型好
实操:选定 embedding 模型后再调 chunk_size,不要先定 chunk_size 再选模型。大多数项目倒过来做了——先按"感觉"切了 500 token、再强行适配某个模型,结果事倍功半。
Hybrid Chunking:多粒度索引
有些生产系统采取多粒度并行索引——同一份文档以多种 chunk_size 同时切分、建多个索引。查询时按问题类型路由:
- 细粒度问题(问具体参数、条款)→ 查 small chunk 索引(200 token)
- 综合性问题(问概念、流程)→ 查 large chunk 索引(1000 token)
成本是 2-3 倍索引空间。收益是 recall 峰值提升 5-10%。大型系统(像 Perplexity、You.com)据信在用类似多索引策略。第 11 章讨论向量数据库时会回到多索引的工程成本。
6.11 代码、表格与对话:特殊内容的分块
前 10 节默认文档是散文性质——段落连续、层级清晰、按语义或结构切即可。但企业知识库里有大量非散文内容:代码仓库、产品规格表、客户访谈记录、会议纪要。这些内容按通用分块策略切、recall 会断崖式下降。每种格式需要专门的分块方法。
代码的 AST-based 分块
代码是最需要专门分块的格式——按字符或行切必然破坏语义。正确做法是按 AST(抽象语法树)的自然边界切:
- 函数级:每个函数 / 方法一个 chunk
- 类级:对于紧凑小类、整个类一个 chunk;大类按方法切
- 模块级:小模块或单文件的 import + 顶层声明作独立 chunk
工具链:tree-sitter(第 5 章讨论过)——多语言 AST 解析库、GitHub Copilot / Cursor 都在用。伪代码:
python
def chunk_code(source, language):
tree = tree_sitter.parse(source, language)
chunks = []
for node in tree.walk():
if node.type in ["function_definition", "method_definition", "class_definition"]:
chunks.append({
"text": node.source,
"symbol_name": extract_name(node),
"type": node.type,
"lines": (node.start_line, node.end_line),
})
return chunks代码 chunk 的 metadata
代码 chunk 的 metadata 比散文丰富:
| 字段 | 用途 |
|---|---|
symbol_name | 函数 / 类名、精确匹配的主 key |
symbol_type | function / class / method |
language | Rust / Python / Go、filter |
file_path | 展示引用时显示 |
line_range | 跳转到源码的精确位置 |
imports | 该 chunk 依赖哪些外部符号 |
docstring | 提取的文档注释、独立索引 |
signatures | 函数签名、作为"精确召回锚点" |
有了这些 metadata、代码 RAG 就能:
- 按
language=rustfilter - 按
symbol_name精确查找(复用 ch12 BM25) - 按
imports做依赖图查询
跨符号引用的处理
代码的特殊挑战:一个 chunk 可能引用另一个 chunk 里定义的符号。比如 UserService.login 的函数 chunk 里调用了 validatePassword——但 validatePassword 定义在另一个 chunk。
两种应对:
- 懒加载:召回
UserService.login后、解析出它调用的符号、按需补充调用目标的 chunk - 预关联:入库时建立符号调用图、chunk 的 metadata 里标
calls: ["validatePassword", "getUserById"]、检索时 LLM 可显式请求展开
Copilot Chat 用的是懒加载 + 符号图混合。对大型代码库 RAG 是必要的。
表格的分块
表格用散文分块必然坏。正确做法:
- 每行一个 chunk:chunk 内容 = 列头 + 当前行数据(如
| 产品 | 价格 | 功能 | 企业版 | 20000/年 | SSO、LDAP、审计 |) - 每 N 行分组:长表按 10-20 行分组、每组带列头
- 小表整块:< 20 行的表作为单 chunk、保留完整结构
关键:每 chunk 都要带列头(第 16 章 §16.15 呼应)——否则下游看到一堆数字没意义。
对话 / 访谈文档的分块
会议纪要、客户访谈、对话日志的特点:按 turn 组织、有 speaker、有时间戳。分块策略:
- 每轮发言一个 chunk:适合简短 turn
- 按话题切:用语义分块检测话题转换、把连续几 turns 合并(长访谈推荐)
- 完整回合合并:Q+A 作为一个 chunk(问答对话最自然)
chunk metadata 必备:
json
{
"text": "...",
"speakers": ["张三", "李四"],
"timestamp_start": "2026-04-20 14:23",
"timestamp_end": "2026-04-20 14:27",
"turn_indices": [15, 16, 17]
}Metadata 让检索能做"张三说过什么 SSO"这类精确查询——纯文本 chunk 做不到。
混合内容 chunk
很多真实文档混合多种格式:产品手册里"功能介绍(散文)+ 规格表(表格)+ 调用示例(代码)"。两条处理思路:
- 按格式切:散文部分按语义分块、表格单独切、代码按 AST 切——每种内容有最优 chunk 方式。缺点:上下文碎片化、LLM 看到时不知道它们属于同一节
- 按结构保留:一个完整的"功能说明章节"作为一个 chunk、内部保留 markdown 结构(标题 / 表格 / 代码块)。LLM 读这种 markdown chunk 理解好、但 chunk 体积大
生产推荐:结构性分块优先(按 ## 标题 / 章节切)、单 chunk 内保留混合内容的 markdown 结构。超长的再按格式细分。
特殊内容分块的评估
通用 chunk 的评估(§6.9)对代码 / 表格不完全适用。专门指标:
- 代码 chunk 的 symbol 完整性:抽样 chunk 看是否完整包含一个函数 / 类、有无半截
- 表格 chunk 的列头覆盖率:多少 chunk 带有列头(应 100%)
- 对话 chunk 的 turn 切分正确率:是否在 speaker 切换时切开
这些指标各自独立、分别跑 gold set 评估。通用 recall@k 不能反映这层质量。
什么时候按特殊格式分块
判断依据:
- 代码占知识库 > 20% → 值得上 AST 分块
- 表格多、且用户会问表格内容 → 必须表格专门处理
- 对话 / 访谈文档有检索需求 → 必须按 turn 分块
- 全散文 → §6.3-§6.6 通用策略够用
早期 MVP 可以全用通用分块应急、但规模化前要按内容类型专门化——否则某类内容的 recall 永远上不去。
6.12 多级 chunking:parent-child 与分层检索
前面几节讨论 chunk size 时都有个困境:小 chunk 检索准但上下文不足、大 chunk 上下文丰富但检索精度降。§6.3 建议 200-500 token 折中、但这是强行平衡、不是最优解。多级 chunking 是 2023-2024 年逐渐成熟的模式——小 chunk 用于检索、大 chunk 用于喂 LLM,同时拿到两端优势。LangChain、LlamaIndex、Anthropic Contextual Retrieval 都在用这个模式。
粒度的两难
单一粒度、总在 precision 和 context 之间二选一。
Parent-child 结构
多级 chunking 的核心思路:
- Child chunk(小):100-200 token、用于 embedding 和检索
- Parent chunk(大):500-1500 token、是一个或多个 child 的父级
- 检索 → 扩展:query 命中 child chunk 后、返回对应 parent chunk 给 LLM
Query embedding 和 child embedding 匹配——精度高。检索命中 child 后、按 parent_id 查 parent、把 parent 送 LLM——上下文足。
实现细节
数据模型:
python
{
"chunk_id": "doc-A:child-5",
"level": "child",
"text": "...", # 150 token
"embedding": [...], # child 文本的 embedding
"parent_id": "doc-A:parent-2",
"order_in_parent": 2
}
{
"chunk_id": "doc-A:parent-2",
"level": "parent",
"text": "...", # 800 token,可以重新 embed 也可以不 embed
"child_ids": ["doc-A:child-4", "doc-A:child-5", "doc-A:child-6"]
}检索时:
python
def retrieve(query, top_k=5):
# 1. 用 child embedding 做相似度检索
children = vector_store.search(query_embed,
filter="level=child",
top_k=top_k)
# 2. 去重 parent(多个 child 命中同 parent 只返回一次)
parent_ids = list(set(c.parent_id for c in children))
# 3. 拉 parent 文本
parents = [get_parent(pid) for pid in parent_ids]
return parents[:top_k] # 返回 parent 给下游为什么比单级好
对比同一份数据的三种分块:
| 策略 | Child embed | 检索精度 | 上下文完整性 |
|---|---|---|---|
| 只用小 chunk (200 token) | 200 token | 高 | 差 |
| 只用大 chunk (800 token) | 800 token | 中 | 好 |
| 多级 child 检索 + parent 返回 | 200 token | 高 | 好 |
多级的 child 用来匹配 query、parent 用来回答 query——两者各司其职。
典型 parent 大小的选择
Parent 的大小不能无限大——太大了浪费 LLM context:
| Parent 大小 | 适用场景 |
|---|---|
| 400-600 token | 一般 RAG、平衡好 |
| 800-1500 token | 长答案、复杂推理场景 |
| 2000+ token | 法律 / 科研文献等深度分析场景 |
Child 相对 parent 的比例:parent = 3-5 × child 是经验甜点。
和 Contextual Retrieval 的关系
§6.6 的 Contextual Chunk(给每个 chunk 注入文档上下文)和多级 chunking 思路互补:
- Contextual Retrieval:每个 chunk 自带文档宏观上下文(前缀)
- Multi-level:每个 chunk 自带段落中观上下文(parent)
两者叠加:child chunk 的 embedding 包含 Contextual 前缀、检索命中后返回 parent——宏观 + 中观 + 精确匹配三维度齐全。这是 Anthropic 论文里 SOTA 设置的核心。
去重的工程细节
多个 child 命中同一 parent 时、不能重复返回同一 parent——浪费 context。去重策略:
- 严格去重:top-k child 映射到 parent、重复的 parent 只留一次。可能 top-5 child 只对应 2 个独立 parent
- 加权融合:child 命中数作为 parent 的分数加权——两个 child 命中的 parent 比一个命中的更相关
- 多 parent 合并:命中的 parent 连续(如 parent-1 和 parent-2)时、考虑合并或保留顺序
生产常用严格去重 + 加权——简单且效果好。
多级 chunking 的其他变体
除了经典 parent-child、还有:
- 三级:child (150t) → section (500t) → document (2000t)——检索命中后按问题复杂度选返回层
- overlapping parent:parent 之间有 overlap(如每个 parent 覆盖 3 个 child、child 可以属于相邻两个 parent)——减少 boundary 问题
- 异构 parent:同一文档有不同粒度的 parent(小段 + 大段)、按 query 类型选
超过三级的嵌套通常不值得——复杂度爆炸、收益边际递减。
常见坑
- Parent 也 embed 进索引:浪费存储、检索精度还不如纯 child
- Parent 尺寸没控制:有的 parent 几千 token、LLM context 被单个 parent 填满
- 去重没做:top-5 都是同一 parent、信息浪费
- 跨文档混用 child:child.parent_id 跨文档、同文档连贯性丢失
- Parent 更新时忘了同步 child:文档改了 parent 改了但 child embedding 还是旧的——索引不一致
什么时候不用多级
多级不是免费——复杂度增加、运维成本升。以下场景用单级更好:
- 文档本身短:推文、FAQ 问答——单级 chunk 就行
- 检索 + 答案都要精确短:如错误码查询——chunk 就是答案
- 极高 QPS 场景:多级多一次 parent 查询、每请求 +5-10ms
- MVP 阶段:还没把单级做好、不要上多级
判断:当 single-level chunking 的"精度 vs 上下文"明显卡住时、上多级解锁。还没到瓶颈就上多级是过度工程。
评估多级 chunking
多级的评估要分两层:
- Child-level recall:child chunk 的检索精度(和单级 chunk 对比)
- Parent-level answer quality:parent 作 LLM context 后的 faithfulness / answer relevance
两个指标都应比 single-level 好——只一个好说明实现有问题。gold set 要针对性建、确保能测到"精度高 + 上下文好"的联合提升。
6.13 Chunk 和 embedding model 的联合调优
§6.3 讲的 chunk size 参数是"独立决定的"——但 chunk 不是在真空里切、它直接喂给 embedding model、决定检索质量。Chunk 策略和 embedding 模型是耦合的、经常被分开调优导致局部最优、全局次优。这节把两者作为系统整体看、给出联合调优方法。
为什么 chunk 和 embedding 必须联合考虑
两者的耦合有三层:
- Context window 约束:chunk 不能超过 embedding model 最大输入(§9.3)
- 训练分布:embedding 模型在特定长度分布上训练、远超训练分布性能下降
- 语义完整性:chunk 切到一半、embedding 的向量表达模糊
Context window 和训练分布的差异
很多工程师把"最大 context"当 chunk size 上限——错误。Embedding 模型的真实"甜点长度"往往远小于 max context:
| 模型 | Max input | 训练分布主峰 | 真实甜点 |
|---|---|---|---|
| bge-small-zh | 512 | ~200 tokens | 150-300 tokens |
| bge-m3 | 8192 | ~500-1000 tokens | 300-1200 tokens |
| text-embedding-3-small | 8192 | ~1000 tokens | 400-1500 tokens |
| voyage-3 | 16000 | 长文本优化 | 1000-3000 tokens |
chunk 远超甜点(比如把 bge-small 用在 1000 token chunk)——向量质量降、recall 降。
实测差异
同一份业务数据、chunk size 从 200 到 2000 的 recall 曲线(bge-m3):
text
chunk size | recall@10 | 存储(GB) 1M chunks
200 | 0.87 | 0.8
400 | 0.94 | 0.8 × 0.5 = 0.4 (chunk 少一半)
600 | 0.96 | 0.27
800 | 0.97 | 0.2
1200 | 0.96 | 0.13
1600 | 0.93 | 0.1
2400 | 0.85 | 0.07观察:
- 600-800 token是 bge-m3 的实际甜点
- 超过 1500 token 性能下降(超出训练主峰)
- 短于 400 token 性能也差(信息密度低)
换个模型这个曲线就变——必须对目标 embedding 实测。
联合调优流程
不要分别调、要联合测:
典型扫描矩阵:
| Embedding | chunk=200 | 400 | 600 | 800 | 1200 |
|---|---|---|---|---|---|
| bge-small | 0.82 | 0.84 | 0.83 | 0.80 | 0.72 |
| bge-m3 | 0.87 | 0.94 | 0.96 | 0.97 | 0.96 |
| text-embed-3-s | 0.83 | 0.91 | 0.93 | 0.94 | 0.94 |
最佳组合:bge-m3 + chunk=800(0.97)。但如果延迟要求严、bge-small + chunk=400 也够(0.84)。
Chunk 长度分布的重要性
不是所有 chunk 都要同一长度——分布比均值重要:
- 均值 500 + 标准差 50(窄分布):chunk 长度整齐、embedding 质量稳定
- 均值 500 + 标准差 400(宽分布):有 100 token 的小 chunk、也有 1500 的大 chunk——短的信息密度低、长的超出模型甜点
控制分布:
- 结构化分块时、小段落合并到目标长度
- 超长段落强制切分到 max 阈值
- 目标:80% 的 chunk 落在
[甜点 × 0.7, 甜点 × 1.3]区间
特殊情况:embedding 支持多粒度
现代 embedding 模型(bge-m3、text-embedding-3)兼容多长度——短 chunk 和长 chunk 质量差距小。这让 parent-child 多级 chunking(§6.12)实用:
- Child chunk:150 token、embedding 精确
- Parent chunk:800 token、embedding 仍可用
- 同一模型、不需要切换
这种情况 chunk size 的单一取舍变成"粒度组合"取舍——更灵活。
联合调优的监控
上线后持续监控:
embed_length_distribution:chunk 输入 embedding 的 token 长度直方图recall_by_chunk_size:按 chunk 长度分桶的 recallembedding_saturation:embedding 向量的 L2 norm 分布——饱和说明 chunk 太短信息不足
分布漂移(比如新数据让 chunk 长度变)——触发重新评估。
常见反模式
- chunk size 拍脑袋:选 500 因为"看起来合理"、不实测
- 换 embedding 不改 chunk:从 bge-small 升到 bge-m3、但仍用 chunk=200——没用上 bge-m3 长 context 能力
- chunk 分布过宽:没控制长度分布、效果不稳
- 只测 recall 不看成本:chunk=200 recall 0.85、chunk=800 recall 0.95 但成本差 4 倍——可能 chunk=800 不值
跨 embedding 迁移的 chunk 重考虑
ch9 §9.13 讲 embedding 升级要重 embed——同时要重评估 chunk 策略。新模型甜点可能变:
- 旧 bge-small(甜点 200)→ 新 bge-m3(甜点 800)
- 老 chunk=250 继续用、浪费 bge-m3 的能力
- 应该同时改 chunk=800、全量重 chunk + 重 embed
大多数团队 embedding 升级时只换模型不改 chunk——留钱在桌子上。
Chunk 和其他 RAG 组件的全局耦合
Chunk 不只和 embedding 耦合——也和:
- Rerank:chunk 太长、cross-encoder 截断(ch14 §14.15)
- Context packing:chunk 太大、top-k 塞不下(ch16)
- LLM context:chunk 总 token 决定 LLM 看多少
全局最优 chunk size 要同时满足:
- Embedding 的甜点
- Rerank 的 512 token 约束
- LLM context 的 token 预算
- 检索的业务延迟
这些约束交叉出的"窗口"可能很窄——400-800 token 是多数 RAG 项目的 sweet spot、不是偶然。
联合调优的工程价值
认真做联合调优的团队能省:
- 成本:存储 / embedding 调用 / 索引内存——小 chunk 可能多 4-10×
- 延迟:向量库 / rerank 的 QPS 和 chunk 数成反比
- 精度:对目标 embedding 的 sweet spot 让 recall +3-5 点
这些加起来是 RAG 整体性价比的 10-20% 差距——很值。
6.14 Chunk 的 debug 与异常识别
前面讲了分块策略和评估指标——但具体的 chunk 出了问题怎么查?生产里有几百万 chunk、一条坏 chunk 可能让某类 query 常年召回差。这节给出一套 chunk debug 的方法——从异常批量识别到单个 chunk 调试、帮团队快速定位 chunking 问题。
Chunk 异常的典型症状
每种症状指向不同问题——先分类、再查。
批量识别异常 chunk
遍历索引、找出统计上异常的 chunk:
python
def find_anomalous_chunks(chunks):
anomalies = {
"too_short": [],
"too_long": [],
"duplicate": [],
"never_retrieved": [],
"broken_boundary": [],
}
# 1. 长度异常
token_counts = [len(c.tokens) for c in chunks]
mean_len = np.mean(token_counts)
std_len = np.std(token_counts)
for c in chunks:
if len(c.tokens) < mean_len - 2*std_len:
anomalies["too_short"].append(c.id)
if len(c.tokens) > mean_len + 2*std_len:
anomalies["too_long"].append(c.id)
# 2. 重复
hashes = {}
for c in chunks:
h = minhash(c.text)
if h in hashes and similarity(c.text, chunks[hashes[h]].text) > 0.95:
anomalies["duplicate"].append(c.id)
hashes[h] = c.id
# 3. 从未被召回(结合 30 天的 retrieval log)
retrieved_ids = set(fetch_recent_retrieved_ids())
for c in chunks:
if c.id not in retrieved_ids and c.indexed_at < 30_days_ago:
anomalies["never_retrieved"].append(c.id)
# 4. 边界断裂(文本以逗号 / 连词开头或结尾)
for c in chunks:
if c.text.startswith(("和", "而且", "但是", ",", "、")):
anomalies["broken_boundary"].append(c.id)
return anomalies每周跑一次、产出 anomaly report——人工 review 样本、决定是否改 chunking 策略。
单 chunk 的 debug 场景
某 chunk 怀疑有问题、具体查:
python
def debug_chunk(chunk_id):
chunk = get_chunk(chunk_id)
print(f"Text (first 500 chars): {chunk.text[:500]}")
print(f"Length: {len(chunk.tokens)} tokens")
print(f"Source: {chunk.source_doc_id}")
# 上下文:前后 chunk
prev_chunk = get_chunk(chunk.prev_id)
next_chunk = get_chunk(chunk.next_id)
print(f"Previous chunk ends: ...{prev_chunk.text[-100:]}")
print(f"This chunk starts: {chunk.text[:100]}...")
print(f"Next chunk starts: {next_chunk.text[:100]}...")
# 看边界切得合理吗?
# 向量分布
print(f"Embedding norm: {np.linalg.norm(chunk.vec)}") # 应 ≈ 1
# 最近邻:看这 chunk 和谁相似
neighbors = vector_db.search(chunk.vec, top_k=5, exclude_self=True)
for n in neighbors:
print(f" {n.id}: sim={n.score:.3f}, text: {n.text[:80]}")
# 检索表现:过去 30 天被召回了多少次
retrievals = query_log.count(chunk.id, last_days=30)
clicks = click_log.count(chunk.id, last_days=30)
print(f"Retrieved: {retrievals}, Clicked: {clicks}, CTR: {clicks/max(retrievals,1):.2f}")这套脚本能回答:
- Chunk 切得合理吗(看上下文边界)
- Chunk 的语义邻居是谁(看相似 chunk)
- Chunk 在生产被用得如何(看召回和点击)
Chunking 策略的对比工具
想知道 "换一种 chunking 策略效果如何"——对同一批文档跑不同策略:
python
def compare_chunking(docs, strategies):
results = {}
for strategy in strategies:
chunks = strategy.chunk(docs)
results[strategy.name] = {
"chunk_count": len(chunks),
"avg_length": np.mean([len(c.tokens) for c in chunks]),
"length_std": np.std([len(c.tokens) for c in chunks]),
"sample_chunks": random.sample(chunks, 5),
}
return results
# 使用
compare_chunking(sample_docs, [
FixedLengthStrategy(size=500),
StructuredStrategy(boundary="heading"),
SemanticStrategy(similarity_threshold=0.7),
ContextualStrategy(context_source="doc_summary"),
])直接看 sample chunks 的文本、判断哪种切法最合理。比纯看数字直观得多。
异常 chunk 到 chunking 改进
识别了异常不是终点——要回流到策略改进:
- 边界断裂 chunk 多 → 当前结构化分块失败、换语义分块
- 超短 chunk 多 → 小段落没合并、加最小长度合并逻辑
- 超长 chunk 多 → 段落太长没切、加最大长度强制切分
- 重复 chunk 多 → 去重逻辑不严、加 simhash 去重
异常的分布指导下一版 chunking 策略的改进方向——不是拍脑袋调参。
从 badcase 到 chunk 问题
§20.18 讲 badcase 归因——当归因到 "找不到" 或 "塞不下"、接下来查是否 chunk 问题:
python
def trace_badcase_to_chunks(badcase):
# 用户问 query、期望答案是 X
# 但系统没找到相关 chunk
# 1. 找 doc 里含答案的部分
gold_doc = find_doc_containing(badcase.expected_answer)
# 2. 这部分在哪个 chunk 里?
gold_chunks = chunks_containing_text(gold_doc, badcase.expected_answer)
# 3. 这些 chunk 被召回了吗?
if not gold_chunks:
print("答案文本被 chunk 边界切碎了!")
# 关键信息跨 chunk、需要改分块
else:
print(f"答案在 chunks: {[c.id for c in gold_chunks]}")
# chunk 存在、但没被召回——检索问题不是 chunking 问题这个 trace 让 badcase 归因到具体 chunk——避免"感觉 chunking 不好"的主观判断。
Chunking 的测试用例库
积累 chunking 测试用例:
yaml
# chunking-test-cases.yaml
cases:
- name: "长表格不应被切"
input: "某包含 50 行表格的文档.md"
assert: "每个 chunk 里表格完整或不包含表格"
- name: "连续段落应合并到 min length"
input: "多个 50 字段落的文档"
assert: "没有少于 200 token 的 chunk"
- name: "代码块边界"
input: "含代码块的 markdown"
assert: "代码块不跨 chunk"每次改 chunking 策略、先过这些测试——防止 regression。
Chunk metadata 的质量检查
Chunking 不只是切文本、还要正确产生 metadata:
- chunk 的
doc_title是否来自正确的父 doc - chunk 的
section_path是否正确层级 - chunk 的
publish_date是否继承自 doc - chunk 的
access_level是否和 doc 一致
抽样检查 100 个 chunk 的 metadata 完整性——经常发现"title 是正确的、但 section_path 丢了"这种问题。
常见反模式
- 不做异常识别:垃圾 chunk 长期污染索引
- 单个 debug 没工具:每次查都从头写脚本、效率低
- 不看实际 chunks:只看 aggregate 指标、不抽样看文本
- Badcase 不归因到 chunk:修错地方、chunk 问题一直在
- 测试用例缺失:改 chunking 策略总 regression
Debug 工具的维护
好的 chunking debug 工具链:
- Anomaly report:每周自动跑、邮件发到团队
- Debug UI:输入 chunk_id 看完整信息
- Compare UI:对比两种策略的 chunks
- Trace 从 badcase 到 chunk:badcase review 时一键查
初期建 1-2 人月、长期每季度维护 0.5 人天——投入小、debug 效率提升数倍。
Chunking 调试的大局观
Chunking 是 RAG 的上游——上游 bug 下游表现各种奇怪。把 debug 工具建起来、让 chunking 变灰盒:
- 每个 chunk 能追源(从哪个 doc、怎么切的)
- 每个 badcase 能追 chunk(是 chunk 问题还是检索问题)
- 每次策略改动有可控的 A/B
好 chunking 不是一次调对、是持续 debug 出来的——和其他 ML 系统一样。
6.15 Chunking 策略变更的迁移工程
项目运行一段时间、会发现当前 chunking 策略不是最优——太短 / 太长、按结构切错、或有了新文档类型。这时要改 chunking 策略。但"改"不是改代码就完事——已经索引的百万 chunk 要重新切、重新 embed——工程上等同于一次局部或全量重建。这节讲 chunking 迁移的具体工程、和 §9.13 embedding 迁移并列。
为什么要改 chunking 策略
每种原因的影响范围不同——先搞清楚是"全量重切"还是"只切新文档"。
影响范围分析
python
def analyze_migration_scope(old_strategy, new_strategy):
# 哪些 doc type 会受影响?
affected_types = diff_affected_types(old_strategy, new_strategy)
# 影响多少 chunk?
affected_count = count_chunks_by_type(affected_types)
# 预估成本(重 embed + 重索引)
embed_cost = affected_count * 0.00005 # per chunk
compute_cost = estimate_compute(affected_count)
return {
"affected_types": affected_types,
"affected_count": affected_count,
"estimated_cost_usd": embed_cost + compute_cost,
"estimated_duration_hours": affected_count / 10000, # 大致
}上迁移前跑这个分析——知道规模才能规划。
三种迁移策略
策略 A:全量重切重建
- 建新索引(绿)、跑新 chunking
- Shadow 流量验证
- 切换、保留旧索引(蓝)72h
- 完全切换
适合:chunking 大改、影响 > 50% chunk
策略 B:按类型局部 backfill
- 只对受影响的 doc_type 重切
- 其他 chunk 不动
- 用 §8.13 的 backfill 机制
适合:chunking 改动只影响某类文档(如新加了"代码专用")
策略 C:新老混用(不推荐)
- 新文档用新策略、老文档保留旧
- 索引里 chunk 粒度不一
适合:实在不能迁移的场景、作短期 workaround。长期索引"拼盘"——会出各种怪问题。
Chunking 和 embedding 的联合迁移
很多时候、chunking 改动伴随 embedding 改动——两者联合:
- 新 chunk size 需要匹配的 embedding 模型
- 重新 embed 是必然的
- 索引格式可能同时变
一次解决两个问题比分两次做好——索引重建代价大、一次迁移合并节省。
A/B 验证
迁移不是盲目切——先 A/B:
python
# 部分 query 走老 chunking 的 index
# 部分走新 chunking 的 index
# 对比 recall / faithfulness / 延迟
@feature_flag("use_new_chunking")
def query(q, user_id):
if feature_flag_on("use_new_chunking", user_id, bucket=10): # 10% 流量
return query_new_index(q)
return query_old_index(q)10% 流量跑 2 周——看新 chunking 的业务指标是否确实更好。没 A/B 数据的迁移是赌博。
回滚
迁移过程中可能发现新 chunking 不如预期:
- 新索引启用后、faithfulness 反而降 2 点
- 某类 query recall 下降
- 某种文档解析出错、chunk 丢失
这时要能回滚:
- 新旧索引并存 72h
- 配置层的切换、分钟级
- Fallback 路径(新索引崩时自动切旧)
没回滚能力、事故要修几天——重建老索引代价高。
迁移的成本估算
以 1000 万 chunk 为例:
- Embedding 成本(OpenAI-large):1000 万 × $0.00013/1K tokens × 500 tokens/chunk = $650
- 自托管 embedding GPU:~$50(按小时算)
- 存储(蓝绿双份索引 72h):额外 ~$100
- 工程时间:2-3 人周
总计:$1000 左右硬成本 + 工程时间。小投入换长期质量提升——大规模项目明显值。
迁移的进度监控
迁移过程持续几小时 / 几天——实时可见:
text
Chunking Migration Progress (started 2026-04-25 10:00)
Target: 8.5M chunks in "legal" doc_type
Progress: [████████░░░░] 62% (5.3M / 8.5M)
Rate: 300 chunks/sec
ETA: 2026-04-25 14:30
Failed: 42 (in DLQ)
Estimated cost so far: $420没这个可见性、团队不知道到哪了——焦虑。
迁移和业务的协调
大迁移影响业务:
- 业务方要知情(查询可能临时慢 / 不一致)
- 事先沟通、不是静默迁移
- 预留 debug 时间、有问题能响应
跨团队协调比技术工作更重要——技术几天搞定、协调不好几周搞不定。
迁移后的验证
迁移完成不等于结束——持续观察 1-2 周:
- Gold set recall 是否维持 / 提升
- Badcase 数量和类型变化
- 用户反馈(满意度 / 投诉)
- 成本 / 延迟指标
1-2 周平稳——才算迁移成功、可以下线旧索引。
常见陷阱
- 不估算规模:突然开始跑、发现几天才结束
- 没 A/B 就切:切了才知道新的不如预期
- 不保留旧索引:回滚不了
- 迁移期间业务方不知情:一通操作、用户投诉
- 不监控迁移进度:不知道到哪了
Chunking 迁移的频率
典型 RAG 项目:
- 大迁移:1-2 年一次(大结构变、embedding 升级)
- 小迁移:季度内小调整、局部 backfill
- 持续优化:Contextual Retrieval / parent-child 等逐步加
别追求完美 chunking 一次到位——渐进演化、每次迁移都有可管理的规模。
迁移能力是团队的 maturity 指标
建起流畅 chunking 迁移能力的团队 vs 没有的:
- 有:每季度小迁移、持续优化、技术债少
- 没:chunking 策略老旧多年不敢动、技术债重
Chunking 迁移能力的工程(backfill 工具、A/B 框架、监控)一次建、长期用——是团队成熟度的 baseline。
迁移文化
成熟团队对迁移的态度:
- 计划性:每季度 review chunking、决定是否迁移
- 数据驱动:有数据支持再迁、不凭感觉
- 小步快跑:小迁移多、大迁移少
- 可回滚:每次迁移都有退路
这套文化让 chunking 成为可以优化的系统、不是"上线后不敢动"的遗产。
和 embedding 迁移的联动
ch9 §9.13 讲了 embedding 迁移、本节 chunking 迁移——很多场景两者一起:
- 升级 bge-m3 → bge-m4:embedding 变 + 可能 chunk size 调整
- 上 long-context embedding:chunk size 从 400 → 800
- 加代码专用 embedding:同时加代码专用 chunking
联合迁移计划比独立做好——一次重建索引 vs 两次。
总结:迁移是持续工程
Chunking 不是"一次设计好就完事"——是持续演化。迁移工程是保证演化可行的基础设施。早期建好这层能力、后期每次优化都顺利——没建好、每次优化都痛苦、最终团队放弃优化、质量停滞。
这是 RAG 所有组件的通用原则——建迁移能力、不怕演化。
6.16 跨书关联:分块与预训练的数据切分
Embedding 模型本身是在某个 chunk 长度分布下训练的。BGE、E5 等模型的训练语料多数是 "512 token 以下的句对"——超长 chunk 在这类模型上的表达能力有限。《Serde 元编程》第 7 章讨论过类似的分块机制——serde 的 zero-copy 反序列化把字节流切成 Visitor 可消费的片段,核心约束也是"一段能自洽消费"。
更深一层:无论是 RAG chunking、分布式数据切片、还是编译器的基本块划分,切点选择都关乎下游消费者的独立可理解性。这是所有"分治"模式的共同哲学。
6.17 本章小结
- Chunk 是可检索的语义单元,不是字符切片——分块决定了下游能否把一段知识作为整体召回
- 三大策略:固定长度(最广)、结构化(保结构)、语义(按内容切)——生产常混用
- 三个工程参数:chunk_size、overlap、split_boundary——按 Embedding 模型特性和 domain 校准
- Contextual Chunk 是 2024 年的关键进展——给每个 chunk 注入文档级上下文,显著抬升检索精度
- 分块策略必须端到端评估——感觉不可靠,gold set + retrieval metric 才是评分标尺
下一章讨论 metadata 和权限模型——怎么让 chunk 带着业务边界进入索引,保证每个用户检索时只看得到自己应该看到的知识。