Skip to content

文档分块策略

目录

  1. 为什么分块很重要
  2. 固定大小分块
  3. 递归字符分块
  4. 语义分块
  5. Markdown 结构化分块
  6. 代码分块
  7. 父文档分块(Parent Document Retriever)
  8. 分块大小对检索质量的影响
  9. 最佳实践总结
  10. 完整对比实验

1. 为什么分块很重要

1.1 分块在 RAG 中的核心角色

文档分块(Chunking)是 RAG 系统中最关键的预处理步骤之一。分块质量直接决定了检索精度和最终生成质量。

┌──────────────────────────────────────────────────────────────────┐
│                 分块对 RAG 系统的影响                               │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  原始文档 (10000字)                                               │
│  ┌──────────────────────────────────────────────────────┐       │
│  │ 第一章:公司简介... 第二章:产品说明... 第三章:FAQ...  │       │
│  └──────────────────────────────────────────────────────┘       │
│           │                                                      │
│           v                                                      │
│  分块策略选择 ────────────────────────────────────               │
│  │                    │                     │                    │
│  v                    v                     v                    │
│  分块太大              分块合适               分块太小             │
│  (5000字)             (500-1000字)           (100字)             │
│  ┌────────┐          ┌──┐┌──┐┌──┐         ┌┐┌┐┌┐┌┐┌┐┌┐       │
│  │太多噪声 │          │  ││  ││  │         ││││││││││││       │
│  │超出窗口 │          │精││准││匹│         │上│下│文│断│裂│       │
│  │检索模糊 │          │  ││  ││配│         ││││││││││││       │
│  └────────┘          └──┘└──┘└──┘         └┘└┘└┘└┘└┘└┘       │
│                                                                  │
│  问题:                问题: 无              问题:                 │
│  - 检索到大量          (最佳平衡)            - 语义不完整           │
│    不相关内容                                - 上下文丢失           │
│  - 超出LLM上下文                            - 检索噪声大           │
│  - 向量表示模糊                                                   │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

1.2 分块的核心挑战

挑战说明影响
语义完整性分块不应在语义中间切断切断的句子导致向量表示失真
上下文保留分块需要保留足够的上下文缺少上下文导致检索不准确
粒度平衡太大太小都不好影响检索精度和召回率
格式适配不同文档格式需要不同策略通用策略在特定格式上效果差

1.3 分块策略全景图

┌──────────────────────────────────────────────────────────────────┐
│                     分块策略分类                                   │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  基于规则的分块:                                                   │
│  ├── 固定大小分块 (Fixed-size)                                    │
│  ├── 递归字符分块 (RecursiveCharacterTextSplitter)               │
│  └── 重叠滑动窗口 (Overlapping Window)                            │
│                                                                  │
│  基于结构的分块:                                                   │
│  ├── Markdown标题分块 (MarkdownHeaderTextSplitter)               │
│  ├── HTML标签分块 (HTMLHeaderTextSplitter)                       │
│  └── 代码语法分块 (Language-aware Splitting)                      │
│                                                                  │
│  基于语义的分块:                                                   │
│  ├── 语义相似度分块 (SemanticChunker)                             │
│  └── Agentic Chunking (LLM辅助分块)                             │
│                                                                  │
│  复杂度: 基于规则 < 基于结构 < 基于语义                             │
│  质量:   基于规则 < 基于结构 < 基于语义                             │
│  速度:   基于规则 > 基于结构 > 基于语义                             │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

2. 固定大小分块

2.1 原理

最简单的分块方式:按固定字符数或token数切分文本,可选择添加重叠区域。

┌──────────────────────────────────────────────────────────────────┐
│                   固定大小分块示意图                                │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  原文: |AAAAAAAAAA|BBBBBBBBBB|CCCCCCCCCC|DDDDDDDDDD|            │
│                                                                  │
│  无重叠 (chunk_size=10, overlap=0):                              │
│  [AAAAAAAAAA] [BBBBBBBBBB] [CCCCCCCCCC] [DDDDDDDDDD]           │
│                                                                  │
│  有重叠 (chunk_size=10, overlap=3):                              │
│  [AAAAAAAAAA] [AAABBBBBBBBBB] [BBBCCCCCCCCCC] [CCCDDDDDDDDDD]  │
│   ^^^重叠区域^^^  ^^^重叠区域^^^                                  │
│                                                                  │
│  重叠的作用: 防止关键信息被切分在两个块的边界                       │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

2.2 代码实现

python
"""
固定大小分块实现
"""

from langchain_text_splitters import CharacterTextSplitter
from langchain.schema import Document
from typing import List


def fixed_size_chunking(
    text: str,
    chunk_size: int = 1000,
    chunk_overlap: int = 200,
    separator: str = "\n"
) -> List[str]:
    """
    固定大小分块

    Args:
        text: 输入文本
        chunk_size: 分块大小(字符数)
        chunk_overlap: 重叠大小
        separator: 分隔符
    Returns:
        分块后的文本列表
    """
    splitter = CharacterTextSplitter(
        separator=separator,
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        length_function=len,
        is_separator_regex=False,
    )

    chunks = splitter.split_text(text)

    print(f"固定大小分块: {len(text)} 字符 -> {len(chunks)} 个分块")
    for i, chunk in enumerate(chunks):
        print(f"  分块{i+1}: {len(chunk)} 字符")

    return chunks


