Skip to main content

文档切分策略

Written: 2026.06

1. 为什么需要这讲

附录 E 讲解了 RecursiveCharacterTextSplitter 的递归降级算法。第 3 讲 §10 展示了 LangChain splitter 的 API 用法。但 Parent-Child Chunking(父子块切分)是本项目最关键的设计决策之一,影响着检索精度和答案质量,目前没有一个地方完整讲解它。 本附录覆盖:

2. 为什么需要切分

2.1 LLM 的上下文窗口是有限的

LLM 的上下文窗口虽然越来越大(qwen-plus 支持 32K token),但这不是无限的。而且:
  • 成本:Prompt 越长,每次调用的 token 费用越高
  • 注意力稀释:LLM 对长文本中间的细节注意力会衰减(“Lost in the Middle” 问题)
  • 检索精度:如果把整本 PDF 当做一个 Document,检索时找到的永远是同一篇文档,无法精确定位到具体段落

2.2 只用一个 Chunk Size 的问题

这张图用一个直观的对比回答了一个核心问题:为什么不能简单地把文档按固定长度一刀切? Chunk 太大(2000 字符)的问题:检索时整篇入职制度被当做一个结果返回,但用户只关心”提交材料”这一小段。向量相似度算的是整个 chunk 和问题的匹配程度——chunk 中 90% 无关内容会”稀释”向量,导致相似度分数不准。更严重的是,LLM 拿到这个 2000 字符的 chunk 后,可能引用其中的无关段落来回答,产生”看起来有关但其实不对”的幻觉。 Chunk 太小(150 字符)的问题:语义信息被切断。“入职需要以下材料:“这一句以冒号结尾,但材料列表在下一个 chunk 里。检索到这个小 chunk 后,LLM 只能看到半句话,后面的关键信息完全丢失。碎片化的 chunk 还会导致”每个 chunk 的语义都差不多”——向量空间中的区分度降低,检索精度反而更差。 Parent-Child(本项目的方案):核心思路是检索用小块,生成用大块。Child chunk(350 字符)粒度细、语义聚焦,检索时精确定位到”提交材料”这一段;Parent chunk(1000 字符)包含完整上下文,LLM 生成时能看到前后的语义关联。两个 chunk 通过 parent_id 关联——Milvus 中存的是 Child 的向量和文本,但 metadata 中携带了完整的 Parent 内容。检索命中 Child 后,构建上下文时取的是 Parent。 具体参数是怎么定的? Parent 1000 字符、Child 350 字符是本项目当前的默认配置,不是 LangChain 或行业标准。设计依据是:Child 要足够短,便于精确检索一个要点;Parent 要足够长,便于给 LLM 提供完整上下文。这里的 token 换算只能粗略估计,因为中文、英文、数字和表格的 token 密度不同。生产环境应结合资料段落长度分布、召回评测和 prompt 成本继续调整。

3. Parent-Child Chunking 完整流程

3.1 切分过程

3.2 检索和生成时的使用

关键设计
  • 检索用 Child(短、精确)—— 350 字符能精确定位到”材料”这个主题
  • 生成用 Parent(长、完整)—— LLM 看到的 1000 字符包含了完整的材料清单,不会遗漏

3.3 代码实现

关键设计点(对照上面的代码阅读):

4. Chunk Size 的选择原理

4.1 Parent Size:为什么是 1000

4.2 Child Size:为什么是 350

4.3 Overlap 的作用

Overlap 比例选择 Parent 使用 10% overlap(100/1000),Child 使用 ~14% overlap(50/350)。Child 的 overlap 比例略高,因为小块更容易在边界处切断语义。

5. 不同文档类型的切分策略

不是所有文档都该用同一种切法。 这张决策树展示了项目中五种文档类型各自对应的切分策略,以及为什么: 分支一:FAQ CSV → 不切分。 FAQ 的每一行已经是一个完整的问答对——问题本身就是最好的检索单位,标准答案作为 metadata.answer 携带。如果对 FAQ 做切分,“问题”和”答案”可能被切开分到两个 chunk 里,检索命中问题 chunk 时拿不到答案。所以 FAQ 是一个 Document = CSV 中的一行。 分支二:Markdown / 文本文档 → Parent-Child 双层(本项目默认策略)。 这类文档是最主要的资料形态(制度、流程、手册),占知识库的 80% 以上。使用 MarkdownHeaderTextSplitter 保留标题层级(######),切分时优先在标题边界处断,保证每个 chunk 内部的语义不跨越章节边界。 分支三:表格 CSV / Excel → 按行切分。 表格的每一行是一个独立的数据记录(如”制裁名单”表中的一行 = 一个国家 + 限制类型 + 法律依据)。跨行切分会把两个不同记录的数据拼在一起,导致检索混乱。每行作为一个 Document,content_type='table_row',保留 sheet_namerow_number 用于溯源。 分支四:法律合同 / 规范 → 更保守的参数。 法律文本中条款之间的引用关系非常紧密——第 5 条可能引用第 3 条的定义,第 10 条可能推翻第 8 条的适用条件。如果切得太碎,LLM 看到的是”孤立的条款”而不是”关联的法律文本”。所以 Parent 放大到 1500-2000 字符,Child 放大到 500-700 字符,overlap 提高到 15-20%,确保关键条款不会被切断。 分支五:API 文档 → 按函数/接口边界切分。 API 文档有清晰的结构——每个函数(或 endpoint)是独立的语义单元,包含签名、参数、返回值、示例。以 h2/h3 标题为边界切分,每个 chunk 恰好是一个完整的 API 说明。 注意: 当前项目中,分支一(FAQ)和分支二(Markdown)是主路径。分支三(表格)已在入库闭环中支持。分支四和五作为扩展策略,配置参数已预留但当前场景未大量使用。

5.1 本项目中的实际配置

6. 语义切分 vs 固定长度切分

6.1 两种策略对比

6.2 本项目选择递归降级的理由

本项目的分隔符列表:["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""]
这个策略保证了切分点优先落在自然的语义边界上(段落 > 句子 > 短语),只有当前面的分隔符切完后某块仍超过限制时,才降级到更细粒度的分隔符。

7. 切分质量的自我验证

7.1 好的 Chunk 长什么样

7.2 项目内置的质量检查

8. 本讲小结

  • Parent 负责给 LLM 完整上下文(1000字符),Child 负责给 Milvus 精确检索(350字符)
  • 检索用 Child,生成用 Parent:Child 精确定位 → 展开 parent_content → LLM 看到完整段落
  • Overlap 防止边界切断语义:Parent 10%(100/1000),Child 14%(50/350)
  • 递归降级切分优先在段落、句子边界切,避免在短语中间截断
  • 不同文档类型用不同策略:FAQ 不切、表格按行、制度文档用 Parent-Child
  • 切分质量 = 答案质量的上限:Chunk 切坏了,后面的检索、Rerank、生成都救不回来