返回洞察
RAG · 检索增强

RAG 分块策略:真正有效的几种

AI Workforce Factory·2026-07-20·9 分钟·来源 · superml.org

RAG 分块策略:真正有效的几种

比较固定大小、递归、语义与命题级(proposition-level)分块,并附基准测试。

比任何其他变量都更决定 RAG 质量的那个变量

你可以花几天选完美的嵌入模型与向量数据库。你可以把提示模板调到完美。但如果你的分块策略是错的,检索质量就会很差——下游什么都救不回来。

核心张力在这里:块需要小到能被精确检索,又要大到包含有意义、自包含的信息。 太大的块会轻微匹配很多查询、却一个都匹配不好。太小的块可能只包含一句没有上下文的话——检索对了,却对 LLM 没用,因为它缺乏周边的解释。

本教程从最简单到最复杂,走一遍四种策略,附代码与一个具体基准来阐明差异。


策略一:固定大小分块(Fixed-Size Chunking)

做法: 每 N 个字符(或 token)切一次,带固定的重叠。

实现:

from langchain_text_splitters import CharacterTextSplitter

splitter = CharacterTextSplitter(
    chunk_size=512,    # characters, not tokens
    chunk_overlap=50,
    separator="\n"     # try to split at newlines; fall back to anywhere
)

chunks = splitter.split_documents(documents)

直觉: 想象你有一本 1,000 页的书,每 4 英寸切一刀,不管段落或句子在哪里结束。便宜又快,但你会不断把句子切成两半。

失败模式:

Original text:
"The indemnification clause requires the vendor to hold the customer
harmless for all third-party claims arising from the vendor's
negligence. This obligation survives termination of the agreement."

Fixed-size chunk boundary falls here:
"...The indemnification clause requires the vendor to hold the customer
harmless for all third-party claims arising from the vendor's
negligen"  ← cut mid-word

Next chunk:
"ce. This obligation survives termination of the agreement. The
payment terms specify net-30 from invoice date..."

单词「negligence」被切到了两个块里。两个块都无法被一个关于 negligence 的查询检索到。第二个块现在以一个句子片段开头。

何时使用: 需要快速基线时的原型与测试。不用于生产。


策略二:递归字符切分(Recursive Character Splitting)

做法: 先尝试在最自然的边界切(段落 → 句子 → 词),只在必要时才退回到更硬的切分。

这是最常见的生产起点,也是 LangChain 的 RecursiveCharacterTextSplitter 所实现的:

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,
    chunk_overlap=50,
    separators=[
        "\n\n",   # paragraph breaks (try first)
        "\n",     # line breaks
        ". ",     # sentence boundaries
        ", ",     # clause boundaries
        " ",      # word boundaries
        ""        # character boundaries (last resort)
    ]
)

chunks = splitter.split_documents(documents)

直觉: 你是一个把长文拆成章节的文案编辑。你先尝试找自然的段落断点。如果一段仍太长,你在句子处切。如果一句太长,你在从句处切。只有被逼无奈时才退到词中间切。结果远比固定大小分块可读。

用 token 计数对齐 LLM:

LLM 上下文限制是按 token 而非字符衡量的。更聪明的做法是使用感知 tokenizer 的切分器:

# pip install tiktoken
from langchain_text_splitters import RecursiveCharacterTextSplitter
import tiktoken

def token_length(text: str) -> int:
    encoding = tiktoken.get_encoding("cl100k_base")  # GPT-4 tokenizer
    return len(encoding.encode(text))

splitter = RecursiveCharacterTextSplitter(
    chunk_size=400,           # tokens
    chunk_overlap=40,         # tokens
    length_function=token_length,
    separators=["\n\n", "\n", ". ", " ", ""]
)

chunks = splitter.split_documents(documents)

# Verify actual token counts
token_counts = [token_length(c.page_content) for c in chunks]
print(f"Mean chunk size: {sum(token_counts)/len(token_counts):.0f} tokens")
print(f"Max chunk size: {max(token_counts)} tokens")
print(f"Min chunk size: {min(token_counts)} tokens")