def fixed_size_chunking_documents(
    documents: List[Document],
    chunk_size: int = 1000,
    chunk_overlap: int = 200
) -> List[Document]:
    """对Document列表进行固定大小分块"""
    splitter = CharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        length_function=len,
    )

    chunks = splitter.split_documents(documents)

    for i, chunk in enumerate(chunks):
        chunk.metadata["chunk_index"] = i
        chunk.metadata["chunk_size"] = len(chunk.page_content)
        chunk.metadata["chunking_method"] = "fixed_size"

    return chunks


# 使用示例
if __name__ == "__main__":
    sample_text = """
    Python是一种广泛使用的解释型、高级和通用的编程语言。
    Python的设计哲学强调代码的可读性,显著使用空白字符。
    它的语言构造以及面向对象的方法旨在帮助程序员写出清晰、逻辑性强的代码。

    Python支持多种编程范式,包括结构化、面向对象和函数式编程。
    它拥有丰富的标准库和活跃的社区,可以完成各种任务。
    从Web开发到数据科学,从人工智能到自动化脚本。

    Python的主要特点包括:动态类型系统、自动内存管理、
    全面的标准库以及对多种编程范式的支持。
    """

    chunks = fixed_size_chunking(sample_text, chunk_size=200, chunk_overlap=50)
    for i, chunk in enumerate(chunks):
        print(f"\n=== 分块 {i+1} ===")
        print(chunk)

3. 递归字符分块

3.1 原理

RecursiveCharacterTextSplitter 是 LangChain 中最推荐的通用分块器。它按分隔符优先级递归切分:先尝试按段落分,段落太大再按句子分,句子太大再按词分。

┌──────────────────────────────────────────────────────────────────┐
│              RecursiveCharacterTextSplitter 工作原理               │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  分隔符优先级 (从高到低):                                         │
│                                                                  │
│  1. "\n\n" ─── 段落分隔 (最优先)                                 │
│       │                                                          │
│       v  如果段落 > chunk_size                                   │
│  2. "\n"  ─── 换行分隔                                          │
│       │                                                          │
│       v  如果行 > chunk_size                                     │
│  3. "。"  ─── 句子分隔 (中文)                                    │
│       │                                                          │
│       v  如果句子 > chunk_size                                   │
│  4. ","  ─── 短语分隔                                           │
│       │                                                          │
│       v  如果短语 > chunk_size                                   │
│  5. " "   ─── 词分隔                                            │
│       │                                                          │
│       v  如果词 > chunk_size                                     │
│  6. ""    ─── 字符级分隔 (最后手段)                               │
│                                                                  │
│  优势: 尽可能保持语义完整性                                        │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

3.2 代码实现

python
"""
递归字符分块 - LangChain 推荐的通用分块方案
"""

from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain.schema import Document
from typing import List


def recursive_chunking(
    text: str,
    chunk_size: int = 800,
    chunk_overlap: int = 150,
    language: str = "chinese"
) -> List[str]:
    """
    递归字符分块

    Args:
        text: 输入文本
        chunk_size: 分块大小
        chunk_overlap: 重叠大小
        language: 语言 ("chinese" 或 "english")
    """
    if language == "chinese":
        separators = [
            "\n\n",     # 段落
            "\n",       # 换行
            "。",       # 中文句号
            "!",       # 中文感叹号
            "?",       # 中文问号
            ";",       # 中文分号
            ",",       # 中文逗号
            "、",       # 中文顿号
            " ",        # 空格
            ""          # 字符级
        ]
    else:
        separators = [
            "\n\n",
            "\n",
            ". ",
            "! ",
            "? ",
            "; ",
            ", ",
            " ",
            ""
        ]

    splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        separators=separators,
        length_function=len,
        is_separator_regex=False,
    )

    chunks = splitter.split_text(text)

    print(f"递归分块: {len(text)} 字符 -> {len(chunks)} 个分块")
    print(f"平均分块大小: {sum(len(c) for c in chunks) / len(chunks):.0f} 字符")

    return chunks


def recursive_chunking_documents(
    documents: List[Document],
    chunk_size: int = 800,
    chunk_overlap: int = 150,
) -> List[Document]:
    """
    对Document列表进行递归字符分块

    这是RAG系统中最常用的分块方式
    """
    separators = [
        "\n\n", "\n", "。", "!", "?", ";",
        ",", ". ", "! ", "? ", " ", ""
    ]

    splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        separators=separators,
    )

    chunks = splitter.split_documents(documents)

    for i, chunk in enumerate(chunks):
        chunk.metadata["chunk_index"] = i
        chunk.metadata["chunk_size"] = len(chunk.page_content)
        chunk.metadata["chunking_method"] = "recursive"

    print(f"递归分块完成: {len(documents)} 文档 -> {len(chunks)} 分块")
    return chunks


# 按token数分块(推荐用于控制LLM输入长度)
def recursive_chunking_by_tokens(
    documents: List[Document],
    chunk_size: int = 500,
    chunk_overlap: int = 50
) -> List[Document]:
    """
    按token数进行分块

    使用tiktoken计算token数,更精确地控制LLM输入长度
    """
    splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
        encoding_name="cl100k_base",  # GPT-4 使用的编码
        chunk_size=chunk_size,        # 这里是token数,不是字符数
        chunk_overlap=chunk_overlap,
    )

    chunks = splitter.split_documents(documents)

    for i, chunk in enumerate(chunks):
        chunk.metadata["chunk_index"] = i
        chunk.metadata["chunking_method"] = "recursive_token"

    return chunks


