第7章:文档切割——Chunking 的策略与最佳实践

第7章:文档切割——Chunking 的策略与最佳实践

系列导读:本章是 RAG 的进阶篇。很多人以为 chunking 就是「切成 500 token」,但实际上它是影响检索质量最关键的工程决策之一。不好的 chunking 策略会让检索系统形同虚设——合适的内容检索不到,不合适的内容塞给 LLM,最终答案质量大打折扣。

一、为什么 chunking 决定 RAG 生死?

RAG 系统里有一个核心事实:

LLM 最终回答的质量,取决于你放进 prompt 里的参考资料质量。

参考资料哪来的?从向量库里检索来的。 向量库里存的是 chunk,chunk 是切割出来的。

如果 chunk 把一段重要信息中间切断了,或者把两段无关内容塞进了一个 chunk,检索出来的就是垃圾。垃圾进,垃圾出。你放再贵的 Embedding 模型、再牛的 LLM,都救不回一块切烂的 chunk。

二、Chunking 的四大基础策略

策略1:固定大小 + 重叠(Fixed Size with Overlap)

    原文:今天天气很好,小明去公园散步。他在湖边看到一只白鹭...
          |-------chunk1-------|--------chunk2-------|
          今天天气很好,小明去   明去公园散步。他在湖边
          (size=12, overlap=4)   看到一只白鹭在水面上
                                 (size=12, overlap=4)

适用场景:通用文本,写起来最快,作为基线方案再用别的方法优化。 参数建议:每 chunk 500-1000 token 是常见起点;重叠 10-20% 防止信息在切分处丢失。 问题:机械切分会切断语义边界。一个句子被拦腰截断,LLM 收到 chunk 后理解困难。

优点:实现极其简单,不需要解析文档结构,可移植性好。几乎所有初级 RAG 教程都用这个方案。

策略2:按语义边界切割(Semantic Chunking)

观察文档结构,在天然边界处切:

  • Markdown/HTML 的标题、段落
  • 文档的章节层次
  • 知识库的主题切换
    【原文】
    # 第一章 Java 基础
    第一节 变量和数据类型
    变量是程序中...

    第二节 控制流
    if 语句用于...

    ### 切分点:标题边界
    chunk1 = "第一节 变量和数据类型\n变量是程序中..."
    chunk2 = "第二节 控制流\nif 语句用于..."

适用场景:结构化的知识库、技术文档、产品手册。 优势:语意完整,检索命中后 LLM 能有完整上下文。 挑战:不同文档格式解析不一致,需要自定义切分逻辑。比如 PDF 就没有天然的分段标记,需要先解析出文本结构再决定切割点。

策略3:按代码逻辑切割

代码不是普通文本。函数、类、接口这些天然边界就是最合理的 chunk 粒度:

    def calculate_discount(price, user_type):
        """根据用户类型计算折扣"""
        if user_type == "vip":
            return price * 0.8
        return price

    class PaymentHandler:
        """支付处理器"""
        def process(self, order_id):
            ...

    chunk1 = "def calculate_discount(...)"  # 函数级
    chunk2 = "class PaymentHandler"           # 类级

适用场景:代码仓库的 RAG 问答。 优势:每个 chunk 保留了一个逻辑单元(一个函数/类),检索命中后 LLM 可以完整理解这个单元的实现。

策略4:父子切割(Parent-Child Chunking)【生产级推荐】

RAG 系统里有一个经典矛盾:

  • chunk 越小,检索匹配越精准(粒度细)
  • chunk 越大,LLM 阅读时上下文越完整(信息多)

父子切割的解决思路

  • 用小块做检索(精准匹配)
  • 匹配命中后,返回这个 chunk 对应的大块(完整上下文)
    父 chunk(大块):
      第3章 Python 数据类型
      3.1 列表操作:列表是 Python 中最常用的数据类型之一...
      [包含完整的章节内容]

    子 chunk(小块):
      列表 append 方法使用说明
      append(x) 在列表末尾添加元素...

    检索 "append 怎么用" → 命中子 chunk → 返回父 chunk 到 LLM

适用场景:几乎所有生产级 RAG 都在用或应该用这个策略。 实现要点:子 chunk 需要存储指向父 chunk 的引用 ID;检索时先查子 chunk,命中后取回对应的父 chunk 全文。

三、Chunk 的 Metadata 设计

每个 chunk 存储时,除了 text 和 vector,还需要丰富的 metadata:

    {
      "text": "列表 append 方法使用说明...",
      "vector": [0.1, 0.02, ...],
      "metadata": {
        "source_file": "python_guide.md",
        "chapter": "3.1",
        "tags": ["data_structure", "list"],
        "doc_type": "api_reference",
        "page_number": 42,
        "created_at": "2026-01-15",
        "parent_id": "chunk_3_1_parent"   // 父子切割时使用
      }
    }

Metadata 的作用

  • 过滤:只检索某个文件类型或某个分类,缩小搜索范围
  • 溯源:LLM 生成的答案可以标注来源出处
  • 去重:避免同一来源的 chunk 全部进答案
  • 父子关联:维系父子切割的关系映射

四、切分粒度的黄金法则

没有绝对的「最优粒度」,取决于你的检索目标:

检索目标 推荐粒度 原因
回答事实性问题 句子/段落级 需要精确匹配术语
回答解释性问题 段落/章节级 需要完整上下文才能理解
代码问答 函数/类级 保留逻辑单元,问答精准到函数
支持多跳推理 父子结构 小块检索精准,大块总结完整

经验法则:先按合理的中等粒度(500-1000 token)做基线,然后通过检索日志分析「有没有该命中没命中」「命中的是否相关」,再逐步调优。不要盲目追求小粒度——检索精度提升了,但 LLM 理解难度可能增加了。

五、常见 bad case 及解决

问题 表现 解决
语义被切断 切在句子中间,LLM 收到后理解出错 以语义边界为切分优先
信息密度不均匀 有些 chunk 太空泛,有些太密集 混合策略,或过滤短 chunk
多语言混合 中英混杂切分点不准确 按 tokenizer 的 token 计数而非字符数
过细噪音多 切太小导致检索到很多无关碎片 加最低长度阈值或使用父子结构
过粗精度低 切太大细节信息被「平均掉」 减小 chunk 大小或加摘要

PDF 文档的 chunking 特别难:解析出来的文本往往丧失段落结构,表格和代码块混在一起。对于这类文档,通常需要先做一次结构解析(用 PDF 解析器提取段落、表格、列表信息),然后再做语义 chunking。

六、本章小结

  • Chunking 是 RAG 系统里最关键也最被低估的工程决策
  • 四大策略:固定大小(基线)、语义边界(结构化文档)、代码逻辑(代码库)、父子结构(兼顾精度+完整上下文)
  • Metadata 设计能让你在检索时做精准过滤、溯源和去重
  • 没有统一的「最好粒度」,先跑基线、有数据再优化,比一把梭效果更稳定
  • 生产级推荐:父子切割(小块检索 + 大块返回),解决精度与完整性的天然矛盾

思考题

  1. 父子切割虽然好用,但实现上需要做父-子映射关系管理。你怎么设计这个映射系统,让它既高效又容错?
  2. 对于 PDF 解析出来的文本(排版丢失、表格混乱),你有什么 chunking 策略?