Chunk 分块工程模板:按文档类型切分 + 参数表 + TopK 排错 + hit@k 脚手架

68 阅读3分钟

RAG(知识库问答)里,分块(chunking)经常被低估:
你后面检索、重排、prompt、甚至换模型的上限,很大程度由 chunk 决定。

这篇不讲“分块玄学”,直接给你工程可落地的东西:

  • 按文档类型的切分策略(制度/合同、Markdown、SOP、PDF、表格)
  • 一张默认参数表(chunk_size/overlap/top_k)
  • 排错流程(打印 TopK)
  • hit@k 最小评测脚手架(能跑起来就行)

0)TL;DR

  • 分块目标:提高 hit@k,降低噪音(不是“切得更细”)
  • 排错第一步:打印 TopK,看是否命中“完整证据单元”
  • 迭代原则:一次只改一个变量,并做回归评测

1)你真正能调的 6 个旋钮

  1. chunk_size(长度)
  2. overlap(重叠)
  3. 边界(标题/段落/句子 vs 固定长度硬切)
  4. hierarchical(保 section_path)
  5. metadata(doc_id/version/source 等)
  6. 结构保留(表格/列表/条款)

2)按文档类型切分(直接照抄)

2.1 制度/合同/规则(条款优先)

  • 按“条款/小节/标题”切
  • chunk 内尽量包含“定义+条件+例外”
  • metadata 必带版本/生效日期

2.2 产品文档/Markdown(heading 优先)

  • # / ## / ### 层级切
  • chunk 内保留小标题,避免语义歧义

2.3 SOP/工单流程(步骤块优先)

  • 按 Step 块切,别把顺序切散
  • 条件/异常分支跟随步骤块

2.4 PDF/OCR(先结构化再切)

扫描 PDF/OCR 的坑很多:

  • 标题层级丢失
  • 表格碎掉
  • 乱码/断行

建议先做“结构修复”(标题/段落/表格)再分块,不然入库就是垃圾。

2.5 表格/清单(先行组化)

两种常用处理:

  • 行组化:保留表头 + N 行一组
  • KV 化:转成 key: value 列表再切

3)默认参数起点(先从可用开始)

参数起点什么时候调
chunk_size300–800 字/Token 等价缺上下文→调大;噪音大→调小
overlap10%–20%关键句被切断→调大;重复太多→调小
top_k5–10找不到证据→调大;噪音污染→调小
rerank可选召回多但排序乱→开;成本敏感→先关

4)排错流程:固定样本 → 打印 TopK → 改分块 → 回归

4.1 固定失败样本(四字段)

  • query
  • expected_keypoints
  • expected_source_ids
  • observed

4.2 打印 TopK(第一生产力)

你要回答:

  1. TopK 里有没有正确证据?
  2. 如果有,排第几?噪音多不多?

如果 TopK 没命中正确证据,先别调 prompt。


5)hit@k 最小评测脚手架(Python)

5.1 评测集格式(jsonl)

{"id":"q001","query":"退款规则是什么?","expected_source_ids":["refund_v2025#p12","refund_v2025#p13"]}

5.2 hit@k 计算

import json
from typing import List

def load_jsonl(path: str):
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            yield json.loads(line)

def hit_at_k(retrieved_ids: List[str], expected_ids: List[str], k: int) -> int:
    topk = set(retrieved_ids[:k])
    return 1 if any(eid in topk for eid in expected_ids) else 0

def evaluate_retrieval(cases_path: str, retrieve):
    ks = [3, 5, 10]
    totals = {k: 0 for k in ks}
    hits = {k: 0 for k in ks}

    for c in load_jsonl(cases_path):
        results = retrieve(c["query"])  # -> [{"id": "...", "text": "..."}]
        retrieved_ids = [r["id"] for r in results]
        for k in ks:
            hits[k] += hit_at_k(retrieved_ids, c["expected_source_ids"], k)
            totals[k] += 1

    return {f"hit@{k}": hits[k] / max(1, totals[k]) for k in ks}

5.3 TopK 打印

def debug_topk(query: str, retrieve, k: int = 5):
    results = retrieve(query)[:k]
    for i, r in enumerate(results, 1):
        print(f"#{i} id={r['id']}")
        print(r['text'][:400])
        print('-' * 40)

6)上线前你一定要补的三件事

  • metadata:doc_id/section_path/version/source(否则过滤、回放、引用都难)
  • 版本一致性:文档/向量/embedding/prompt 版本可追溯,可回滚
  • 可观测:retrieved_ids/top_k/latency/token/retries(否则只能猜)

资源区

如果你要做多模型 A/B 或评测,建议先把接入层统一(OpenAI 兼容协议多数情况下只改 base_url/api_key)。
我自己用的是 147ai 官网(参数以其文档与控制台为准)。