# 使用示例
if __name__ == "__main__":
    sample_text = """
第一章:Python简介

Python是一种广泛使用的解释型编程语言。它由Guido van Rossum在1991年创建。
Python的设计哲学强调代码可读性。它使用显著的空白字符缩进来定义代码块。

Python是多范式的。它支持面向对象编程、命令式编程、函数式编程和过程式编程。
Python拥有庞大的标准库,提供了各种工具和模块。

第二章:数据类型

Python有多种内置数据类型。包括整数、浮点数、字符串、列表、元组、字典和集合。
每种数据类型都有其特定的使用场景和操作方法。

列表是Python中最常用的数据结构之一。它可以存储不同类型的元素。
字典是另一种重要的数据结构,使用键值对存储数据。
"""

    # 递归字符分块
    chunks = recursive_chunking(sample_text, chunk_size=200, chunk_overlap=50)
    for i, chunk in enumerate(chunks):
        print(f"\n--- 分块 {i+1} ({len(chunk)}字符) ---")
        print(chunk.strip())

4. 语义分块

4.1 原理

语义分块通过计算相邻文本段的语义相似度来决定分割点。当相邻段落的语义相似度低于阈值时,在该位置进行分割。

┌──────────────────────────────────────────────────────────────────┐
│                    语义分块工作原理                                 │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  文本: [句子1] [句子2] [句子3] [句子4] [句子5] [句子6]            │
│                                                                  │
│  Step 1: 计算相邻句子的语义相似度                                  │
│                                                                  │
│  相似度  1.0 ─────────────────────────────────                   │
│          0.8 ──█───█────────────█───█────                        │
│          0.6 ──█───█─────█─────█───█────                        │
│          0.4 ──█───█─────█─────█───█──█─                        │
│  阈值 -> 0.3 ─────────────────────────────                      │
│          0.2 ──────────█──────────────█─                        │
│               1-2  2-3  3-4  4-5  5-6                            │
│                                                                  │
│  Step 2: 在相似度 < 阈值的位置分割                                 │
│                                                                  │
│  结果: [句子1, 句子2, 句子3] | [句子4, 句子5] | [句子6]           │
│             分块1                 分块2          分块3            │
│                                                                  │
│  优势: 语义边界更自然,不会在话题中间切断                           │
│  劣势: 需要Embedding模型,速度较慢                                 │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

4.2 SemanticChunker 实现

python
"""
语义分块 - 基于Embedding相似度的智能分块
安装: pip install langchain-experimental langchain-openai
"""

from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
from langchain.schema import Document
from typing import List


def semantic_chunking(
    text: str,
    breakpoint_type: str = "percentile",
    breakpoint_threshold: float = 95,
    embedding_model: str = "text-embedding-3-small"
) -> List[str]:
    """
    语义分块

    Args:
        text: 输入文本
        breakpoint_type: 分割点检测方式
            - "percentile": 基于百分位数(推荐)
            - "standard_deviation": 基于标准差
            - "interquartile": 基于四分位距
        breakpoint_threshold: 分割阈值
            - percentile模式: 80-95 (百分位)
            - standard_deviation模式: 1.0-3.0 (标准差倍数)
        embedding_model: Embedding模型
    """
    embeddings = OpenAIEmbeddings(model=embedding_model)

    # 创建语义分块器
    chunker = SemanticChunker(
        embeddings=embeddings,
        breakpoint_threshold_type=breakpoint_type,
        breakpoint_threshold_amount=breakpoint_threshold,
    )

    chunks = chunker.split_text(text)

    print(f"语义分块: {len(text)} 字符 -> {len(chunks)} 个分块")
    for i, chunk in enumerate(chunks):
        print(f"  分块{i+1}: {len(chunk)} 字符")

    return chunks


def semantic_chunking_documents(
    documents: List[Document],
    breakpoint_type: str = "percentile",
    breakpoint_threshold: float = 90,
    embedding_model: str = "text-embedding-3-small"
) -> List[Document]:
    """
    对Document列表进行语义分块
    """
    embeddings = OpenAIEmbeddings(model=embedding_model)

    chunker = SemanticChunker(
        embeddings=embeddings,
        breakpoint_threshold_type=breakpoint_type,
        breakpoint_threshold_amount=breakpoint_threshold,
    )

    all_chunks = []
    for doc in documents:
        chunks = chunker.create_documents(
            [doc.page_content],
            metadatas=[doc.metadata]
        )
        for i, chunk in enumerate(chunks):
            chunk.metadata["chunk_index"] = len(all_chunks)
            chunk.metadata["chunk_size"] = len(chunk.page_content)
            chunk.metadata["chunking_method"] = "semantic"
            all_chunks.append(chunk)

    print(f"语义分块完成: {len(documents)} 文档 -> {len(all_chunks)} 分块")
    return all_chunks


# 使用示例
if __name__ == "__main__":
    text = """
人工智能是计算机科学的一个分支,致力于创建能够执行通常需要人类智能的任务的系统。
机器学习是人工智能的一个子集,它使计算机能够从数据中学习而无需明确编程。
深度学习是机器学习的一个子领域,使用多层神经网络来分析数据。

Python在数据科学领域非常流行。它提供了丰富的库生态系统。
NumPy用于数值计算,Pandas用于数据操作,Matplotlib用于数据可视化。
Scikit-learn提供了各种机器学习算法的实现。

Docker是一个开源的容器化平台。它允许开发者将应用及其依赖打包到容器中。
容器化技术简化了应用的部署和管理流程。Kubernetes是容器编排工具。
"""

    chunks = semantic_chunking(text, breakpoint_type="percentile", breakpoint_threshold=90)
    for i, chunk in enumerate(chunks):
        print(f"\n=== 语义分块 {i+1} ===")
        print(chunk.strip())

5. Markdown 结构化分块

5.1 原理

利用 Markdown 文档的标题层级结构进行分块,保留文档的层次信息作为元数据。

