Deeptoai RAG系列教程

分块(Chunking)策略实现

从规则到语义与 LLM 分块的工程权衡与最佳实践

为什么 Chunking 至关重要

分块策略直接决定检索召回上限、上下文利用率与成本。过大的 chunk 会导致无关信息噪声,过小的 chunk 则会丢失上下文连贯性。合理的分块是构建高质量 RAG 系统的第一步。

分块(Chunking)策略实现

背景与核心问题

什么是 Chunking?

Chunking 是将长文档切分成更小、语义连贯的片段的过程。在 RAG 系统中,这些片段(chunks)会被转换为向量嵌入并存储到向量数据库中,供检索时使用。

核心挑战

  1. 粒度权衡:chunk 太大 → 噪声多、成本高;chunk 太小 → 上下文不足、召回差
  2. 语义边界:如何在自然的语义边界处切分(段落、句子、主题)
  3. 重叠策略:如何设置 overlap 避免关键信息被切断
  4. 跨结构保持:如何处理表格、代码块、列表等结构化内容
  5. 多模态处理:图像、公式等非文本内容如何与文本协同分块

关键决策树

何时选择哪种策略?

策略适用场景优势劣势
Token-based通用场景、成本敏感精确控制、与 LLM token 限制对齐可能在词中间切断
Character-based简单文本、快速原型实现简单、快速语义边界差
Sentence-based对话、问答语义完整、可读性好chunk 大小不均
Semantic长篇文档、主题明确语义连贯、召回质量高计算成本高
Recursive结构化文档(Markdown/HTML)保持结构层次配置复杂
LLM-based高质量要求、成本不敏感最优语义边界成本极高、延迟高

跨项目实现对照

依赖库对比

项目第三方库实现方式质量评级
LightRAG无(自研 + tiktoken)内置实现⭐⭐⭐⭐⭐
RAG-Anything继承 LightRAG + 模板扩展实现⭐⭐⭐⭐⭐
onyxtiktoken + 自研 SentenceChunker企业级实现⭐⭐⭐⭐⭐
VerbaLangChain Text Splitters直接依赖⭐⭐⭐⭐
kotaemonLlamaIndex Splitters包装器⭐⭐⭐⭐
UltraRAGChonkie直接依赖⭐⭐⭐⭐
SurfSenseDocling + Chonkie直接依赖⭐⭐⭐⭐
ragflow自研 naive_merge内置实现⭐⭐⭐

核心实现对比

1. LightRAG:Token-based 精确分块

特点:使用 tiktoken 进行 token 级精确控制,支持字符分隔符辅助切分。

lightrag/operate.py
def chunking_by_token_size(
    tokenizer: Tokenizer,
    content: str,
    split_by_character: str | None = None,
    split_by_character_only: bool = False,
    overlap_token_size: int = 128,
    max_token_size: int = 1024,
) -> list[dict[str, Any]]:
    """
    Token-based chunking with configurable overlap
    
    Args:
        tokenizer: Tiktoken tokenizer instance
        content: Raw text content
        split_by_character: Optional character delimiter (e.g., '\n\n')
        overlap_token_size: Overlap between chunks
        max_token_size: Maximum tokens per chunk
    """
    tokens = tokenizer.encode(content)
    results: list[dict[str, Any]] = []
    
    if split_by_character:
        # Character-based splitting logic with token control
        raw_chunks = content.split(split_by_character)
        # ... processing logic
    else:
        # Pure token-based splitting with overlap
        for index, start in enumerate(
            range(0, len(tokens), max_token_size - overlap_token_size)
        ):
            chunk_content = tokenizer.decode(tokens[start : start + max_token_size])
            results.append({
                "tokens": min(max_token_size, len(tokens) - start),
                "content": chunk_content.strip(),
                "chunk_order_index": index,
            })
    
    return results

优势

  • ✅ Token 级精确控制,与 LLM 限制完美对齐
  • ✅ 支持异步处理,性能优秀
  • ✅ 可扩展自定义分块函数

劣势

  • ❌ 不支持语义感知分块
  • ❌ 需要 RAG-Anything 扩展才能处理多模态

2. RAG-Anything:多模态模板分块

特点:在 LightRAG 基础上增加多模态内容处理,为不同内容类型应用专门模板。

rag_anything/chunking.py
def _apply_chunk_template(
    self, content_type: str, original_item: Dict[str, Any], description: str
) -> str:
    """
    Apply specialized templates for different content types
    
    Ensures optimal chunking for images, tables, equations, code
    """
    templates = {
        "text": self.text_template,
        "image": self.image_template,        # 图像描述模板
        "table": self.table_template,        # 表格结构化模板
        "equation": self.equation_template,  # 公式语义化模板
        "code": self.code_template           # 代码块模板
    }
    
    template = templates.get(content_type, self.default_template)
    return template.format(
        content=original_item.get('content', ''), 
        description=description
    )

