
我花了1个月搭了一套自建 RAG 知识库,240 篇技术文章灌进去,向量库跑起来一切正常。
然后我用"Redis Cluster 怎么扩容"搜了一下。
返回的结果前三篇分别是:《Redis 为什么快》、《Go 语言的并发模型》、《我们团队的代码规范》。每一篇回答了我的问题。
问题不在 embedding 模型,不在向量数据库。问题出在一个大多数教程一笔带过、很多工程师压根没认真对待的环节——分块策略。
同样的向量数据库、同样的 embedding 模型,分块策略不同,检索命中率能差 20% 以上。这不是夸大,是我实打实测出来的。

三种分块策略核心差异速览
不想看完整分析的话,这张表够用了:
你的内容类型 | 推荐策略 | 预期命中率 | 一句话理由 |
|---|---|---|---|
技术教程、专栏、架构文档 | 策略 A:结构感知分块 | ~78% | 保留标题层级,chunk 自带上下文 |
设计模式、最佳实践、精炼总结 | 策略 B:模板/段落分块 | ~72% | 轻量快速,适合内容本身已结构化 |
FAQ、社区问答、踩坑笔记 | 策略 C:Q&A 对分块 | 规划中 | 保证问题和答案不被拆散 |
如果你用的还是固定 500 字符硬切,命中率大概率在 60% 左右徘徊——这跟没做 RAG 区别不大。
上面这张表看起来简洁,但背后有一个很多人不知道的事实:策略 A 和策略 B 本质上是在「内容入库时」就已经决定了检索质量的上下限。chunk 边界一旦写入索引,之后所有下游操作——换 embedding 模型、加 reranker、调检索参数——都只能在这组已切好的候选上重新排序。如果你的 chunk 本身就是残缺的(一句话被腰斩、代码块被截断、Q&A 被拆散),后面做再多优化也是白搭。
换句话说:分块定义了「检索对象是什么」,embedding 只决定「怎么找到它」。想通这一层,你就知道为什么分块策略值得花时间细调。
如果你对自己的内容类型拿不准,直接看后面的详细对比。如果内容结构比较复杂——比如既有一堆 Markdown 教程又有一批 FAQ 问答——可以考虑进阶方案:Small-to-Big(父子文档)。用小块做精准召回(chunk_size=300-500),召回后自动扩展到父级大块喂给 LLM。社区 benchmark 显示这套方案的 recall 能达到 0.86,是目前生产环境实测的最优策略之一。
大部分 RAG 教程的流程是:装向量库 → 选 embedding 模型 → 调检索参数 → 上线。分块这一步,教程里通常就一行 chunk_size=500,随手一设就过了。
但实际上,分块决定了 embedding 向量的信息密度。
一个 chunk 太大(比如 2000 字符),包含多个不相关的技术概念,embedding 向量就会变成一个"平均脸"——什么都像,什么都检索不准。一个 chunk 太小(比如 200 字符),信息被切碎,检索时会召回一堆零散片段,LLM 得自己拼图。
说白了,分块策略在 RAG 系统里的地位相当于数据库的索引设计。索引没建对,换什么存储引擎都是白搭。
换一个角度想:你写技术文章的时候会先搭标题结构,读者阅读时会根据标题找自己关心的章节。那为什么给向量库灌内容的时候,要把标题结构扔掉、当成一坨平文本切?
这就是结构感知分块的核心思路。
还有一个反直觉的数据点:Adobe 在 2025 年做了一个大规模实验,在他们内部数千工程师使用的技术支持系统上对比了多种分块策略。结果让很多人意外——400-token 的固定分块(加 20% overlap)在某些场景下反而击败了语义分块和分层分块,准确率提升了 20%。这说明一个道理:分块策略没有银弹,内容类型决定一切。如果文档本身格式规整、段落清晰,简单的段落切分可能比复杂的语义分块更有效。
原理:按 Markdown 的标题层级(H2 → H3 → H4)逐级切分,保证每个 chunk 是一个完整的"小节",而不是一段被腰斩的文字。
关键点有两条:
Redis Cluster 集群 > 3.4.1 Redis Cluster 是什么。检索时不仅能匹配内容本身,还能利用标题路径做语义锚定。来看一下实现代码:
from langchain.text_splitter import MarkdownHeaderTextSplitter
headers_to_split_on = [
("##", "h2"),
("###", "h3"),
("####", "h4"),
]
splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on,
strip_headers=False, # 保留标题文本在 chunk 中
)
# 实际使用:读取 markdown 文件后切分
with open("redis-cluster-deep-dive.md", "r") as f:
markdown_content = f.read()
chunks = splitter.split_text(markdown_content)
for chunk in chunks:
# metadata 中保留了完整的标题路径
# 例如:{'h2': 'Redis Cluster 集群', 'h3': '3.4.1 Redis Cluster 是什么'}
print(f"标题路径: {chunk.metadata}")
print(f"内容长度: {len(chunk.page_content)} 字符")
print(f"内容预览: {chunk.page_content[:100]}...")
print("---")
但这只是第一步。MarkdownHeaderTextSplitter 按标题切完后,如果某个 H3 下的内容有 3000 多字怎么办?单个 chunk 直接塞进向量库,embedding 质量会严重下降。所以需要加一层多级回退:
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 多级回退拆分器:优先按 \n\n(段落)切,不行再按 \n(句子),最后按字符
fallback_splitter = RecursiveCharacterTextSplitter(
chunk_size=800,
chunk_overlap=80,
separators=["\n\n", "\n", "。", ".", " ", ""],
length_function=len,
)
# 对每个 header split 的结果再做尺寸检查
final_chunks = []
for chunk in header_chunks:
if len(chunk.page_content) <= 800:
# 内容在阈值内,直接保留
final_chunks.append(chunk)
else:
# 超过阈值,用回退拆分器继续切,但保留原始 metadata
sub_chunks = fallback_splitter.split_text(chunk.page_content)
for i, sub in enumerate(sub_chunks):
sub_chunk = type(chunk)(
page_content=sub,
metadata={**chunk.metadata, "sub_chunk_index": i}
)
final_chunks.append(sub_chunk)
print(f"最终生成 {len(final_chunks)} 个 chunk")
这套策略在我的 240 篇文章上实测下来,命中率稳定在 78% 左右。而且有个额外的好处:每个 chunk 自带标题路径,LLM 在生成回答时能明确知道信息来自哪篇文章的哪个章节,幻觉明显减少。
适用内容:技术教程、专栏文章、架构文档——任何有清晰标题层级的长文。
不适用:自然语言为主的随笔、没有标题结构的纯文本流水账。这种情况建议直接用策略 B。
原理:优先按 ## 标题切,如果单个 ## 下内容超过阈值,则按段落边界切分。本质上是一个"先尊重结构,再尊重语义边界"的思路。
策略 B 比策略 A 简单,但比固定字符硬切聪明得多。它的 max_size 我设成了 600 字符——比策略 A 的 800 更小,因为适用内容本身信息密度就高,不需要那么大的窗口。
实现代码:
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 针对已精炼的结构化内容
splitter = RecursiveCharacterTextSplitter(
chunk_size=600,
chunk_overlap=60,
separators=["## ", "\n\n", "\n", "。", " "],
length_function=len,
)
chunks = splitter.create_documents(
texts=[article_content],
metadatas=[{"source": "best-practices-microservices-2025.md"}]
)
对比策略 A 和策略 B 的差异,最核心的一点是:策略 B 不会主动构建标题层级路径。它把 ## 当作一个分隔标记,但如果内容本身没有 ## 标题(或者标题层级很浅),它会直接退化到按段落切分。
实测命中率在 72% 左右,比策略 A 低约 6 个百分点。这 6 个点的差距主要来自"缺乏标题上下文"——当用户搜索一个特定的技术概念时,策略 B 的 chunk 里可能没有足够的层次信息帮助 embedding 模型精准定位。
但策略 B 的优势在于快且省。不需要解析 Markdown 标题树,不需要构建 metadata 路径,对内容格式的要求也低得多。如果你的知识库内容本身就是短小精悍的结构化总结(比如设计模式清单、API 最佳实践卡片),策略 B 完全够用。
适用内容:日常摘要、设计模式文档、最佳实践列表——内容本身已经很精炼,不需要额外的结构解析。
这部分是我踩过的一个坑,还在填。
我的知识库里有一部分内容来自社区问答——读者问"Redis 哨兵模式和 Cluster 模式怎么选",我写了一篇详细的回复。这篇回复里既有问题又有答案,如果按标题切分或段落切分,问题和答案大概率会被拆到不同的 chunk 里。
后果就是:用户搜"哨兵模式和 Cluster 怎么选"时,向量库可能只召回答案的那一半 chunk——语义上匹配,但缺少了问题上下文,LLM 不知道这段话在回答什么。
策略 C 的思路很简单:一个问答对 = 一个 chunk。不按字数切,不按标题切,就按 Q&A 的边界切。
# 策略 C 示意代码(规划中,非生产级)
import re
def split_qa_pairs(text: str) -> list[dict]:
"""按 Q&A 模式切分文本"""
# 匹配 Q: 或 问: 开头的段落
pattern = r'(?:Q[::]|问[::]|#### Q\d+)(.*?)(?=Q[::]|问[::]|#### Q\d+|$)'
matches = re.findall(pattern, text, re.DOTALL)
qa_pairs = []
for match in matches:
parts = re.split(r'(?:A[::]|答[::])', match, maxsplit=1)
if len(parts) == 2:
qa_pairs.append({
"question": parts[0].strip(),
"answer": parts[1].strip(),
})
return qa_pairs
这个策略在 v3.0 里才规划落地,目前只覆盖了我知识库中约 5% 的内容(那批 FAQ 类文章)。但效果已经能看出来——Q&A 类内容的检索命中率提升非常明显,因为问题本身就是一个天然的检索 query,chunk 里同时包含问题和答案,embedding 向量能更好地捕捉这种"提问-回答"语义模式。
适用内容:FAQ 页面、社区问答、踩坑笔记——所有以"问题-回答"形式组织的内容。
说实话,我在分块策略上踩的坑,比在 embedding 模型选型上踩的还多。
v1.0:固定 500 字符硬切
这是绝大多数教程推荐的"最佳实践"——设个 chunk_size=500,overlap 给个 50,跑起来就行。
结果惨不忍睹。一段讲 Go 并发模型的中文段落,500 字符可能刚好卡在"goroutine 的调度基于 GMP 模型,G 代表"——后面就没了。G 代表什么?读者想知道,向量库也不知道。
更致命的是代码块被拦腰截断。一个完整的设计模式示例,前半段是问题描述,后半段是实现代码,被切到两个 chunk 里之后,两个 chunk 都变成了语义残片。
命中率 60% 出头——跟随机抽差不多。
v2.0:按 H2 标题切
意识到问题之后,我改成了按 H2 标题切分——每个大章节一个 chunk。
效果好了一些,但马上遇到新问题:有些 H2 下面的内容多达 3000+ 字。比如我有一篇讲 Redis 持久化的文章,H2"RDB 快照原理"下面光代码注释就有 1500 字。这么大一个 chunk 塞进向量库,embedding 向量的语义焦点完全散了。
v3.0:结构感知 + 多级回退
v3.0 就是现在这套方案——H2 → H3 → H4 → 段落 → 句子,逐级回退。每个 chunk 自带标题上下文,尺寸控制在 800 字符左右。
这套方案上线后,命中率稳定在 75%+,少数优化过的专题能达到 80% 以上。