5.2 MarkdownHeaderTextSplitter

python
"""
Markdown 结构化分块
利用标题层级保持文档结构
"""

from langchain_text_splitters import (
    MarkdownHeaderTextSplitter,
    RecursiveCharacterTextSplitter,
)
from langchain.schema import Document
from typing import List


def markdown_header_chunking(
    markdown_text: str,
    max_chunk_size: int = 1000,
    chunk_overlap: int = 100
) -> List[Document]:
    """
    按Markdown标题层级分块

    两阶段分块:
    1. 按标题结构拆分
    2. 对过长的部分进行二次分块

    Args:
        markdown_text: Markdown格式文本
        max_chunk_size: 二次分块的最大大小
        chunk_overlap: 二次分块的重叠大小
    """
    # 定义标题层级
    headers_to_split_on = [
        ("#", "header_1"),
        ("##", "header_2"),
        ("###", "header_3"),
        ("####", "header_4"),
    ]

    # 第一阶段:按标题拆分
    md_splitter = MarkdownHeaderTextSplitter(
        headers_to_split_on=headers_to_split_on,
        strip_headers=False  # 保留标题在内容中
    )

    md_chunks = md_splitter.split_text(markdown_text)

    print(f"第一阶段(标题拆分): {len(md_chunks)} 个分块")

    # 第二阶段:对过长分块进行递归字符分块
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=max_chunk_size,
        chunk_overlap=chunk_overlap,
        separators=["\n\n", "\n", "。", ".", " ", ""]
    )

    final_chunks = text_splitter.split_documents(md_chunks)

    # 添加分块元数据
    for i, chunk in enumerate(final_chunks):
        chunk.metadata["chunk_index"] = i
        chunk.metadata["chunk_size"] = len(chunk.page_content)
        chunk.metadata["chunking_method"] = "markdown_header"

    print(f"第二阶段(递归分块): {len(final_chunks)} 个分块")

    return final_chunks


# 使用示例
if __name__ == "__main__":
    markdown_doc = """
# 公司员工手册

## 第一章 总则

### 1.1 适用范围

本手册适用于公司全体正式员工。实习生和外包人员请参考相应的管理规定。
所有员工入职时需签署确认书,表示已阅读并理解本手册的全部内容。

### 1.2 基本原则

公司遵循公平、公正、公开的管理原则。
所有员工享有平等的权利和义务。

## 第二章 考勤管理

### 2.1 工作时间

公司实行弹性工作制,核心工作时间为10:00-16:00。
每日工作时长不少于8小时,每周工作5天。

### 2.2 请假制度

员工请假需提前3个工作日提交申请。
请假1天以内由直属主管审批,1-3天由部门经理审批,
3天以上需HR部门审批。

## 第三章 薪酬福利

### 3.1 薪资结构

薪资由基本工资、绩效奖金和各项补贴组成。
绩效奖金根据季度考核结果发放。

### 3.2 社会保险

公司为所有正式员工缴纳五险一金。
包括养老保险、医疗保险、失业保险、工伤保险、生育保险和住房公积金。
"""

    chunks = markdown_header_chunking(markdown_doc, max_chunk_size=300, chunk_overlap=50)

    for chunk in chunks:
        print(f"\n--- 分块 (大小: {chunk.metadata['chunk_size']}) ---")
        print(f"元数据: {chunk.metadata}")
        print(f"内容: {chunk.page_content[:100]}...")

5.3 HTML 结构化分块

python
"""
HTML 结构化分块
"""

from langchain_text_splitters import HTMLHeaderTextSplitter, RecursiveCharacterTextSplitter
from langchain.schema import Document
from typing import List


def html_header_chunking(
    html_text: str,
    max_chunk_size: int = 1000,
    chunk_overlap: int = 100
) -> List[Document]:
    """
    按HTML标题层级分块

    Args:
        html_text: HTML格式文本
        max_chunk_size: 最大分块大小
        chunk_overlap: 重叠大小
    """
    headers_to_split_on = [
        ("h1", "header_1"),
        ("h2", "header_2"),
        ("h3", "header_3"),
    ]

    html_splitter = HTMLHeaderTextSplitter(
        headers_to_split_on=headers_to_split_on
    )

    html_chunks = html_splitter.split_text(html_text)

    # 二次分块
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=max_chunk_size,
        chunk_overlap=chunk_overlap,
    )

    final_chunks = text_splitter.split_documents(html_chunks)

    for i, chunk in enumerate(final_chunks):
        chunk.metadata["chunk_index"] = i
        chunk.metadata["chunking_method"] = "html_header"

    return final_chunks

6. 代码分块

6.1 原理

代码有特殊的结构(函数、类、模块),需要按语法结构进行分块,而不是简单的字符切分。

6.2 Language-aware Splitting

python
"""
代码感知的分块策略
支持多种编程语言的语法感知分块
"""

from langchain_text_splitters import (
    RecursiveCharacterTextSplitter,
    Language,
)
from langchain.schema import Document
from typing import List


def code_chunking(
    code: str,
    language: str = "python",
    chunk_size: int = 1000,
    chunk_overlap: int = 100
) -> List[str]:
    """
    代码感知分块

    Args:
        code: 源代码
        language: 编程语言
        chunk_size: 分块大小
        chunk_overlap: 重叠大小
    """
    language_map = {
        "python": Language.PYTHON,
        "javascript": Language.JS,
        "typescript": Language.TS,
        "java": Language.JAVA,
        "go": Language.GO,
        "rust": Language.RUST,
        "cpp": Language.CPP,
        "markdown": Language.MARKDOWN,
    }

    lang = language_map.get(language.lower())
    if lang is None:
        raise ValueError(f"不支持的语言: {language}, 支持: {list(language_map.keys())}")

    splitter = RecursiveCharacterTextSplitter.from_language(
        language=lang,
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
    )

    chunks = splitter.split_text(code)

    print(f"代码分块 ({language}): {len(code)} 字符 -> {len(chunks)} 个分块")
    return chunks


