跳转至

第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 过滤清除。模型升级时全量重建并用双集合灰度切换。

练习题

  1. 分块对比实验:取一篇 2000 字技术文章,分别用 chunk_size=256/512/1024 配合 overlap=64 分块,对同一组 5 个问题检索 Top-3,记录命中块数差异。
  2. 递归分块实现:不使用 LangChain,用纯 Python 实现一个支持 ["\n\n", "\n", "。"] 分隔符的递归分块器。
  3. 索引选型思考:当你的知识库从 10 万文档增长到 1000 万文档时,索引策略需要做哪些调整?HNSW 的 ef_search 该如何变化?

小结

分块与索引是 RAG 系统的地基,决定了检索质量的上限。本章介绍了固定大小、按句子/段落、递归、语义、结构化五种分块策略,重叠窗口的作用,以及 Flat/IVF/HNSW 三类向量索引的原理与选型。生产环境推荐"递归分块 + HNSW 索引"作为默认组合,并用业务评测集持续校准分块大小。下一章将深入向量检索数据库 Qdrant/Milvus/PGVector 的具体使用与对比。