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

> 发布日期：2026-09-01　作者：模界数智
> 原文链接：https://mojiedata.com/insights/rag-accuracy-before-retrieval

---

多数 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 通过，**真实内容零丢失**。

| | 修复前 | 修复后 |
|---|---|---|
| 切片数 | 6961 | **2884** |
| 关键词词表 | 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 | **实体硬过滤** | 入库 + 查询 | 中 | ★★★★★ 不相关内容根本进不了候选 |
| 2 | **Contextual 前缀** | 入库 | **极低** | ★★★★★ 让孤立切片有身份 |
| 3 | **词典驱动的实体归一 + 同义扩展** | 两侧共用 | 低 | ★★★★☆ 解决「同一个东西十种叫法」 |
| 4 | **结构化旁路** | 两侧 | 低 | ★★★★☆ 把「必须完整正确」的问题移出概率系统 |
| 5 | **合成切片** | 入库 | 低 | ★★★★☆ 补上语料的结构性空洞 |
| 6 | **数据清洗与正确的格式解析** | 入库 | 中 | ★★★★★ 不做这个，上面全是白搭 |
| 7 | 混合检索（关键词 + 向量） | 查询 | 中 | ★★★☆☆ 精确匹配与语义泛化互补 |
| 8 | 精排 | 查询 | 中 | ★★★☆☆ 候选池内排序质量 |
| 9 | **业务规则微调**（时效性、场景加权） | 查询 | 低 | ★★★☆☆ 表达模型不懂的业务优先级 |
| 10 | 多轮软继承 + 证据共识 | 查询 | 高 | ★★★☆☆ 只在多轮场景值得 |
| 11 | **证据门禁**（拒答 / 澄清） | 查询 | 低 | ★★★★☆ 防幻觉性价比最高的一招 |
| 12 | 生成侧提示词约束 | 生成 | 极低 | ★★☆☆☆ 必要但**最不可靠**，不能当主要防线 |

三条判断准则：

> **准则一：越靠近数据入库的杠杆，越便宜、越可靠、越持久。**
> 提示词工程是最后一公里，不是第一公里。

> **准则二：能用确定性代码保证的，就不要用模型去「倾向于」。**
> 门禁、硬过滤、结构化查表、实体名剥离——都是「保证」；提示词、精排、嵌入——都是「倾向」。

> **准则三：为你的领域找到那个「特有的错误」，并为它专门建指标。**
> 本项目的北极星不是通用回答准确率，而是**内容污染率**——答案里混进了别的产品的步骤。因为在技术支持场景，「给 A 的用户一段 B 的步骤」造成的损害远大于「答得不够全」。**指标错了，优化方向就全错了。**

## 九、把每次踩的坑变成一条永久断言

每次构建后自动跑两类检查。

**数据质检**（断言式，不是打印式）：切片总数、角色分布、实体分布；未标实体且非共享的比例必须低于 3%；清洗残留必须为零（扫描是否含格式锚点、页脚标记、OCR 表格标签）；标记为共享的切片必须有适用范围。

**抗污染探针**——13 条金标准查询，这是本项目最值得抄的实践：

```python
{"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，而应该是回到入库那一侧，看看进去的到底是什么。

—— 模界数智