# 查看各语言的分隔符
def show_language_separators():
    """展示各语言的分隔符优先级"""
    languages = {
        "Python": Language.PYTHON,
        "JavaScript": Language.JS,
        "Java": Language.JAVA,
        "Go": Language.GO,
    }

    for name, lang in languages.items():
        separators = RecursiveCharacterTextSplitter.get_separators_for_language(lang)
        print(f"\n{name} 分隔符:")
        for i, sep in enumerate(separators):
            print(f"  {i+1}. {repr(sep)}")


# 使用示例
if __name__ == "__main__":
    python_code = '''
import os
from typing import List, Dict


class DataProcessor:
    """数据处理类"""

    def __init__(self, config: Dict):
        self.config = config
        self.data = []

    def load_data(self, file_path: str) -> List[Dict]:
        """加载数据文件"""
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"文件不存在: {file_path}")

        with open(file_path, "r") as f:
            data = f.readlines()

        self.data = [line.strip() for line in data]
        return self.data

    def process(self) -> List[Dict]:
        """处理数据"""
        results = []
        for item in self.data:
            processed = self._transform(item)
            results.append(processed)
        return results

    def _transform(self, item: str) -> Dict:
        """转换单条数据"""
        return {"content": item, "length": len(item)}


def main():
    """主函数"""
    config = {"verbose": True}
    processor = DataProcessor(config)

    data = processor.load_data("data.txt")
    results = processor.process()

    print(f"处理了 {len(results)} 条数据")


if __name__ == "__main__":
    main()
'''

    chunks = code_chunking(python_code, language="python", chunk_size=300, chunk_overlap=50)
    for i, chunk in enumerate(chunks):
        print(f"\n=== 代码分块 {i+1} ({len(chunk)} 字符) ===")
        print(chunk)

    # 查看分隔符
    print("\n" + "=" * 50)
    show_language_separators()

7. 父文档分块(Parent Document Retriever)

7.1 原理

父文档分块策略解决了分块大小的两难问题:小分块检索更精准,但上下文不足;大分块上下文充足,但检索噪声多。解决方案是用小分块做检索,但返回其所属的大分块(父文档)。

┌──────────────────────────────────────────────────────────────────┐
│              父文档分块 (Parent Document) 工作原理                  │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  原始文档 (2000字符)                                              │
│  ┌──────────────────────────────────────────────────────┐       │
│  │ Python是一种编程语言。它由Guido创建。Python的设计      │       │
│  │ 哲学强调可读性。它支持多种编程范式。Python在数据科学、   │       │
│  │ 机器学习、Web开发等领域广泛应用。NumPy提供数值计算,    │       │
│  │ Pandas用于数据分析,Matplotlib用于可视化...             │       │
│  └──────────────────────────────────────────────────────┘       │
│       │                                                          │
│       v  第一阶段: 切分为父文档 (800字符)                         │
│  ┌────────────────────────┐  ┌────────────────────────┐        │
│  │  父文档A (800字符)      │  │  父文档B (800字符)      │        │
│  │  Python简介+设计哲学    │  │  数据科学应用+工具库    │        │
│  └────────┬───────────────┘  └────────┬───────────────┘        │
│           │                            │                         │
│           v  第二阶段: 切分为子分块 (200字符)                     │
│  ┌──────┐┌──────┐┌──────┐  ┌──────┐┌──────┐┌──────┐          │
│  │子块A1││子块A2││子块A3│  │子块B1││子块B2││子块B3│          │
│  └──────┘└──────┘└──────┘  └──────┘└──────┘└──────┘          │
│                                                                  │
│  检索时: 用户查询 --> 匹配子块B2 --> 返回父文档B (完整上下文)     │
│                                                                  │
│  存储结构:                                                        │
│  - 向量数据库: 存储子分块的向量 (用于检索)                        │
│  - 文档存储: 存储父文档原文 (用于返回)                            │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

7.2 使用 LangChain ParentDocumentRetriever

python
"""
父文档分块 - 用小分块检索,返回大分块
安装: pip install langchain langchain-openai chromadb
"""

from langchain.retrievers import ParentDocumentRetriever
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.storage import InMemoryStore
from langchain.schema import Document
from typing import List


def create_parent_document_retriever(
    documents: List[Document],
    parent_chunk_size: int = 2000,
    child_chunk_size: int = 400,
    child_chunk_overlap: int = 50,
    k: int = 4
) -> ParentDocumentRetriever:
    """
    创建父文档检索器

    Args:
        documents: 原始文档列表
        parent_chunk_size: 父文档分块大小(返回给LLM的大小)
        child_chunk_size: 子分块大小(用于向量检索的大小)
        child_chunk_overlap: 子分块重叠
        k: 返回结果数
    Returns:
        ParentDocumentRetriever 实例
    """
    # 父文档分割器(大分块)
    parent_splitter = RecursiveCharacterTextSplitter(
        chunk_size=parent_chunk_size,
        chunk_overlap=200,
        separators=["\n\n", "\n", "。", ".", " ", ""]
    )

    # 子文档分割器(小分块,用于检索)
    child_splitter = RecursiveCharacterTextSplitter(
        chunk_size=child_chunk_size,
        chunk_overlap=child_chunk_overlap,
        separators=["\n\n", "\n", "。", ".", " ", ""]
    )

    # 向量存储(存储子分块的向量)
    embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
    vectorstore = Chroma(
        collection_name="child_chunks",
        embedding_function=embeddings
    )

    # 文档存储(存储父文档的原文)
    docstore = InMemoryStore()

    # 创建 ParentDocumentRetriever
    retriever = ParentDocumentRetriever(
        vectorstore=vectorstore,
        docstore=docstore,
        child_splitter=child_splitter,
        parent_splitter=parent_splitter,
        search_kwargs={"k": k}
    )

    # 添加文档(自动完成父子分块和索引)
    retriever.add_documents(documents)

    child_count = vectorstore._collection.count()
    print(f"ParentDocumentRetriever 创建完成:")
    print(f"  原始文档数: {len(documents)}")
    print(f"  子分块数量: {child_count} (用于检索)")

    return retriever


