分块的两难困境
RAG 系统里有一个经典矛盾:
- Chunk 太小:向量匹配精准,但返回给 LLM 的内容是片段,缺乏上下文,无法完整回答问题
- Chunk 太大:内容完整,但语义太分散,embedding 质量下降,检索命中率降低
这不是调参能解决的问题,而是 Naive 分块的结构性缺陷。
小块适合检索,大块适合生成------这两个需求本来就是矛盾的,用同一个尺寸的 chunk 同时满足两者,必然顾此失彼。
本篇介绍两种突破这一困境的方案:
- Parent-Child Chunking:用小块做检索,命中后返回对应的大块
- Contextual Retrieval(Anthropic 方案):给每个 Chunk 加上文档上下文描述,让 embedding 更"聪明"
Parent-Child Chunking
核心思路
erlang
索引阶段:
父文档(800字)→ 存储在 docstore(InMemoryStore)
↓ 切割
子 Chunk(200字)→ 存入向量库
检索阶段:
query → 向量检索匹配子 Chunk(精准)
→ 找到子 Chunk 对应的父文档
→ 返回父文档给 LLM(完整)
检索用的是小 chunk,LLM 拿到的是大 chunk。两个需求,各自最优,互不干扰。
代码实现
LangChain 的 ParentDocumentRetriever 封装了这个逻辑:
python
from langchain_classic.retrievers import ParentDocumentRetriever
from langchain_classic.storage import InMemoryStore
from langchain_text_splitters import RecursiveCharacterTextSplitter
child_splitter = RecursiveCharacterTextSplitter(
chunk_size=200, # 小块:用于向量检索
chunk_overlap=20,
)
parent_splitter = RecursiveCharacterTextSplitter(
chunk_size=800, # 大块:命中后返回给 LLM
chunk_overlap=50,
)
vectorstore = Chroma(collection_name="parent_child", embedding_function=embeddings)
store = InMemoryStore() # 存储父文档的 docstore
retriever = ParentDocumentRetriever(
vectorstore=vectorstore,
docstore=store,
child_splitter=child_splitter,
parent_splitter=parent_splitter,
search_kwargs={"k": 4},
)
retriever.add_documents(parent_docs)
调用时和普通 retriever 完全一样,框架自动处理子→父的映射:
python
docs = retriever.invoke("中文场景用哪个 Embedding 模型?")
# 返回的是 800 字的父文档,而不是 200 字的子 chunk
Contextual Retrieval
核心思路
Anthropic 在 2024 年发布的 Contextual Retrieval 方案,解决的是另一个问题:Chunk 孤立后丢失了它在文档中的位置信息。
举个例子。假设文档里有这样一段:
"该方法与前文提到的方式相比,准确率提升了 12%。"
切成 chunk 之后,"前文提到的方式"是什么完全丢失了。向量 embedding 只看到这一段,完全不知道上下文,语义严重缺失。
Contextual Retrieval 的做法:用 LLM 为每个 Chunk 生成一段上下文描述,拼接到 chunk 前面,再做 embedding:
arduino
原始 Chunk:
"该方法与前文提到的方式相比,准确率提升了 12%。"
加上上下文描述后:
"本段描述的是 RAG 混合检索策略与传统向量检索的性能对比,属于文章的实验结果部分。
该方法与前文提到的方式相比,准确率提升了 12%。"
embedding 看到的信息更完整,检索质量自然更高。
Prompt 设计
python
CONTEXT_PROMPT = ChatPromptTemplate.from_messages([
("system", "你是一个文档分析助手。"),
("human",
"以下是一篇完整文档:\n\n<document>\n{doc_content}\n</document>\n\n"
"以下是文档中的一个片段:\n\n<chunk>\n{chunk_content}\n</chunk>\n\n"
"请用 1-2 句话概括这个片段在整篇文档中的作用和背景,"
"帮助理解该片段的含义。只输出描述文字,不要加任何前缀。"),
])
代码实现
python
context_chain = CONTEXT_PROMPT | llm | StrOutputParser()
docs = []
for item in raw_data:
full_content = f"标题:{item['title']}\n{item['content']}"
chunks = splitter.split_text(full_content)
for chunk in chunks:
# 为每个 chunk 生成上下文描述
context_desc = context_chain.invoke({
"doc_content": full_content,
"chunk_content": chunk,
})
# 拼接后作为实际 embedding 内容
enriched_content = f"{context_desc}\n\n{chunk}"
docs.append(Document(page_content=enriched_content, ...))
注意: 每个 chunk 需要单独调用一次 LLM,索引阶段成本比 Naive 高。知识库 8 篇文档约 30-40 个 chunk,开销可接受;超大知识库需要考虑批量处理和成本控制。
实验设计
复用知识库(8 篇 RAG 技术文档)和测试集(8 条问题),对比三种策略:
| 策略 | 检索单元 | 返回给 LLM |
|---|---|---|
| Naive Chunking | 512 字 chunk | 512 字 chunk |
| Parent-Child | 200 字子 chunk(检索) | 800 字父文档 |
| Contextual Retrieval | 512 字 + 上下文描述(检索) | 512 字 + 上下文描述 |
核心关注指标:context_recall(完整信息是否被召回)和 context_precision(排序是否准确)。
实验结果
diff
======================================================================
RAGAS 指标对比(三种分块策略)
======================================================================
指标 Naive Parent-Child Contextual
────────────────────────────────────────────────────────────
context_recall 0.625 0.875 ◀ 0.875 ◀
context_precision 0.583 0.938 ◀ 0.736
faithfulness 0.846 0.969 ◀ 0.981 ◀
answer_relevancy 0.406 0.454 0.480 ◀
======================================================================
数字解读:
-
context_recall:Naive 0.625 → Parent-Child / Contextual 均达 0.875(+0.250) 最显著的提升。Naive 分块时,一个完整的概念被切成多个小 chunk,top-4 未必能把所有相关 chunk 都捞出来;Parent-Child 返回大块,信息更完整;Contextual Retrieval 的语义增强让检索更精准,也达到相同效果。
-
context_precision:Naive 0.583 → Parent-Child 0.938(+0.355) Parent-Child 的排序质量提升极为显著。原因:小块 embedding 语义更聚焦,相关文档能稳定排在前面;Contextual Retrieval 也有提升(0.736),但不如 Parent-Child 明显。
-
faithfulness:Naive 0.846 → Parent-Child 0.969 / Contextual 0.981 上下文质量改善后,LLM 幻觉率随之下降。三个核心指标的联动效应再次印证:排序和召回的改善会传递到生成质量上。
-
answer_relevancy:三者相近,Contextual 略优(0.480 vs Naive 0.406) 回答直接性有小幅改善,整体变化不大。
两种方案的适用场景
| 维度 | Parent-Child | Contextual Retrieval |
|---|---|---|
| 解决的问题 | 小块检索 vs 大块返回的矛盾 | Chunk 孤立导致语义缺失 |
| 索引成本 | 低(只需多存一份父文档) | 高(每个 chunk 调用一次 LLM) |
| 查询延迟 | 与 Naive 相同 | 与 Naive 相同(成本在索引阶段) |
| 效果优势 | context_precision 提升更显著 | 对语义复杂文档效果更好 |
| 适合场景 | 通用场景,知识库体量任意 | 文档逻辑性强、段落间依赖多 |
| 不适合场景 | 文档本身没有层级结构 | 超大知识库(索引成本高) |
实用建议:
- 大多数场景先试 Parent-Child,成本低,提升明显
- 文档类型为技术手册、学术论文、长篇报告等,段落间依赖强,可以叠加 Contextual Retrieval
- 两者也可以组合:Parent-Child + Contextual(子 chunk 加上上下文描述),效果会更好,但索引成本更高
完整代码
代码已开源:
核心文件:
advanced_chunking.py--- 三种分块策略的完整对比实验
运行方式:
bash
git clone https://github.com/chendongqi/llm-in-action
cd 12-advanced-chunking
cp .env.example .env # 填入 Embedding API Key 和 LLM API Key
pip install -r requirements.txt
python advanced_chunking.py
小结
本文通过代码实验对比了三种分块策略:
- Naive Chunking------简单直接,但小块检索和大块生成的矛盾无法解决,context_recall 只有 0.625
- Parent-Child------用小块检索、大块返回,context_recall 和 context_precision 双双大幅提升,是最具性价比的升级方案
- Contextual Retrieval------LLM 为每个 chunk 生成上下文描述,从 embedding 层面提升语义质量,对逻辑密集型文档效果尤佳
核心结论:分块不只是切割,更是信息组织策略。检索时需要什么(精准匹配),生成时需要什么(完整上下文),这两个问题要分开回答,而不是强行用同一个 chunk 大小折中。