跳转至

文档分块

演示 RAG 中将长文档切分为语义块的核心策略:固定长度分块(带重叠)、按句子分块、 递归分块。分块质量直接影响检索召回率。 对应教程:第6卷《RAG》第1章 分块与索引。

文件路径: examples/volume-06-rag\chunking.py


源代码

"""文本分块(Chunking)示例。

演示 RAG 中将长文档切分为语义块的核心策略:固定长度分块(带重叠)、按句子分块、
递归分块。分块质量直接影响检索召回率。

对应教程:第6卷《RAG》第1章 分块与索引。
"""

from __future__ import annotations

import re
from dataclasses import dataclass


@dataclass(frozen=True)
class Chunk:
    """一个文本块。

    Attributes:
        text: 块文本。
        start: 在原文中的起始字符偏移。
        end: 结束字符偏移。
        index: 块序号。
    """

    text: str
    start: int
    end: int
    index: int


def split_by_chars(text: str, chunk_size: int = 100, overlap: int = 20) -> list[Chunk]:
    """按固定字符数分块(带重叠)。

    Args:
        text: 原始文本。
        chunk_size: 每块字符数。
        overlap: 相邻块重叠字符数。

    Returns:
        ``Chunk`` 列表。

    Raises:
        ValueError: 参数非法时。
    """
    if chunk_size <= 0:
        raise ValueError("chunk_size 必须为正")
    if overlap < 0 or overlap >= chunk_size:
        raise ValueError("overlap 必须 >= 0 且 < chunk_size")
    chunks: list[Chunk] = []
    if not text:
        return chunks
    step = chunk_size - overlap
    idx = 0
    start = 0
    while start < len(text):
        end = min(start + chunk_size, len(text))
        chunks.append(Chunk(text=text[start:end], start=start, end=end, index=idx))
        idx += 1
        if end >= len(text):
            break
        start += step
    return chunks


# 中英文句末标点
_SENTENCE_END_RE = re.compile(r"(?<=[。!?!?\.])\s*")


def split_sentences(text: str) -> list[str]:
    """按句子切分(兼容中英文标点)。

    Args:
        text: 原始文本。

    Returns:
        句子列表(保留标点,去除纯空白项)。
    """
    if not text:
        return []
    parts = _SENTENCE_END_RE.split(text)
    return [s.strip() for s in parts if s.strip()]


def split_by_sentences(
    text: str,
    max_chars: int = 120,
    overlap_sentences: int = 0,
) -> list[Chunk]:
    """按句子聚合分块,每块不超过 ``max_chars`` 字符。

    Args:
        text: 原始文本。
        max_chars: 每块最大字符数。
        overlap_sentences: 相邻块重叠的句子数。

    Returns:
        ``Chunk`` 列表。
    """
    if max_chars <= 0:
        raise ValueError("max_chars 必须为正")
    sentences = split_sentences(text)
    if not sentences:
        return []
    chunks: list[Chunk] = []
    current: list[str] = []
    current_len = 0
    idx = 0

    def flush(start: int) -> int:
        nonlocal idx
        if not current:
            return start
        chunk_text = "".join(current)
        end = start + len(chunk_text)
        chunks.append(Chunk(text=chunk_text, start=start, end=end, index=idx))
        idx += 1
        return end

    pos = 0
    for sent in sentences:
        if current and current_len + len(sent) > max_chars:
            pos = flush(pos)
            # 处理重叠:保留最后 overlap_sentences 句
            if overlap_sentences > 0:
                tail = current[-overlap_sentences:]
                current = list(tail)
                current_len = sum(len(s) for s in current)
                pos -= current_len
            else:
                current = []
                current_len = 0
        current.append(sent)
        current_len += len(sent)
    flush(pos)
    return chunks


def split_recursive(text: str, separators: list[str] | None = None) -> list[str]:
    """递归分块:依次尝试多种分隔符,尽量在更高级别切分。

    Args:
        text: 原始文本。
        separators: 分隔符优先级列表,默认 ``["\n\n", "\n", "。", " "]``。

    Returns:
        非空文本块列表。
    """
    if separators is None:
        separators = ["\n\n", "\n", "。", " "]
    if not text:
        return []
    if not separators:
        return [text] if text.strip() else []

    sep = separators[0]
    rest = separators[1:]
    if sep == "":
        return [text]

    parts = text.split(sep)
    result: list[str] = []
    for part in parts:
        if not part.strip():
            continue
        # 若该部分已经足够短,直接保留;否则用下一级分隔符继续切
        if len(part) <= 80 or not rest:
            result.append(part.strip())
        else:
            result.extend(split_recursive(part, rest))
    return result


def main() -> None:
    """演示三种分块策略。"""
    text = (
        "AI Agent 是能感知环境、自主决策并执行任务的智能体。\n"
        "它由大语言模型驱动,结合工具调用与记忆机制。\n\n"
        "RAG 通过检索增强生成,缓解幻觉。"
    )
    print("== 按字符分块 ==")
    for c in split_by_chars(text, chunk_size=30, overlap=10):
        print(f"  [{c.index}] {c.text!r} ({c.start}-{c.end})")

    print("\n== 按句子分块 ==")
    for c in split_by_sentences(text, max_chars=40):
        print(f"  [{c.index}] {c.text!r}")

    print("\n== 递归分块 ==")
    for i, c in enumerate(split_recursive(text)):
        print(f"  [{i}] {c!r}")


if __name__ == "__main__":
    main()