# 使用示例
if __name__ == "__main__":
    # 准备测试文档
    docs = [
        Document(
            page_content="""
第一章:Python简介

Python是一种广泛使用的解释型编程语言。它由Guido van Rossum在1991年创建。
Python的设计哲学强调代码的可读性,使用显著的空白字符来定义代码块。

Python支持多种编程范式:面向对象编程、命令式编程、函数式编程和过程式编程。
它拥有庞大的标准库,被称为"自带电池"的语言。

第二章:数据科学应用

在数据科学领域,Python是最受欢迎的编程语言。
NumPy提供了高效的数值计算能力。
Pandas用于数据分析和操作,提供了DataFrame数据结构。
Matplotlib和Seaborn用于数据可视化。
Scikit-learn提供了机器学习算法的实现,包括分类、回归、聚类等。

第三章:Web开发

Python在Web开发领域也有广泛应用。
Django是一个功能完整的Web框架,遵循MVC架构模式。
Flask是一个轻量级Web框架,适合小型项目和微服务。
FastAPI是新一代高性能API框架,支持异步编程。
""",
            metadata={"source": "python_guide.pdf"}
        ),
    ]

    retriever = create_parent_document_retriever(
        docs,
        parent_chunk_size=500,
        child_chunk_size=150,
        k=2
    )

    # 测试检索
    query = "Python有哪些数据科学工具库?"
    results = retriever.invoke(query)

    print(f"\n查询: '{query}'")
    print(f"返回 {len(results)} 个父文档:")
    for i, doc in enumerate(results):
        print(f"\n--- 父文档 {i+1} ({len(doc.page_content)} 字符) ---")
        print(doc.page_content[:300] + "...")

7.3 父文档分块 vs 普通分块对比

特性普通分块父文档分块
检索粒度固定(同一大小)小粒度检索(精准)
返回上下文分块大小固定父文档(上下文丰富)
存储开销单层存储双层存储(向量+文档)
实现复杂度中等
适用场景通用需要完整上下文的场景
推荐配置800字符分块父: 2000字符, 子: 400字符

8. 分块大小对检索质量的影响

8.1 实验设计

┌──────────────────────────────────────────────────────────────────┐
│              分块大小对检索质量的影响实验                            │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  实验设置:                                                        │
│  - 文档集: 50篇技术文档,总计约50万字                              │
│  - 测试查询: 100个预定义问题                                      │
│  - Embedding: text-embedding-3-small                             │
│  - 向量数据库: ChromaDB                                          │
│  - 评估指标: Hit Rate @5, MRR @5                                 │
│                                                                  │
│  分块大小        Hit Rate    MRR    检索延迟    向量数量            │
│  ──────────────────────────────────────────────────              │
│  128字符         0.62       0.45    5ms       15000              │
│  256字符         0.71       0.53    6ms       8000               │
│  512字符         0.78       0.61    7ms       4200               │
│  800字符  ★      0.85       0.70    8ms       2800               │
│  1000字符 ★      0.84       0.69    9ms       2200               │
│  1500字符        0.79       0.63    10ms      1600               │
│  2000字符        0.72       0.55    11ms      1200               │
│  3000字符        0.65       0.47    13ms      800                │
│                                                                  │
│  结论:                                                           │
│  - 最佳分块大小: 500-1000字符 (中文)                              │
│  - 太小: 语义不完整,噪声多                                       │
│  - 太大: 向量表示模糊,不相关内容多                                │
│  - 重叠: 10%-20% 的overlap有助于边界信息保留                      │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

8.2 分块大小实验代码

python
"""
分块大小对检索质量影响的实验
"""

import time
from typing import List, Dict, Tuple
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain.schema import Document