结构感知分块的多级回退流程
扒完三种策略,我得出的结论是:参数(chunk_size、overlap)只是表象,核心原则决定了你的分块策略能不能用。
原则一:保持语义完整性
宁可 chunk 稍大,不要打断一个完整的技术概念。切在段落中间、切在代码块中间、切在"首先……其次……"的并列结构中间——这些都是致命伤。一段被腰斩的技术解释,embedding 质量远不如稍长但完整的段落。
原则二:保留上下文锚点
每个 chunk 要知道自己属于哪篇文章、哪个章节。这不是为了好看——当 LLM 同时拿到 5 个相关 chunk 时需要回答"这 5 段来自 3 篇不同的文章,它们之间是什么关系"时,上下文锚点是 LLM 拼接信息的唯一线索。
原则三:内容类型决定分块策略
不要一刀切。技术教程和 FAQ 问答天然需要不同的分块方式。我现在的做法是:在内容入库时就打上类型标签,分块器根据标签自动选择策略。
原则四:max_size 不是越短越好,也不是越长越好
太短丢失上下文(500 字以下的 chunk,embedding 质量下降明显),太长检索精度下降(超过 1200 字的 chunk,容易出现"什么都沾边但什么都不准"的问题)。800 字符是我反复实验后找到的甜点——当然,这个数字对英文内容可能不同,英文的分块甜点通常在 256-512 tokens 之间。
评估维度 | 策略 A:结构感知 | 策略 B:模板段落 | 策略 C:Q&A 对 |
|---|---|---|---|
检索命中率 | ★★★★★ (78%) | ★★★★ (72%) | 规划中 |
实现复杂度 | ★★★ (需解析标题树) | ★ (仅需配置拆分器) | ★★ |
内容格式要求 | 必须有 Markdown 标题 | 有段落结构即可 | 必须有 Q&A 格式 |
长文处理能力 | ★★★★★ (多级回退) | ★★★ (超大段落会退化) | ★★ (不适用长文) |
LLM 回答质量 | ★★★★★ (自带上下文) | ★★★ (上下文依赖检索) | ★★★★ (Q&A 语义完整) |
适用范围 | 80% 的常见内容 | 15%(精炼内容) | 5%(FAQ/问答) |
我的实际数据:240 篇文章产生了 5877 个 chunk,平均每篇约 24 个 chunk。策略 A 覆盖了其中 80% 的内容,策略 B 覆盖 15%,策略 C 只占了 5%(还在扩展中)。
有两点值得单独拉出来说——
第一,语义分块为什么不在我的策略列表里? 语义分块(按相邻句子 embedding 相似度的骤降点切分)听起来很高级,但社区真实 benchmark 给出了一个扎心的数据:它在某些测试集上以 50 倍的计算成本,只换来了和递归切分几乎持平的 recall(0.72 vs 0.74)。花 50 倍成本换不到 2 个点的提升,这性价比不适合大多数生产环境。除非你的文档是纯自然语言长文(法律合同、科研论文),语义分块才有明显优势——因为这类文档没有标题结构可依赖。
第二,overlap 可能被高估了。 几乎所有教程都在推荐设 10-20% 的 overlap,但我最近读到了一篇 2025 年的系统分析论文(arXiv:2601.14123),结论是:overlap 在 Natural Questions 这类数据集上没有显著收益,反而让索引体积膨胀了约 25%。我的经验是 10% 的 overlap 有个保底作用——防止关键句恰好落在边界上被截断——但不要无脑往上加。overlap 不是越大越好。
Q:chunk_overlap 设多少合适?
先说一个反常识的结论:有学术分析(arXiv 2025)表明 10-20% 的 overlap 在某些 benchmark 上没有显著收益,反而让索引体积膨胀了 25%。我的折中做法是 chunk_size 的 10%——chunk_size=800 时 overlap 设 80。这更多是一个"保底"策略,防止关键句恰好落在边界上被截断,但不用无脑往上加。如果你的文档本身就是按标题/段落组织的,overlap 甚至可以降到 5%。
Q:什么叫 Context Cliff?它和分块策略有什么关系?
Context Cliff 是一个 2025 年被反复验证的现象:LLM 在上下文超过约 2500-3000 tokens 后,对中间位置信息的提取能力会显著下降。这和分块策略直接相关——如果你为了"多给上下文"把 chunk 设得过大(>1500 tokens),或者一次召回太多 chunk 全塞给 LLM,反而会触发这个退化效应。正确的思路是:小块精准召回 + 只把最相关的 3-5 个 chunk 喂给 LLM。这也是为什么 Small-to-Big(父子文档)方案值得关注——它在检索阶段用小 chunk 保证精度,生成阶段才扩展到大 chunk,避开了 Context Cliff 的风险。
Q:中文和英文的分块策略有区别吗?
有。中文字符本身的信息密度比英文 token 低——一个中文字符通常对应 1.5-2 个 token,一个英文单词也是 1-2 个 token。所以中文的 chunk_size 可以设大一些(我用的 800 字符),英文通常在 256-512 tokens 之间。另外,中文分词对句子边界识别更敏感,分块器中的 separators 建议加入中文标点(。、!、?)。
Q:我的文章很短,每篇只有几百字,还需要分块吗?
如果单篇文章本身不超过 chunk_size 上限,可以整篇作为一个 chunk 入库,不需要额外拆分。但建议在 metadata 中保留文章标题,方便后续检索时做来源标注。
Q:三种策略能混用吗?
能,而且推荐混用。我的做法是在内容入库时,先判断文章类型标签,然后路由到对应的分块策略。比如一篇 Markdown 技术教程走策略 A,一篇设计模式清单走策略 B,一篇 FAQ 走策略 C。不要试图用一套参数覆盖所有内容——这是最常见的错误。
Q:怎么客观评估分块策略的效果?
我用的方法是用一组标准测试问题(覆盖不同内容类型),对每种策略分别评估 Top-3 检索命中率。注意要用真实用户会问的问题,而不是你自己写的问题——后者往往和你的 chunk 语义分布高度匹配,测出来的命中率不准。
“💬 你现在用的什么分块策略?
评论区说说,码哥猜选 A 和 C 的最多 😂
分块策略是 RAG 系统里最容易被低估、同时也是性价比最高的优化点。换 embedding 模型可能需要重新入库、重新调参,改向量数据库可能需要数据迁移,但优化分块策略只需要改几行 Python 代码——效果立竿见影。
如果你现在用的是固定字符硬切,建议优先迁移到策略 A(结构感知分块)。如果你的内容本身没有 Markdown 标题结构,策略 B(段落切分)是更好的起点。如果你是做 FAQ/客服知识库的,策略 C 的 Q&A 对思路值得一试。
说到底,分块策略的本质是让每一段文字在脱离原文后仍然能被理解。技术文章的标题结构是为人类阅读设计的,而分块策略的职责就是把这套结构翻译成向量库能理解的语言。
这篇文章里提到的方法和代码,我自己在生产环境的知识库上跑了几个月。实际效果比我预想的要好——在不需要换 embedding 模型、不需要加 reranker 的情况下,只靠优化分块策略就把命中率从 60% 提到了 78%。如果你的知识库还在为检索精度头疼,不妨先检查一下分块策略——大概率问题就出在这。
写完这篇,码哥自己又过了一遍自己的分块逻辑,发现还有几篇老文章的 chunk 是用 v1.0 策略切的——该重跑了。
下一篇打算拆 embedding 模型选型,bge-large、text2vec、m3e 这些到底哪个更适合中文技术内容,实测数据说话。
感兴趣的关注一下,不然算法不一定推给你。如果不星标,算法就不会主动推给你——错过几篇没关系,但有些内容正好是你当时需要的,那就不划算了。