何时使用: 这是你的默认。每个新项目都从这里开始。只有当你测出检索质量不足时才升级。


策略三:语义分块(Semantic Chunking)

做法: 不按大小切,而是按_含义变化_的地方切。顺序嵌入句子,并在相邻句子间余弦相似度显著下降处切分。

直觉: 想象你在读一份技术文档。前几句在讨论认证(authentication)。然后出现一个清晰的概念转折,接下来的句子在讨论授权(authorization)。语义分块检测那个转折,并把边界放在那里——把认证的讨论放在一起,把授权的讨论放在一起,即便这意味着块大小各不相同。

# pip install langchain-experimental
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

splitter = SemanticChunker(
    embeddings,
    breakpoint_threshold_type="percentile",
    breakpoint_threshold_amount=95  # split at the top 5% of similarity drops
)

chunks = splitter.split_documents(documents)

# Chunks will vary in size — some might be 200 tokens, others 800
sizes = [len(c.page_content.split()) for c in chunks]
print(f"Chunk sizes (words): min={min(sizes)}, max={max(sizes)}, mean={sum(sizes)/len(sizes):.0f}")

权衡:

维度递归(Recursive)语义(Semantic)
速度非常快(无 API 调用)慢(必须为每个句子嵌入)
成本免费约每 1M token $0.02(OpenAI small)
边界质量好(自然标点)更好(语义连贯性)
块大小方差低(大小可预测)高(可能很长或很短)

何时使用: 当你的领域有密集、技术性的文本,且同一段落里出现多个主题,而你已测出递归切分产生了糟糕的检索时。法律文档、医学文献与密集的技术规范受益最大。


策略四:命题级分块(Proposition-Level Chunking)

做法: 用一个 LLM 把每个文档分解成原子化的事实陈述(命题)。每个命题成为一个块。

这个技术由 Dense X Retrieval 论文提出,产出最高质量的块——代价也相当可观。

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.documents import Document

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

decompose_prompt = ChatPromptTemplate.from_template("""
Decompose the following text into a list of simple, self-contained factual propositions.
Each proposition should:
- Express a single fact
- Be understandable without the surrounding context
- Be a complete sentence

Text:
{text}

Return one proposition per line, no numbering, no bullet points.
""")

def propositionize(documents: list[Document]) -> list[Document]:
    propositions = []

    for doc in documents:
        # Only process chunks of reasonable size
        if len(doc.page_content.split()) < 20:
            continue

        response = llm.invoke(
            decompose_prompt.format_messages(text=doc.page_content)
        )

        for line in response.content.strip().split('\n'):
            line = line.strip()
            if len(line) > 20:  # filter very short lines
                propositions.append(Document(
                    page_content=line,
                    metadata={
                        **doc.metadata,
                        "proposition_source": doc.page_content[:100]
                    }
                ))

    return propositions

# First do recursive splitting, then propositionize
base_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
base_chunks = base_splitter.split_documents(documents)

propositions = propositionize(base_chunks)
print(f"Generated {len(propositions)} propositions from {len(base_chunks)} chunks")

转换示例:

原始块:

"The API uses OAuth 2.0 for authentication. Access tokens expire after
one hour and must be refreshed using the refresh token endpoint at
/auth/refresh. Rate limits are applied per API key at 1000 requests
per minute for the Standard tier."

命题级块:

"The API uses OAuth 2.0 for authentication."
"Access tokens expire after one hour."
"Expired access tokens must be refreshed using the refresh token endpoint."
"The refresh token endpoint is located at /auth/refresh."
"Rate limits are applied per API key."
"The Standard tier allows 1000 requests per minute."

当用户问「Standard tier 的限流是多少?」时,命题「The Standard tier allows 1000 requests per minute」会与查询有非常高的余弦相似度——远高于原始的三句话块。

成本: 每个文档块都需要一次 LLM 调用才能分解。对于 500 页的语料,这可能是 2,000-5,000 次 LLM 调用。按 GPT-4o-mini 价格(约每 1M 输入 token $0.15),对中等规模语料通常是 $2-10。对于大型语料,成本会变得可观。


