分块策略为什么至关重要

在RAG系统中,文档分块(Chunking)是将长文档拆分为适合向量检索的小片段的过程。这个看似简单的步骤,实际上直接影响着检索质量和最终回答的准确性。分块太大(如2000 token),检索到的片段可能包含太多无关信息,稀释了关键内容的权重;分块太小(如100 token),可能丢失上下文信息,导致检索到的片段语义不完整。更重要的是,分块策略还决定了检索到的片段能否与用户问题形成有效的语义匹配。

很多RAG项目失败的原因不是模型不够好,也不是向量数据库选错了,而是分块策略没有针对具体场景进行优化。一个法律合同的RAG系统需要的分块策略与一个技术文档的RAG系统完全不同。本文将深入讲解四种主流分块策略的原理、优缺点和适用场景。

策略一:固定大小分块

固定大小分块是最简单直接的方式——将文档按固定token数切割。例如每个chunk 512 token,使用滑动窗口重叠128 token。这种方式的优点是实现简单、性能可预测;缺点是完全不考虑语义边界,可能在句子中间切断,导致每个chunk的语义不完整。

实现时需要注意两个关键参数:chunk_size(块大小)和chunk_overlap(重叠大小)。重叠的存在是为了防止关键信息恰好落在两个chunk的边界上。一般建议chunk_size在256-1024之间,overlap在chunk_size的10%-25%之间。对于中文文档,建议chunk_size=512(约800-1000个中文字符),overlap=100。

from openai import OpenAI

client = OpenAI(api_key="your-deepseek-api-key", base_url="https://api.deepseek.com")

class FixedSizeChunker:
    def __init__(self, chunk_size=512, overlap=100):
        self.chunk_size = chunk_size
        self.overlap = overlap

    def chunk_text(self, text):
        """简单按字符数分块"""
        chunks = []
        start = 0
        while start < len(text):
            end = min(start + self.chunk_size, len(text))
            chunk = text[start:end]
            chunks.append({
                "text": chunk,
                "index": len(chunks),
                "start_char": start,
                "end_char": end
            })
            start = end - self.overlap
        return chunks

    def chunk_with_metadata(self, text, source="", title=""):
        """带元数据的分块"""
        chunks = self.chunk_text(text)
        for c in chunks:
            c["source"] = source
            c["title"] = title
            c["char_count"] = len(c["text"])
        return chunks

chunker = FixedSizeChunker(chunk_size=800, overlap=120)
text = "DeepSeek是深度求索公司开发的大语言模型..." * 50
chunks = chunker.chunk_with_metadata(text, source="deepseek_intro.md", title="DeepSeek介绍")
print(f"生成了 {len(chunks)} 个文档块")

策略二:语义分块

语义分块不再按固定长度切割,而是根据文本的语义边界——如段落、章节、话题变换点——来决定切割位置。这种方式产生的每个chunk语义更完整,检索质量通常优于固定大小分块。但实现复杂度更高,需要模型来判断语义边界。

