【学习记录】RAG优化系列:切块策略深度解析------固定长度 vs 自适应标题(含AI评估与面试指南)
在 RAG 系统中,文本切块(Chunking) 是决定检索质量的第一步,也是最容易被忽视的优化点。切得太碎丢失上下文,切得太粗引入噪声。本文从原理、代码实现到 AI 评估,全面对比固定长度切块 和自适应标题切块两种策略,并附带面试常见问答。读完本文,你将能科学地选择分块策略,并量化评估其效果。
📌 目录
- 为什么切块策略很重要
- [固定长度切块(Fixed-size Chunking)](#固定长度切块(Fixed-size Chunking))
- [自适应标题切块(Adaptive Heading Chunking)](#自适应标题切块(Adaptive Heading Chunking))
- [AI 评估:用大模型给检索块打分](#AI 评估:用大模型给检索块打分)
- [面试官怎么问 & 怎么答](#面试官怎么问 & 怎么答)
- 总结与最佳实践
一、为什么切块策略很重要?
RAG 的核心流程:文档 → 切块 → 向量化 → 检索 → LLM 生成。切块处于最上游,其质量直接影响:
- 检索召回率:关键信息是否完整存在于某个块中。
- 答案忠实度:块内语义是否连贯,避免引入无关内容。
- 成本控制:块大小影响 embedding 和 LLM 的 token 消耗。
| 糟糕的切块 | 后果 |
|---|---|
| 块太小 | 信息碎片化,模型难以理解完整含义 |
| 块太大 | 包含大量噪声,检索精度下降,token 成本升高 |
| 切断语义单元 | 标题与正文分离,或打断关键句子 |
因此,选择合适的切块策略是 RAG 优化的首要任务。
二、固定长度切块(Fixed-size Chunking)
2.1 原理
将文本按固定的字符数或 token 数切分,相邻块之间保留一定的重叠(overlap)。这是一种与文档结构无关的通用方法。
数学表达:
- 设原始文本长度为
L,chunk_size = C,overlap = O(O < C)。 - 则块数为
⌈(L - C) / (C - O)⌉ + 1。
2.2 流程
原始文本
按 chunk_size 分割
保留 overlap 重叠
生成 Nodes
2.3 代码实现(LlamaIndex)
python
from llama_index.core.node_parser import SimpleNodeParser
from llama_index.core import Document
# 示例文本
doc_text = "这是一个很长的文档..." * 100
documents = [Document(text=doc_text)]
# 固定长度切块
parser = SimpleNodeParser.from_defaults(chunk_size=512, chunk_overlap=50)
nodes = parser.get_nodes_from_documents(documents)
print(f"切块数量: {len(nodes)}")
print(f"第一个块: {nodes[0].text[:100]}...")
手动实现(不依赖 LlamaIndex):
python
def fixed_chunking(text, chunk_size=512, overlap=50):
chunks = []
start = 0
while start < len(text):
end = min(start + chunk_size, len(text))
chunks.append(text[start:end])
start += chunk_size - overlap
return chunks
2.4 优缺点与复杂度
| 项目 | 描述 |
|---|---|
| ✅ 优点 | 实现简单,速度极快,与文档格式无关 |
| ❌ 缺点 | 可能切断句子、段落、标题,破坏语义完整性 |
| ⏱ 时间复杂度 | O(N),N 为文本长度 |
| 💾 空间复杂度 | O(N)(存储所有块) |
| 🎯 适用场景 | 纯文本、日志、聊天记录、无结构文档 |
三、自适应标题切块(Adaptive Heading Chunking)
3.1 原理
根据文档的标题层级(如 Markdown 中的 #、##)进行切分,每个标题及其下属内容作为一个独立的块。这是一种结构感知的方法。
核心理念:保留文档的逻辑结构,使每个块内部语义内聚。
3.2 流程
原始文本
识别标题行
按标题分割章节
每个章节为一个块
可选:超长章节二次切分
3.3 代码实现(自定义)
python
import re
from llama_index.core.schema import Node
def split_by_heading(text: str) -> list:
"""按 Markdown 标题切分,保留标题及其内容"""
lines = text.splitlines()
sections = []
current = []
for line in lines:
if re.match(r'^#{1,6}\s+', line): # 匹配 # 到 ######
if current:
sections.append("\n".join(current).strip())
current = []
current.append(line)
if current:
sections.append("\n".join(current).strip())
return sections if sections else [text]
class AdaptiveHeadingParser:
def __init__(self, fallback_chunk_size=512):
self.fallback_chunk_size = fallback_chunk_size
def parse(self, doc):
sections = split_by_heading(doc.text)
nodes = []
for sec in sections:
# 如果章节仍然过长,可二次切分(如按段落或固定长度)
if len(sec) > self.fallback_chunk_size:
sub_chunks = fixed_chunking(sec, self.fallback_chunk_size)
for sub in sub_chunks:
nodes.append(Node(text=sub, metadata={"chunk_type": "subsection"}))
else:
nodes.append(Node(text=sec, metadata={"chunk_type": "section"}))
return nodes
# 使用示例
doc = Document(text=sample_doc_with_headings)
parser = AdaptiveHeadingParser()
nodes = parser.parse(doc)
3.4 优缺点与复杂度
| 项目 | 描述 |
|---|---|
| ✅ 优点 | 保持语义完整,检索时更容易命中完整章节 |
| ❌ 缺点 | 依赖文档有清晰的标题结构;无标题时退化 |
| ⏱ 时间复杂度 | O(N)(正则扫描一次) |
| 💾 空间复杂度 | O(N) |
| 🎯 适用场景 | 技术文档、论文、法律文书、博客、Wiki |
四、AI 评估:用大模型给检索块打分
为了客观比较两种策略的效果,我们可以使用大语言模型(LLM)作为"评判员",对检索到的块与问题的相关性进行打分(0~10)。
4.1 评估流程
- 固定一组查询(如 10 个问题)。
- 对每个查询,分别用两种切块策略构建索引,检索 top‑k 个块。
- 调用 LLM 为每个块打分。
- 计算平均分、命中率等指标。
4.2 打分函数实现(同步版)
python
import openai
def llm_score_relevance(query: str, chunk: str) -> int:
prompt = f"""请根据以下文本块与用户问题的相关程度,给出一个 0 到 10 之间的整数分数。
0 表示完全不相关,10 表示完全回答了问题。只输出数字。
用户问题:{query}
文本块:
{chunk}
分数:"""
response = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": prompt}],
temperature=0.0,
max_tokens=2
)
try:
score = int(response.choices[0].message.content.strip())
return max(0, min(10, score))
except:
return 0
4.3 异步版本(避免阻塞)
python
import httpx
import asyncio
async def async_score_relevance(query: str, chunk: str, client: httpx.AsyncClient) -> int:
# 使用异步 HTTP 调用 LLM API
# 具体实现略(需适配具体 API)
pass
4.4 评估脚本示例
python
def evaluate_strategy(chunks, queries, top_k=3):
total_score = 0
total_queries = len(queries)
for q in queries:
# 检索 top_k
retrieved = retrieve(q, chunks, top_k)
for chunk, _ in retrieved:
score = llm_score_relevance(q, chunk)
total_score += score
avg_score = total_score / (total_queries * top_k)
return avg_score
结论示例:
- 固定长度策略平均得分:6.8
- 自适应标题策略平均得分:9.1
→ 自适应标题策略在此数据集上显著更优。
五、面试官怎么问 & 怎么答
Q1:固定长度切块和基于标题的自适应切块有什么区别?各自适用什么场景?
答:
- 固定长度:与文档结构无关,简单按 token 数切分。适合纯文本、日志、无标题文档,或作为 baseline。
- 自适应标题:根据标题层级切分,保持章节完整性。适合技术文档、法律条文、Markdown 笔记等结构清晰的内容。
- 实践中可混合:先按标题切,对过长章节再按固定长度二次切分。
Q2:如何评估不同切块策略的效果?有哪些量化指标?
答:
- 常用指标:
- Hit Rate@k:正确答案是否出现在前 k 个检索结果中。
- MRR(平均倒数排名):正确结果位置的倒数平均值。
- LLM 相关性评分:用大模型给每个检索块打分,计算平均分。
- 具体做法:构建测试集(问题 + 答案所在的块),离线运行检索,计算上述指标,选择最优策略。
Q3:固定长度切块的 overlap 参数有什么作用?如何选择?
答:
- overlap 使相邻块共享部分文本,防止重要信息恰好被切在边界上而丢失(例如一个长句子跨越两个块)。
- 一般设置为 chunk_size 的 10%~20%。若发现检索结果丢失边缘信息,可适当增加 overlap。
- 极端情况(如句子非常长)可基于句子边界进行智能 overlap。
Q4:自适应切块中,如果某章节太长(超过 2048 token),怎么办?
答:
- 我会对该章节内部再按固定长度或按段落进一步切分,同时保留该章节的标题作为前缀,确保子块仍带有上下文信息。这种"层次化切块"能兼顾结构完整性和长度限制。
- 另外,可以调整 LLM 的上下文窗口(如使用 32K 模型)来容纳更长章节,但这会增加成本。
Q5:你如何在实际项目中部署切块策略的对比实验?
答:
- 步骤:
- 收集代表性文档集和问答对。
- 实现两种切块策略,构建两个独立的索引。
- 对每个查询,分别检索并记录 top‑k 结果。
- 使用 LLM 评分或人工评估,计算平均分、Hit Rate。
- 选择得分更高的策略上线。
- 可自动化运行,并将结果可视化(如柱状图),用于团队决策。
六、总结与最佳实践
| 策略 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|
| 固定长度 | 实现简单,通用 | 切断语义 | 无结构文本、日志 |
| 自适应标题 | 结构完整,检索精准 | 依赖标题格式 | 技术文档、书籍、Wiki |
| 混合策略 | 兼顾结构+长度限制 | 实现稍复杂 | 复杂长文档(如论文) |
最佳实践建议:
- 优先使用自适应标题切块,如果文档有清晰的 Markdown/HTML 标题。
- 设置合理的 chunk_size:常用 512~1024 token(约 400~800 中文字符)。
- 加入 overlap:推荐 chunk_size 的 10%~20%。
- 用 AI 评估代替主观判断:LLM 打分能快速比较多种策略。
- 监控线上指标:如问答准确率、用户反馈,持续优化分块参数。
通过科学的切块策略和量化评估,你可以显著提升 RAG 系统的检索质量,让大模型真正"读得懂、找得准"。