LLMNotes

第 08 章:切块策略

zjc 于 2026-01-08 发布

这是《LLM 应用开发 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 切块把长文档转换成可检索的知识单元。块太大,向量语义被稀释;块太小,上下文不足。切块策略要与文档结构、检索方式和生成目标匹配。

8.1 为什么要切块

长文档
  -> 解析 blocks
     -> chunk
        -> embedding
           -> index
              -> retrieve top K
                 -> rerank
                    -> context

切块目标:

  1. 每个块语义相对完整;
  2. 关键信息不被切断;
  3. 大小适合向量模型输入;
  4. 保留标题、页码和来源;
  5. 避免重复内容过多;
  6. 支持权限和版本过滤;
  7. 支持增量更新。

8.2 常见策略

策略 适用 优点 缺点
固定长度 日志、纯文本 简单 易切断语义
递归字符切块 通用文档 平衡效果好 仍依赖分隔符
按标题切块 手册、政策 结构清晰 需要解析质量
按段落合并 文章 语义自然 长度不均
表格整块 结构化数据 行列完整 可能超长
代码块整体 技术文档 保持语法 缺上下文
语义切块 高价值语料 更贴近语义 计算成本高

8.3 固定长度切块

def fixed_chunk(text: str, size: int = 600, overlap: int = 80):
    chunks = []
    step = max(size - overlap, 1)
    for start in range(0, len(text), step):
        chunk = text[start:start + size]
        if chunk:
            chunks.append(chunk)
        if start + size >= len(text):
            break
    return chunks

固定长度适合基线,不适合作为最终复杂文档方案。

8.4 递归切块

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    separators=["\n## ", "\n### ", "\n\n", "\n", "", "", " ", ""],
    chunk_size=600,
    chunk_overlap=80,
)

chunks = splitter.split_text(
    "## 退款政策\n标准订单可在签收后 7 天内申请退款。\n生鲜商品不支持无理由退款。"
)

分隔符优先从自然结构到字符级,尽量避免句子被硬切。

8.5 按标题切块

def heading_chunk(blocks, max_chars=900):
    chunks = []
    current = {"title": "root", "parts": []}

    for block in blocks:
        if block["type"] == "heading":
            if current["parts"]:
                chunks.append(current)
            current = {"title": block["text"], "parts": []}
        else:
            current["parts"].append(block["text"])

        size = sum(len(p) for p in current["parts"])
        if size >= max_chars:
            chunks.append(current)
            current = {"title": current["title"], "parts": []}

    if current["parts"]:
        chunks.append(current)
    return chunks

每个 chunk 建议写入完整标题路径:

员工手册 / 考勤 / 远程办公

8.6 Overlap

重叠用于降低边界信息丢失:

chunk 1: A B C D
chunk 2: C D E F
chunk 3: E F G H

设置建议:

  1. 通用文本 10% 到 20%;
  2. 条款文本可按条重复标题;
  3. 代码块不宜机械重叠;
  4. 表格不要随机重叠;
  5. 重叠会提高存储成本;
  6. 用召回实验验证收益。

8.7 大小选择

块大小 效果
太小 语义不完整,检索噪声多
太大 向量语义稀释,上下文成本高
合适 单一主题,能独立回答局部问题

常见起点:

中文:300-800 字符
英文:300-1000 tokens
表格:按表或按逻辑行组
FAQ:一问一答一个块

不同文档类型可以配置不同策略,不要全库统一硬切。

8.8 元数据与父块

{
  "chunk_id": "policy-001#c12",
  "doc_id": "policy-001",
  "parent_id": "policy-001#sec3",
  "title_path": ["退款政策", "特殊商品"],
  "page": 8,
  "effective_time": "2026-01-01",
  "acl": ["cs_agent"],
  "content": "生鲜商品不支持无理由退款。"
}

父子检索:

向量命中小块
  -> 找父块或相邻块
     -> 送入 rerank
        -> 构造上下文

小块负责召回,父块负责上下文,这是长文档常用方案。

8.9 增量更新

新文档版本
  -> checksum 比较
     -> 未变化:跳过
     -> 变化:解析新版
        -> diff blocks
           -> 新增/替换 chunk
              -> 旧版标记 inactive
                 -> 保留审计

注意事项:

  1. 文档删除要同步索引;
  2. 避免只覆盖部分旧 chunk;
  3. 权限变化要即时生效;
  4. 保留版本便于回滚;
  5. 监控索引与源文档一致性。

8.10 评估

构造问答对验证切块:

指标 说明
Recall@K 相关 chunk 是否进入候选
MRR 首个相关结果排名
Boundary Error 关键句被切断
Context Sufficiency 候选能否支持回答
Cost per Query 检索 token 成本

比较实验:

策略 A:600 字符,80 overlap
策略 B:标题 + 900 字符
策略 C:小块召回 + 父块生成

本章小结

切块要保留语义完整性和结构元数据。常见做法是按标题与自然段落递归切块,为 FAQ、表格和代码设计专用规则,并通过小块召回、父块补上下文的方式兼顾精度与完整性。切块参数应通过召回实验确定。

思考题

  1. chunk_overlap 解决什么问题,带来什么代价?
  2. 为什么表格和普通文本不适合同一策略?
  3. 父子块检索如何工作?
  4. 文档更新时如何避免新旧版本同时被检索?
  5. 如何评估一个切块策略是否更好?