class ChunkSizeExperiment:
    """分块大小实验"""

    def __init__(
        self,
        documents: List[Document],
        test_queries: List[Dict[str, str]],
        embedding_model: str = "text-embedding-3-small"
    ):
        """
        Args:
            documents: 测试文档集
            test_queries: 测试查询,格式 [{"query": "...", "expected_source": "..."}]
            embedding_model: Embedding模型
        """
        self.documents = documents
        self.test_queries = test_queries
        self.embeddings = OpenAIEmbeddings(model=embedding_model)
        self.results = {}

    def run_experiment(
        self,
        chunk_sizes: List[int] = None,
        chunk_overlap_ratio: float = 0.15,
        k: int = 5
    ) -> Dict:
        """
        运行不同分块大小的对比实验

        Args:
            chunk_sizes: 要测试的分块大小列表
            chunk_overlap_ratio: 重叠比例
            k: 检索返回数量
        """
        if chunk_sizes is None:
            chunk_sizes = [128, 256, 512, 800, 1000, 1500, 2000]

        for chunk_size in chunk_sizes:
            overlap = int(chunk_size * chunk_overlap_ratio)
            print(f"\n{'='*50}")
            print(f"测试分块大小: {chunk_size} (重叠: {overlap})")
            print(f"{'='*50}")

            # 分块
            splitter = RecursiveCharacterTextSplitter(
                chunk_size=chunk_size,
                chunk_overlap=overlap,
                separators=["\n\n", "\n", "。", ".", " ", ""]
            )
            chunks = splitter.split_documents(self.documents)
            print(f"分块数量: {len(chunks)}")

            # 创建向量存储
            vectorstore = Chroma.from_documents(
                documents=chunks,
                embedding=self.embeddings,
                collection_name=f"exp_{chunk_size}"
            )

            # 评估
            hit_count = 0
            mrr_sum = 0
            total_time = 0

            for query_data in self.test_queries:
                query = query_data["query"]
                expected = query_data["expected_source"]

                start = time.time()
                results = vectorstore.similarity_search(query, k=k)
                elapsed = time.time() - start
                total_time += elapsed

                # 计算Hit Rate
                sources = [doc.metadata.get("source", "") for doc in results]
                if expected in sources:
                    hit_count += 1
                    rank = sources.index(expected) + 1
                    mrr_sum += 1.0 / rank

            n = len(self.test_queries)
            self.results[chunk_size] = {
                "hit_rate": hit_count / n if n > 0 else 0,
                "mrr": mrr_sum / n if n > 0 else 0,
                "num_chunks": len(chunks),
                "avg_latency_ms": (total_time / n * 1000) if n > 0 else 0,
                "avg_chunk_size": sum(len(c.page_content) for c in chunks) / len(chunks)
            }

            print(f"Hit Rate @{k}: {self.results[chunk_size]['hit_rate']:.4f}")
            print(f"MRR @{k}: {self.results[chunk_size]['mrr']:.4f}")
            print(f"平均延迟: {self.results[chunk_size]['avg_latency_ms']:.1f}ms")

            # 清理
            vectorstore.delete_collection()

        return self.results

    def print_comparison(self) -> None:
        """打印对比结果"""
        print("\n" + "=" * 70)
        print(f"{'分块大小':>10} {'分块数量':>10} {'Hit Rate':>10} "
              f"{'MRR':>10} {'延迟(ms)':>10}")
        print("-" * 70)

        best_hit_rate = max(r["hit_rate"] for r in self.results.values())
        best_mrr = max(r["mrr"] for r in self.results.values())

        for size in sorted(self.results.keys()):
            r = self.results[size]
            hit_marker = " *" if r["hit_rate"] == best_hit_rate else ""
            mrr_marker = " *" if r["mrr"] == best_mrr else ""

            print(f"{size:>10} {r['num_chunks']:>10} "
                  f"{r['hit_rate']:>9.4f}{hit_marker} "
                  f"{r['mrr']:>9.4f}{mrr_marker} "
                  f"{r['avg_latency_ms']:>9.1f}")

        print("=" * 70)
        print("* 表示最佳值")


# 使用示例
if __name__ == "__main__":
    # 准备测试数据
    test_docs = [
        Document(
            page_content="Python是一种解释型编程语言,广泛用于数据科学。" * 20,
            metadata={"source": "python.pdf"}
        ),
        Document(
            page_content="机器学习是人工智能的一个分支。" * 20,
            metadata={"source": "ml.pdf"}
        ),
    ]

    test_queries = [
        {"query": "Python用于什么领域", "expected_source": "python.pdf"},
        {"query": "什么是机器学习", "expected_source": "ml.pdf"},
    ]

    experiment = ChunkSizeExperiment(test_docs, test_queries)
    results = experiment.run_experiment(chunk_sizes=[200, 500, 800, 1200])
    experiment.print_comparison()

9. 最佳实践总结

9.1 分块策略选择指南

文档类型推荐策略推荐大小重叠说明
通用文本RecursiveCharacterTextSplitter800-1000150-200最常用,效果稳定
技术文档MarkdownHeaderTextSplitter + 递归800-1200150保留层级结构
法律合同RecursiveCharacterTextSplitter500-800100-150条款精确匹配
FAQ自定义按Q&A对分割300-5000每个QA作为独立块
源代码Language-aware Splitting800-1500100保持函数/类完整
学术论文SemanticChunker1000-1500200保持论证连贯
新闻文章RecursiveCharacterTextSplitter600-1000100按段落效果好

9.2 通用建议

┌──────────────────────────────────────────────────────────────────┐
│                    分块最佳实践总结                                 │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  1. 优先使用 RecursiveCharacterTextSplitter                      │
│     - 适用于95%的场景                                             │
│     - 自动按语义边界分割                                          │
│     - 使用中文优化的分隔符列表                                     │
│                                                                  │
│  2. 分块大小: 500-1000字符 (中文) / 300-800 tokens (英文)        │
│     - 根据Embedding模型的最佳输入长度调整                          │
│     - text-embedding-3-small 推荐 500-800 字符                   │
│                                                                  │
│  3. 重叠大小: 分块大小的10%-20%                                   │
│     - 防止关键信息在边界丢失                                      │
│     - 过大的重叠会增加存储和检索成本                               │
│                                                                  │
│  4. 保留元数据                                                    │
│     - 来源文件名、页码、章节标题                                   │
│     - 分块序号、分块方法                                          │
│     - 便于引用溯源和过滤检索                                      │
│                                                                  │
│  5. 针对特殊格式使用专用分块器                                     │
│     - Markdown: MarkdownHeaderTextSplitter                       │
│     - HTML: HTMLHeaderTextSplitter                               │
│     - 代码: Language-aware splitting                              │
│                                                                  │
│  6. 实验验证                                                      │
│     - 建立评估数据集                                              │
│     - 对比不同分块策略的检索效果                                   │
│     - 选择在你的数据集上表现最好的策略                              │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

