RAG-01数据切分-Chunking-centric-RAG

本文属于 RAG 工程框架中的「1 数据接入与文档切分」环节,聚焦「Chunking centric RAG」方法。可先阅读 RAG-00.方法概述 再进入本篇。

原理

优先优化“文档如何切块”,再做索引与检索。

1
2
3
4
5
6
7
8
9
flowchart LR
D[原始文档] --> P[结构解析]
P --> C1[固定窗口切分]
P --> C2[语义切分]
P --> C3[层级切分]
C1 --> Eval[离线评估]
C2 --> Eval
C3 --> Eval
Eval --> Best[最佳切分策略]

优缺。

  • 优点:低成本提升召回相关性;对所有 RAG 都有效。
  • 缺点:需要领域样本反复试验。

性能/资源

  • 离线迭代成本中等。
  • 在线几乎无额外成本。

应用场景

  • 文档结构稳定(规章、SOP、技术文档)体系。
  • 作为 RAG 质量优化第一步。

统一合成数据示例

输入数据片段

1
2
3
4
5
{
"doc_id": "D01",
"title": "差旅报销制度 v2",
"text": "机票报销上限 2000 元,需在出差结束后 30 天内提交。"
}

中间结果(不同切分策略对比 + 离线指标)

1
2
3
4
5
6
[
{"strategy": "fixed_80_overlap_0", "chunk_id": "D01-C1", "text": "机票报销上限 2000 元"},
{"strategy": "fixed_80_overlap_0", "chunk_id": "D01-C2", "text": "需在出差结束后 30 天内提交"},
{"strategy": "fixed_200_overlap_40", "chunk_id": "D01-C1", "text": "机票报销上限 2000 元,需在出差结束后 30 天内提交。"},
{"eval_query": "报销最晚什么时候交?", "metric": "recall@5", "fixed_80_overlap_0": 0.62, "fixed_200_overlap_40": 0.88}
]

要点:短窗口把「上限」与「30 天」拆进不同块,多约束问句容易只命中一半;带 overlap 的较大窗口更贴合「切分-centric」要优化的对象——语义完整性与召回边界的折中

最终输出示例(给下游索引)

1
2
3
4
5
6
7
{
"selected_strategy": "fixed_200_overlap_40",
"reason": "同查询集上 recall@5 更高且块数更少",
"chunks": [
{"chunk_id": "D01-C1", "doc_id": "D01", "text": "机票报销上限 2000 元,需在出差结束后 30 天内提交。", "metadata": {"section": "报销标准"}}
]
}

原始发表与工程实现

  • 代表性原始发表:TextTiling (Hearst, 1997)。
  • 核心解决问题:解决文档切分边界与语义完整性冲突。
  • 成熟实现工具:langchain-text-splitters, llama-index, unstructured。

详细原理拆解

  • chunk_size 与 overlap 联合优化,目标是最大化 recall@k 并控制 token 成本。
  • 典型实现可拆为:输入预处理 -> 方法核心计算 -> 候选/证据构建 -> 生成与引用。
  • 工程调优重点:质量(准确率/引用率)与成本(时延/token)的联合优化。
1
2
3
4
5
flowchart LR
In[输入 Query 与知识] --> Core[方法核心计算]
Core --> Rank[匹配/路由/排序]
Rank --> Build[证据组装]
Build --> Out[答案与引用]

工程落地扩展示例

伪代码

1
2
3
4
5
6
7
8
9
10
11
def chunking_centric_build(docs, strategies, eval_queries, embedder):
"""离线:多策略切块 → 建临时索引 → 用同一批查询算 recall@k,再选定策略写正式索引。"""
best = None
for name, splitter in strategies.items():
chunks = [splitter.split(d) for d in docs]
index = build_vector_index(chunks, embedder)
recall = offline_recall_at_k(eval_queries, index, k=5)
if best is None or recall > best["recall_at_5"]:
best = {"strategy": name, "recall_at_5": recall, "chunks": chunks}
persist_chunks_and_index(best["chunks"], embedder)
return best

参数示例

1
2
3
4
5
6
chunk_strategies:
fixed_80_overlap_0: {chunk_size: 80, overlap: 0}
fixed_200_overlap_40: {chunk_size: 200, overlap: 40}
eval_queries_file: dev/travel_policy_queries.jsonl
metric_primary: recall_at_5
embedder: bge-m3

常见失败案例

  • 失败模式 1:只在单一查询上肉眼看切块,未用离线 recall@k,上线后复杂问句仍漏约束。
  • 失败模式 2:overlap 过大导致块近乎重复,索引膨胀、检索变慢,收益不成正比。
  • 失败模式 3:切分与领域强相关(表格/条款),一套参数打全部文档,子语料质量差。

Demo 数据带入计算示例

1
2
3
4
评测问句 q:「报销最晚提交时间?」需同时命中「30 天」约束。
fixed_80_overlap_0:含「30 天」的块排名第 7 → recall@5=0
fixed_200_overlap_40:同约束在同一 chunk 内 → rank=2 → recall@5=1
故选 fixed_200_overlap_40 作为上线切分策略。
-------------本文结束感谢您的阅读-------------