一个 RAG 项目的复盘:我们高估了检索,低估了切片
这是我做的第二个 RAG 项目,也是让我真正理解”切片决定上限”的项目。之前在《为什么大多数 RAG 系统都过不了第一版验收》里讲过方法论,这篇是那个方法论诞生前的血泪史——正是这次复盘让我形成了后来的判断框架。
项目背景
一个制造业客户,要做设备故障知识问答。知识源是 300 多份设备手册(PDF,含大量表格和流程图)+ 2 年的历史维修工单(约 1.2 万条)。目标:维修工程师遇到故障时,用自然语言提问,系统返回排查步骤和相关工单。
demo 阶段一切顺利。我们用了标准方案:PDF 解析 → RecursiveCharacterTextSplitter(chunk_size=1000, overlap=200)→ 向量化 → 检索 Top-5 → LLM 生成答案。10 个 demo 问题准确率 90%,客户很满意,直接进入验收阶段。
第一周:demo 惊艳
demo 的 10 个问题是我们和客户一起挑的”典型问题”,大多长这样:“CNC-088 主轴异响怎么排查?""液压系统压力不足的可能原因?“——标准问句、明确指向、手册里有对应章节。
这种问题检索一命中,LLM 就能答得很好。准确率 90% 看起来很美,但我们忽略了一个事实:这 10 个问题都是”为检索优化过”的。它们的表述方式和手册标题高度一致,检索几乎不需要”理解”,只需要”匹配”。
第二周:开始调优
验收时客户准备了 40 个问题,准确率掉到了 62%。我们开始按常规思路调优。
rerank 调优
第一反应是”检索质量不够”。我们加了 rerank:先把检索 Top-20 用 bge-reranker 重排,再取 Top-5。准确率从 62% 提到 68%。
有效,但提升有限。而且我们注意到一个现象:有些问题 rerank 前后排序完全没变——说明这些问题本来检索就命中了正确的文档,rerank 没起作用。真正改善的是那些”正确文档在 Top-10 但不在 Top-5”的问题。
混合检索
第二步是加混合检索:向量检索 + BM25 关键词检索,用 RRF 融合。这一步把准确率从 68% 提到了 73%。
提升主要来自”设备型号 + 故障代码”类问题。这类问题关键词匹配比语义匹配更准,因为故障代码是固定编号,向量检索反而会把语义相近但型号不同的结果排前面。
到这一步我们花了两周,准确率从 62% 提到 73%。看起来在进步,但离客户要求的 85% 还差很远。更让人不安的是:每周提升幅度在递减。第一周 +6%,第二周 +5%。按这个趋势,再调两周可能只能再提 3–4 个百分点。
第三周:bad case 归因
进度停滞让我决定停下来做 bad case 归因。我把 40 个验收问题里答错的 11 个全部拉出来,逐个看检索结果和 LLM 输出。
归因结果让我意外:
- 检索没召回正确文档:3 个(27%)。这是真正的检索问题。
- 检索召回了但被 rerank 挤掉:1 个(9%)。rerank 的锅。
- 检索召回了正确文档,但关键信息被切到了另一个 chunk:5 个(45%)。切片问题。
- 检索和切片都没问题,LLM 没用好上下文:2 个(18%)。prompt 问题。
**关键发现:**54% 的错误(切片 + prompt)和检索质量无关。我们花两周调检索,只覆盖了 36% 的问题。真正的瓶颈在切片。
切片重设计
我仔细看了那 5 个”切片问题”,发现三种典型模式:
模式一:表格被切成两半。设备手册里有大量故障排查表(故障现象 / 可能原因 / 排查步骤 / 所需工具)。1000 字的 chunk 经常把表格从中间切开,检索只命中了”故障现象”那一行,“排查步骤”在下一个 chunk 里。LLM 拿到现象但拿不到步骤,只能自己编。
模式二:判断和条件被分开。手册里常见这种结构:“如果压力低于 3MPa,检查 A 阀门;如果压力在 3–5MPa,检查 B 阀门”。切片时把条件和判断分开了,LLM 拿到”检查 A 阀门”但不知道前提条件是”压力低于 3MPa”。
模式三:工单的问和答切开。历史工单里”故障描述”和”处理方案”是两个字段,切片时可能分到不同 chunk。检索命中的是故障描述,但处理方案在另一个 chunk 里。
基于这三个模式,我重新设计了切片策略:
- 表格整张切。不按字数切,按表格边界切。一张表再长也保持完整。代价是 chunk 长度不齐(有的 200 字,有的 3000 字),但下游 LLM 能处理。
- 条件 + 判断一起切。“如果…则…”结构不切开。用规则识别条件句,把条件和它的判断绑在一个 chunk 里。
- 工单整条切。一条工单(故障描述 + 处理方案 + 结果)作为一个 chunk,不再拆分。
重设计后,chunk 数量从 4200 增加到 5800(因为有些大表格不能再切),平均长度从 800 字变成 1100 字,方差变大。但准确率从 73% 直接跳到了 87%——一周的切片调整,比两周的检索调优效果还好。
教训
这次复盘给了我三个教训:
教训一:调任何东西之前先做归因。我们花了两周调检索,但 54% 的问题和检索无关。如果第一周就做归因,能省一周。归因的成本是半天,不做的成本是两周。
教训二:切片是 RAG 最被低估的环节。检索决定了”能不能拿到”,切片决定了”拿到的完不完整”。一个被切散的答案,检索再准也拼不回来。而且切片是前置决策——一旦切好了,后续所有环节都在这个基础上运行。切片错了,后面再怎么调都是治标不治本。
教训三:chunk 长度均匀不是目标,语义完整才是。标准做法追求 chunk 长度均匀(好做 batching),但均匀的代价是语义被切断。正确做法是以语义完整为单位切,让下游处理长度不齐。LLM 完全有能力处理 200–3000 字长度不等的 chunk,比处理被切断的语义容易得多。
结语
这个项目之后,我在所有 RAG 项目里都加了两个硬性规则:切片必须以语义完整单位为边界,调参之前必须做 bad case 归因。
这两条规则不高级,但它们让我的 RAG 项目第一版验收通过率从 30% 提到了 70%。剩下 30% 才是模型、向量、prompt 这些真正需要打磨的部分。而在此之前,先确保你的切片没有把答案切碎——这是最基本、也最容易被忽略的一步。