模 模界数智
技术实践

企业 RAG 的准确率是在入库前挣出来的:12 个杠杆、六条铁律与一次 4000 个垃圾切片的事故

模界数智 官网首发
污染 Chunk 被清除,真实内容沿元数据轨道形成更小、更可信的索引

企业 RAG 的准确率主要取决于什么?

取决于入库前把数据结构化到什么程度,而不是检索那一步用了什么算法。检索侧只是把入库时打好的结构用起来。在一个已上线的企业知识库项目中,同一批源文件因为解析方式错误产生了约 4000 个 base64 垃圾切片,语料虚高到 6961 条、词表 47 万,答案「像是、但不对」;修正后回落到 2884 条真实切片、词表 10 万,真实内容零丢失,13 条抗污染探针全部通过。

多数 RAG 教程把篇幅花在检索侧——用什么向量库、怎么调 top-k、要不要加 rerank。而在真实的企业知识库项目里,决定成败的工作发生在这些之前。

本文复盘一个已经上线的企业知识库项目:十几种来源的产品文档,最终 2884 个切片,13 条抗污染探针全部通过,无第三方 RAG 框架。客户身份与全部产品标识已匿名处理,方法、数字与事故记录为真实过程。

关键结论

  1. RAG 的准确率不是在检索那一步挣出来的,是在入库前把数据结构化挣出来的。
  2. 永远按文件头判断格式,不要信扩展名——这个低级错误造成了本项目最大的一次事故。
  3. 数量异常是最好的报警器。「切片数比预期多一倍」「词表大到离谱」比任何人工抽查都灵敏。
  4. 能用确定性代码保证的,就不要交给模型去「倾向于」。
  5. 找到你这个领域特有的错误类型,为它单独建指标。指标错了,优化方向全错。

一、教程讲的和真正决定成败的,不是同一件事

先看这条流水线,左侧是本文的重点,右侧是常规教程的重点:

原始来源(十几类)
  │
  │ ① 格式归一化   各种格式 → 干净 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 通过,真实内容零丢失。

修复前修复后
切片数69612884
关键词词表47 万10 万
抗污染探针—13 / 13

三条通用规律:

  1. 永远按文件头判类型。 数据处理流水线的第一个函数就该是 sniff_format()。
  2. 数量异常是最好的报警器。 本项目正是靠这两个指标发现问题的,不是靠人工抽查。
  3. 静默降级比报错危险。 转换工具不报错地产出垃圾,比直接失败糟糕得多。所以流水线必须有「产物合理性检查」。

三、六条铁律

这六条写在方案文档的第一部分,是全项目所有取舍的裁判标准。

  1. 像素与噪音不进嵌入 —— 只有承载信息的文字进向量。图标、格式残留、OCR 标签、MIME 源码、base64、图片文件名全部清掉。
  2. 元数据是一等公民 —— 检索精度由过滤字段决定,不是靠嵌入去猜。
  3. 共享内容去重一份、专属内容按实体实例化 —— 跨实体的共用内容存一份并标「适用范围」;有分歧的步骤拆开并标注分歧点。
  4. 单一规范源 —— 同一内容有多种形态(原文 / 问答 / 表格)时只入一种,否则自己和自己打架。
  5. 可追溯、可回滚 —— 每条切片记来源;清理数据只「移动到搁置区 + 写清单」,绝不删除。
  6. 数据自包含 —— 图、文、索引全部归集到一处,运行时不依赖原始目录,源文档可整体归档。

第 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 之间要开哪些端口」,向量检索返回「某几个看起来像端口表的片段」——可能漏掉一半,可能混进别的设备对,而且永远无法保证完整性。端口配错等于现场部署失败。

正确做法,三步:

  1. 行转句——每一行变成一句自包含的自然语言,仍可被词法和向量召回: 「A 与 B 之间使用 TCP 8443 端口(双向)。用途:设备注册、授权、事件传输」
  2. 保留结构化旁路——同一条记录的结构化形态原样存一份。
  3. 查询时不走向量,走查表——路由识别到端口意图,从问句里认出设备(含别名),按设备对精确过滤全部记录,返回全部匹配,不做 top-k 截断,不做精排。

可迁移的判据:当一个问题的正确答案是「满足条件的全部记录」而不是「最相关的几段话」时,它就不该走向量检索。端口、价格、型号兼容性、配额、SLA 指标、字段字典都属于这一类。

RAG 系统里应该允许存在不走向量的旁路。这不是妥协,这是正确架构。

合成切片:回答语料里不存在的问题

用户一定会问「你们有哪些产品」「X 有哪些功能」,但没有任何一份原始文档正面回答——手册讲细节,功能列表是碎片。朴素 RAG 在这里必然失败,检索到的都是碎片,模型只能瞎凑。

解法是从已有的结构化数据确定性地生成切片:遍历实体词典拼出「产品列表」,从功能列表抽出全部分类名和概要拼成「功能全景」,并在末尾埋入关键词(「有哪些功能」「能力清单」等)。

两条纪律:① 必须由结构化数据确定性生成,不是模型编的;② 来源要标明是合成的。 由词典生成的好处是词典改了它自动跟着改,永不过期。

敏感内容:脱敏的职责边界

售前方案含真实客户名,不能直接入库。这里的做法精确划分了模型和代码的边界:

模型负责:抽取客户实体 → 严格 JSON 输出
         (提示词明确排除自家产品名、通用技术词、行业词)
代码负责:确定性替换 → 按长度降序(先长后短,避免长名被短名截断)
                    + 品牌前缀扫描
                    + 四类个人信息正则(IP / 邮箱 / 手机 / URL)
★ 绝不让模型重写全文

为什么不让模型重写?因为重写会悄悄改变技术内容,而你无法逐字复核几百份文档。 抽实体是低风险任务(错了顶多漏脱敏一个词,可人工复核清单),重写是高风险任务。

八、12 个杠杆,按性价比排序

这个顺序就是建议的实施顺序。 很多团队从第 7 条开始做,事倍功半。

#杠杆在哪一侧成本效果
1实体硬过滤入库 + 查询中★★★★★ 不相关内容根本进不了候选
2Contextual 前缀入库极低★★★★★ 让孤立切片有身份
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,而应该是回到入库那一侧,看看进去的到底是什么。

—— 模界数智

参考资料

  1. 本文素材来自一个已上线的企业知识库项目,客户身份与产品标识已全部匿名处理

正在规划企业 AI 相关项目?

建议从真实业务场景切入,优先完成项目诊断:评估业务价值、核查数据基础、识别安全与权限风险,审慎评估之后,再决定是否启动 PoC 原型建设,避免无效投入。

预约 30 分钟交流