企业 RAG 的准确率是在入库前挣出来的:12 个杠杆、六条铁律与一次 4000 个垃圾切片的事故
企业 RAG 的准确率主要取决于什么?
取决于入库前把数据结构化到什么程度,而不是检索那一步用了什么算法。检索侧只是把入库时打好的结构用起来。在一个已上线的企业知识库项目中,同一批源文件因为解析方式错误产生了约 4000 个 base64 垃圾切片,语料虚高到 6961 条、词表 47 万,答案「像是、但不对」;修正后回落到 2884 条真实切片、词表 10 万,真实内容零丢失,13 条抗污染探针全部通过。
多数 RAG 教程把篇幅花在检索侧——用什么向量库、怎么调 top-k、要不要加 rerank。而在真实的企业知识库项目里,决定成败的工作发生在这些之前。
本文复盘一个已经上线的企业知识库项目:十几种来源的产品文档,最终 2884 个切片,13 条抗污染探针全部通过,无第三方 RAG 框架。客户身份与全部产品标识已匿名处理,方法、数字与事故记录为真实过程。
关键结论
- RAG 的准确率不是在检索那一步挣出来的,是在入库前把数据结构化挣出来的。
- 永远按文件头判断格式,不要信扩展名——这个低级错误造成了本项目最大的一次事故。
- 数量异常是最好的报警器。「切片数比预期多一倍」「词表大到离谱」比任何人工抽查都灵敏。
- 能用确定性代码保证的,就不要交给模型去「倾向于」。
- 找到你这个领域特有的错误类型,为它单独建指标。指标错了,优化方向全错。
一、教程讲的和真正决定成败的,不是同一件事
先看这条流水线,左侧是本文的重点,右侧是常规教程的重点:
原始来源(十几类)
│
│ ① 格式归一化 各种格式 → 干净 Markdown + 抽图 ← 脏活,决定天花板
│ ② 切分 一来源一提取器,按语义边界 ← 脏活
│ ③ 元数据标注 实体 / 能力 / 通道 / 范围 / 受众 ← ★胜负手
│ ④ 上下文前缀 把元数据写进正文头部,一起参与嵌入 ← ★便宜且高收益
│ ⑤ 双索引 关键词 + 向量
▼
│ ⑥ 查询理解 → 路由 → 硬过滤 → 混合召回 → 精排 → 证据门禁 → 生成
▼ ← 常规教程的全部内容
答案
建议把 60% 的力气花在 ①②③④,40% 花在 ⑥。 多数团队反过来,结果就是「模型不行」的错觉。
二、事故:18 个文件如何产生 4000 个垃圾切片
这是本项目最值得讲的一段,因为它症状隐蔽、代价巨大、根因低级。
现象:语料数量虚高到 6961 条,关键词索引词表 47 万词项,检索结果里混进大量乱码,答案「像是、但不对」。
过程:18 个扩展名为 .doc 的用户手册,被当作 Word 文档交给文档转换工具处理。工具不报错,它把整个 MIME 源码当成文本转了出来——于是 191 个乱码切片,加上约 4000 个纯 base64 编码的「切片」,全部进了索引和向量库。
根因:这些文件是协作平台的「导出为 Word」,本质是 MHTML(以 MIME-Version / multipart/related 开头的多部分文档,图片以 base64 内嵌)。扩展名骗了所有人。
修复:写专用转换器,读文件前 400 字节按 MIME 头识别,用邮件解析库处理,base64 图片段按 magic byte 判类型落盘。
结果:22 本手册零 MIME 残留,额外抽出 405 张图,语料回落到 2884 条真实切片,词表 10 万,探针 13/13 通过,真实内容零丢失。
| 修复前 | 修复后 | |
|---|---|---|
| 切片数 | 6961 | 2884 |
| 关键词词表 | 47 万 | 10 万 |
| 抗污染探针 | — | 13 / 13 |
三条通用规律:
- 永远按文件头判类型。 数据处理流水线的第一个函数就该是
sniff_format()。 - 数量异常是最好的报警器。 本项目正是靠这两个指标发现问题的,不是靠人工抽查。
- 静默降级比报错危险。 转换工具不报错地产出垃圾,比直接失败糟糕得多。所以流水线必须有「产物合理性检查」。
三、六条铁律
这六条写在方案文档的第一部分,是全项目所有取舍的裁判标准。
- 像素与噪音不进嵌入 —— 只有承载信息的文字进向量。图标、格式残留、OCR 标签、MIME 源码、base64、图片文件名全部清掉。
- 元数据是一等公民 —— 检索精度由过滤字段决定,不是靠嵌入去猜。
- 共享内容去重一份、专属内容按实体实例化 —— 跨实体的共用内容存一份并标「适用范围」;有分歧的步骤拆开并标注分歧点。
- 单一规范源 —— 同一内容有多种形态(原文 / 问答 / 表格)时只入一种,否则自己和自己打架。
- 可追溯、可回滚 —— 每条切片记来源;清理数据只「移动到搁置区 + 写清单」,绝不删除。
- 数据自包含 —— 图、文、索引全部归集到一处,运行时不依赖原始目录,源文档可整体归档。
第 5 条值得单独强调。本项目清理约 3800 个 OCR 中间产物时,脚本只搬走了 4 个明确重复的文件就停下来等人确认,大目录不敢动。这个克制是对的——数据处理流水线里,不可逆操作必须是人的决定。
四、切分:不做通用切分器
十几类来源各写一个提取器,全部产出统一结构的切片对象。
为什么不用一个通用的递归字符切分器(比如 800 字符、100 重叠)?因为固定长度对这批数据全是错的:
- 一个手册主题就是一个完整语义单元,切开会把「步骤 3」和「步骤 4」分到两个切片,检索命中其一就答不全;
- 一篇故障排查文章是一个问题的完整排障过程,切开等于把「现象」和「解决方案」拆散;
- 端口表切 800 字符是灾难(见第六节);
- 规格表按字符切会把表头和数据行切散。
规律:切分单位应当是源数据自身的语义边界——一个主题、一篇文章、表格的一个逻辑分组、表格的一行——而不是一个字符数。只有真正没有结构的长文本才退化到按长度切,而且要在行边界断开,绝不切断一行。
实测的切分粒度:平均 1264 字符,中位数 1057,P90 2484。这不是调出来的参数,是语义边界的自然产物。
还有一条:真实数据里「标准格式」从来不标准,解析器要有三级退路。 本项目的故障排查文档有三种 frontmatter 写法(--- 包裹、代码围栏、裸 YAML),解析器依次尝试,YAML 解析失败(标题里含冒号是常见元凶)再退到宽松的按行解析。
五、胜负手:元数据是一等公民
每个切片带一组决定检索收敛的字段:内容角色、所属实体、能力、通道、范围(共享 / 专属)、适用范围列表、内容类别、受众。
标注用确定性瀑布:先按目录、文件名、标题模式等确定性规则逐级判定,模型只做兜底。配套四份词典(实体别名、术语索引、同义词、分类法),同时服务入库和查询——这一点很关键,标注和检索必须共享同一个真相源,否则入库时叫 A、查询时叫 B,永远对不上。
共享内容的处理是这一层的难点。 跨实体的共用能力只存一份,带「适用哪些实体」的列表;查询时的过滤规则是确定性的:
内容属于目标实体,或者 内容是共享的且适用范围包含目标实体。
这条规则写在过滤层,不交给模型判断。
六、Contextual 前缀:最便宜的一招
一个切片被切下来之后,它就不知道自己是谁了。「点击策略配置,选择新建规则」——这段话属于哪个产品?哪个版本?面向管理员还是开发者?纯从文本看不出来。
解法是把元数据写进正文头部,一起参与嵌入和关键词检索:
[产品: X] [版本: …] [文档类型: 操作步骤] [能力: 数据识别/指纹] [通道: 邮件]
[适用范围: 共享,适用 X/Y/Z]
点击策略配置,选择新建规则……
成本极低——就是拼一个字符串。收益是让每个孤立切片重新有了身份。
一个精细但重要的取舍:图片路径要从嵌入文本里剥掉(文件名没有语义,只会稀释向量),只保留 alt 说明;完整的图文关系保留在原始文本字段里,供原文渲染使用。
七、不同形态的数据要用不同的组织范式
看到一份数据,先问它是什么形态,再决定怎么组织。
反面教材:关系型表格
错误做法:把端口表当文档,切成 800 字符的块丢进向量库。用户问「A 和 B 之间要开哪些端口」,向量检索返回「某几个看起来像端口表的片段」——可能漏掉一半,可能混进别的设备对,而且永远无法保证完整性。端口配错等于现场部署失败。
正确做法,三步:
- 行转句——每一行变成一句自包含的自然语言,仍可被词法和向量召回:
「A 与 B 之间使用 TCP 8443 端口(双向)。用途:设备注册、授权、事件传输」 - 保留结构化旁路——同一条记录的结构化形态原样存一份。
- 查询时不走向量,走查表——路由识别到端口意图,从问句里认出设备(含别名),按设备对精确过滤全部记录,返回全部匹配,不做 top-k 截断,不做精排。
可迁移的判据:当一个问题的正确答案是「满足条件的全部记录」而不是「最相关的几段话」时,它就不该走向量检索。端口、价格、型号兼容性、配额、SLA 指标、字段字典都属于这一类。
RAG 系统里应该允许存在不走向量的旁路。这不是妥协,这是正确架构。
合成切片:回答语料里不存在的问题
用户一定会问「你们有哪些产品」「X 有哪些功能」,但没有任何一份原始文档正面回答——手册讲细节,功能列表是碎片。朴素 RAG 在这里必然失败,检索到的都是碎片,模型只能瞎凑。
解法是从已有的结构化数据确定性地生成切片:遍历实体词典拼出「产品列表」,从功能列表抽出全部分类名和概要拼成「功能全景」,并在末尾埋入关键词(「有哪些功能」「能力清单」等)。
两条纪律:① 必须由结构化数据确定性生成,不是模型编的;② 来源要标明是合成的。 由词典生成的好处是词典改了它自动跟着改,永不过期。
敏感内容:脱敏的职责边界
售前方案含真实客户名,不能直接入库。这里的做法精确划分了模型和代码的边界:
模型负责:抽取客户实体 → 严格 JSON 输出
(提示词明确排除自家产品名、通用技术词、行业词)
代码负责:确定性替换 → 按长度降序(先长后短,避免长名被短名截断)
+ 品牌前缀扫描
+ 四类个人信息正则(IP / 邮箱 / 手机 / URL)
★ 绝不让模型重写全文
为什么不让模型重写?因为重写会悄悄改变技术内容,而你无法逐字复核几百份文档。 抽实体是低风险任务(错了顶多漏脱敏一个词,可人工复核清单),重写是高风险任务。
八、12 个杠杆,按性价比排序
这个顺序就是建议的实施顺序。 很多团队从第 7 条开始做,事倍功半。
| # | 杠杆 | 在哪一侧 | 成本 | 效果 |
|---|---|---|---|---|
| 1 | 实体硬过滤 | 入库 + 查询 | 中 | ★★★★★ 不相关内容根本进不了候选 |
| 2 | Contextual 前缀 | 入库 | 极低 | ★★★★★ 让孤立切片有身份 |
| 3 | 词典驱动的实体归一 + 同义扩展 | 两侧共用 | 低 | ★★★★☆ 解决「同一个东西十种叫法」 |
| 4 | 结构化旁路 | 两侧 | 低 | ★★★★☆ 把「必须完整正确」的问题移出概率系统 |
| 5 | 合成切片 | 入库 | 低 | ★★★★☆ 补上语料的结构性空洞 |
| 6 | 数据清洗与正确的格式解析 | 入库 | 中 | ★★★★★ 不做这个,上面全是白搭 |
| 7 | 混合检索(关键词 + 向量) | 查询 | 中 | ★★★☆☆ 精确匹配与语义泛化互补 |
| 8 | 精排 | 查询 | 中 | ★★★☆☆ 候选池内排序质量 |
| 9 | 业务规则微调(时效性、场景加权) | 查询 | 低 | ★★★☆☆ 表达模型不懂的业务优先级 |
| 10 | 多轮软继承 + 证据共识 | 查询 | 高 | ★★★☆☆ 只在多轮场景值得 |
| 11 | 证据门禁(拒答 / 澄清) | 查询 | 低 | ★★★★☆ 防幻觉性价比最高的一招 |
| 12 | 生成侧提示词约束 | 生成 | 极低 | ★★☆☆☆ 必要但最不可靠,不能当主要防线 |
三条判断准则:
准则一:越靠近数据入库的杠杆,越便宜、越可靠、越持久。 提示词工程是最后一公里,不是第一公里。
准则二:能用确定性代码保证的,就不要用模型去「倾向于」。 门禁、硬过滤、结构化查表、实体名剥离——都是「保证」;提示词、精排、嵌入——都是「倾向」。
准则三:为你的领域找到那个「特有的错误」,并为它专门建指标。 本项目的北极星不是通用回答准确率,而是内容污染率——答案里混进了别的产品的步骤。因为在技术支持场景,「给 A 的用户一段 B 的步骤」造成的损害远大于「答得不够全」。指标错了,优化方向就全错了。
九、把每次踩的坑变成一条永久断言
每次构建后自动跑两类检查。
数据质检(断言式,不是打印式):切片总数、角色分布、实体分布;未标实体且非共享的比例必须低于 3%;清洗残留必须为零(扫描是否含格式锚点、页脚标记、OCR 表格标签);标记为共享的切片必须有适用范围。
抗污染探针——13 条金标准查询,这是本项目最值得抄的实践:
{"q": "邮件网关如何配置数据识别指纹", "expect": "邮件网关", "forbid": {"Web网关","终端","管理平台"}},
{"q": "Web 网关如何配置数据识别指纹", "expect": "Web网关", "forbid": {"邮件网关","终端"}},
{"q": "终端怎么管控 U 盘", "expect": "终端", "forbid": {"邮件网关","Web网关"}},
{"q": "A 和 B 之间要开哪些端口", "expect_role": "port", "expect_min_results": 5},
{"q": "数据识别指纹原理", "expect_shared": True},
{"q": "两者的指纹有什么区别", "expect_status": "comparison"},
# 多轮话题漂移专项:
{"q": "隐形水印和显性水印有什么区别", "inherit": ["A"], "expect": "终端"}, # 该漂就漂
{"q": "它的检测内容怎么配", "inherit": ["B"], "expect": "B"}, # 共享能力,保持
断言的形态很关键:它断言的是结构性事实——命中哪个实体、top 结果是不是共享内容、结果数够不够、是不是走了查表旁路——而不是断言答案文本。
文本会随模型版本变,结构不会。这让这套测试能长期存活。
上线时 13/13 通过。第一版是 7 条,新增的 6 条全部来自后来踩的坑。
后来更换解析层重新回归时,一条多轮话题漂移探针(上一轮在 A 产品、本轮该切到 B 产品,系统却留在 A)转红,回归基线冻结为 12/13,作为已知问题跟踪,而不是删掉这条探针让数字好看。这正是探针的用处:它让退化可见。
十、五个真实翻车
| # | 现象 | 根因 | 教训 |
|---|---|---|---|
| 1 | 语料虚高、词表爆炸、答案似是而非 | 导出的 .doc 实为 MHTML,被当文本转入 | 按文件头判类型;数量异常是最好的报警器 |
| 2 | 用户明明写了产品名,系统却反问「你说的是哪个产品」 | 查询扩展把用户写的产品名给删了 | 改写不能损失原始信息 |
| 3 | 多轮里话题已经变了,答案还死绑上一轮的产品 | 模型从对话历史把上一轮产品名带进了扩展查询 | 对模型输出永远要有代码兜底 |
| 4 | 证据一致指向同一产品,系统仍要澄清 | 门禁只看「用户有没有写产品名」 | 门禁要看证据,不只看查询 |
| 5 | 原文页里代码块渲染成字面、长行溢出 | 切分把代码围栏切断了 | 切分会破坏跨切片的结构,展示层要能还原 |
第 5 条附带一个很好的设计:原文查看不是显示单个切片,而是把同一来源的所有切片拼起来整篇渲染。 这样代码围栏自然配平,图文关系也完整。
切分是为了检索;展示时应该缝回去。
十一、什么时候不该这么做
这套方法很贵——十几个提取器、四份词典、上百行人工规则表,都是人写的。诚实的适用边界:
| 适合 | 不适合 |
|---|---|
| 语料相对稳定、长期维护(产品文档、制度、法规) | 每天新增大量文档、结构还各不相同 |
| 有明确的实体维度(产品线、部门、版本、客户) | 完全无结构的开放语料 |
| 错答代价高(配置错误导致故障、合规风险) | 闲聊式、容错高的场景 |
| 有领域专家能定义分类法和分歧点 | 没有人能说清楚「什么算对」 |
| 语料规模 10³–10⁵ 量级切片 | 10⁷ 量级(那时要上真正的向量数据库和分布式) |
如果你的场景不适合,退而求其次的顺序是:先做第八节里的 1、2、3、6、11 这五条(实体硬过滤、Contextual 前缀、词典归一、正确解析、证据门禁)。这五条投入产出比最高,且几乎在任何场景都成立。
十二、自研还是用框架
本项目没有用任何第三方 RAG 框架,运行期依赖只有三个库,纯 Python,检索与索引全本地,生成可按合规切换本地或云端模型。
理由很具体:
- 项目的核心逻辑——实体锁定、共享内容的适用范围继承判定、结构化旁路、场景加权、证据门禁——通用框架都不提供,而且很难在框架的抽象里表达;
- 反过来,框架擅长的部分(向量存储、嵌入调用、精排)本来就是几十行代码或一次 API 调用。
判断方法:如果你要做的事,框架的抽象层里没有对应概念,那就自己写;如果框架已经把它做成一个参数,那就别重复造。
自研上层业务逻辑,成熟底座做通用能力。
如果要从这篇里只带走一句话,那就是第一节那条:RAG 的准确率不是在检索那一步挣出来的。 当一个知识库项目的回答质量「差且没有规律」时,第一动作不应该是换向量库或加 rerank,而应该是回到入库那一侧,看看进去的到底是什么。
—— 模界数智
参考资料
- 本文素材来自一个已上线的企业知识库项目,客户身份与产品标识已全部匿名处理
正在规划企业 AI 相关项目?
建议从真实业务场景切入,优先完成项目诊断:评估业务价值、核查数据基础、识别安全与权限风险,审慎评估之后,再决定是否启动 PoC 原型建设,避免无效投入。
预约 30 分钟交流