第1章 分块与索引¶
学习目标¶
- 理解文档分块(Chunking)在 RAG 链路中的关键作用
- 掌握固定大小、按句子/段落、递归分块、语义分块等主流分块策略
- 学会根据文档类型与业务目标选择分块大小与重叠窗口
- 理解向量索引原理:Flat、IVF、HNSW 三类索引的取舍
- 能够使用 Python(LangChain / qdrant-client)完成端到端的分块与索引构建
问题导入¶
某金融科技公司把一份 200 页的招股说明书整篇喂给 LLM 试图回答"该公司 2024 年研发费用占比",结果上下文超长、Token 成本爆炸且模型注意力被稀释,回答严重跑题。工程师尝试把文档切成 512 token 的固定块,但又出现了把"研发费用"这个词被从中间切断、把表格切成无意义数字堆的情况,检索 Top-3 中只有 1 条真正相关。
问题的根源是:RAG 的检索质量,在分块阶段就已经决定了一半。分块太大会让检索精度下降、上下文冗余;分块太小会让语义不完整、噪声变多;切分位置不当会把一个完整的概念撕裂成两半。本章系统讲解分块策略、重叠窗口、向量索引原理,让你在 RAG 的"地基"阶段就打好基础。
核心概念¶
1. 为什么必须分块¶
LLM 的上下文窗口有限(即便 128K 也有成本与注意力衰减问题),且向量检索在小而精的片段上表现更好。分块的三大目标:
- 语义完整:每块表达一个完整概念,避免被从中间切断
- 检索精准:小块更易与 query 在向量空间对齐,提升 Top-K 命中率
- 上下文可控:拼装 Prompt 时按需取用,避免一次性塞入过多内容
2. 主流分块策略对比¶
| 策略 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 固定大小 | 按 token 数(如 512)硬切 | 实现简单、速度最快 | 可能切断句子/表格 | 快速原型、纯文本 |
| 按句子/段落 | 在 . \n\n 等边界切 |
语义边界自然 | 块大小不均 | 文章、博客 |
| 递归分块 | 先按章节,再按段落,再按句子,层级递进 | 兼顾层级与大小约束 | 实现略复杂 | 通用文档首选 |
| 语义分块 | 用 Embedding 相邻句子相似度,相似度骤降处切分 | 语义聚簇最优 | 需 Embedding 调用,成本高 | 高质量问答 |
| 文档结构化 | 按 Markdown 标题、HTML 标签、PDF 页码切 | 保留文档结构 | 依赖文档格式 | 技术文档、合同 |
3. 分块大小选择¶
分块大小是 RAG 效果最敏感的旋钮之一。经验值:
- 256-512 token:精确事实型问答("产品 SKU 是多少")
- 512-1024 token:通用知识问答(推荐起步值)
- 1024-2048 token:长摘要、段落级理解任务
选择原则:先用 512 跑通基线,再用业务评测集对比 256/512/1024 的 Recall@5 与答案准确率,按数据决策而非凭感觉。
4. 重叠窗口(Overlap)¶
相邻块之间保留 10%-20% 重叠(如块大小 512、重叠 64),目的是:
- 避免在切分边界丢失关键信息
- 让被切分的概念在两个块中都能完整出现
- 提升检索鲁棒性
代价是存储与索引成本略增。对于事实密集型文档(法律、医疗)建议重叠 100-128 token。
5. 向量索引原理¶
向量入库后,检索时需要快速找到与 query 向量最近的 K 个邻居。暴力计算(Flat)复杂度 O(N),无法支撑百万级以上数据。三种主流近似最近邻(ANN)索引:
Flat(暴力索引):线性扫描所有向量,计算精确余弦相似度。精度 100% 但速度慢,适合 <10 万向量的小数据集或评测基准。
IVF(倒排文件):用 K-means 把向量空间划分为 nlist 个聚类簇,检索时只在最近的 nprobe 个簇内搜索。nprobe 越大召回越高、速度越慢,可在线调整。适合百万级数据。
HNSW(分层可导航小世界图):构建多层近邻图,从顶层稀疏图逐层下沉到底层密集图,检索在图上贪心游走。查询速度极快、召回率高,是当前生产环境主流。参数 M(邻居数)和 ef_construction(构建时候选数)影响质量与内存。
| 索引 | 构建速度 | 查询速度 | 召回率 | 内存 | 适用规模 |
|---|---|---|---|---|---|
| Flat | 最快 | 慢 | 100% | 低 | <10 万 |
| IVF | 中 | 中 | 90%-98% | 中 | 10 万-1000 万 |
| HNSW | 慢 | 快 | 95%-99% | 高 | 100 万-10 亿 |
6. 索引构建与更新¶
企业知识库是动态的,索引需要支持增量更新:
- 新增文档:分块 → 向量化 → upsert 到集合,HNSW 支持在线插入
- 删除文档:按文档 ID 删除其所有分块(需在 payload 中记录
doc_id) - 更新文档:先删后插,避免新旧块共存导致检索冲突
- 重建索引:当 Embedding 模型升级或分块策略调整时,需全量重建
原理图¶
flowchart TD
Doc[原始文档<br/>PDF/Word/Markdown] --> Parse[文档解析]
Parse --> Strategy{选择分块策略}
Strategy -->|固定大小| Fixed[按 token 切分]
Strategy -->|递归| Recursive[章节→段落→句子]
Strategy -->|语义| Semantic[Embedding 相似度切分]
Fixed --> Overlap[加重叠窗口]
Recursive --> Overlap
Semantic --> Overlap
Overlap --> Meta[附加元数据<br/>doc_id/chunk_id/page]
Meta --> Embed[Embedding 向量化]
Embed --> Index{选择索引}
Index -->|小数据| Flat[Flat 暴力索引]
Index -->|中等| IVF[IVF 倒排]
Index -->|生产| HNSW[HNSW 图索引]
Flat --> VDB[(向量数据库)]
IVF --> VDB
HNSW --> VDB
VDB -.增量.-> Update[文档新增/更新/删除]
图:分块与索引构建流程,含策略选择与索引选型
示例代码¶
使用 LangChain 的递归分块器 + Qdrant 构建索引的完整示例:
import os
import uuid
from langchain_text_splitters import RecursiveCharacterTextSplitter
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
from openai import OpenAI
os.environ.setdefault("OPENAI_API_KEY", "sk-xxx")
client = OpenAI()
qdrant = QdrantClient(":memory:") # 生产环境改为 host/port
EMBED_DIM = 1536
COLLECTION = "kb_docs"
def build_collection() -> None:
"""创建集合,使用 HNSW 索引(Qdrant 默认)。"""
if qdrant.collection_exists(COLLECTION):
qdrant.delete_collection(COLLECTION)
qdrant.create_collection(
collection_name=COLLECTION,
vectors_config=VectorParams(size=EMBED_DIM, distance=Distance.COSINE),
)
def embed(text: str) -> list[float]:
return client.embeddings.create(
model="text-embedding-3-small", input=text
).data[0].embedding
def chunk_document(text: str, doc_id: str) -> list[dict]:
"""递归分块 + 重叠窗口。"""
splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=64,
separators=["\n\n", "\n", "。", ".", " ", ""],
length_function=len,
)
chunks = splitter.split_text(text)
return [
{
"id": str(uuid.uuid4()),
"text": c,
"doc_id": doc_id,
"chunk_index": i,
}
for i, c in enumerate(chunks)
]
def index_document(text: str, doc_id: str) -> int:
"""分块并向量化入库,返回入库块数。"""
chunks = chunk_document(text, doc_id)
points = [
PointStruct(
id=c["id"],
vector=embed(c["text"]),
payload={
"text": c["text"],
"doc_id": doc_id,
"chunk_index": c["chunk_index"],
},
)
for c in chunks
]
qdrant.upsert(collection_name=COLLECTION, points=points)
return len(chunks)
def delete_document(doc_id: str) -> None:
"""按 doc_id 删除其所有分块。"""
qdrant.delete(
collection_name=COLLECTION,
points_selector={"filter": {"must": [{"key": "doc_id", "match": {"value": doc_id}}]}},
)
if __name__ == "__main__":
build_collection()
sample = (
"Qwen3 是阿里 2025 年发布的开源大模型系列。\n\n"
"它支持 0.6B 到 235B 多种规模,覆盖端侧到云端。\n\n"
"在 MMLU、HumanEval 等基准上达到同规模最优。"
)
n = index_document(sample, "doc-001")
print(f"已索引 {n} 个分块到集合 {COLLECTION}")
运行结果说明:脚本会把示例文本递归切成约 2-3 个带重叠的块,每块附带 doc_id 与 chunk_index 元数据,向量化后写入 Qdrant 的内存集合。删除文档时通过 payload 过滤即可精准清除其所有分块。
企业案例¶
案例 1:技术文档站索引¶
某开源软件公司用 LangChain 递归分块器(chunk_size=512、overlap=64)处理 800 篇 Markdown 文档,按 H2 标题附加 section 元数据。检索 Top-5 命中率从固定分块的 71% 提升至 89%,且答案引用能精确到章节。
案例 2:合同条款检索¶
某法务团队对 5000 份合同按"条款"做结构化分块(以 第X条 为边界),而非固定 token 切分,避免把一条义务条款切成两半。配合 HNSW 索引,10 万级分块检索延迟稳定在 30ms 以内。
案例 3:增量更新实践¶
某新闻平台每日新增 2000 篇文章,采用"先删后插"策略按 doc_id 更新,HNSW 索引在线插入无停机。当 Embedding 模型从 text-embedding-ada-002 升级到 text-embedding-3-small 时,全量重建 800 万分块耗时 6 小时,重建期间双集合灰度切换保证服务不中断。
常见问题¶
Q1:分块大小选 256 还是 1024? A:看任务。事实型问答(产品参数、人名)选 256-512;摘要型问答(总结某段内容)选 1024+。务必用评测集对比,不要拍脑袋。
Q2:重叠窗口设多大合适? A:一般取块大小的 10%-20%。512 块配 50-100 重叠是稳妥起点。重叠过大增加存储与重复检索,过小则边界信息易丢。
Q3:HNSW 的 M 和 ef_construction 怎么调?
A:M 默认 16,数据量大或召回要求高可调到 32-48;ef_construction 默认 100,提升到 200 召回更优但构建更慢。ef_search(查询时候选数)在线可调,越大召回越高、越慢。
Q4:表格和图片怎么分块? A:表格建议先转成结构化 JSON 或 Markdown 表格整体作为一个块,不要按 token 切;图片用多模态 Embedding 或先 OCR/描述再向量化。
面试题¶
1. 简述 RAG 中分块策略对检索质量的影响。 参考答案:分块大小影响精度与上下文完整性——小块检索精准但语义易碎,大块语义完整但易引入噪声。切分位置影响语义边界——在自然边界(句子/段落/标题)切分优于硬切。重叠窗口减少边界信息丢失。整体上分块质量直接决定 RAG 上限。
2. Flat、IVF、HNSW 三种索引的取舍?
答题要点:Flat 精度最高但 O(N) 无法扩展;IVF 用聚类减少搜索范围,可控 nprobe 平衡速度与召回;HNSW 用分层图实现 log 级查询,召回与速度均优但内存占用高。生产环境百万级以上首选 HNSW。
3. 语义分块(Semantic Chunking)的原理与代价? 参考答案:用 Embedding 计算相邻句子相似度,在相似度骤降(语义转折)处切分。优点是语义聚簇最优;代价是每个句子都要调 Embedding,成本与延迟显著高于规则分块,适合对质量要求极高的场景。
4. 如何处理文档的增量更新?
答题要点:每块 payload 记录 doc_id;新增直接 upsert;更新先按 doc_id 删除旧块再插入新块;删除按 doc_id 过滤清除。模型升级时全量重建并用双集合灰度切换。
练习题¶
- 分块对比实验:取一篇 2000 字技术文章,分别用 chunk_size=256/512/1024 配合 overlap=64 分块,对同一组 5 个问题检索 Top-3,记录命中块数差异。
- 递归分块实现:不使用 LangChain,用纯 Python 实现一个支持
["\n\n", "\n", "。"]分隔符的递归分块器。 - 索引选型思考:当你的知识库从 10 万文档增长到 1000 万文档时,索引策略需要做哪些调整?HNSW 的
ef_search该如何变化?
小结¶
分块与索引是 RAG 系统的地基,决定了检索质量的上限。本章介绍了固定大小、按句子/段落、递归、语义、结构化五种分块策略,重叠窗口的作用,以及 Flat/IVF/HNSW 三类向量索引的原理与选型。生产环境推荐"递归分块 + HNSW 索引"作为默认组合,并用业务评测集持续校准分块大小。下一章将深入向量检索数据库 Qdrant/Milvus/PGVector 的具体使用与对比。