RAG 文本分块策略总结:固定分块、语义分块、结构分块与父子分块
RAG 文本分块策略总结:从固定分块到语义分块、结构分块与父子分块
本文只聚焦 RAG 数据准备阶段中的 文本分块 Chunking。
目标是理解:为什么要分块、有哪些分块策略、每种策略如何工作、输出结果会是什么样、实际项目该怎么选。
目录
- 为什么 RAG 需要文本分块
- chunk_size 和 chunk_overlap 的意义
- 固定大小分块:CharacterTextSplitter
- 递归分块:RecursiveCharacterTextSplitter
- 语义分块:SemanticChunker
- 语义分块后的超长 chunk 如何处理
- 基于文档结构的分块:MarkdownHeaderTextSplitter
- 结构分块 + 递归分块组合
- 父子分块:小块检索,大块返回
- Unstructured 中基于文档元素的分块
- LlamaIndex 中基于 Node 的分块
- 各种分块策略对比
- 实战场景下如何选择分块策略
- 最佳实践总结
1. 为什么 RAG 需要文本分块
RAG 的核心流程可以理解为:
原始文档
↓
文本分块
↓
每个 chunk 做 embedding
↓
存入向量数据库
↓
用户问题向量化
↓
检索最相关的 chunk
↓
交给 LLM 生成答案
文本分块的本质是:
把一整篇长文档拆成很多较小的“知识卡片”。
如果不分块,直接把整篇文档作为一个整体,会有几个明显问题:
-
Embedding 模型有输入长度限制
文本太长会被截断,导致后面的内容丢失。 -
LLM 有上下文窗口限制
检索出来的内容最终要塞进 Prompt,太长会放不进去。 -
整篇文档一个向量太粗
一篇文章可能同时讲很多主题,如果只生成一个向量,这个向量会表达整篇文章的平均语义,无法精准匹配用户的具体问题。 -
分块能提高检索精度
用户问“RAG 的工作流程是什么?”,系统可以直接找到讲“工作流程”的 chunk,而不是把整篇文章都拿出来。
可以用一个非常直观的比喻理解:
不分块:把整本书直接丢给 AI,让它自己翻。
分块:先把书整理成一张张知识卡片,问什么就找相关卡片。
2. chunk_size 和 chunk_overlap 的意义
在很多分块器里,最常见的两个参数是:
chunk_size = 500
chunk_overlap = 100
2.1 chunk_size
chunk_size 表示每个文本块的目标大小。
比如:
chunk_size=200
可以理解为:
尽量让每个 chunk 控制在 200 个字符左右。
chunk_size 太小
优点:
检索更精准,每个 chunk 主题更集中。
缺点:
上下文容易碎,LLM 可能只看到半句话或半个知识点。
chunk_size 太大
优点:
上下文更完整。
缺点:
检索变粗,chunk 里可能混入多个主题,向量语义会被稀释。
2.2 chunk_overlap
chunk_overlap 表示相邻两个 chunk 之间保留多少重复内容。
例如:
chunk_size=100
chunk_overlap=20
大致效果是:
chunk 1:第 0 ~ 100 个字符
chunk 2:第 80 ~ 180 个字符
chunk 3:第 160 ~ 260 个字符
为什么需要 overlap?
因为文本可能刚好在边界处被切断。重叠一部分内容,可以让相邻 chunk 之间保留上下文连续性。
例子
原文:
RAG 包含检索、增强和生成三个阶段。检索阶段会先把用户问题转换成向量,然后在向量数据库中查找相似内容。
如果没有 overlap,可能切成:
chunk 1:
RAG 包含检索、增强和生成三个阶段。检索阶段会先把用户问题
chunk 2:
转换成向量,然后在向量数据库中查找相似内容。
chunk 2 单独看,会缺少“谁转换成向量”。
如果有 overlap,可能是:
chunk 1:
RAG 包含检索、增强和生成三个阶段。检索阶段会先把用户问题
chunk 2:
先把用户问题转换成向量,然后在向量数据库中查找相似内容。
这样 chunk 2 的语义更完整。
3. 固定大小分块:CharacterTextSplitter
固定大小分块是最简单的一类分块策略。
它的思路是:
不管文本内容是什么意思,每隔固定长度切一刀。
可以理解成:
拿尺子切文章。
3.1 示例代码
from langchain.text_splitter import CharacterTextSplitter
from langchain_community.document_loaders import TextLoader
loader = TextLoader("../../data/C2/txt/蜂医.txt")
docs = loader.load()
text_splitter = CharacterTextSplitter(
chunk_size=200,
chunk_overlap=10
)
chunks = text_splitter.split_documents(docs)
print(f"文本被切分为 {len(chunks)} 个块")
for i, chunk in enumerate(chunks[:5]):
print("=" * 60)
print(f"块 {i+1},长度:{len(chunk.page_content)}")
print(chunk.page_content)
3.2 数据分块例子
原始文本:
RAG 是检索增强生成技术。它会先从外部知识库中检索相关资料,然后把这些资料和用户问题一起放入提示词中,最后由大语言模型生成答案。RAG 的优势是可以减少幻觉、支持知识更新,并提升回答的可解释性。
假设固定 chunk_size=45,不考虑语义,可能得到:
chunk 1:
RAG 是检索增强生成技术。它会先从外部知识库中检索相关资料,然后
chunk 2:
把这些资料和用户问题一起放入提示词中,最后由大语言模型生成答案。
chunk 3:
RAG 的优势是可以减少幻觉、支持知识更新,并提升回答的可解释性。
如果切分位置不巧,可能出现更糟糕的结果:
chunk 1:
RAG 是检索增强生成技术。它会先从外部知识库中检索相关
chunk 2:
资料,然后把这些资料和用户问题一起放入提示词中,最后由大语言模型
chunk 3:
生成答案。RAG 的优势是可以减少幻觉、支持知识更新,并提升回答的可解释性。
你会发现:
“检索相关资料”被切开了;
“大语言模型生成答案”也被切开了。
这就是固定大小分块的缺点。
3.3 优缺点
优点
缺点
4. 递归分块:RecursiveCharacterTextSplitter
递归分块是实际 RAG 中非常常用的策略。
它不是简单按字数切,而是:
优先按照自然边界切,实在切不开时才按字符强制切。
自然边界通常包括:
段落
换行
句号
问号
感叹号
空格
字符
4.1 示例代码
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=100,
separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""]
)
chunks = text_splitter.split_documents(docs)
4.2 数据分块例子
原始文本:
RAG 是检索增强生成技术。它会先从外部知识库中检索相关资料。然后把这些资料和用户问题一起放入提示词中。最后由大语言模型生成答案。
RAG 的优势是可以减少幻觉。它还支持知识实时更新。对于企业知识库问答来说,RAG 是一种非常常见的方案。
如果使用固定大小分块,可能会在任意位置切。
但是递归分块会优先按段落和句号切,可能得到:
chunk 1:
RAG 是检索增强生成技术。它会先从外部知识库中检索相关资料。然后把这些资料和用户问题一起放入提示词中。最后由大语言模型生成答案。
chunk 2:
RAG 的优势是可以减少幻觉。它还支持知识实时更新。对于企业知识库问答来说,RAG 是一种非常常见的方案。
如果 chunk 1 仍然太长,它会继续尝试按句号切:
chunk 1:
RAG 是检索增强生成技术。它会先从外部知识库中检索相关资料。
chunk 2:
然后把这些资料和用户问题一起放入提示词中。最后由大语言模型生成答案。
它的核心优势是:
尽量不要把一句话从中间切断。
4.3 和固定分块的区别
| 对比项 | 固定大小分块 | 递归分块 |
|---|---|---|
| 切分依据 | 字符数量 | 段落、换行、句子、字符 |
| 是否考虑自然边界 | 基本不考虑 | 会优先考虑 |
| 语义完整性 | 较差 | 更好 |
| 实战常用程度 | 一般 | 很常用 |
5. 语义分块:SemanticChunker
语义分块更加“智能”。
它的核心不是看长度,而是看:
文本的意思有没有发生明显变化。
可以理解成:
这一段还在讲同一个主题,就继续放一起;
突然换了主题,就在这里切开。
5.1 示例代码
import os
# os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
from langchain_experimental.text_splitter import SemanticChunker
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.document_loaders import TextLoader
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-small-zh-v1.5",
model_kwargs={"device": "cpu"},
encode_kwargs={"normalize_embeddings": True}
)
text_splitter = SemanticChunker(
embeddings,
breakpoint_threshold_type="percentile"
)
loader = TextLoader("../../data/C2/txt/蜂医.txt")
documents = loader.load()
docs = text_splitter.split_documents(documents)
5.2 语义分块的内部流程
它大致做这几步:
5.3 数据分块例子
原始文本:
蜜蜂是一种昆虫。蜜蜂会采花蜜。蜂群中有蜂王、工蜂和雄蜂。人工智能是一门研究机器智能的学科。大语言模型可以生成文本。RAG 可以让大模型结合外部知识回答问题。
先拆句:
句子1:蜜蜂是一种昆虫。
句子2:蜜蜂会采花蜜。
句子3:蜂群中有蜂王、工蜂和雄蜂。
句子4:人工智能是一门研究机器智能的学科。
句子5:大语言模型可以生成文本。
句子6:RAG 可以让大模型结合外部知识回答问题。
计算相邻句子的语义距离:
句子1 - 句子2:距离小,都在讲蜜蜂
句子2 - 句子3:距离小,都在讲蜜蜂
句子3 - 句子4:距离大,从蜜蜂跳到人工智能
句子4 - 句子5:距离小,都在讲 AI
句子5 - 句子6:距离小,都在讲大模型/RAG
最终切分结果:
chunk 1:
蜜蜂是一种昆虫。蜜蜂会采花蜜。蜂群中有蜂王、工蜂和雄蜂。
chunk 2:
人工智能是一门研究机器智能的学科。大语言模型可以生成文本。RAG 可以让大模型结合外部知识回答问题。
这就是语义分块的核心:
在“话题发生明显变化”的地方切开。
5.4 buffer_size 的意义
buffer_size 的作用是:
判断一个句子的语义时,不只看当前句子,还会带上前后几个句子。
例如:
句子1:RAG 会先检索外部知识。
句子2:它可以提高回答准确性。
如果单独看句子2:
它可以提高回答准确性。
不知道“它”是谁。
如果带上前一句,就知道“它”指的是 RAG。
所以 buffer_size 可以减少语义判断误差。
5.5 breakpoint_threshold_type 的几种方式
percentile
把所有语义距离排序,选出最大的那一批作为断点。
通俗理解:
差异最大的前 5% 位置,就认为是换话题了。
standard_deviation
计算所有距离的平均值和标准差。
如果某个距离明显大于平均水平,就认为是断点。
interquartile
用四分位距 IQR 判断异常值。
如果某个距离远远超过正常范围,就认为是断点。
gradient
不只看距离大小,还看距离变化率。
如果距离突然变大,就认为可能是断点。
5.6 优缺点
优点
缺点
6. 语义分块后的超长 chunk 如何处理
语义分块不保证每个 chunk 一定小。
因为它看的是:
语义有没有明显变化
不是:
长度有没有超过限制
如果一整节都在讲同一个主题,它可能会切出一个很大的 chunk。
6.1 示例
原始文本:
RAG 的工作流程包括检索、增强和生成。检索阶段会把用户问题转成向量。然后在向量数据库中查找相关文档。增强阶段会把问题和检索内容拼入 Prompt。生成阶段由大语言模型输出答案。这个流程可以减少幻觉并提升可解释性。除此之外,RAG 还可以支持知识库的实时更新。
因为这整段都在讲 RAG 工作流程,所以语义分块可能输出:
chunk 1:
RAG 的工作流程包括检索、增强和生成。检索阶段会把用户问题转成向量。然后在向量数据库中查找相关文档。增强阶段会把问题和检索内容拼入 Prompt。生成阶段由大语言模型输出答案。这个流程可以减少幻觉并提升可解释性。除此之外,RAG 还可以支持知识库的实时更新。
这个 chunk 语义完整,但可能太长。
6.2 解决方案:语义分块 + 递归分块兜底
推荐做法:
第一步:用 SemanticChunker 按语义切
第二步:检查每个 chunk 的长度
第三步:如果太长,再用 RecursiveCharacterTextSplitter 切小
示例代码:
from langchain_experimental.text_splitter import SemanticChunker
from langchain_text_splitters import RecursiveCharacterTextSplitter
semantic_splitter = SemanticChunker(
embeddings,
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=95
)
fallback_splitter = RecursiveCharacterTextSplitter(
chunk_size=800,
chunk_overlap=100,
separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""]
)
semantic_chunks = semantic_splitter.split_documents(documents)
final_chunks = []
for doc in semantic_chunks:
if len(doc.page_content) <= 800:
final_chunks.append(doc)
else:
smaller_chunks = fallback_splitter.split_documents([doc])
final_chunks.extend(smaller_chunks)
可以总结成一句话:
语义分块负责聪明地找边界;
递归分块负责兜底控制大小。
7. 基于文档结构的分块:MarkdownHeaderTextSplitter
对于 Markdown、HTML、LaTeX 这类有明显结构的文档,可以优先使用结构分块。
结构分块的核心是:
按标题、章节、标签等文档结构来切。
例如 Markdown:
# RAG 入门
## 工作流程
## 技术优势
## 局限性
这些标题本身就告诉我们文章的结构。
7.1 示例代码
from langchain_text_splitters import MarkdownHeaderTextSplitter
markdown_text = """
# RAG 入门
## 工作流程
RAG 包含检索、增强和生成三个阶段。
检索阶段会先把用户问题向量化。
增强阶段会把问题和检索内容拼接进 Prompt。
生成阶段由大语言模型输出答案。
## 技术优势
RAG 可以利用外部知识。
RAG 可以减少幻觉。
RAG 可以支持知识实时更新。
"""
headers_to_split_on = [
("#", "Header 1"),
("##", "Header 2"),
("###", "Header 3"),
]
markdown_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on
)
chunks = markdown_splitter.split_text(markdown_text)
7.2 数据分块例子
原始 Markdown:
# RAG 入门
## 工作流程
RAG 包含检索、增强和生成三个阶段。
检索阶段会先把用户问题向量化。
## 技术优势
RAG 可以利用外部知识。
RAG 可以减少幻觉。
输出结果可能是:
chunk 1 content:
RAG 包含检索、增强和生成三个阶段。
检索阶段会先把用户问题向量化。
chunk 1 metadata:
{
"Header 1": "RAG 入门",
"Header 2": "工作流程"
}
chunk 2 content:
RAG 可以利用外部知识。
RAG 可以减少幻觉。
chunk 2 metadata:
{
"Header 1": "RAG 入门",
"Header 2": "技术优势"
}
这里的 metadata 很重要。
它相当于 chunk 的“地址”:
RAG 入门 > 工作流程
RAG 入门 > 技术优势
7.3 优缺点
优点
缺点
8. 结构分块 + 递归分块组合
单纯按标题切,有时会导致某个章节太长。
所以更推荐:
先按标题切,保留结构;
再按大小切,控制长度。
8.1 示例代码
from langchain_text_splitters import MarkdownHeaderTextSplitter
from langchain_text_splitters import RecursiveCharacterTextSplitter
headers_to_split_on = [
("#", "Header 1"),
("##", "Header 2"),
("###", "Header 3"),
]
markdown_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on
)
header_chunks = markdown_splitter.split_text(markdown_text)
recursive_splitter = RecursiveCharacterTextSplitter(
chunk_size=200,
chunk_overlap=50
)
final_chunks = recursive_splitter.split_documents(header_chunks)
8.2 数据分块例子
原始 Markdown:
# RAG 入门
## 工作流程
RAG 包含检索、增强和生成三个阶段。
检索阶段会先把用户问题向量化。
然后在向量数据库中查找相似文档。
增强阶段会把问题和检索内容拼进 Prompt。
最后由大语言模型生成答案。
第一步按标题切:
大块 1:
Header 1 = RAG 入门
Header 2 = 工作流程
内容:
RAG 包含检索、增强和生成三个阶段。
检索阶段会先把用户问题向量化。
然后在向量数据库中查找相似文档。
增强阶段会把问题和检索内容拼进 Prompt。
最后由大语言模型生成答案。
如果这个大块太长,第二步递归切小:
chunk 1:
RAG 包含检索、增强和生成三个阶段。
检索阶段会先把用户问题向量化。
metadata:
{
"Header 1": "RAG 入门",
"Header 2": "工作流程"
}
chunk 2:
然后在向量数据库中查找相似文档。
增强阶段会把问题和检索内容拼进 Prompt。
最后由大语言模型生成答案。
metadata:
{
"Header 1": "RAG 入门",
"Header 2": "工作流程"
}
重点是:
虽然内容被进一步切小了,但标题 metadata 仍然保留。
9. 父子分块:小块检索,大块返回
父子分块是高质量 RAG 中非常常见的策略。
核心思想是:
小块负责找得准;
大块负责答得全。
也可以叫:
Parent-Child Chunking
小块检索,大块返回
检索粒度和回答粒度分离
9.1 普通分块的问题
只用小块
优点:
检索精准。
缺点:
上下文不完整。
只用大块
优点:
上下文完整。
缺点:
检索不够精准,向量语义容易变混。
父子分块就是为了解决这个矛盾。
9.2 数据分块例子
原始文本:
2.1 满足模型上下文限制
将文本分块的首要原因,是为了适应 RAG 系统中两个核心组件的硬性限制。
嵌入模型负责将文本块转换为向量。这类模型有严格的输入长度上限。如果文本块太长,超出 embedding 模型窗口,就会被截断,导致信息丢失。
大语言模型负责根据检索到的上下文生成答案。LLM 同样有上下文窗口限制。如果单个块过大,可能会导致只能容纳少数几个相关块。
父块 parent chunk:
Parent 1:
2.1 满足模型上下文限制
将文本分块的首要原因,是为了适应 RAG 系统中两个核心组件的硬性限制。
嵌入模型负责将文本块转换为向量。这类模型有严格的输入长度上限。如果文本块太长,超出 embedding 模型窗口,就会被截断,导致信息丢失。
大语言模型负责根据检索到的上下文生成答案。LLM 同样有上下文窗口限制。如果单个块过大,可能会导致只能容纳少数几个相关块。
子块 child chunks:
Child 1-1:
将文本分块的首要原因,是为了适应 RAG 系统中两个核心组件的硬性限制。
parent_id = Parent 1
Child 1-2:
嵌入模型负责将文本块转换为向量。这类模型有严格的输入长度上限。
parent_id = Parent 1
Child 1-3:
如果文本块太长,超出 embedding 模型窗口,就会被截断,导致信息丢失。
parent_id = Parent 1
Child 1-4:
大语言模型负责根据检索到的上下文生成答案。LLM 同样有上下文窗口限制。
parent_id = Parent 1
用户提问:
为什么 embedding 模型需要分块?
检索阶段:
系统检索到 Child 1-2 和 Child 1-3。
但最终不是把 child 给 LLM,而是根据 parent_id 找回:
Parent 1
然后把完整 parent 给 LLM 回答。
这样答案会更完整。
9.3 LangChain 示例
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain.storage import InMemoryStore
from langchain.retrievers import ParentDocumentRetriever
from langchain_chroma import Chroma
from langchain_huggingface import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-small-zh-v1.5",
model_kwargs={"device": "cpu"},
encode_kwargs={"normalize_embeddings": True}
)
vectorstore = Chroma(
collection_name="child_chunks",
embedding_function=embeddings
)
store = InMemoryStore()
parent_splitter = RecursiveCharacterTextSplitter(
chunk_size=1200,
chunk_overlap=200
)
child_splitter = RecursiveCharacterTextSplitter(
chunk_size=300,
chunk_overlap=50
)
retriever = ParentDocumentRetriever(
vectorstore=vectorstore,
docstore=store,
child_splitter=child_splitter,
parent_splitter=parent_splitter
)
retriever.add_documents(documents)
docs = retriever.invoke("为什么文本分块很重要?")
9.4 推荐参数
child chunk:200 ~ 500 字符
parent chunk:800 ~ 2000 字符
可以理解成:
child 小一些,用于精准检索;
parent 大一些,用于完整回答。
10. Unstructured 中基于文档元素的分块
Unstructured 的分块思想和普通 TextSplitter 不太一样。
它通常先做:
Partitioning:把 PDF、HTML、Word 等文档解析成元素。
元素可能包括:
Title
NarrativeText
ListItem
Table
Header
Footer
然后再做:
Chunking:把这些元素组合成 chunk。
10.1 basic 策略
basic 是默认方式。
它的逻辑是:
按文档顺序连续组合元素,直到达到
max_characters上限。
数据分块例子
Unstructured 解析后的元素:
Element 1 (Title):
RAG 工作流程
Element 2 (NarrativeText):
RAG 包含检索、增强和生成三个阶段。
Element 3 (ListItem):
检索:从知识库中获取相关信息。
Element 4 (ListItem):
增强:将查询和检索内容放入 Prompt。
Element 5 (ListItem):
生成:由大语言模型生成答案。
使用 basic 后可能组合成:
chunk 1:
RAG 工作流程
RAG 包含检索、增强和生成三个阶段。
检索:从知识库中获取相关信息。
增强:将查询和检索内容放入 Prompt。
生成:由大语言模型生成答案。
如果超过 max_characters,它会开启新的 chunk。
10.2 by_title 策略
by_title 会把 Title 元素视为新章节的开始。
也就是说:
遇到标题,就尽量从这里开始一个新 chunk。
数据分块例子
Unstructured 解析后的元素:
Element 1 (Title):
RAG 工作流程
Element 2 (NarrativeText):
RAG 包含检索、增强和生成三个阶段。
Element 3 (ListItem):
检索:从知识库中获取相关信息。
Element 4 (Title):
RAG 技术优势
Element 5 (NarrativeText):
RAG 可以减少幻觉,并支持知识实时更新。
使用 by_title 后可能得到:
chunk 1:
RAG 工作流程
RAG 包含检索、增强和生成三个阶段。
检索:从知识库中获取相关信息。
chunk 2:
RAG 技术优势
RAG 可以减少幻觉,并支持知识实时更新。
这样就不会把“工作流程”和“技术优势”混在同一个 chunk 里。
10.3 Unstructured 分块适合什么场景
适合:
PDF
HTML
Word
PPT
网页转 PDF
报告
论文
书籍
复杂版面文档
它的优势是:
先理解文档元素,再组合元素。
不是简单把纯文本按字符切开。
11. LlamaIndex 中基于 Node 的分块
LlamaIndex 里常用的概念不是 chunk,而是:
Node
可以理解为:
Node = LlamaIndex 里的文档片段对象。
LlamaIndex 的分块通常是通过:
Node Parser
完成的。
11.1 常规分块:SentenceSplitter
SentenceSplitter 会尽量按句子边界切。
数据分块例子
原文:
RAG 是检索增强生成技术。它会先检索外部知识。然后大模型根据检索结果生成答案。RAG 可以减少幻觉。
切分后可能是:
Node 1:
RAG 是检索增强生成技术。它会先检索外部知识。
Node 2:
然后大模型根据检索结果生成答案。RAG 可以减少幻觉。
11.2 TokenTextSplitter
TokenTextSplitter 按 token 数量控制 chunk 大小。
适合你想严格控制模型输入长度的场景。
数据分块例子
原文:
RAG 包含检索、增强和生成三个阶段。检索阶段负责查找外部知识,增强阶段负责构造 Prompt,生成阶段负责输出答案。
如果每个 Node 控制在较小 token 数,可能得到:
Node 1:
RAG 包含检索、增强和生成三个阶段。
Node 2:
检索阶段负责查找外部知识,增强阶段负责构造 Prompt。
Node 3:
生成阶段负责输出答案。
11.3 MarkdownNodeParser
MarkdownNodeParser 按 Markdown 结构切。
数据分块例子
原始 Markdown:
# RAG 入门
## 工作流程
RAG 包含检索、增强和生成。
## 技术优势
RAG 可以减少幻觉。
切分后可能得到:
Node 1:
内容:RAG 包含检索、增强和生成。
metadata:RAG 入门 > 工作流程
Node 2:
内容:RAG 可以减少幻觉。
metadata:RAG 入门 > 技术优势
11.4 SemanticSplitterNodeParser
它和 LangChain 的 SemanticChunker 类似。
核心是:
根据句子之间的语义距离决定在哪里切。
数据分块例子
原文:
RAG 是检索增强生成技术。它可以结合外部知识回答问题。Redis 是一种内存数据库。Redis 常用于缓存和分布式锁。
切分后可能得到:
Node 1:
RAG 是检索增强生成技术。它可以结合外部知识回答问题。
Node 2:
Redis 是一种内存数据库。Redis 常用于缓存和分布式锁。
因为从 RAG 跳到了 Redis,语义发生明显变化。
11.5 SentenceWindowNodeParser
这是一个非常适合 RAG 的策略。
它的核心是:
检索时用单句,回答时带上前后窗口。
数据分块例子
原文:
句子1:RAG 是检索增强生成技术。
句子2:它会先检索外部知识。
句子3:然后大模型根据检索结果生成答案。
句子4:这种方式可以减少幻觉。
生成的 Node 可能是:
Node text:
它会先检索外部知识。
metadata window:
RAG 是检索增强生成技术。
它会先检索外部知识。
然后大模型根据检索结果生成答案。
检索时:
用 Node text 做精准匹配。
回答时:
把 metadata window 中的上下文交给 LLM。
它的思想和父子分块类似:
小粒度负责精准检索;
大上下文负责完整回答。
11.6 CodeSplitter
CodeSplitter 适合代码文件。
它不是简单按字符切代码,而是尽量按类、函数、方法结构切。
数据分块例子
原始代码:
public class UserService {
public User getUserById(Long id) {
return userMapper.selectById(id);
}
public void updateUser(User user) {
userMapper.updateById(user);
}
}
普通固定分块可能会把一个方法从中间切断。
CodeSplitter 更可能得到:
Node 1:
public User getUserById(Long id) {
return userMapper.selectById(id);
}
Node 2:
public void updateUser(User user) {
userMapper.updateById(user);
}
这样更适合代码检索。
12. 各种分块策略对比
| 分块策略 | 核心思想 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|---|
| 固定大小分块 | 按字符数切 | 简单、快 | 死板、易切断语义 | Demo、日志、简单文本 |
| 递归分块 | 优先按段落/句子切 | 通用、稳定 | 不真正理解语义 | 大多数普通文本 |
| 语义分块 | 按语义变化切 | 主题更集中 | 慢、依赖 embedding、大小不稳定 | 主题变化明显的长文 |
| 语义 + 递归兜底 | 先按语义切,超长再递归切 | 兼顾语义和长度 | 实现稍复杂 | 高质量文本分块 |
| Markdown 结构分块 | 按标题层级切 | 保留文档结构和 metadata | 标题下内容可能太长 | Markdown、技术文档 |
| 结构 + 递归 | 先按标题切,再控制大小 | 保留结构且大小合理 | 两阶段处理 | 文档结构清晰的知识库 |
| 父子分块 | 小块检索,大块返回 | 检索准、回答全 | 实现更复杂 | 高质量 RAG |
| Unstructured basic | 连续组合元素 | 适合复杂文档解析结果 | 章节边界不一定清晰 | PDF、HTML、Word |
| Unstructured by_title | 遇到标题开新块 | 更符合章节结构 | 依赖 Title 识别准确性 | 报告、论文、书籍 |
| SentenceWindow | 单句检索,窗口回答 | 精准且上下文完整 | metadata 处理复杂 | LlamaIndex 高质量检索 |
| CodeSplitter | 按代码结构切 | 保留函数/类完整性 | 依赖语言解析支持 | 代码知识库 |
13. 实战场景下如何选择分块策略
普通中文长文本
推荐:
RecursiveCharacterTextSplitter
原因:
通用、稳定、简单,比固定分块更自然。
Markdown 技术文档
推荐:
MarkdownHeaderTextSplitter + RecursiveCharacterTextSplitter
原因:
先保留标题结构和 metadata,再控制 chunk 大小。
主题跳跃明显的文章
推荐:
SemanticChunker + RecursiveCharacterTextSplitter 兜底
原因:
先按语义边界切,超长块再二次切小。
企业知识库 / 高质量 RAG
推荐:
父子分块 Parent-Child Chunking
或者:
SentenceWindowNodeParser
原因:
小粒度检索更精准,大上下文返回更完整。
PDF / Word / HTML
推荐:
Unstructured partition + basic / by_title chunking
原因:
复杂文档需要先解析成 Title、NarrativeText、ListItem 等元素。
代码文件
推荐:
CodeSplitter
原因:
按函数、类、方法切分,比按字符切代码更合理。
14. 最佳实践总结
14.1 不要迷信单一分块策略
实际项目中常用的是组合策略:
结构分块 + 递归分块
语义分块 + 递归分块
小块检索 + 大块返回
Unstructured 元素解析 + by_title 分块
14.2 优先保证语义完整性
一个好的 chunk 应该尽量满足:
14.3 metadata 很重要
对于结构化文档,尽量保留 metadata:
文件名
章节标题
页码
标题路径
文档来源
metadata 就像 chunk 的地址,可以提高 RAG 的可解释性和可追踪性。
14.4 chunk_size 不是越小越好
小 chunk 虽然检索精准,但上下文容易不足。
大 chunk 虽然上下文完整,但检索容易变粗。
所以要根据数据类型调参。
一般可以从下面配置开始实验:
chunk_size=500
chunk_overlap=100
对于更长的技术文档,可以尝试:
chunk_size=800
chunk_overlap=150
14.5 高质量 RAG 推荐父子分块
如果你希望系统回答更稳定、更完整,可以考虑:
child chunk:用于向量检索
parent chunk:用于最终回答
一句话总结:
小块负责定位,大块负责解释。
15. 最终记忆口诀
可以用下面几句话记住所有分块策略:
固定分块:按尺子切,简单但死板。
递归分块:按段落和句子切,通用稳定。
语义分块:按意思变化切,更聪明但更慢。
结构分块:按标题目录切,适合 Markdown 和技术文档。
父子分块:小块负责找,大块负责答。
Unstructured:先识别文档元素,再组合成块。
LlamaIndex:先转成 Node,再通过 Node Parser 切分。
最终目标只有一个:
让每个 chunk 既能被模型完整处理,又能在检索时准确命中用户问题,并为 LLM 提供足够完整的上下文。

浙公网安备 33010602011771号