引言

分块(Chunking)是RAG系统中承上启下的关键环节:上承文档解析,下接向量检索和生成。分块策略直接影响检索精度和生成质量——太大或太小的块都会导致问题。本文对主流分块策略进行系统对比评估,并提供选型建议。

分块为何重要

分块大小的影响

块过大的问题:单个向量难以准确表示大段内容的语义;检索时相似度分数被稀释;上下文窗口浪费在不相关信息上;增大embedding计算成本。

块过小的问题:语义不完整,检索到的块可能缺少上下文;需要检索更多块才能获得完整信息;增加索引存储和检索开销。

分块边界的影响

即使大小合适,切分位置不当也会导致问题:在句子中间切断导致语义破碎;在段落中间切分丢失段落级语义;切断实体与其描述的关联。

主流分块策略

策略一:固定大小分块

按固定token数切分,通常配合一定重叠:

def fixed_size_chunk(text, chunk_size=512, overlap=50):
    """固定大小分块"""
    tokens = tokenizer.encode(text)
    chunks = []
    
    for i in range(0, len(tokens), chunk_size - overlap):
        chunk_tokens = tokens[i:i + chunk_size]
        chunk_text = tokenizer.decode(chunk_tokens)
        chunks.append(chunk_text)
        
        if i + chunk_size >= len(tokens):
            break
    
    return chunks

优势:实现简单、速度极快、块大小可控。

劣势:可能在句子中间切断、不感知内容结构、不同文档可能需要不同大小。

适用场景:快速原型、格式统一的文档、对速度要求高的场景。

策略二:句子级分块

以句子为最小单位进行分块:

import nltk

def sentence_chunk(text, max_chunk_size=512):
    """按句子分块"""
    sentences = nltk.sent_tokenize(text)
    chunks = []
    current_chunk = []
    current_size = 0
    
    for sent in sentences:
        sent_size = len(tokenizer.encode(sent))
        
        if current_size + sent_size > max_chunk_size and current_chunk:
            chunks.append(" ".join(current_chunk))
            current_chunk = [sent]
            current_size = sent_size
        else:
            current_chunk.append(sent)
            current_size += sent_size
    
    if current_chunk:
        chunks.append(" ".join(current_chunk))
    
    return chunks

优势:保持句子完整性、语义边界更合理。

劣势:块大小不均匀、长句子可能导致超大块、段落结构可能被破坏。

适用场景:自然语言文本、新闻文章、博客内容。

策略三:段落级分块

按段落切分,合并过小段落:

def paragraph_chunk(text, max_chunk_size=512, min_chunk_size=100):
    """按段落分块"""
    paragraphs = text.split("\n\n")
    chunks = []
    current_chunk = ""
    current_size = 0
    
    for para in paragraphs:
        para_size = len(tokenizer.encode(para))
        
        if para_size > max_chunk_size:
            # 段落过大,降级为句子分块
            sub_chunks = sentence_chunk(para, max_chunk_size)
            chunks.extend(sub_chunks)
        elif current_size + para_size > max_chunk_size and current_chunk:
            chunks.append(current_chunk)
            current_chunk = para
            current_size = para_size
        else:
            current_chunk = current_chunk + "\n\n" + para if current_chunk else para
            current_size += para_size
    
    if current_chunk and current_size >= min_chunk_size:
        chunks.append(current_chunk)
    elif current_chunk and chunks:
        chunks[-1] += "\n\n" + current_chunk
    
    return chunks

优势:保持段落完整性、适合结构化文档、语义连贯性好。

劣势:段落定义不一致(不同文档的段落分隔不同)、超大段落需特殊处理。

适用场景:结构化文档、报告、论文、技术文档。

策略四:语义分块

基于嵌入相似度动态确定分块边界:

from sentence_transformers import SentenceTransformer
import numpy as np