基准测试:同一查询,四种策略

让我们用一个可度量的比较让它具体起来。使用一份 50 页的技术手册,查询「When the rate limit is exceeded, what happens to API calls?」:

设置:

from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter, CharacterTextSplitter
from langchain_experimental.text_splitter import SemanticChunker

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
query = "What happens to API calls when the rate limit is exceeded?"

strategies = {
    "fixed_size": CharacterTextSplitter(chunk_size=512, chunk_overlap=50),
    "recursive": RecursiveCharacterTextSplitter(chunk_size=512, chunk_overlap=50),
    "semantic": SemanticChunker(embeddings, breakpoint_threshold_type="percentile"),
}

results = {}
for name, splitter in strategies.items():
    chunks = splitter.split_documents(documents)
    db = Chroma.from_documents(chunks, embeddings)
    retrieved = db.similarity_search(query, k=3)
    results[name] = retrieved
    print(f"\n{name.upper()} — {len(chunks)} total chunks")
    for i, doc in enumerate(retrieved):
        print(f"  Chunk {i+1}: {doc.page_content[:150]}")

典型结果:

策略创建的块数Top 3 中的相关块Top 结果预览
Fixed-size4121/3"...limit exceeded. The API returns a 4..."(被截断)
Recursive3872/3"When rate limit is exceeded, the API returns HTTP 429..."
Semantic2983/3"API rate limit exceeded responses return HTTP 429 with a Retry-After header..."
Proposition1,8473/3"HTTP 429 Too Many Requests is returned when the rate limit is exceeded."

模式是一致的:更复杂的策略检索到更多相关块。从 fixed 到 recursive 的提升很大。从 recursive 到 semantic 的提升中等。Proposition 以高成本换来精度。


选择正确的策略:决策树

Start with: Recursive Character Splitting
  chunk_size=400 tokens, chunk_overlap=40 tokens

↓

Measure RAGAS Context Precision (covered in Lesson 9)

Context Precision > 0.7?
  Yes → You're done. Ship it.
  No → Continue

↓

Is your content domain-specific with dense topic shifts?
(Legal docs, medical literature, scientific papers)
  Yes → Try Semantic Chunking
  No → Try reducing chunk_size (experiment with 200-300 tokens)

↓

Still below 0.7 Context Precision after tuning?
  Yes → Try Proposition-level chunking
  (Accept the cost; it often jumps precision from 0.65 to 0.85)

实用技巧

技巧 1:把 chunk_overlap 设为 chunk_size 的约 10% 对于 512 字符的块,用 50 的重叠。对于 1000 字符的块,用 100。重叠太少会丢失边界内容。太多则会创建近似重复的块,浪费索引空间。

技巧 2:为你的领域调 chunk_size 简短、事实性的内容(FAQ、政策):更小的块(200-300 token)效果好。长解释性内容(教程、手册):更大的块(500-800 token)能保留更多上下文。

技巧 3:不同文档类型用不同策略 你不必对一切都用同一策略。手册用递归,FAQ 用命题级——因为每条问答是原子化的。

技巧 4:给块加上下文头(context header) 一个来自 Anthropic「Contextual Retrieval」研究的技巧:在嵌入前,给每个块前置一个文档级摘要与章节标题。这对措辞含糊(「it」「this」「the system」)却缺上下文的块,能大幅提升检索。

def add_context_header(chunk: Document, doc_title: str, section: str) -> Document:
    header = f"Document: {doc_title}\nSection: {section}\n\n"
    chunk.page_content = header + chunk.page_content
    return chunk

总结

策略速度成本质量何时使用
Fixed-size最快免费绝不用在生产
Recursive免费默认起点
Semantic更好密集的领域特定内容
Proposition最慢中等最好高精度关键系统

从递归开始。测量。只有当你的 RAGAS 分数证明增加的复杂度与成本合理时,才升级。

#AI#tutorial#superml#rag#chunking#embedding#retrieval

有类似的场景想落地?聊聊看。