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 许可发布