def semantic_chunk(text, threshold=0.5, min_chunk_size=100):
    """基于语义相似度分块"""
    model = SentenceTransformer('all-MiniLM-L6-v2')
    
    # 按句子切分
    sentences = nltk.sent_tokenize(text)
    
    # 计算相邻句子的嵌入相似度
    embeddings = model.encode(sentences)
    
    # 计算相邻句子的余弦相似度
    similarities = []
    for i in range(len(embeddings) - 1):
        sim = cosine_similarity(embeddings[i], embeddings[i+1])
        similarities.append(sim)
    
    # 在相似度低于阈值处切分
    chunks = []
    current_start = 0
    
    for i, sim in enumerate(similarities):
        if sim < threshold:
            chunk = " ".join(sentences[current_start:i+1])
            if len(tokenizer.encode(chunk)) >= min_chunk_size:
                chunks.append(chunk)
                current_start = i + 1
    
    # 添加最后一个块
    if current_start < len(sentences):
        chunk = " ".join(sentences[current_start:])
        chunks.append(chunk)
    
    return chunks

优势:语义连贯性最好、自适应内容变化、减少上下文断裂。

劣势:计算开销大(需要嵌入模型)、阈值调优困难、速度慢。

适用场景:高质量检索需求、长文档、异构内容混合。

策略五:递归分块

递归地按层级分隔符切分:

def recursive_chunk(text, max_chunk_size=512, separators=None):
    """递归分块"""
    if separators is None:
        separators = ["\n\n\n", "\n\n", "\n", ". ", " ", ""]
    
    if len(tokenizer.encode(text)) <= max_chunk_size:
        return [text]
    
    for sep in separators:
        if sep in text:
            parts = text.split(sep)
            chunks = []
            current = ""
            
            for part in parts:
                candidate = current + sep + part if current else part
                if len(tokenizer.encode(candidate)) <= max_chunk_size:
                    current = candidate
                else:
                    if current:
                        chunks.append(current)
                    # 递归处理超大部分
                    if len(tokenizer.encode(part)) > max_chunk_size:
                        chunks.extend(recursive_chunk(part, max_chunk_size, separators[separators.index(sep)+1:]))
                        current = ""
                    else:
                        current = part
            
            if current:
                chunks.append(current)
            return chunks
    
    return [text]

优势:保持层级结构、灵活适应不同文档格式、在大小和语义间平衡。

劣势:实现较复杂、分隔符选择需要经验。

适用场景:Markdown文档、代码文件、混合格式文档。

策略六:文档结构感知分块

利用文档的结构信息(标题层级)进行分块:

def structure_aware_chunk(document):
    """基于文档结构的分块"""
    chunks = []
    
    for section in document.sections:
        # 每个标题下内容作为一个块
        section_text = f"# {section.title}\n\n{section.content}"
        
        if len(tokenizer.encode(section_text)) > MAX_SIZE:
            # 超大section降级处理
            sub_chunks = recursive_chunk(section.content)
            # 为每个子块添加标题上下文
            for sub in sub_chunks:
                chunks.append(f"# {section.title}\n\n{sub}")
        else:
            chunks.append(section_text)
    
    return chunks

优势:保持文档逻辑结构、每个块有清晰的主题、检索结果更易理解。

劣势:需要文档结构解析前置、依赖文档格式规范。

适用场景:技术文档、产品手册、学术论文。

对比评估

策略实现复杂度语义连贯性速度块大小均匀性适用文档类型
固定大小极低极快通用
句子级自然语言
段落级中高结构化
语义分块高质量需求
递归分块中高中高混合格式
结构感知中高结构化文档

选型建议

  1. 快速原型:固定大小分块,chunk_size=512, overlap=50
  2. 自然语言文档:句子级或段落级分块
  3. 技术文档:结构感知分块,利用标题层级
  4. 混合格式:递归分块
  5. 高精度检索:语义分块,投入计算换取质量
  6. 生产环境推荐:递归分块 + 结构感知的混合策略

评估方法

检索质量评估

构建评估集,对比不同分块策略的检索效果:

  • 召回率:相关文档被检索到的比例
  • 精确率:检索结果中相关块的比例
  • MRR:相关结果的平均排名

生成质量评估

使用相同的检索结果和LLM,对比不同分块策略下生成答案的质量:事实准确性、信息完整性、答案相关性。

结语

分块策略没有银弹,最佳策略取决于文档类型、检索需求和计算资源。实践中建议:从简单策略开始建立基线,在评估集上对比不同策略的效果,根据结果选择或组合策略。记住,分块只是RAG的一个环节,它需要与嵌入模型、检索策略和生成模型协同优化,才能获得最佳效果。

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。