做 RAG(知识库问答)时,很多团队会遇到一个反直觉现象:
Chunk 切得越细,检索越“像”,答案反而越不准;
Chunk 切得越长,证据更全,但噪音更大、模型更容易跑偏。
这不是模型问题,而是“分块策略”没有对齐工程目标:让 TopK 更容易命中正确证据,并且把干净的证据交给生成模型。
本文从工程视角把“分块”拆成可调的旋钮、可复现的排错流程、可量化的指标,并给一个最小评测脚手架,让你把分块优化从玄学变成工程。
在构建RAG系统时,服务与基础设施的稳定性是关键。选择一个可靠、低延迟、合规的模型服务平台,可以有效避免因网络或服务问题导致的生成质量波动。例如,我使用的就是大模型中转平台 147AI(https://147ai.com。)
0)TL;DR(先给结论)
- 分块的目标不是“越细越好”,而是提高检索命中率(hit@k)并降低上下文噪音。
- 分块效果必须用指标评:至少量化 hit@k(检索命中率)/ cite_acc(引用正确率)/ ans_acc(答案正确率),否则迭代会变玄学。
- 排错最快路径:固定失败样本 → 打印 TopK → 调分块(chunk_size/overlap/边界)→ 回归评测。
1)为什么会“越切越差”(症状 → 根因)
症状 A:TopK 命中看起来相关,但答案总缺关键条件/例外
常见根因:
- 语义单元被切断:把“条件/例外/定义”切散,单个 chunk 不完整
- chunk 太短:只召回到碎片,模型只能补全猜测
症状 B:TopK 命中了正确文档,但答案跑偏/泛泛总结
常见根因:
- chunk 太长/噪音太多:上下文污染,模型抓错重点
- 拼接顺序不对:先喂例外,再喂定义
症状 C:总说“资料不足”
常见根因:
- 分块边界导致关键句缺失:证据不足以支持结论
- 过滤/版本字段缺失:召回到过期或不适用的 chunk,被规则拒答
2)分块的工程目标到底是什么?
分块不是为了“看起来结构化”,而是为了这三件事:
- 检索目标:TopK 更容易命中正确证据(hit@k 提升)
- 生成目标:上下文更干净、更少噪音、更可追溯(cite_acc 提升)
- 系统目标:索引体积、延迟、成本可控(吞吐与稳定性)
如果你分块后 hit@k 没提升,说明你切法没有服务检索;如果 cite_acc 下降,说明上下文更脏或引用约束不足。
3)分块的 6 个关键旋钮(真正能调的变量)
3.1 chunk_size(长度)
- 太小:语义不完整、条件/例外缺失
- 太大:噪音大、跑偏、成本高
3.2 overlap(重叠)
- 作用:避免关键句被切断
- 风险:重叠过大带来重复噪音与成本
3.3 边界策略(按什么切)
优先级建议:
- 标题层级(section/heading)
- 段落(空行)
- 句子(标点)
不要纯按固定长度硬切,会把“语义单元”切碎。
3.4 层级分块(hierarchical)
把 chunk 变成“带路径的语义单元”:
doc_id+section_path+chunk_text
工程价值:引用、审计、回放更容易;检索到 chunk 也能知道它属于哪一节。
3.5 元数据(metadata)
建议最小集(后面做过滤/版本/权限都靠它):
doc_id/source/section_pathversion/effective_date(制度类非常关键)page或offset(便于定位)
3.6 “结构保留”能力(表格/列表/条款)
分块前要先回答:你的文档里表格、列表、条款多不多?
如果多,必须先保结构再切,否则检索召回的是“破碎文本”。
4)不同文档类型怎么切(实战建议)
4.1 制度/合同/规则类(强建议按条款与层级)
目标:召回到“条款完整段落”,并保留版本信息。
- 按
第X条/小节/标题切 - chunk 内保留“定义 + 条件 + 例外”
- metadata 里必须有版本/生效日期
4.2 产品文档/Markdown(按 heading 层级)
- 按
# / ## / ###层级切 - chunk 内保留子标题,方便模型理解上下文
4.3 SOP/工单流程(按步骤块)
关键是不要把“步骤顺序”切碎:
- 按“步骤块”切(Step 1/2/3)
- 把“前置条件/异常分支”跟随步骤块一起保留
4.4 PDF/OCR 文档(先结构化,再分块)
如果是扫描 PDF:
- 先 OCR
- 再修复标题/段落结构
- 再分块
直接对 OCR 乱码分块入库,会让后续所有优化都无效。
4.5 表格/清单(先行组化)
表格常见两种处理方式:
- 行组化:每 N 行成一个 chunk,保留表头
- 转结构:把表转成
key: value列表再切
5)默认参数起点(给一张可复制表)
这些不是“真理”,是工程起点。要靠评测与日志去迭代。
| 参数 | 建议起点 | 何时调大/调小 |
|---|---|---|
| chunk_size | 300–800 字/Token 等价 | 缺上下文→调大;噪音大→调小 |
| overlap | 10%–20% | 关键句被切断→调大;重复太多→调小 |
| top_k | 5–10 | 找不到资料→调大;噪音污染→调小 |
| rerank | 可选 | 召回多但排序乱→开;成本敏感→先关 |
| context_budget | 可控上限 | 超长跑偏→限制;证据不全→增加 |
6)评测与排错:把分块优化变成工程
6.1 最小指标体系
- hit@k:TopK 是否命中正确证据段落(先评检索)
- cite_acc:引用是否真正支持结论(制度/合同场景必做)
- ans_acc:答案关键点是否覆盖且无严重错误(可先人工)
6.2 排错流程(建议照着做)
- 固定失败样本(query/期望关键点/正确证据/实际输出)
- 打印 TopK(是否命中正确证据)
- 一次只改一个变量:chunk_size → overlap → 边界策略 → top_k → rerank
- 回归评测:指标上升才算“优化成功”
7)最小评测脚手架(Python:hit@k + TopK 打印)
7.1 评测集格式(jsonl)
{"id":"q001","query":"退款规则是什么?","expected_keypoints":["7天无理由","特殊商品除外"],"expected_source_ids":["refund_v2025#p12","refund_v2025#p13"]}
7.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}
7.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)
8)一页 Checklist(收藏用)
- 文档结构是否可靠(标题/段落/表格是否被破坏)
- metadata 是否齐全(doc_id/section_path/version/source)
- chunk 是否包含完整语义单元(定义+条件+例外)
- 先跑 hit@k,再谈 prompt
- 打印 TopK 定位问题段
- 一次只改一个变量并回归评测
- 上线后保留可观测字段(retrieved_ids/prompt_version/latency/token)

被折叠的 条评论
为什么被折叠?