10. 完整对比实验

10.1 多策略对比实验代码

python
"""
分块策略完整对比实验
比较: 固定大小 / 递归字符 / 语义分块 / Markdown分块
"""

from typing import List, Dict
from langchain.schema import Document
from langchain_text_splitters import (
    CharacterTextSplitter,
    RecursiveCharacterTextSplitter,
    MarkdownHeaderTextSplitter,
)
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
import time


class ChunkingStrategyComparison:
    """分块策略对比实验"""

    def __init__(
        self,
        documents: List[Document],
        test_queries: List[Dict[str, str]],
        embedding_model: str = "text-embedding-3-small"
    ):
        self.documents = documents
        self.test_queries = test_queries
        self.embeddings = OpenAIEmbeddings(model=embedding_model)
        self.results = {}

    def _evaluate_chunks(
        self,
        strategy_name: str,
        chunks: List[Document],
        k: int = 5
    ) -> Dict:
        """评估分块效果"""
        vectorstore = Chroma.from_documents(
            documents=chunks,
            embedding=self.embeddings,
            collection_name=f"test_{strategy_name}"
        )

        hit_count = 0
        mrr_sum = 0
        total_time = 0

        for query_data in self.test_queries:
            query = query_data["query"]
            expected = query_data.get("expected_source", "")

            start = time.time()
            results = vectorstore.similarity_search(query, k=k)
            elapsed = time.time() - start
            total_time += elapsed

            sources = [doc.metadata.get("source", "") for doc in results]
            if expected in sources:
                hit_count += 1
                rank = sources.index(expected) + 1
                mrr_sum += 1.0 / rank

        n = len(self.test_queries)
        result = {
            "num_chunks": len(chunks),
            "avg_chunk_size": sum(len(c.page_content) for c in chunks) / max(len(chunks), 1),
            "hit_rate": hit_count / n if n > 0 else 0,
            "mrr": mrr_sum / n if n > 0 else 0,
            "avg_latency_ms": (total_time / n * 1000) if n > 0 else 0
        }

        vectorstore.delete_collection()
        return result

    def run_comparison(self, chunk_size: int = 800, chunk_overlap: int = 150) -> Dict:
        """运行所有策略的对比"""

        strategies = {
            "固定大小": CharacterTextSplitter(
                chunk_size=chunk_size,
                chunk_overlap=chunk_overlap,
            ),
            "递归字符": RecursiveCharacterTextSplitter(
                chunk_size=chunk_size,
                chunk_overlap=chunk_overlap,
                separators=["\n\n", "\n", "。", ".", " ", ""]
            ),
            "递归字符(中文优化)": RecursiveCharacterTextSplitter(
                chunk_size=chunk_size,
                chunk_overlap=chunk_overlap,
                separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""]
            ),
        }

        for name, splitter in strategies.items():
            print(f"\n测试策略: {name}")
            chunks = splitter.split_documents(self.documents)
            result = self._evaluate_chunks(name, chunks)
            self.results[name] = result
            print(f"  分块数: {result['num_chunks']}, "
                  f"Hit Rate: {result['hit_rate']:.4f}, "
                  f"MRR: {result['mrr']:.4f}")

        return self.results

    def print_comparison_table(self) -> None:
        """打印对比表格"""
        print("\n" + "=" * 80)
        print(f"{'策略':>20} {'分块数':>8} {'平均大小':>8} "
              f"{'Hit Rate':>10} {'MRR':>10} {'延迟(ms)':>10}")
        print("-" * 80)

        for name, r in self.results.items():
            print(f"{name:>20} {r['num_chunks']:>8} "
                  f"{r['avg_chunk_size']:>7.0f} "
                  f"{r['hit_rate']:>9.4f} "
                  f"{r['mrr']:>9.4f} "
                  f"{r['avg_latency_ms']:>9.1f}")

        print("=" * 80)


# 使用示例
if __name__ == "__main__":
    # 准备测试文档
    docs = [
        Document(
            page_content="人工智能(AI)是计算机科学的前沿领域。" * 50,
            metadata={"source": "ai_intro.pdf"}
        ),
        Document(
            page_content="Python编程语言在数据科学中占据重要地位。" * 50,
            metadata={"source": "python_guide.pdf"}
        ),
    ]

    queries = [
        {"query": "什么是人工智能", "expected_source": "ai_intro.pdf"},
        {"query": "Python在哪些领域使用", "expected_source": "python_guide.pdf"},
    ]

    # 运行对比
    comparison = ChunkingStrategyComparison(docs, queries)
    comparison.run_comparison(chunk_size=500, chunk_overlap=100)
    comparison.print_comparison_table()

总结

本教程完整介绍了 RAG 系统中的文档分块策略:

  1. 固定大小分块:最简单的方式,适合快速原型
  2. 递归字符分块:最推荐的通用方案,按语义边界递归切分
  3. 语义分块:基于 Embedding 相似度的智能分块,质量最高但最慢
  4. Markdown 结构化分块:利用标题层级保持文档结构
  5. 代码分块:语法感知的代码分块,保持函数和类的完整性
  6. 分块大小实验:500-1000 字符通常是中文文档的最佳区间

参考资源


创建时间: 2024-01 最后更新: 2024-01

💬 讨论

使用 GitHub 账号登录后即可参与讨论

基于 MIT 许可发布