主题
RAG 切片策略:文档怎么切才不掉链子
这篇解决一个常见问题:知识库里明明躺着答案,提问却召不回来,或者召回的是半句话。多数时候不是模型不行,而是切片(chunking,把长文档切成一段段可检索单元)没切对。切片决定了检索的最小单位,也就决定了这套 RAG 的上限。
切片的本质:决定检索的最小单位
RAG(检索增强生成)的链路是:文档切片 → 每片算向量(embedding)入库 → 提问时召回最相似的几片喂给大模型。一片就是一个检索单位,模型看不到没被召回的内容。所以切片直接定义了什么算「一段知识」:切得碎,语义被拆散;切得粗,一片里混了几个主题,向量被稀释,谁都匹配不上。
固定长度切:能用,但别当终点
最省事的做法是按字符数硬切。LangChain 的 RecursiveCharacterTextSplitter 会按 ["\n\n", "\n", " ", ""] 的优先级递归降级找切点,默认 chunk_overlap 是 200 个字符,chunk_size 在较新版本里默认 4000(早期文档和示例里常见 1000),实际以官方文档和你装的版本为准。
优点是简单、每片长度可控,方便按 token 估算成本。缺点是它不认识语义,一刀下去可能正好把「因为……所以……」拆到两片里,所以适合当兜底,不适合当唯一策略。
overlap 为什么必要
滑动窗口重叠(overlap)是给硬切打的补丁:相邻两片共享一段尾巴和开头。没有它,一句结论落在切点另一侧就彻底消失;有了它,至少一片能保留完整上下文。代价是向量总数和存储变大,一般取 chunk_size 的 10%~20%,别超过三分之一,否则召回里全是重复内容。
按结构切通常更好
Markdown 标题、代码块、表格本身就是天然边界——作者用 ## 就告诉了你「这里换主题了」。按标题切出的片,语义完整度通常远高于按字数硬切。LangChain 的 MarkdownHeaderTextSplitter 默认会把用来切分的标题行从正文里去掉(strip_headers=True),我一般选择保留标题,或把标题路径写进元数据。
ts
// 按 Markdown 标题层级切分的示意实现
type Chunk = { text: string; path: string[] };
export function splitByHeading(md: string): Chunk[] {
const chunks: Chunk[] = [];
const stack: string[] = []; // 当前标题路径,如 ["Guide", "Install"]
let buf: string[] = [];
const flush = () => {
const text = buf.join("\n").trim();
if (text) chunks.push({ text, path: [...stack] });
buf = [];
};
for (const line of md.split("\n")) {
const h = /^(#{1,6})\s+(.*)$/.exec(line);
if (h) {
flush(); // 遇到标题先结算上一片
stack.length = h[1].length - 1; // 截断到当前层级
stack[h[1].length - 1] = h[2].trim();
} else {
buf.push(line);
}
}
flush();
return chunks; // 再把 path 拼进正文或元数据
}一个问题横跨两段怎么缓解
这是切片天生的缺陷,只能缓解:一是 overlap,用重叠区兜住跨界的句子;二是父子块(small-to-big),用小片做向量检索保证命中精度,命中后把它的父级大块喂给模型;三是把标题路径拼进每片正文开头,补回上下文的「坐标」;四是同一份文档用两种粒度各建一次索引,多路召回再合并去重。组合起来最稳。
切片大小与上下文预算的关系
embedding 模型有硬上限:text-embedding-3-small 单条输入最多 8191 token,默认 1536 维(可用 dimensions 参数降到 256);bge-m3 支持到 8192 token、1024 维。但「能塞进去」不等于「该塞进去」:向量是所有内容的平均,塞得越长主题越糊。真正该算的是:召回 k 片 × 每片长度 = prompt 预算,留足给问题和回答,这才是 chunk_size 的落点。
表格与代码怎么特殊处理
表格别按行切,整表一片,并在片头重复表头列名,否则「3.2 秒」单独出现毫无意义。代码别按行切,按函数或类边界切,每片带上文件路径、语言和一个说明用途的注释;注释里往往藏着最好检索的关键词,值得保留。
粒度怎么评估
建个二十来条的小测试集,覆盖「答案是一整段」和「答案是表格里一格」两类问题,跑一遍看召回率。召回不中先怀疑切片,而不是先换模型或调 top-k。把答错的问题逐条拉出来,确认召回的 chunk 里到底有没有答案:有答案但答错是 prompt 的问题,压根没召回含答案的片,就是切片粒度和边界的问题。
常见坑
按字数硬切却把 overlap 设成 0,跨切点的句子直接丢失;chunk_size 设得比 embedding 模型上限还大,超出部分会被静默截断,你以为入库了其实没有;只用小片检索却不做上下文回补,模型拿到的是没有主语的残句;把所有文档塞进同一个集合、共用一套切分规则,质量自然参差。