文档不能整篇丢进索引,有两个独立的原因:其一,上下文预算有限,一次只能送进去有限的内容;其二,也是更根本的——一整篇文档的 embedding 会把所有主题平均成一个模糊的向量,检索时既匹配不上具体问题,也无法告诉你答案在哪一段。
于是要切成 chunk(块)。而 chunk 该多大,是 RAG 里第一个真正的两难:
| chunk 小 | chunk 大 | |
|---|---|---|
| 检索精度 | ✓ 向量主题集中,匹配准 | ✗ 多主题被平均,向量变钝 |
| 上下文完整性 | ✗ 指代、前提、条件被切断 | ✓ 保留了完整语境 |
| 上下文预算 | ✓ 单块便宜,可多召回几块 | ✗ 一块就占掉大量预算 |
| 噪声 | ✓ 送进模型的都是相关内容 | ✗ 夹带大量无关文本,干扰生成 |
注意这个表格的结构:「检索要小、理解要大」——两个需求指向相反的方向,所以不存在一个普适的最佳 chunk size。任何给你一个具体数字(「就用 512」)的建议都跳过了最重要的一步:先搞清楚你的文档结构和查询形态。
① 固定长度切分——按 token 数硬切,通常带 10–20% 的重叠(overlap)。重叠的作用是打补丁:万一某个关键句正好落在边界上,相邻 chunk 里还留着一份。简单、可预测、对任何文档都能跑,是合理的起点,但它对文档结构完全无知。
② 结构感知切分——沿文档自身的结构边界切:Markdown 的标题层级、HTML 的章节、代码的函数与类、法律文本的条款。这通常是投入产出比最高的做法,因为文档作者已经替你标好了语义边界,你只是别把它破坏掉而已。
③ 语义切分——计算相邻句子的向量相似度,在「话题明显转折」处下刀。听起来最聪明,但代价是索引构建时要额外跑一遍 embedding,且效果高度依赖阈值调参。在结构清晰的文档上,它通常打不过结构感知切分。值得用的场景是那些没有任何结构标记的长篇纯文本(如会议转录、访谈记录)。
① 给每个 chunk 加上下文头。把文档标题、章节路径、日期等元信息拼在 chunk 正文前面(如 《售后服务手册》> 第 3 章 退款 > 3.2 适用条件)。代价是每个 chunk 多几十个 token,收益是被单独召回时不再失去语境,且这些元信息本身也会进入 embedding,提升主题匹配度。这是性价比最高的一个改动。
② 检索的单位不必等于送进模型的单位。这是本章最重要的一条,却经常被忽略:可以用小 chunk 做检索(向量主题集中、匹配准),命中后把它所在的父级大块(或前后相邻块)送进模型(语境完整)。这个模式常被称为 small-to-big 或 parent-document retrieval,它直接绕开了上文那个两难——因为两难成立的前提是「检索单位 = 生成单位」,而这个前提本来就不必成立。
遇到取舍时,先检查取舍的前提。这和 2.1 案例中 Sarathi-Serve 的思路是同一个:prefill 与 decode 的冲突源于「prefill 不可分割」这个假设,切开它冲突就消失了;chunk 大小的两难源于「检索与生成用同一单位」这个假设,解耦它两难也就消失了。当你发现自己在一条曲线上艰难地选点时,去质疑那条曲线。
系统:产品售后手册的问答机器人。文档是结构清晰的 Markdown,含大量条款、表格与「例外情况」说明。切分采用默认配置:固定 512 token、重叠 50。原型阶段抽查十几个问题,效果很好,于是上线。
症状:客服反馈答案「大部分是对的,但经常漏掉限制条件」——比如告诉用户可以退款,却没说必须在 7 日内;或者引用了适用于普通用户的规则,却漏掉了 VIP 例外。没有任何报错,检索也确实命中了相关文档。
排查过程:把失败案例的召回 chunk 原文打印出来看,问题立刻显形——三类系统性的切坏:
为什么原型阶段没发现:抽查的十几个问题恰好都命中了完整的 chunk。切分错误的分布是不均匀的——它只影响那些正好跨越边界的内容,在小样本抽查中很容易全部躲过。这是 RAG 系统一个普遍的陷阱:原型效果好,是因为你测的样本太少,而不是因为系统好。
修复,按投入产出排序:① 改用结构感知切分,沿 Markdown 标题切,并把表格作为不可分割的原子单元(表格若超长则整表保留并单独成块);② 给每个 chunk 加上标题路径头;③ 把「例外」「注意」「限制」类段落合并进它所修饰的主条款,而不是独立成块;④ 建立一个覆盖各类条款的 golden set,把这三类失败固化成回归用例(见 3.5)。
最值得带走的一条:这个 bug 的根因不在检索算法、不在 embedding 模型、也不在 prompt——它在数据预处理里。RAG 系统的质量上限往往由索引构建阶段决定,而团队的注意力通常都集中在检索和生成上。答案错了先去看召回的原文,不要先去调 prompt。
起点配置:结构感知优先;无结构文档退回固定长度,chunk 取 256–512 token、重叠 10–20%。把它当作待优化的基线而不是答案——正确的 chunk size 要靠 3.5 的检索指标扫出来,通常扫 3–4 个档位就能看出趋势。
按 token 数切,不要按字符数切。字符数与 token 数的比例随语言剧烈变化(1.1),按字符切会导致中英文文档的实际 chunk 大小差出一倍以上。
PDF 是最大的坑:多栏排版、页眉页脚、跨页表格、扫描件——PDF 解析质量往往比切分策略更能决定 RAG 效果。上线前务必肉眼检查解析后的纯文本,很多团队的 RAG 效果差,根因是 PDF 解析把两栏文字交错拼在了一起。
代码文档:按函数/类切分,并保留 import 与所属文件路径作为上下文头。跨函数的调用关系无法靠切分解决,需要在检索层做扩展(召回一个函数时连带召回其定义)。
框架默认值不可信:各编排框架的默认 splitter 配置是为通用场景选的,与你的文档结构大概率不匹配。至少要把默认切分的结果抽样打印出来看几十条——这是投入五分钟就能避免一整类线上问题的检查。
A 合同 → 按条款切,条款是原子单元。合同的语义边界就是条款编号,绝不能把一个条款切成两半(法律文本里「但书」和例外条款往往在最后一句,切掉就完全反了意思)。超长条款宁可单独成块也不要硬切。必须带标题路径头。
B 客服对话 → 按完整对话轮次或会话切,不能按长度切。对话中大量使用指代(「这个」「刚才说的」),单独一句话脱离上下文后毫无意义。通常以「一次完整问题的解决过程」为一块,并在头部附上问题摘要。
C API 文档 → 按端点/函数切。每个端点的说明、参数、返回值、示例应在同一块内。这是结构最清晰的一类,结构感知切分几乎不需要调优。注意把公共的认证说明等前置内容复制到每块中,而不是让它们单独成块。
D 会议转写 → 语义切分的主场。没有任何结构标记,且话题转换是渐进的。这是少数值得上语义切分的场景。补充手段:先用模型给转写稿生成结构化的主题分段与摘要,再按主题切——用一次离线的模型调用换来长期的检索质量,通常很划算。
为什么大块检索差:embedding 把整块文本压成一个固定长度的向量,本质是对块内所有内容做了一次「平均」。块越大、主题越杂,这个平均向量就越接近于所有主题的中心,离任何一个具体主题都不近——它对所有查询都「有点像」,对哪个查询都不「特别像」,区分度因此下降。
为什么解耦有效:因为「检索精度」和「语境完整」这两个需求作用在不同的环节上。检索精度只关心向量的区分度(用小块),语境完整只关心送进模型的文本(用大块)。两者本来就不必是同一段文本,绑在一起纯粹是实现上的偷懒。
实现方式:索引里存小块的向量,同时记录每个小块所属的父块 ID;检索命中小块后,取出父块(或前后相邻块)送进 prompt。代价是索引结构复杂一点、存储多一点,收益通常显著。
核心问题:整个计划跳过了「先看数据」这一步,四个动作全部在下游。
应该插在最前面的一步:拿 20 个失败案例,把每个案例实际召回的 chunk 原文打印出来,逐条人工判断——正确答案是否在召回结果里?
这一步的诊断价值极高:
• 若正确答案根本不在召回结果里 → 问题在检索或索引(切分、embedding、召回数量),换生成模型和调 prompt 完全无效;
• 若正确答案在召回结果里但排序很靠后 → 这才是 rerank 该解决的问题(③ 对症);
• 若正确答案召回了、排序也靠前,模型却答错了 → 才轮到 prompt 和生成模型(②④ 对症);
• 若召回的 chunk 本身就是残缺的 → 问题在切分,前四步全都治不了。
更深层的问题:这个计划没有评估指标。「效果不佳」和「改好了」都靠感觉判断,四个改动做完也说不清哪个有用、有没有引入新的退化。正确的顺序是先建 golden set 和分层指标(3.5),再动手——否则就是在没有仪表的情况下调发动机。