前置知识: 自然语言处理

分块策略与RAG

5 minIntermediate

理解固定大小、句子感知、语义和递归分块策略及其对RAG的影响

分块策略与RAG

80%的RAG失败可追溯到摄取和分块,不是模型。团队花几周调提示,而检索悄悄每三次查询返回错误上下文。

类型: 构建 语言: Python 前置条件: Phase 5 · 14(信息检索),Phase 5 · 22(嵌入模型) 时间: ~75 分钟

问题

你有一份50页的PDF。嵌入模型接受512个token。你需要将文档分割成块,使每个块:(1) 足够小以适应嵌入模型,(2) 足够大以携带有意义的信息,(3) 在语义边界上断开而非句子中间。

分块是RAG中最被低估的步骤。错误的分块策略破坏检索,无论嵌入模型或LLM有多好。

概念

固定大小分块。 每块N个token,重叠M个token。简单、可预测、忽略语义边界。适合快速基线。

句子感知分块。 在句子边界上分割。使用spaCy或NLTK的句子分割器。保留句子完整性。可能产生非常短或非常长的块。

语义分块。 计算相邻句子的嵌入相似度。当相似度低于阈值时分割。在语义边界上断开。计算成本高但质量更好。

递归分块。 使用分隔符层次结构(段落 → 句子 → 字符)。尝试在最高级别分割,如果块太大则降级。LangChain的默认策略。

父文档模式。 检索小”子”块以提高精确度。当同一父节的多个子块出现时,换入父块以保留上下文。持续提升答案质量。

分块参数

参数典型值影响
块大小256-1024 token更小 = 更精确检索,更少上下文
重叠10-25%防止跨块边界信息丢失
分隔符句子、段落、标题语义边界保持完整性
最小块大小50-100 token过小的块合并

构建它

步骤 1:固定大小分块

def fixed_chunk(text, chunk_size=512, overlap=50):
    tokens = text.split()
    chunks = []
    start = 0
    while start < len(tokens):
        end = start + chunk_size
        chunk = " ".join(tokens[start:end])
        chunks.append(chunk)
        start += chunk_size - overlap
    return chunks

步骤 2:递归分块

def recursive_chunk(text, max_size=512, separators=None):
    if separators is None:
        separators = ["\n\n", "\n", ". ", " ", ""]

    if len(text.split()) <= max_size:
        return [text]

    sep = separators[0]
    remaining = separators[1:]

    parts = text.split(sep)
    chunks = []
    current = ""

    for part in parts:
        candidate = (current + sep + part).strip() if current else part
        if len(candidate.split()) <= max_size:
            current = candidate
        else:
            if current:
                chunks.append(current)
            if len(part.split()) > max_size and remaining:
                chunks.extend(recursive_chunk(part, max_size, remaining))
            else:
                current = part
    if current:
        chunks.append(current)
    return chunks

步骤 3:语义分块

from sentence_transformers import SentenceTransformer
import numpy as np

model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")

def semantic_chunk(text, threshold=0.5):
    sentences = text.split(". ")
    if len(sentences) <= 1:
        return [text]

    embeddings = model.encode(sentences, normalize_embeddings=True)
    similarities = [float(embeddings[i] @ embeddings[i + 1]) for i in range(len(embeddings) - 1)]

    chunks = []
    current = [sentences[0]]
    for i, sim in enumerate(similarities):
        if sim < threshold:
            chunks.append(". ".join(current))
            current = [sentences[i + 1]]
        else:
            current.append(sentences[i + 1])
    if current:
        chunks.append(". ".join(current))
    return chunks

步骤 4:评估分块质量

def chunk_quality(chunks, questions, answers, embed_model, top_k=3):
    total_recall = 0
    for q, a in zip(questions, answers):
        q_emb = embed_model.encode([q], normalize_embeddings=True)
        chunk_embs = embed_model.encode(chunks, normalize_embeddings=True)
        sims = (chunk_embs @ q_emb.T).flatten()
        top_indices = np.argsort(-sims)[:top_k]
        relevant_text = " ".join(chunks[i] for i in top_indices)
        overlap = len(set(a.lower().split()) & set(relevant_text.lower().split()))
        total = len(set(a.lower().split()))
        total_recall += overlap / total if total else 0
    return total_recall / len(questions)

使用它

场景选择
快速基线固定大小,256 token,10%重叠
通用文档递归分块,512 token
技术文档/手册语义分块 + 标题感知
法律/合规句子感知,保留条款完整性
代码AST感知分块(函数/边界)
长文档父文档模式

2026年生产RAG的来之不易的经验

  • 80%的RAG失败可追溯到摄取和分块,不是模型。 先修复分块。
  • 分块策略比分块大小更重要。 固定大小分割破坏表格、代码和嵌套标题。
  • 父文档模式持续提升答案质量。 检索小”子”块,换入父块。
  • k_rerank=3通常最优。 超过这个的每个额外块增加成本但不提升质量。
  • 上下文预算低于8K token。 在该限制下持续命中意味着重排序器阈值太松。

交付它

将结果保存为 outputs/skill-chunking-picker.md

练习

  1. 简单。 在相同文档上比较固定大小和递归分块。测量块大小分布。
  2. 中等。 在技术文档上实现语义分块。与递归分块比较检索recall@5。
  3. 困难。 实现父文档模式。测量有和无父文档提升的答案质量。

关键术语

术语通俗说法实际含义
分块切文档将文档分割为嵌入模型可处理的片段。
重叠安全网相邻块之间共享的token,防止边界信息丢失。
语义分块智能分割在语义相似度下降处分割。
父文档小检索大上下文检索小子块,返回大父块。
摄取文档入流水线分块 + 嵌入 + 索引的完整文档处理过程。

延伸阅读