RAG 系统中,文档切分不合理导致召回差,如何排查与解决?

文章目录

    • [引言:切分为什么是 RAG 的"隐形地基"](#引言:切分为什么是 RAG 的"隐形地基")
    • 一、先分清:问题真是切分引起的吗?
      • [第 1 段:生成能力正常吗?(Oracle 测试)](#第 1 段:生成能力正常吗?(Oracle 测试))
      • [第 2 段:答案真的在库里吗?(库内存在性检查)](#第 2 段:答案真的在库里吗?(库内存在性检查))
      • [第 3 段:这条 chunk 长什么样?(切分归因)](#第 3 段:这条 chunk 长什么样?(切分归因))
      • 归因决策树(速查)
    • 二、切分为什么会搞坏召回:三个必须建立的直觉
      • [直觉 1:向量是 chunk 的"平均印象",不是"精确索引"](#直觉 1:向量是 chunk 的"平均印象",不是"精确索引")
      • [直觉 2:相似度是相对排序,颗粒度不一致会天然"霸榜"](#直觉 2:相似度是相对排序,颗粒度不一致会天然"霸榜")
      • [直觉 3:边界效应------答案跨在切口上,两边都不像答案](#直觉 3:边界效应——答案跨在切口上,两边都不像答案)
    • [三、四类典型"切分病":现象 + 正反例](#三、四类典型"切分病":现象 + 正反例)
      • [病 1:定长硬切,切口随机切断语义(最高发)](#病 1:定长硬切,切口随机切断语义(最高发))
      • [病 2:chunk 太大,多个知识点挤在一起(向量稀释)](#病 2:chunk 太大,多个知识点挤在一起(向量稀释))
      • [病 3:chunk 太小,主语和限定条件丢失(碎片化)](#病 3:chunk 太小,主语和限定条件丢失(碎片化))
      • [病 4:表格、代码、列表被硬拆(实体丢失)](#病 4:表格、代码、列表被硬拆(实体丢失))
      • [附:overlap 用错了也会帮倒忙](#附:overlap 用错了也会帮倒忙)
    • 四、对症下药:从轻到重的四层方案
      • [第 1 层:先把朴素硬切的参数校准(结构简单的纯文本)](#第 1 层:先把朴素硬切的参数校准(结构简单的纯文本))
      • [第 2 层:结构感知切分,跟着文档的"骨架"切(制度/手册类优先推荐)](#第 2 层:结构感知切分,跟着文档的"骨架"切(制度/手册类优先推荐))
      • [第 3 层:语义切分(无结构长文本专用,注意权衡)](#第 3 层:语义切分(无结构长文本专用,注意权衡))
      • [第 4 层:父子切分(small-to-big)解决"召回准 vs 上下文全"的矛盾(大厂进阶方案)](#第 4 层:父子切分(small-to-big)解决"召回准 vs 上下文全"的矛盾(大厂进阶方案))
      • [进阶补充:上下文注入(Contextual Retrieval)](#进阶补充:上下文注入(Contextual Retrieval))
      • 别跟风:这些网上很火的切分方法,企业场景慎用
    • 五、落地工程:让切分可维护的两个关键设计
    • 六、用数据说话:评估与调参,别靠感觉
      • [1. 建黄金集(最容易被跳过,但最关键)](#1. 建黄金集(最容易被跳过,但最关键))
      • [2. 三个指标,分清是"没召回"还是"没排上"](#2. 三个指标,分清是"没召回"还是"没排上")
      • [3. 单变量对照实验 + 参数扫描](#3. 单变量对照实验 + 参数扫描)
      • [4. 上线验收表(防止改坏)](#4. 上线验收表(防止改坏))
    • 七、一个完整实战案例:企业制度问答系统优化全流程
    • 八、总结:可以带走的判断清单

适用场景 :做私有化知识库 RAG(内部制度、产品手册、技术文档等长文档)时,遇到"库里有答案、模型却答非所问"。

核心观点 :在长文档场景,召回差最常见的隐藏根因不是 Embedding 模型不行、不是向量库不好,而是文档切分策略不合理------同样一篇文档,切分方式不同,召回率可以差 30% 以上。


引言:切分为什么是 RAG 的"隐形地基"

把 RAG 想象成一场开卷考试:

  • 用户提问 = 考生看到的考题
  • 向量召回 = 考生翻笔记找相关内容
  • 大模型生成答案 = 考生照着笔记作答

而文档切分,就是你帮考生整理的那本"笔记页"。笔记整理得不好,考生再聪明也找不到答案:

  • 笔记页混着无关内容(上半页讲报销、下半页讲考勤)→ 考生翻到也找不到要的答案;
  • 笔记页把一个完整知识点劈成两半("年假累计满 X 年可休 Y 天..."一半在第 3 页、一半在第 7 页)→ 翻到哪页都凑不齐答案;
  • 笔记页只剩单个词(只写了"年假"两个字)→ 考生根本不知道对应完整规则。

三类问题的本质都是同一个结果:向量被稀释------一段文字混的内容越杂、切得越碎,它的向量方向就越模糊,和用户问题的匹配度就越低,最后就是"明明库里有答案,就是召不回来"。


一、先分清:问题真是切分引起的吗?

很多人的排查顺序是错的:一发现召回差就把 chunk_size 从 500 改成 100,改完更差了还不知道为什么。动手调参前,先花 10 分钟做责任划分。

RAG 完整链路是「解析 → 切分 → 向量化 → 检索 → 重排 → 生成」,任何一环出问题都表现为"答不好"。用三段归因法,从后往前剥:

第 1 段:生成能力正常吗?(Oracle 测试)

人工把标准答案所在的原文段落直接拼进 prompt,绕过检索让模型回答。

  • 模型答对 → 生成段没问题,问题在检索侧,继续往下查;
  • 模型答错 → 别查切分了,先解决 prompt / 模型 / 上下文过长的问题。

第 2 段:答案真的在库里吗?(库内存在性检查)

这是最容易被跳过、也最能一锤定音的一步:把答案里最关键的那句原话,拿到向量库里做全量字符串匹配。

  • 搜不到 → 内容根本没进库。查解析(PDF 扫描件、表格丢失、编码问题)和写入链路,与切分无关;
  • 搜得到,但这条 chunk 不在 topK 里 → 这是切分或排序问题,进入下一段;
  • 搜得到,也在 topK 里,但模型没用 → 是重排 / prompt 组装问题(比如 topK 太长,答案被挤到中间)。

第 3 段:这条 chunk 长什么样?(切分归因)

把包含答案的那条 chunk 打印出来,对照三个问题:

  1. 答案句是否完整? 主语、谓语、宾语、限定条件是否都在同一个 chunk 里?
  2. 无关内容占比多少? 这条 chunk 里和 query 无关的字数占比超过 60% 吗?
  3. 指代是否有落点? chunk 里出现的"它""该方案""上述配置",指代对象还在不在这个 chunk 里?

三个问题里只要有一个"否",基本可以判定是切分问题。

归因决策树(速查)

text 复制代码
答不好
 ├─ 人工塞原文能答对?
 │   ├─ 否 → 生成侧问题(prompt / 模型 / 上下文过长)
 │   └─ 是 ↓
 ├─ 答案原句在库里能搜到?
 │   ├─ 否 → 解析或写入问题(切分无关)
 │   └─ 是 ↓
 ├─ 这条 chunk 在 topK 内?
 │   ├─ 否 → ① 切分边界破坏语义  ② 颗粒度太小  ③ 排序被霸榜
 │   └─ 是 ↓
 └─ chunk 内容完整、无稀释、无裸指代?
     ├─ 否 → 切分问题(本文正题)
     └─ 是 → 重排 / topK 截断 / prompt 组装问题

一句话心法:动参数之前,先打开一条 chunk 看一眼数据。 别在没打开过任何一条 chunk 的情况下,就开始网格搜索 chunk_size。


二、切分为什么会搞坏召回:三个必须建立的直觉

直觉 1:向量是 chunk 的"平均印象",不是"精确索引"

把一条 chunk 想成一张合影 ,它的向量就是合影的整体印象。检索时,系统拿 query 去和每张合影的"整体印象"比对。这直接推出两个结论:

  • chunk 切太碎 → 合影里只有半张脸,信息不全,query 描述的是一个完整的人,自然对不上;
  • chunk 切太大 → 合影里有二十个人,query 只喊其中一个人的名字,这个人的特征被其他人平均掉了,相似度被稀释。

所以 chunk 大小没有"最优值",只有"和你的 query 粒度匹配的值"。

直觉 2:相似度是相对排序,颗粒度不一致会天然"霸榜"

向量检索返回的是排序 ,不是绝对阈值。一条又长又泛的 chunk 只要包含大量高频词,就很容易在多个 query 上都拿到中等偏上的相似度,从而稳定占据 topK 名额,把真正精确的小块挤出 topK------表现出来就是"召回率不低,但 Precision 很差"。

直觉 3:边界效应------答案跨在切口上,两边都不像答案

拿一段真实的 Redis 文档举例:

text 复制代码
## 3.2 持久化
Redis 提供 RDB 和 AOF 两种持久化方式。RDB 在指定时间间隔内将内存中的数据集
快照写入磁盘,恢复速度快,但可能丢失最后一次快照之后的数据。AOF 以日志形式
记录每个写操作,可以做到秒级数据不丢失,但文件体积更大、恢复更慢。

如果按 100 字硬切,切口恰好落在"AOF 以"后面,于是得到两条 chunk:

  • chunk A:讲 RDB,末尾挂着半句"AOF 以";
  • chunk B:开头是"日志形式记录每个写操作,可以做到秒级数据不丢失......"。

用户问:"AOF 持久化会不会丢数据?"

  • chunk A 里有 "AOF" 这个词,但通篇讲的是 RDB,语义不匹配;
  • chunk B 内容是对的,但开头没有主语------"日志形式记录每个写操作"里既没有 "Redis" 也没有 "AOF",向量里缺少最关键实体词。

结果两条 chunk 谁的相似度都不够高,正确答案就这么丢了。这不是模型不行,是切口把主语和内容分家了。


三、四类典型"切分病":现象 + 正反例

以下都用同一份企业内部制度样例作演示:

text 复制代码
4.2 年假申请一般规定
4.2.1 员工累计工作已满1年不满10年的,年休假5天;已满10年不满20年的,年休假10天;已满20年的,年休假15天。
4.2.2 员工有下列情形之一的,不享受当年的年休假:事假累计20天以上且单位按照规定不扣工资的;累计工作满1年不满10年的员工,请病假累计2个月以上的。
4.2.3 年假需要提前3个工作日在OA系统提交申请,由直属主管审批。

病 1:定长硬切,切口随机切断语义(最高发)

  • 现象:答案腰斩、跨段落混装、chunk 开头出现半句话。
  • 错误配置 :chunk_size=300,chunk_overlap=50,按字符硬切,刚好在 4.2.1 末尾切断,4.2.2、4.2.3 被切到下一块。
  • 后果 :用户问"哪些情况员工不能享受当年年假?",正确答案在 4.2.2,却只召回 4.2.1 那一块,答案丢失。
  • 本质:不尊重文档本身自带的章节、条款边界,只看字符数量------等于把作者已经免费标好的语义边界扔掉。

病 2:chunk 太大,多个知识点挤在一起(向量稀释)

  • 现象:召回的 chunk 是一整节超长内容,答案只占其中一句话;或与无关 chunk 撞向量、误召回。
  • 错误配置 :chunk_size=1500,把 4.2 整个小节(年假天数定义 + 不享受情形 + 审批流程)全塞进同一个 chunk。
  • 后果:用户问"年假怎么提交申请?",该 chunk 向量同时含好几个主题,特征混杂,容易误召回或排序靠后。

❌ 反面教材(一个 chunk 串三个不相关主题):

报销申请需在费用发生后 30 天内提交,由直属主管审批。公司为全体员工投保了补充商业医疗保险,覆盖门诊和住院。绩效考核每年 6 月和 12 月各开展一次,结果作为晋升依据。

用户问"补充医疗保险怎么报销",根本匹配不上------向量是三个方向的平均值。

✅ 正面教材(一个 chunk 只讲一件事):

【制度 3.4.2 条】员工补充商业医疗保险报销要求:

  1. 报销范围:门诊及住院费用,符合保险条款约定的均可申报
  2. 报销流程:凭发票及病历材料提交至 HR 保险专员
  3. 处理时限:材料齐全后 5 个工作日内完成打款

一个 chunk 只讲一个知识点,向量方向非常清晰,用户问对应问题时直接就能命中。

病 3:chunk 太小,主语和限定条件丢失(碎片化)

  • 现象:chunk 信息残缺,向量含义模糊,容易误匹配。
  • 错误配置 :chunk_size=80,4.2.2 一句话被切成两块:

块 A:员工有下列情形之一的,不享受当年的年休假:事假累计 20 天以上且单位按照规定不扣工资的

块 B:累计工作满 1 年不满 10 年的员工,请病假累计 2 个月以上的。

单独看块 A,信息残缺,很容易匹配到别的"事假"相关文档。同理,一条 chunk 是"它支持水平扩展,单集群可承载千万级连接",却不知道"它"是谁------句子级切分把指代链切断了,人类阅读能靠上文补全,向量模型没有上文。

病 4:表格、代码、列表被硬拆(实体丢失)

  • 现象 :问"银行卡退款多久到账",召回的 chunk 只有"银行卡 3-7 个工作日",没有表头,也没有"退款""时效"两个词;或者代码块被从中间切断,函数的 def 和 return 分居两块。
  • 原因 :表格的语义分布在表头与单元格之间。切开后,单元格成了孤立的词串,向量里最关键的列名信息彻底丢失。
  • 修复 :把表格当作原子块 ,并做行级展开------每一行都拼上表头,转成自解释的自然语言。原始表:
支付方式 退款到账时效
微信支付 1-3 个工作日
支付宝 实时到账
银行卡 3-7 个工作日

转换后,每一行变成一条独立 chunk:

text 复制代码
【退款规则表】支付方式=银行卡;退款到账时效=3-7 个工作日。
【退款规则表】支付方式=支付宝;退款到账时效=实时到账。

这样"退款""到账""时效""银行卡"四个关键实体全部落在同一条 chunk 里,无论向量检索还是 BM25 都能命中。代码块同理:整体保留,并在块前补一行注释说明它做什么(# 计算订单退款的应退金额,含优惠券摊销),这行注释往往比代码本身更容易被检索到。

附:overlap 用错了也会帮倒忙

很多人把 overlap 当"保险参数",习惯性设成 20%~40%。但 overlap 的直接代价是:库里 30% 的内容是重复的,而 topK 又是固定名额 。举例:chunk_size=512、overlap=200,意味着每条 chunk 有 39% 的内容和上一条重复;如果一段答案横跨三条 chunk,你会用掉三个名额去描述同一件事。

正确做法:

  • overlap 控制在 10%~15%,只用来保证句子不被拦腰截断;
  • 对法条、代码这类"绝对不能被切断"的内容,用结构边界(条款号、函数定义)而不是 overlap 来保完整。

四、对症下药:从轻到重的四层方案

排查出问题后,不必一上来就用最复杂的方案,按从轻到重的梯度选即可。

第 1 层:先把朴素硬切的参数校准(结构简单的纯文本)

90% 的人一开始用的都是 RecursiveCharacterTextSplitter,但参数全是抄网上的,没结合自己业务。正确校准方法:

  1. 先统计业务文档里单个完整知识点的平均长度(比如制度文档里每个条款平均 300 字);
  2. chunk_size ≈ 平均知识点长度 × 1.2,企业技术文档一般设 300~400 字,绝对不要超过 500 字;
  3. overlap = chunk_size × 10%~15%,避免切断跨句子的内容;
  4. 切分分隔符优先选文档结构边界(标题、段落、句子),最后才选标点。
python 复制代码
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 企业制度/规范文档的推荐配置
splitter = RecursiveCharacterTextSplitter(
    chunk_size=350,          # 对应制度条款平均长度 300-400 字
    chunk_overlap=50,        # 15% 重叠,避免切断跨章节句子
    separators=["\n## ", "\n### ", "\n", "。", ",", " "],  # 绝不把一个句子从中间劈开
    length_function=len,
)

适用场景:结构简单的纯文本 FAQ、产品说明文档。

第 2 层:结构感知切分,跟着文档的"骨架"切(制度/手册类优先推荐)

核心思路:不看字数,跟着文档本身的结构切,绝不把两个不同章节的内容切到同一个 chunk 里。 这是收益往往最大的方案。

具体要做:

  1. 带标题的 Word/Markdown:按 H1/H2/H3 标题作为切分边界,每个三级标题下的内容独立成 chunk;
  2. 表格内容:把整个表格转成 Markdown 格式作为独立 chunk,不要把表格硬拆成零散文字碎片;
  3. 预处理先洗垃圾:切分前先把 PDF 里的页眉页脚、水印、重复目录去掉------这些噪音进 chunk 一定会稀释向量。
python 复制代码
from langchain_text_splitters import MarkdownHeaderTextSplitter

headers_to_split_on = [
    ("#", "h1"), ("##", "h2"), ("###", "h3"), ("####", "h4"),
]
splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
# 切分后,header 信息会自动存入 metadata,检索时可以做过滤、增强

如果是 PDF 文档,建议先通过解析工具还原标题层级、转成结构化 markdown 之后再切分,不要直接对原始 PDF 文本硬切。

踩坑提醒:曾有团队把报销审批流程规范表按 500 字硬切,把三列内容切成三个 chunk,每个 chunk 只有半行表格字,向量完全不对;改成整个表格作为一个 chunk 后,相关问题召回率从 20% 升到 90%。

第 3 层:语义切分(无结构长文本专用,注意权衡)

思路:把文本按句拆开,计算相邻句子的向量相似度,在相似度骤降处断开。适合会议纪要、客服对话、无标题的 PDF 扫描件这类没有规整标题的文本。

必须说清的 trade-off:

  • 边界不稳定,同一份文档跑两次可能切出不同的块;
  • 阈值对语料极其敏感,换一个领域就要重新标定;
  • 每句都要做 embedding,成本是结构切分的数倍。

结论:它适合无结构长文本,不适合已经有良好标题体系的文档------有标题就用标题,那是作者免费送给你的语义边界。

第 4 层:父子切分(small-to-big)解决"召回准 vs 上下文全"的矛盾(大厂进阶方案)

很多人会遇到两难:切小了召回准但没上下文、大模型拿片段答不对;切大了上下文全但召回不准。父子切分把两者解耦:

  • 子块(100~300 字):细粒度知识点,用来向量化、入库检索------粒度细,匹配准;
  • 父块(400~1200 字):子块所在的完整章节,命中子块后回溯父块喂给大模型------上下文全,回答准;
  • 关键细节 :检索时用子块多召回一些(如 topK×3),按 parent_id 去重父块,同一父块只取一次,避免重复占位。
python 复制代码
from langchain_text_splitters import ParentDocumentRetriever

retriever = ParentDocumentRetriever(
    vectorstore=vector_store,
    docstore=docstore,
    child_splitter=RecursiveCharacterTextSplitter(chunk_size=100),  # 细粒度子块,用于召回
    parent_splitter=RecursiveCharacterTextSplitter(chunk_size=500), # 完整父块,喂给大模型
)

进阶细节 :在子块的检索文本前拼上标题路径(如"运维手册 > 3 持久化 > 3.2 AOF"),子块只有 250 字时,这串路径提供的语义信息可能比正文还多。

适用场景:长技术规范、制度文档、操作手册。

进阶补充:上下文注入(Contextual Retrieval)

如果 chunk 太小丢了主语,可给每条 chunk 用 LLM 生成一句"这段在讲什么"(50 字内,点明主体对象),拼在检索文本前再向量化------这是 Anthropic 在 2024 年提出的思路。工程上三个降本手段:

  • 用便宜的小模型批量处理,质量损失很小;
  • 缓存同一章节的公共前缀,只生成差异部分;
  • 只对检索文本做上下文注入,送进 LLM 的生成文本保持原样,避免上下文说明污染答案。

别跟风:这些网上很火的切分方法,企业场景慎用

  1. LLM 语义切分:效果确实好,但成本极高------切 1000 页文档要花几百块、速度慢,企业海量文档场景根本不实用;
  2. 超大 chunk(1000 字以上):只适合极短的 FAQ 场景,长文档里向量稀释会非常严重;
  3. 切分粒度越细越好:chunk 切到单个词语时,向量没有足够语义信息,召回会乱套------用户问任何和"报销"相关的问题都可能召回到"报销"二字的孤立 chunk,反而丢失完整规则。

五、落地工程:让切分可维护的两个关键设计

设计 1:检索文本与生成文本分离(全文心法)

一个 chunk 其实需要两份文本,别混在一起:

用途 内容构成 目标
检索文本 retrieval_text 标题路径 + 上下文摘要 + 正文(可含表格展开) 最大化与 query 的语义匹配
生成文本 generation_text 干净的原文 + 页码/来源引用 让 LLM 看到准确、无噪声的证据

很多团队的困境------"加了上下文能让召回变好,但加进去又让 LLM 答得更啰嗦"------本质是把这两个用途混在了一份文本里。分开之后,两个目标不再互相打架。

设计 2:给每个 chunk 一套规范字段

字段 说明
chunk_id 全局唯一,建议 {doc_id}#{section}#{seq},便于人工定位
doc_id / doc_title 溯源与引用展示
heading_path 标题路径数组,如 ["3 持久化", "3.2 AOF"]
parent_id 父块 ID,支撑父子切分
retrieval_text 用于向量化与 BM25 的文本
generation_text 送入 prompt 的原文
page / char_range 原文定位,用于高亮回链
chunk_type text / table_row / code / list,方便差异化检索

chunk_type 价值很高:可以对 table_row 类型的块放宽相似度阈值(文本短、实体密度高),也可以对 code 类型单独走一条关键词检索通道。

文档类型路由表(经验起点,不是标准答案)

切分策略没有银弹,没有一套参数适合所有文档。企业知识库建议做文档分类路由:

文档类型 建议块长 overlap 优先策略
FAQ / 客服话术 150~300 0 一问一答作为原子块,不要跨问答切
产品文档 / 技术手册 400~700 10% 按标题层级 + 表格行展开
合同 / 法条 按条款 0 条款号是天然边界,绝不跨条
研报 / 论文 800~1200(父块 2000) 10% 按章节 + 父子切分
会议纪要 / 对话 200~400 0 语义切分,按发言人 + 话题分段

这张表只是起点。任何"通用最佳 chunk size"的说法都忽略了语料差异------query 的粒度和文档的粒度共同决定了答案。


六、用数据说话:评估与调参,别靠感觉

1. 建黄金集(最容易被跳过,但最关键)

没有评测集的切分调优,本质上是在盲调。最小可用版本:

  • 规模:30~50 条 query 就够启动,不必等几百条;
  • 来源:线上真实提问日志(去掉重复意图)+ 业务专家手写;
  • 标注粒度 :标到句子级/片段级,而不是文档级------因为你要判断的正是"边界",只标文档 ID 无法定位切分问题。

标注文件用 JSONL,一条一行:

jsonl 复制代码
{"qid": "q017", "query": "AOF 持久化会不会丢数据", "gold_doc": "redis-ops-manual.md", "gold_span": "AOF 以日志形式记录每个写操作,可以做到秒级数据不丢失"}
{"qid": "q018", "query": "银行卡退款多久到账", "gold_doc": "payment-faq.md", "gold_span": "银行卡 3-7 个工作日"}

gold_span 这一列是精髓:即使 chunk 重新切了、ID 全变了,你依然可以用文本包含关系重新判定命中,评测集不会因为切分策略变化而失效。

2. 三个指标,分清是"没召回"还是"没排上"

指标 含义 判读
Recall@5 正确 chunk 是否出现在前 5 条 直接对应线上体验
Recall@50 正确 chunk 是否出现在前 50 条 区分"检索不到"和"排序不佳"
MRR@10 正确 chunk 排在第几位 反映排序质量

关键判读逻辑:

  • Recall@5 低,Recall@50 也低 → 内容没进库,或切分彻底破坏了语义,回到第一节第 2 段确认;
  • Recall@50 高,Recall@5 低 → chunk 在库里、能被检索到,但排不上去。典型病因是颗粒度不一致、有超长块霸榜,或缺少重排;
  • Recall@5 高,但回答仍然错 → 问题不在召回,去看 prompt 组装和上下文截断。

如果已经在用 RAGAS,可以用它的 context_recall 和 context_precision,但注意它依赖 LLM 判定、成本高、抖动大。定位切分问题时,基于字符串匹配的确定性指标更可靠------它不会骗你。

3. 单变量对照实验 + 参数扫描

只改切分,冻结 embedding 模型、向量库、重排模型、topK,用同一套黄金集跑一次。示例结果(数据为演示用途):

切分策略 平均块长 Recall@5 Recall@50
定长 512 / overlap 50 512 0.61 0.79
定长 512 + 标题路径注入 556 0.68 0.86
按标题层级切分 730 0.81 0.93
标题层级 + 表格行级展开 702 0.87 0.95

这张表最重要的信息不是"提升了 26 个点",而是 Recall@50 从 0.79 到 0.95 ------说明有 16% 的 query,正确内容原本就检索不到,纯粹是被切分弄丢了。这类提升是换 embedding 模型换不来的。

chunk_size 也要扫参数网格。注意最后一点:Recall 最高点往往不是成本最优的点------chunk 越大,top5 塞进 prompt 的 token 越多,延迟和费用同步上涨,选点上要同时看召回和 token 成本。

4. 上线验收表(防止改坏)

切分策略一改,影响是全局的。上线前至少跑完这张表:

类别 指标 达标线
效果 Recall@5 不低于改动前,理想提升 10 个点以上
效果 端到端答案准确率(人工抽检 50 条) 不下降
效果 引用准确率(回答引用的段落是否真的支持结论) 不下降
成本 平均 chunk 数 / 文档 记录变化,评估存储与 embedding 成本
成本 平均 prompt token 数 记录变化,评估推理费用
质量 chunk 重复率(overlap 造成的重复占比) 低于 15%
质量 短块占比(< 100 字的块) 低于 10%,否则人工检查是否切碎
质量 无主语块占比 抽样 50 条,异常率低于 5%

最后三项"质量指标"建议加进监控------它们不需要标注数据,可以在每次全量重建索引时自动算,提前预警切分退化。


七、一个完整实战案例:企业制度问答系统优化全流程

  1. 初始问题:用户问"补充医疗保险怎么报销",召回回来的 chunk 是讲考勤制度的,测试集 Recall@5 只有 35%。
  2. 根因定位:之前用 500 字硬切,把报销、保险、绩效考核三个不同章节的内容切到同一个 chunk 里,向量被平均了。
  3. 优化动作 :
    • 第一步:先洗文档,把 PDF 里的页眉页脚水印、重复目录全部去掉;
    • 第二步:按三级标题做结构感知切分,每个章节独立成 chunk;
    • 第三步:配置父子切分,100 字的子块用来召回,500 字的父块喂给大模型;
    • 第四步:把 chunk_size 从 500 调到 350,overlap 从 0 调到 50。
  4. 优化结果 :Recall@5 从 35% 升到 88%,用户问制度相关问题基本都能准确召回到对应条款。全程没换 Embedding 模型、没加昂贵的重排序模型,只是把切分从硬切改成了结构切分,效果就提升了一大截。

八、总结:可以带走的判断清单

  1. 切分的目标不是均匀,而是可检索。 长度整齐的 chunk 看起来很工程化,但对检索毫无意义。
  2. 先排查切分,再调 Embedding、Rerank、Query 改写。 切分是 RAG 最底层环节,优先排除它。
  3. 检索单元 ≠ 生成单元。 小块负责被找到,大块负责被理解,用父子切分(small-to-big)把两者解耦。
  4. 优先利用文档原生结构(标题、编号、条款),结构化切分收益往往最大。
  5. 边界的两大天敌是指代和表格。 切口落在哪里,决定了主语和表头还在不在。
  6. overlap 是补充手段,不是万能药。 它解决跨句子的连续语义,修复不了被切断的独立条款。
  7. 没有黄金集的切分调优都是玄学。 30 条标注就能让你从"感觉不对"走到"知道哪里不对"。

如果只能记住一句话:先打开一条 chunk 看看,再去调参数。

相关推荐
Flynn Liang11 天前
内存/Redis/PGVector/Milvus/ES 怎么选?Spring AI VectorStore 抽象与向量库选型对照表
redis·elasticsearch·milvus·rag·springai·pgvector·向量库
梦想的颜色1 个月前
【AI速览】RAG 向量数据库选型:Milvus、Chroma、Qdrant、PGVector 全维度横向对比
数据库·milvus·chroma·rag·pgvector·向量库·qdrant
thesky1234562 个月前
智能体面试准备(五十九):智能体长期记忆架构与检索增强记忆——MemGPT、记忆巩固与多会话隔离
智能体·memgpt·向量库·长期记忆·记忆架构·检索增强记忆·多会话隔离
xfchn多多学习学习3 个月前
ChatBI 语义层设计详解
chatbi·向量模型·向量库·语义层建设
马优晨6 个月前
大语言模型(LLM)、Embedding 模型、reranker重排序模型 有什么关系
人工智能·语言模型·embedding·embedding 模型·大语言模型(llm)
装不满的克莱因瓶8 个月前
【从零搭建】SpringAI Alibaba + RAG + Milvus + Qwen 项目实战
人工智能·ai·大模型·milvus·rag·springai·向量库
Lw中8 个月前
Chroma查询集合:从基础检索到高级过滤的完整指南
搜索引擎·向量库·chroma向量库
CCPC不拿奖不改名9 个月前
RAG基础:基于LangChain 的文本分割实战+文本分块
人工智能·python·langchain·知识库·改行学it·rag·向量库
递归尽头是星辰10 个月前
大模型与向量检索的融合:从核心原理到 Spring AI 落地
人工智能·大模型·向量检索·rag·spring ai·向量库