# 缓存机制优化性能
async def _get_from_parse_cache(self, cache_key: str, file_path: Path) -> Optional[tuple]:
    """Retrieve parsed content from cache to avoid redundant processing"""
    cache_file = self.cache_dir / f"{cache_key}.json"
    if cache_file.exists():
        try:
            with open(cache_file, 'r') as f:
                return json.load(f)
        except Exception as e:
            self.logger.warning(f"Cache read failed: {e}")
    return None

优势

  • ✅ 完整多模态支持(图像/表格/公式)
  • ✅ 灵活模板设计,易于扩展
  • ✅ 智能缓存机制

劣势

  • ❌ 强依赖 LightRAG
  • ❌ 模板维护成本高

3. onyx:企业级上下文感知分块

特点:多通道分块、上下文 RAG、大 chunk 支持,面向企业级部署。

onyx/chunking.py
class Chunker:
    def __init__(
        self,
        tokenizer: BaseTokenizer,
        enable_multipass: bool = False,        # 多通道索引
        enable_large_chunks: bool = False,     # 大 chunk 支持
        enable_contextual_rag: bool = False,   # 上下文 RAG
        blurb_size: int = BLURB_SIZE,
        chunk_token_limit: int = 1024,
        chunk_overlap: int = 128,
        mini_chunk_size: int = 256,            # Mini-chunk 大小
    ) -> None:
        # Token 计数器
        def token_counter(text: str) -> int:
            return len(tokenizer.encode(text))
        
        # 主分块器:基于句子的语义边界
        self.chunk_splitter = SentenceChunker(
            tokenizer_or_token_counter=token_counter,
            chunk_size=chunk_token_limit,
            chunk_overlap=chunk_overlap,
            return_type="texts",
        )
        
        # Mini-chunk 分块器(用于多通道索引)
        self.mini_chunk_splitter = (
            SentenceChunker(
                tokenizer_or_token_counter=token_counter,
                chunk_size=mini_chunk_size,
                chunk_overlap=0,
                return_type="texts",
            )
            if enable_multipass
            else None
        )

上下文 RAG(Contextual RAG)原理

  1. 为每个 chunk 生成文档级上下文摘要
  2. 在检索时将上下文与 chunk 内容一起提供给 LLM
  3. 预留 token 空间给上下文信息

这比传统语义分块更先进,因为:

  • 考虑整个文档的全局上下文
  • 使用 LLM 主动生成上下文信息
  • 在检索时保留上下文利用空间

优势

  • ✅ 最先进的上下文感知分块
  • ✅ Token 级精确控制
  • ✅ 企业级配置选项

劣势

  • ❌ 实现复杂度高
  • ❌ 配置参数多,学习曲线陡

4. Chonkie 库(UltraRAG/SurfSense)

特点:专业分块库,提供多种策略。

chonkie_example.py
import chonkie

# Token-based chunking
if chunk_strategy == "token":
    chunker = chonkie.TokenChunker(
        tokenizer=tokenizer_name_or_path, 
        chunk_size=chunk_size
    )

# Sentence-based chunking
elif chunk_strategy == "sentence":
    chunker = chonkie.SentenceChunker(
        tokenizer_or_token_counter=tokenizer_name_or_path, 
        chunk_size=chunk_size
    )

# Recursive chunking for structured content
elif chunk_strategy == "recursive":
    chunker = chonkie.RecursiveChunker(
        tokenizer=tokenizer_name_or_path,
        chunk_size=chunk_size,
        separators=["\n\n", "\n", " ", ""]
    )

SurfSense 配置示例

surfsense/config/__init__.py
# 根据 embedding 模型自动配置 chunk 大小
EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL")
embedding_model_instance = AutoEmbeddings.get_embeddings(EMBEDDING_MODEL)

chunker_instance = RecursiveChunker(
    chunk_size=getattr(embedding_model_instance, "max_seq_length", 512)
)

code_chunker_instance = CodeChunker(
    chunk_size=getattr(embedding_model_instance, "max_seq_length", 512)
)

优势

  • ✅ 专业库,经过充分测试
  • ✅ 多种策略选择
  • ✅ 与 embedding 模型自动对齐

劣势

  • ❌ 外部依赖
  • ❌ 缺乏高级语义分块

实现要点与最佳实践

参数配置指南

推荐初始参数

