max_content_length 与 min_content_length 参数评估方法
这两个参数需要基于实际文档结构和下游模型约束来评估。下面以万用表文档为例,演示完整的评估过程。
第一步:分析文档的 section 长度分布
对万用表文档按标题切分后,得到 22 个 section,body 长度分布如下:
| Text Only |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 | 序号 标题 body长度 title+body
─────────────────────────────────────────────────────
1 (文档开头) 0 12 <-- 极短
2 # 使用说明书 190 199 <-- 较短
3 # 安全手册 610 618
4 # 安全标识 381 389
5 # 控制与端口 151 160 <-- 较短
6 # 功能符号指示 123 133 <-- 较短
7 # 规格 1351 1357 <-- 较长
8 # 技术指标说明 1016 1026 <-- 较长
9 # 电池安装 182 190 <-- 较短
10 # 操作指导 0 8 <-- 极短
11 # 数值保持按键HOLD 79 93 <-- 极短
12 # 背光灯键 202 221
13 # 测量非接触交流电压 198 211 <-- 较短
14 # 直流电压测量 172 182 <-- 较短
15 # 交流电压测量 374 384
16 # 直流电流测量 460 470
17 # 电阻测量 281 289
18 # 短路蜂鸣测试 140 150 <-- 较短
19 # 二极管测试 128 137 <-- 较短
20 # 电池测试 330 338
21 # 更换电池 132 140 <-- 较短
22 # 更换保险丝 215 224
|
统计摘要:
| Text Only |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | 总 section 数: 22
body 长度:
最小值: 0
最大值: 1351
平均值: 305
中位数: 198
长度区间分布:
[0, 100) 3 个
[100, 200) 9 个 <-- 大量集中在这里
[200, 500) 7 个
[500, 1000) 1 个
[1000, 2000) 2 个
[2000, +∞) 0 个
|
关键发现:大部分 section(12/22 = 55%)的 body 不到 200 字,只有 3 个 section 超过 500 字,最长的是"规格"表格(1351 字)。
第二步:确定 max_content_length(上限)
需要考虑的约束条件:
| Text Only |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | 约束 1: Embedding 模型的 Token 上限
|
| BGE-M3: 8192 Token
| 1 个中文字符 ≈ 1.5 Token
| 理论上限 ≈ 8192 / 1.5 ≈ 5400 字符
| 但不建议用满(太长的向量语义模糊)
|
约束 2: LLM 上下文窗口的利用效率
|
| 通常注入 3~5 个 chunk 作为上下文
| 如果每个 chunk 2000 字,5 个就是 10000 字
| 会挤占用户对话和指令的空间
|
约束 3: 检索精度与 chunk 大小的关系
|
| chunk 越小 → 向量越聚焦 → 检索越精确
| chunk 越大 → 向量越模糊 → 检索越粗糙
|
v
综合权衡: max_content_length 通常设在 500 ~ 2000 之间
|
用实际数据验证不同 max 值的切分效果:
| Text Only |
|---|
| max=500 需要二次切分: 3/22 个 (安全手册、规格、技术指标)
max=800 需要二次切分: 2/22 个 (规格、技术指标)
max=1000 需要二次切分: 2/22 个 (规格、技术指标)
max=1500 需要二次切分: 0/22 个 (全部保持原样)
max=2000 需要二次切分: 0/22 个 (全部保持原样)
|
分析:
max=500:会把 3 个 section 切碎,但大部分 section 本身就不到 500,切分比例适中
max=1000:只有"规格"和"技术指标"两个含表格的大 section 被切分,其余全部保留原始结构,是比较理想的平衡点
max=2000:没有任何 section 需要切分。看起来省事,但"规格"(1351 字)和"技术指标"(1016 字)的 chunk 体积偏大,向量语义会比较模糊
推荐值:对于这类技术文档,max_content_length = 800 ~ 1000 是比较理想的范围。
第三步:确定 min_content_length(下限)
需要考虑的因素:
| Text Only |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | min 太小 (如 50):
几乎不合并,保留大量碎片
--> 很多 chunk 只有一两句话
--> 向量语义稀薄,检索区分度差
min 太大 (如 800):
疯狂合并,几乎所有 section 都被吞掉
--> 丢失原有的标题边界
--> 合并后的 chunk 包含多个不相关主题
min 合适:
只合并确实太短的碎片
--> 保留有意义的标题边界
--> 每个 chunk 有足够的信息密度
|
用实际数据验证不同 min 值的合并效果:
| Text Only |
|---|
| min=50 可能被合并: 2/22 个 (只有空的 section)
min=100 可能被合并: 3/22 个 (极短的 3 个)
min=200 可能被合并: 12/22 个 (超过一半!) <-- 临界点
min=300 可能被合并: 15/22 个 (合并过度)
min=500 可能被合并: 19/22 个 (几乎全部)
min=800 可能被合并: 20/22 个 (只剩 2 个)
|
分析:
min=500(当前值):22 个 section 中有 19 个 body < 500,意味着绝大部分 section 都会尝试合并。最终产出的 chunk 数量很少,每个 chunk 包含了多个原始章节,主题可能混杂
min=200:12 个 section 会尝试合并,但这正好是那些确实太短的(100~200 字的操作步骤、警告提示)。像"交流电压测量"(374 字)、"电池测试"(330 字)这些内容完整的 section 会被保留
min=100:只合并 3 个极短的 section(body < 100 字),最大程度保留原始结构
推荐值:min_content_length = 100 ~ 200。具体取决于你的文档特征:如果文档中有大量简短的操作步骤适合合并,取 200;如果每个标题下的内容都相对独立,取 100。
第四步:参数评估方法论总结
在实际项目中,可以用以下脚本快速评估参数:
| Python |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40 | import re
def analyze_sections(md_path: str):
"""分析文档的 section 长度分布,辅助参数设定"""
with open(md_path, "r", encoding="utf-8") as f:
content = f.read()
heading_re = re.compile(r"^\s*(#{1,6})\s+(.+)")
sections = []
current_title = ""
body_lines = []
in_fence = False
def flush():
body = "\n".join(body_lines).strip()
if current_title or body:
sections.append({"title": current_title, "body_len": len(body)})
for line in content.split("\n"):
if line.strip().startswith("```") or line.strip().startswith("~~~"):
in_fence = not in_fence
match = heading_re.match(line) if not in_fence else None
if match:
flush()
current_title = line.strip()
body_lines = []
else:
body_lines.append(line)
flush()
lengths = [s['body_len'] for s in sections]
print(f"section 数: {len(sections)}")
print(f"body 长度: 最小={min(lengths)}, 最大={max(lengths)}, "
f"平均={sum(lengths)//len(lengths)}, 中位数={sorted(lengths)[len(lengths)//2]}")
# 建议
p75 = sorted(lengths)[int(len(lengths) * 0.75)]
p25 = sorted(lengths)[int(len(lengths) * 0.25)]
print(f"\n建议 max_content_length: {max(p75 * 2, 800)} ~ {max(p75 * 3, 1500)}")
print(f"建议 min_content_length: {max(p25, 100)} ~ {max(p25 * 2, 200)}")
|
评估流程:
| Text Only |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | Step 1: 对样本文档运行分析脚本
--> 得到 section 长度分布
|
Step 2: 确定 max
--> 参考 P75 的 2~3 倍
--> 同时不超过 Embedding 模型 Token 上限的 1/3
--> 确保 90% 以上的 section 无需切分
|
Step 3: 确定 min
--> 参考 P25
--> 确保只合并确实太短的碎片(< 20% 的 section)
--> 合并后不应丢失重要的标题边界
|
Step 4: 用测试文档验证
--> 查看 chunks.json 的实际效果
--> 人工抽查 3~5 个 chunk 的语义完整性
--> 微调参数
|
如何理解"确保 90% 以上的 section 无需切分"?
按标题切出来的 section 本身就是作者组织好的语义完整单元。二次切分是不得已而为之,会破坏原始语义结构。所以 max 的设定目标是:只对少数异常长的 section 动刀,绝大部分保持原样。
| Text Only |
|---|
| max=500 需要二次切分: 3/22 个 --> 86% 无需切分 (切得太多)
max=1000 需要二次切分: 2/22 个 --> 91% 无需切分 ✓ (只切异常长的)
max=1500 需要二次切分: 0/22 个 --> 100% 无需切分 (但大 chunk 语义模糊)
|
如果把 max 设得太小(比如 300),90% 的 section 都要被切碎,等于标题切分白做了,退化成了暴力按字数切。
如何理解"确保只合并确实太短的碎片(< 20% 的 section)"?
和 max 的 90% 是同一个思路,只不过方向相反。合并意味着把两个不同标题的内容混在一起,title 也会回退为 parent_title,丢失了原始标题信息。所以 min 的设定目标是:只对那些确实太短、单独存在没有意义的碎片做合并,其余保持原样。
| Text Only |
|---|
| min=100 可能被合并: 3/22 个 --> 14% 需要合并 ✓ (只合并极短碎片)
min=200 可能被合并: 12/22 个 --> 55% 需要合并 (临界,需要人工判断)
min=500 可能被合并: 19/22 个 --> 86% 需要合并 (过度!)
|
以 min=500 为例,86% 的 section 都会尝试合并,最终一个 chunk 可能包含了"电池安装"+"操作指导"+"数值保持按键"+"背光灯键"四个完全不同的主题。用户问"怎么开背光灯"时,检索到的 chunk 里掺杂了大量无关内容,影响 LLM 的回答质量。而 min=100 只会合并 body 为空的"# 操作指导"和只有一句话的"# 数值保持按键HOLD"这类真正的碎片。
万用表文档的最终推荐参数:
| 参数 |
当前值 |
推荐值 |
理由 |
max_content_length |
2000 |
1000 |
文档最长 section 为 1351 字,1000 可以切开它;P75=330,1000 ≈ P75 × 3 |
min_content_length |
500 |
200 |
P25=137,200 ≈ P25 × 1.5;min=500 会导致 86% 的 section 被合并,过于激进 |