别再无脑 RAG 了!11 种分块策略我只留了 3 种(附实测数据)
别再无脑 RAG 了!11 种分块策略我只留了 3 种(附实测数据)
RAG(检索增强生成)你肯定听过。但你知道吗?同样的文档、同样的 Embedding 模型、同样的 LLM,换一种分块策略,回答准确率能差 40%。
我信了网上"直接按 500 token 切"的教程,做出来的 RAG 系统被用户骂"答非所问"。后来花了一周时间测了 11 种分块策略,最终只留了 3 种。
这篇文章就是我的实测过程和最终选择。
为什么分块策略这么重要
RAG 的流程是:用户提问 → 检索相关文档 → 把文档喂给 LLM → LLM 生成回答。
问题出在"检索相关文档"这一步。如果你的文档切得不好——
# 举个例子:一段关于"JWT 认证流程"的文档
原始文档:
"JWT 认证分为三个步骤。第一步,用户提交用户名和密码。
第二步,服务端验证通过后生成 JWT Token,包含 Header、
Payload、Signature 三部分。第三步,客户端将 Token 存储
在 localStorage 中,后续请求在 Authorization 头中携带。"
如果按 50 字硬切,可能变成:
chunk1: "JWT 认证分为三个步骤。第一步,用户提交用户名和密码。"
chunk2: "第二步,服务端验证通过后生成 JWT Token,包含 Header、"
chunk3: "Payload、Signature 三部分。第三步,客户端将 Token 存储"
用户问"JWT 的 Payload 是什么"→ 检索到 chunk2 和 chunk3
→ 两个 chunk 都不完整,LLM 回答不出。
11 种分块策略速览
我测了以下 11 种,按复杂度从低到高排列:
#策略原理复杂度 1固定长度切分按字符/token 数硬切⭐ 2按段落切分以 `\n\n` 为分隔符⭐ 3按句子切分以句号为分隔符⭐ 4递归字符切分先按段落,再按句子,再按字符⭐⭐ 5按 Markdown 标题切分以 `#` `##` 为分隔符⭐⭐ 6按语义相似度切分相邻句子语义变化大时切一刀⭐⭐⭐ 7按代码结构切分识别函数/类边界⭐⭐⭐ 8滑动窗口重叠固定长度 + 重叠区域⭐⭐ 9按文档结构切分识别目录、章节、附录⭐⭐⭐ 10混合策略结构 + 语义 + 重叠⭐⭐⭐⭐ 11Agentic Chunking用 LLM 判断哪里该切⭐⭐⭐⭐⭐实测:我怎么评估的
测试数据集:我们内部的安全知识库,约 200 篇文档(技术文档 + 操作手册 + 历史告警记录),总计约 50 万字。
评估方法:准备了 50 个测试问题,每个问题有标准答案。用不同分块策略构建 RAG 系统,看回答准确率。
# 评估脚本核心逻辑
import json
from langchain.text_splitter import (
RecursiveCharacterTextSplitter,
MarkdownHeaderTextSplitter,
CharacterTextSplitter
)
def evaluate_strategy(chunks, test_questions):
"""评估某个分块策略的效果"""
correct = 0
total = len(test_questions)
for q in test_questions:
# 检索最相关的 3 个 chunk
relevant_chunks = retrieve_top_k(q["question"], chunks, k=3)
# 拼成 context 喂给 LLM
context = "\n\n".join(relevant_chunks)
answer = llm_answer(q["question"], context)
# 检查答案是否包含关键信息
if q["key_point"].lower() in answer.lower():
correct += 1
return correct / total # 准确率
实测结果:3 个赢家
冠军:递归字符切分 + 滑动窗口重叠
准确率:82%
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 每个 chunk 最大 500 字符
chunk_overlap=100, # 重叠 100 字符(约 2-3 句话)
separators=["\n\n", "\n", "。", ",", " ", ""]
# 先按段落切,段落太长按换行切,再不行按句号切……
)
chunks = splitter.split_text(document)
为什么好:separators 参数让它优先在"自然断点"(段落、句子)处切分,不会把一句话切成两半。chunk_overlap 确保相邻 chunk 有重叠,关键信息不会因为恰好落在边界上而丢失。
一句话总结:90% 的场景用这个就够了,别折腾花哨的。
亚军:按 Markdown 标题切分
准确率:79%
from langchain.text_splitter import MarkdownHeaderTextSplitter
headers_to_split_on = [
("#", "标题"),
("##", "二级标题"),
("###", "三级标题"),
]
markdown_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on
)
# 对每篇 Markdown 文档
md_header_splits = markdown_splitter.split_text(markdown_content)
为什么好:技术文档天然有标题结构,按标题切分出来的 chunk 语义完整——一个 ## 登录流程 下面的内容,肯定都是讲登录的。
适用场景:你的文档是 Markdown 或 RST 格式(技术博客、Wiki、README)。
一句话总结:文档有标题结构的,直接用这个,效果比硬切好很多。
季军:按语义相似度切分
准确率:77%
# 需要安装: pip install semantic-text-splitter
from semantic_text_splitter import TextSplitter
from tokenizers import Tokenizer
# 用 token 计数而不是字符数
tokenizer = Tokenizer.from_pretrained("bert-base-uncased")
splitter = TextSplitter.from_huggingface_tokenizer(
tokenizer,
500, # 最大 token 数
overlap=50
)
chunks = splitter.chunks(document)
为什么好:当相邻两句话的语义突然变化时(比如从"安装步骤"跳到"配置参数"),它会在这里切一刀。切出来的每个 chunk 都是"一个话题"。
缺点:慢。比递归切分慢 10 倍左右,因为要做 Embedding 计算。50 万字的文档集大概要跑 20 分钟。
一句话总结:追求极致准确率可以用,但性价比不如前两个。
那 8 种被淘汰的,为什么不行
策略淘汰原因 固定长度硬切经常把句子切断,准确率只有 58% 按段落切分段落长度差异太大(有的 20 字,有的 2000 字) 按句子切分切得太碎,丢失上下文,LLM 拿到的是碎片 按代码结构切分只对代码有效,对文档反而更差 按文档结构切分依赖文档格式,格式不规范就崩了 Agentic Chunking太慢了!200 篇文档要跑 3 小时,成本约 15 元 滑动窗口(无递归)固定窗口经常在句子中间切,重叠也没用 混合策略调参太复杂,4 个超参数排列组合,收益不大最终方案:两层策略
我最终用的是两层策略——先用 Markdown 标题切大块,再用递归字符切小块:
def two_level_split(markdown_content):
"""两层分块策略"""
# 第一层:按标题切分
headers = [("#", "h1"), ("##", "h2"), ("###", "h3")]
title_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers)
sections = title_splitter.split_text(markdown_content)
# 第二层:对超过 500 字的 section 做递归切分
recursive_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=100,
separators=["\n\n", "\n", "。", ",", " ", ""]
)
final_chunks = []
for section in sections:
if len(section.page_content) > 500:
sub_chunks = recursive_splitter.split_text(section.page_content)
final_chunks.extend(sub_chunks)
else:
final_chunks.append(section.page_content)
return final_chunks
准确率:85%,比单独用任何一种都高。
一句话总结
分块是 RAG 里最容易被忽视但影响最大的环节。别再无脑 500 token 硬切了——先用递归字符切分 + 滑动窗口重叠,文档有标题结构的再加一层 Markdown 切分,够用了。
如果你试了还是不行,那八成不是分块的问题——是 Embedding 模型或检索策略的问题。
关注「安全值班室」公众号
每天AI安全早报 + 实战攻防案例 + 网安学习路线连载
浙公网安备 33010602011771号