基于 9 个项目的实践经验,以下是通用推荐参数:

  • chunk_size: 512-1024 tokens(对应约 384-768 英文词或 256-512 中文字)
  • overlap: 128-256 tokens(约 20-25% overlap)
  • separators: ["\n\n", "\n", ". ", "。", "!", "!", "?", "?"]

不同场景的参数调整

场景chunk_sizeoverlap分隔符策略
短文本问答256-51264-128句子级分隔
长文档检索1024-2048256-512段落级分隔
代码文档512-1024128-256函数/类边界
对话历史512128轮次边界
学术论文1024-2048256-512章节/段落

最佳实践清单

1. 预处理清洗

preprocessing.py
def preprocess_text(text: str) -> str:
    """文本预处理最佳实践"""
    # 1. 移除多余空白
    text = re.sub(r'\s+', ' ', text)
    
    # 2. 统一换行符
    text = text.replace('\r\n', '\n').replace('\r', '\n')
    
    # 3. 移除特殊控制字符
    text = ''.join(char for char in text if ord(char) >= 32 or char in '\n\t')
    
    # 4. 处理中英文混排空格
    text = re.sub(r'([一-龥])\s+([a-zA-Z])', r'\1 \2', text)
    text = re.sub(r'([a-zA-Z])\s+([一-龥])', r'\1 \2', text)
    
    return text.strip()

2. 结构感知分块

structure_aware_chunking.py
def chunk_with_structure_awareness(content: str, max_tokens: int = 1024):
    """保持 Markdown 结构的分块"""
    sections = []
    current_section = []
    current_tokens = 0
    
    for line in content.split('\n'):
        # 检测标题
        if line.startswith('#'):
            # 如果当前 section 不为空且即将超出,先保存
            if current_tokens > max_tokens * 0.8:
                sections.append('\n'.join(current_section))
                current_section = []
                current_tokens = 0
        
        line_tokens = len(tokenizer.encode(line))
        current_section.append(line)
        current_tokens += line_tokens
        
        # 超过限制则切分
        if current_tokens > max_tokens:
            sections.append('\n'.join(current_section))
            current_section = []
            current_tokens = 0
    
    # 保存最后一个 section
    if current_section:
        sections.append('\n'.join(current_section))
    
    return sections

3. 质量评估与迭代

evaluation.py
def evaluate_chunking_quality(chunks: List[str]) -> Dict[str, float]:
    """分块质量评估指标"""
    metrics = {}
    
    # 1. Size 分布均匀度(标准差/平均值)
    sizes = [len(tokenizer.encode(c)) for c in chunks]
    metrics['size_cv'] = np.std(sizes) / np.mean(sizes)
    
    # 2. 语义连贯性(相邻 chunk embedding 相似度)
    embeddings = [get_embedding(c) for c in chunks]
    similarities = [
        cosine_similarity(embeddings[i], embeddings[i+1])
        for i in range(len(embeddings)-1)
    ]
    metrics['avg_coherence'] = np.mean(similarities)
    
    # 3. 覆盖率(chunk 边界是否覆盖关键信息)
    # 提取关键句并检查是否被完整包含在某个 chunk 中
    key_sentences = extract_key_sentences(original_text)
    coverage = sum(
        any(sent in chunk for chunk in chunks)
        for sent in key_sentences
    ) / len(key_sentences)
    metrics['key_coverage'] = coverage
    
    return metrics

常见问题与解决方案

高级技巧

1. 动态 Chunk Size 调整

根据文档类型动态调整:

dynamic_chunking.py
def get_adaptive_chunk_size(doc_type: str, doc_length: int) -> int:
    """根据文档类型和长度自适应调整 chunk 大小"""
    base_size = 512
    
    if doc_type == "code":
        return 1024  # 代码需要更大上下文
    elif doc_type == "dialogue":
        return 256   # 对话可以较小
    elif doc_type == "academic":
        # 长文档用更大 chunk 减少总数
        return min(2048, base_size + (doc_length // 10000) * 128)
    
    return base_size

2. 与 Retrieval 协同优化

retrieval_aware_chunking.py
# 为混合检索优化:生成 chunk 变体
def create_chunk_variants(chunk: str) -> Dict[str, str]:
    """为混合检索创建 chunk 变体"""
    return {
        "original": chunk,
        "summary": summarize(chunk, max_length=128),  # 用于粗排
        "keywords": extract_keywords(chunk),          # 用于 BM25
        "questions": generate_questions(chunk)        # 用于 HyDE
    }

延伸阅读

相关教程文档:

参考文献

  • LangChain Text Splitters - RecursiveCharacterTextSplitter design
  • LlamaIndex Splitters - Sentence/Markdown/Code splitters
  • Chonkie - General-purpose chunking library

下一步:进入 存储方案对比 了解如何存储和管理这些 chunks。