语义分块的核心是识别文本中的"自然断点":段落分隔符(双换行)、标题标记(Markdown的#)、话题转换句(如"另一方面""接下来讨论")、以及通过embedding相似度检测的语义漂移点。一个实用的策略是:先用段落或章节作为第一级切分,如果段落仍然超过阈值,再用更细粒度的方法切分。

import numpy as np

class SemanticChunker:
    def __init__(self, max_chunk_size=1000, similarity_threshold=0.7):
        self.max_chunk_size = max_chunk_size
        self.similarity_threshold = similarity_threshold

    def get_embedding(self, text):
        """使用DeepSeek获取文本向量"""
        response = client.embeddings.create(
            model="deepseek-chat",
            input=text[:8000]
        )
        return np.array(response.data[0].embedding)

    def chunk_by_paragraphs(self, text):
        """先按段落切分"""
        paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
        return paragraphs

    def merge_similar(self, paragraphs):
        """将语义相似的段落合并"""
        if len(paragraphs) <= 1:
            return paragraphs
        chunks = []
        current = paragraphs[0]
        for i in range(1, len(paragraphs)):
            combined = current + "\n\n" + paragraphs[i]
            if len(combined) <= self.max_chunk_size:
                current = combined
            else:
                chunks.append(current)
                current = paragraphs[i]
        chunks.append(current)
        return chunks

chunker = SemanticChunker(max_chunk_size=1000)
paras = chunker.chunk_by_paragraphs(long_document)
merged = chunker.merge_similar(paras)
print(f"从 {len(paras)} 段合并为 {len(merged)} 个语义块")

策略三:递归分块

递归分块是一种折中方案:先用粗粒度的分隔符(如双换行)尝试切分,如果切出的块还是太大,再用更细粒度的分隔符(如单换行、句号、逗号)继续切分。这种逐级细化的方式既考虑了语义边界,又保证了块大小在可控范围内。LangChain的RecursiveCharacterTextSplitter就是这种策略的典型实现。

class RecursiveChunker:
    def __init__(self, chunk_size=512, overlap=50):
        self.chunk_size = chunk_size
        self.overlap = overlap
        self.separators = ["\n\n", "\n", "。", ".", ";", ";", ",", ",", " ", ""]

    def split(self, text):
        """递归切分"""
        return self._recursive_split(text, self.separators)

    def _recursive_split(self, text, separators):
        if len(text) <= self.chunk_size:
            return [text] if text.strip() else []
        if not separators:
            # Last resort: force split
            return [text[i:i+self.chunk_size] for i in range(0, len(text), self.chunk_size-self.overlap)]
        sep = separators[0]
        remaining = separators[1:]
        parts = text.split(sep)
        chunks = []
        current = ""
        for part in parts:
            if len(current) + len(part) + len(sep) <= self.chunk_size:
                current = (current + sep + part) if current else part
            else:
                if current:
                    chunks.extend(self._recursive_split(current, remaining))
                current = part
        if current:
            chunks.extend(self._recursive_split(current, remaining))
        return chunks

rchunker = RecursiveChunker(chunk_size=512, overlap=50)
chunks = rchunker.split(long_chinese_text)
print(f"递归分块结果:{len(chunks)} 块,平均长度 {sum(len(c) for c in chunks)//len(chunks)} 字符")

策略四:句子感知分块

句子感知分块确保每个chunk的边界恰好落在句子末尾,而不是在句子中间截断。这种策略对于中文尤其重要——中文的句子边界不如英文清晰(英文有大写字母作为句子起始标志),需要通过句号、问号、感叹号等标点来识别。配合spaCy或jieba等NLP工具,可以实现更精准的句子边界检测。

句子感知分块通常是其他策略的补充——在固定大小或递归分块的基础上,确保切割点落在句子末尾。这样做的好处是每个chunk都是一个或多个完整句子的组合,语义更加自洽,检索结果的可读性也更好。

分块策略选择指南

技术文档/API文档:推荐固定大小分块(512-1024 token)。这类文档结构清晰,代码和文字交替,语义边界不明显。法律合同/政策文档:推荐语义分块。条款和段落之间有明确的语义边界,按条款切分能保证每块的语义完整性。新闻/博客文章:推荐递归分块或句子感知分块。文章结构多变,需要在语义完整性和块大小之间平衡。对话记录:推荐按轮次分块。每轮对话作为一个chunk,保留对话的交互性。混合文档集:推荐自适应分块——先用规则检测文档类型,再选择对应的分块策略。

没有银弹——最佳分块策略需要通过实验来确定。建议对每种候选策略进行检索质量评估(如Recall@k、MRR),选择在你的数据集上表现最好的方案。同时,分块策略不是一成不变的,随着文档集的更新和增加,定期重新评估并调整策略是必要的。

实践中的常见误区

误区一:分块越大越好。很多开发者认为"大块包含更多上下文,检索质量更高"。但实际测试表明,chunk_size从512增加到2048时,检索相关性通常先升后降,最优区间在512-1024。过大的chunk会导致向量表示过于"平均",丢失了细粒度的语义信息。误区二:重叠越大越好。重叠确实能减少边界信息丢失,但过度重叠(>30%)会显著增加存储成本和检索噪音——同一个信息出现在多个chunk中,检索时可能返回多个高度相似的chunk。误区三:分块策略一次设定就万事大吉。随着文档集的变化(新增文档类型、文档风格变化),原有的分块策略可能不再最优。建议每季度或每次文档集大幅更新后,重新评估分块策略的效果。

进阶:基于内容的动态分块

对于文档类型多样的场景,固定策略很难面面俱到。一个进阶方案是动态分块——先用一个小模型分析文档结构(检测标题层级、表格边界、代码块范围等),再根据文档的局部特征动态选择分块粒度。例如,对于代码块保持完整(不分割),对于长段落按句子边界切分,对于表格保持整表。这种方法实现复杂度较高,但在文档质量要求极高的场景(如法律文书检索)中能带来显著的质量提升。建议先从固定策略开始,在积累了足够的反馈数据后,逐步向动态策略演进。

想亲手编排这个技能链?

在技能链中打开 →