RAG(知识库问答)里,分块(chunking)经常被低估:
你后面检索、重排、prompt、甚至换模型的上限,很大程度由 chunk 决定。
这篇不讲“分块玄学”,直接给你工程可落地的东西:
- 按文档类型的切分策略(制度/合同、Markdown、SOP、PDF、表格)
- 一张默认参数表(chunk_size/overlap/top_k)
- 排错流程(打印 TopK)
- hit@k 最小评测脚手架(能跑起来就行)
0)TL;DR
- 分块目标:提高 hit@k,降低噪音(不是“切得更细”)
- 排错第一步:打印 TopK,看是否命中“完整证据单元”
- 迭代原则:一次只改一个变量,并做回归评测
1)你真正能调的 6 个旋钮
- chunk_size(长度)
- overlap(重叠)
- 边界(标题/段落/句子 vs 固定长度硬切)
- hierarchical(保 section_path)
- metadata(doc_id/version/source 等)
- 结构保留(表格/列表/条款)
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_size | 300–800 字/Token 等价 | 缺上下文→调大;噪音大→调小 |
| overlap | 10%–20% | 关键句被切断→调大;重复太多→调小 |
| top_k | 5–10 | 找不到证据→调大;噪音污染→调小 |
| rerank | 可选 | 召回多但排序乱→开;成本敏感→先关 |
4)排错流程:固定样本 → 打印 TopK → 改分块 → 回归
4.1 固定失败样本(四字段)
- query
- expected_keypoints
- expected_source_ids
- observed
4.2 打印 TopK(第一生产力)
你要回答:
- TopK 里有没有正确证据?
- 如果有,排第几?噪音多不多?
如果 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 官网(参数以其文档与控制台为准)。