别急着上线 RAG!90% 的检索效果差,都死在「上线之前」这一步

1 阅读22分钟

🔥 上一篇讲完 RAG 是什么,很多人以为「切块 → 向量化 → 存库」一行字就过去了。 但真动手做过的人都知道:这一行字里的水,比后面所有环节加起来还深。 这篇把「离线入库」这条线从例子讲到流水线,再拆成三段细抠。建议先赞后看,不然刷没了。


写在前面:那个被你一眼带过的例子

你问模型:「我们公司报销制度里,出差高铁票超过多少要提前审批?」它答不上来,因为没读过你们制度。RAG 的做法是:先去《报销制度.pdf》里捞出相关的几段,拼进提示词,让模型照着答。

流程看着很顺。但它悄悄跳过了一件事——

那份 PDF,是怎么变成「可以被检索的几段」的?

RAG 的最小流程,第一步通常就一行:

入库(离线):把《报销制度.pdf》切块 → 每块向量化 → 存进向量库。

一行字,三个动作。讲 RAG 时注意力都在检索和生成上,这一行就这么被带过了。但真要做,你会发现这行字里的水,比后面所有环节加起来还深。


一、把例子做厚:300 页手册的「灵魂三问」

把资料换真实一点。你现在要做的不是 30 页报销制度,而是某设备厂商的售后知识库——一份 300 页的《产品服务手册》:

  • 📝 主体是文字章节,讲安装、保养、故障排查
  • 📊 中间夹着十几张参数表(型号 / 保修期 / 服务方式)
  • 🖼️ 几页是安装流程图,信息全在图里
  • 📷 最后 20 页是扫描件——老型号纸质文档拍的照片
  • 🗂️ 文件夹里还躺着三个版本:手册 v1.pdf手册 v2.pdf手册(最终版).pdf

客服要用它答客户问题,比如「XR-300 的保修期是多久」。

现在回头看「切块 → 向量化 → 存进向量库」,你会发现每一步都在反问你

那行字它实际在问你
「切块」切成多大?按什么切?那张参数表切成两半后,「3 年」还知道属于哪个型号吗?
「向量化」用哪个模型?中文技术手册和英文通用语料,用同一个模型合适吗?
「存进向量库」三百页切下来上千个块,再乘以几百份手册,用户问一句要等几秒?

这三个问题,分别对应资料准备、转向量、存库三个环节。它们合起来,就是入库要做的事。


二、一句话说清痛点:资料和「能被检索」之间,隔着好几道转换

你的资料和「能被检索」之间,隔着好几道转换。

  • PDF 不是文本;
  • 表格不只是字;
  • 三百页不能整个塞进去;
  • 中文技术词和通用语义不是一回事;
  • 几十万个块不能挨个比对。

而且这些转换有个共同特点:它们全发生在用户提问之前。 用户问「XR-300 保修期多久」,他等的是那零点几秒的响应,不会想到你在背后花了三小时把三百页手册拆成上千个块。

💡 一句话记住离线:提前把重活干完,让真正应答的时候只做最轻的事。


三、先分清两件事:离线 vs 在线(分界线图)

往下讲之前,先钉死两个词,后面全靠它们。

还是那份手册。要让客服能查它,你得先干一件事:把三百页拆块、转向量、存索引。这一步花三小时,做一次,之后能用好几个月。干这活时系统还没上线,客服也还没问。

→ 这叫离线(也叫入库、建索引)。

做完系统上线,客服开始用。他每敲一个问题,系统就跑一遍:问题变向量 → 去索引找 → 排序 → 喂模型 → 返回。一天可能跑几千次。

→ 这叫在线(也叫应答、检索)。

🎯 区分标准只有一条:看它多久跑一次。 做一次、之后长期用的是离线;每次提问都跑一遍的,是在线。

切块 → 向量化 → 存进向量库          离线,做一次
                     ↓
                向量库  ← 两侧的分界
                     ↓
问题 → 检索 → 拼进提示词 → 生成       在线,每次都跑

中间那个向量库就是分界线:离线往里写,在线往外读。 本篇只讲上半截——从原始文件到索引那一段。


四、为什么它要单独拎出来讲?心态差决定成败

因为它的节奏,和在线那一侧完全不同。

  • 离线:做一次能用很久。做错了也没人知道,大不了重跑。三百页花三小时慢慢解析?没关系,这是一次性成本。所以你尽可以用最贵、最慢、但最准的工具。
  • 在线:用户每问一次它就跑一次。慢一点用户干等,答错了用户立刻看见。你在这加任何环节,成本都要乘以每天的调用量

⚠️ 最常见的心态错位:挑 PDF 解析器时纠结「这个一页要两秒,太慢了」——这纠结是错的。三百页跑一次十分钟,跑完就完了,你该关心它准不准、表格保不保得住。 反过来,等后面讲「重排」时还抱着「慢慢来」给它上个很重的模型,上线就会发现每次问答多等两秒,用户开始抱怨。

同样一句「用更好的工具」,在入库这一侧几乎总是划算的,到了检索那一侧就要精打细算。


五、离线流水线全景:7 步走完

它是一条流水线,把原始文件变成可检索的索引。拿那份手册从头到尾走一遍:

步骤动作手里拿的 → 交出去的要盯的点
① 解析PDF+20 页扫描件 → 纯文本原始文件 → 纯文本参数表散架没?章节标题还在不在?扫描件单独走 OCR
② 清洗去噪、理版本、保结构纯文本 → 干净文本三个版本理清留一个,保住标题层级给 ④ 用
③ 标元数据标位置(第几页/章/条/版本/谁能看)干净文本 → 带位置的文本位置信息最全,切完就找不回
④ 切块512 字一块、相邻留重叠带位置文本 → 上千个块句子被拦腰切断没?
⑤ 向量化上千块过 embedding 模型上千个块 → 上千个向量选哪个模型?中文看中文榜,客服要用同一个
⑥ 建索引建向量索引 + 字面索引 + 过滤配置上千向量 → 可检索索引配好按字段过滤(哪些归哪个部门看)
⑦ 维护新版/下架/权限变了,索引跟着变可检索索引 → 跟着变化更新最容易漏的一环
原始文件
  ↓ ① 解析
纯文本
  ↓ ② 清洗
干净文本
  ↓ ③ 标元数据
带位置的文本
  ↓ ④ 切块
上千个块
  ↓ ⑤ 向量化
上千个向量
  ↓ ⑥ 建索引
可检索的索引
  ↓ ⑦ 维护
跟着资料变化更新

🔥 三次「带代价的变换」(这张表建议截图)

流水线里有三次变换是牺牲了东西换来的,值得单独记住:

步骤丢掉了什么后来在哪补回来
① 解析版面信息(哪是表格、哪是标题、图里画了啥)→ 抽成纯文本就没了第 ② 步专门「保结构」留下来
⑤ 向量化字面精确性(XR-200 和 XR-300 向量可能很像)第 ⑥ 步同时建的字面索引补回
⑥ 建索引一点精度换速度(找到的未必绝对最近,但足够近)——(这是 ANN 的天然权衡)

🧠 实用心法:后面遇到任何一个技术,你都能问一句「它补的是哪一次损失」。答得出来,这个技术为什么存在就清楚了。

这三段合起来就是:①②③④ = 资料准备,⑤ = 转向量,⑥⑦ = 存库。 下面分别展开。


六、第一段:资料准备(①②③④)

对应解析、清洗、标元数据、切块。

📌 它解决什么痛点

你的资料,从格式到内容,几乎每个地方都在和「变成可检索的文本」作对。

  • PDF 的双栏陷阱:PDF 全称 Portable Document Format,设计目标是「任何设备看起来都一样」,本质是一套排版指令。它有文字层,但文字顺序未必是你眼睛看到的阅读顺序。一份双栏手册,抽出来可能变成「左栏第一段、右栏第一段、左栏第二段」这种交错。

  • 表格散架:表格在 PDF 里往往只是一堆带坐标的字符。你看到的是:

    型号      保修期    服务方式
    XR-200    3年       上门
    XR-300    5年       返厂
    

    抽出来变成:

    型号 XR-200 3年 上门 XR-300 5年 返厂
    

    表头还在,但「3 年」属于哪个型号、是哪一列,全丢了。检索到这块时既含「3 年」也含「5 年」,模型只能猜。

  • 扫描件:最极端——压根没文字层,整页就是一张图,不做 OCR 一个字都拿不到。你那 20 页老型号文档就属于这类。

💡 为什么需要它

因为后面所有环节处理的都是这一步的产出。内容在这步丢了,后面再强的模型、再精巧的检索,也只是在一堆残缺的块里挑挑拣拣。RAG 的效果上限,很大程度由资料本身质量决定——这话不是随便说的,这一步才是「资料质量」真正发生的地方。

🧩 它是什么

输入是 PDF、Word、网页、扫描件(乱七八糟什么都有);输出是上千个块,每块几百字,带着它来自哪一页、哪一条、谁能看。

🔧 四个动作,按顺序来

① 解析(把格式变成文字)

坑按格式分:

  • PDF:双栏乱序 + 表格散架
  • Word / HTML:自带结构标签,相对好办,但容易把「看起来像标题的东西」当成真标题
  • 真正难点是表格——不是把字抽出来,而是抽出结构,知道哪一行属于哪个表头
  • 扫描件:没 OCR 一个字都没有
  • 图文混排:流程图、图表里的信息在纯文本里直接消失

工具梯度(按难度往上走):

工具定位适用
PyMuPDF快而轻文字层干净的 PDF
MinerU / Marker做了版面分析,保标题层级 + 表格结构中文 PDF 表现更好
PaddleOCROCR扫描件
Databricks ai_parse_document高质量版面解析(非通用文本抽取)表格/层级保不保得住是关键区别

怎么验收解析质量:笨办法但有效——随机挑几页,把抽出来的文本和原文对着读一遍(尤其表格页、带层级的章节页);更工程化的办法——换两种解析器跑同一批问题,看检索指标差多少。差得多,说明解析就是瓶颈。这步偷懒,后面所有调试都会变成无头公案。

② 清洗(让抽出来的字能用),三件事:

  • 去噪:页眉页脚页码水印乱码。看着无害,但每页都重复,会被切进每一个块。三百页每页页眉都是「XX 公司产品服务手册 · 内部资料」,你的向量库就会有一大批块开头十几个字一模一样——重复内容稀释语义、降低区分度。
  • 归一化:去重 + 处理版本。你那三个版本(v1/v2/最终版)高度重叠,不处理,检索会召回好几个几乎一样的块挤占候选,还不知该信哪个。
  • 保结构:标题层级、列表、表格。最易被跳过、后果最严重——因为切块主流做法是按文档结构切(按标题/章节/条款),标题层级在清洗时打平了,这条路就断了,只能退回按字数硬切。

🎯 清洗的判据不是「看着干净了」,而是下游需要的信息还在不在

③ 标元数据(给每块贴「从哪来」)

  • 显式元数据(客观事实:哪个文件、第几页、第几章第几条、什么时间、谁有权看):
    • 引用溯源:答「5 年」能附「来自《产品服务手册》第 47 页 3.2 条」,没这个答案没法核验,RAG 的溯源优势也没了;
    • 权限过滤:检索时先筛掉该用户看不到的块,而不是答完再删(后者等于已经泄露)。
  • 语义元数据(给检索补信号,如文档摘要或块所在章节标题,拼进块一起向量化):孤立的块往往缺上下文。第 47 页有块写「保修期为 5 年」,自己没说哪个产品;前面拼上「第三章 XR-300 产品规格」,才能被正确检索到。

⚠️ 必须在切块之前做:元数据是按块标的,你得先知道每块落在文档的什么位置。切完再补等于重新定位,而位置信息已经没了。

④ 切块(切成多大一块)

两难:上下文窗口有限,三百页必须切块只捞相关;但切法影响质量——太碎句子被拦腰切断读不懂,太大混两个不相干主题、向量被稀释什么都不像。这题没有理论最优解,答案是实验出来的。

七种切法(越往下越精确,但对上游结构依赖越强、成本越高):

切法做法适合
固定大小(按字数)最省事永远从这里起步
递归字符切法先段落 → 不行按句子 → 再按字符通用文本
文档结构切法按标题/条款/章节手册、规范、合同(前提:标题层级在清洗时保住)
语义分块按相邻句子向量相似度找边界叙述性长文
父子块小块检索、命中返回所在大块既要定位精确又要上下文完整
句子窗口句子为单位检索、返回带前后几句事实型问答
命题级切块切成原子命题精度最高,代价是入库多跑一次模型

参数两个:

  • 块大小三档:256 偏精确事实、512 均衡、1024 偏上下文完整。问「保修期多久」小块够,问「整个安装流程分几步」块太小会漏。
  • 重叠:相邻两块故意重复一小段,专防一句话被切断。

😂 这个坑有多常见:我拿《RAG 基础概念》那份文档切块,固定 120 字符、重叠 30,其中一个块结尾是:

为什么要切:上

后半句跑到下一块去了。这个块被检索到时,读到的是一句没说完的话。

🎯 怎么选(别拍脑袋):先用固定大小跑出基线,再根据暴露的问题换策略(连基线什么样都不知道,就没法判断换策略有没有变好);然后用检索指标做网格实验,把块大小和重叠各取几档,跑评测集看 Recall@k 怎么变。这步不难,但绝大多数人跳过了,凭感觉定个 512 就一直用下去。

🔗 顺序为什么不能乱

解析 → 清洗 → 标元数据 → 切块是链式的。两个最常见错误:

  • 先切块再想起来标元数据 → 位置信息已丢,只能拿块内容反推;
  • 清洗时把标题层级打平了,然后想按文档结构切 → 结构信息在清洗那步就没了。

这一环节真正的纪律不是「每一步做得多好」,而是顺序不能乱,且每一步都留下下游需要的信息

🚀 两个进阶项

  • 多模态入库(处理流程图):第一档让视觉语言模型(VLM)给图片生成文字描述当普通文本块入库,改动最小但细节会丢;第二档 ColPali,跳过 OCR 直接把页面图像编码成多向量,保住表格/版式/图注。文字为主用第一档,图表密集用第二档,混合的就两路都走再融合。
  • 图谱构建(从文本抽实体和关系建知识图谱):三种范式——规则抽取快但脆(否定句是硬伤)、LLM 抽取准但贵、混合流水线折中。注意区分:建图谱是入库时做的事,用图谱做检索是另一回事,放更后面讲。

🧪 验收

一份 300 页的产品服务手册,你会怎么切?为什么?


七、第二段:转向量(⑤)

对应向量化。

📌 它解决什么痛点

文字之间,没法直接算「像不像」。

客户问「XR-300 能保多久」,手册写的是「XR-300 保修期 5 年」。「能保多久」和「保修期」字面上一共重合几个字?一个都没有——按字面匹配搜,一个都搜不到。

反过来,手册里同时有「XR-200 保修期 3 年」和「XR-300 保修期 5 年」,这两句字面重合度极高。按字面搜「XR-200」,两句都命中,模型看着两个数字开始猜。

字面匹配两头不讨好。需要的不是「字像不像」,而是「意思像不像」。

💡 为什么需要它

因为检索要按语义来,不是按字面来。这是整个 RAG 的地基——所谓「把问题变成可检索的形式(通常是向量)」,说的就是这一步。

🧩 它是什么

把一段文本变成一串数字。核心性质只有一句:语义相近的文本,向量也相近。 有了这个性质,「意思像不像」就成了一道能算的题——两串数字方向多接近,用余弦相似度一算就知道,不需要任何字面重合。

🔧 主要流程

选一个 embedding 模型,把上千个块挨个喂进去,得到上千个向量。流程简单得没什么可说,难点全在选型。

🚨 它有个特殊身份:唯一横跨两条管道的部件。 离线侧用它把文档变向量,在线侧还要用它把用户问题变向量。所以有个踩了就全盘皆错的约束:两边必须是同一个模型。 原因:不同模型把文本映射到的向量空间不一样。A 模型建索引、B 模型算问题,再比相似度——等于在两张不同的地图上量距离,数字毫无意义。而且这种错不会报错,只会让你的检索效果莫名其妙地差。

📚 涉及的知识点

  • 余弦相似度:两个向量方向的接近程度,检索时的打分依据。
  • 维度:向量有多长,常见 768 / 1024 / 1536。同一任务必须统一,不能一半 768 一半 1536 混着来。
  • 多语言:要做跨语言检索就得选支持多语言的模型。
  • Matryoshka(嵌套表征):可变维度,允许把向量截断一截换速度,精度损失可控。想先跑起来再优化延迟,这个特性很好用。
  • 多向量表示:一个块不只用一个向量,而是多个。是晚交互路线(ColBERT 那类)的基础,比单向量准,代价是存储膨胀。
  • 怎么选型(看三个基准)
    • MTEB:50 多个数据集上的 embedding 综合排行榜;
    • CMTEB:中文榜——中文场景一定要看这个,别只盯英文榜,两边排名差异可以很大;
    • BEIR:18 个数据集的零样本检索基准,更偏检索器整体选型。
  • BGE-M3 值得单独提:一个模型同时出稠密向量、稀疏向量、多向量,等于把后面混合检索的成本砍掉一半。中文场景基本是默认选项之一。
  • 要不要微调:垂域可以微调 embedding,但判定标准要清楚——大多数场景不需要。只有你的领域黑话密集到通用模型明显抓不住,而且你有标注数据,才值得做。
  • ⚠️ 必须记住的坑:换 embedding 模型等于全量重建索引。新旧向量不在同一空间,不能混用。所以选型要在建库之前定下来,别建了一半换。

🧪 验收

中文法律文档问答,你选哪个 embedding 模型?依据是什么?


八、第三段:存库(⑥⑦)

对应建索引与之后的维护。

📌 它解决什么痛点

块少的时候没这问题。三百页切出一千个块,挨个算相似度也就几毫秒。

但真实场景是几百份手册、几十万个块。用户问一句,你把几十万个向量挨个比一遍——这个延迟没人受得了。

💡 为什么需要它

速度。而且不只是速度,还有过滤:检索时要能按条件缩圈,比如这个客服只能看消费级产品文档,不能看工业级的。

🧩 它是什么

把向量组织成一种结构,让「找出最像的几个」这件事足够快。

🔧 主要流程

建索引,同时并行建一份稀疏索引,配好过滤条件。

📚 涉及的知识点

  • 为什么用近似检索:精确最近邻要挨个比对,量一大做不到。ANN(近似最近邻) 接受「找到的未必绝对最近,但足够近」,用一点点精度换回大量速度。核心权衡是召回率、延迟、内存三者平衡。

  • 两种索引结构

    索引原理特点可调参数(本质都是召回率↔延迟)
    HNSW图索引快,但吃内存efSearch
    IVF + 量化聚类 + 压缩省内存,精度略低nprobe
  • 稀疏索引要并行建(容易漏):后面混合检索是向量一路 + 字面匹配(BM25)一路,各出一张榜再融合。融合发生在检索时,但字面那一路的倒排索引必须在入库时就建好。BM25 参数k1 一般 1.2~2.0,b 取 0.75。

  • 元数据过滤两种做法pre-filter 先按条件缩圈再找近邻;post-filter 先检索再筛。它的隐藏身份:同时就是权限控制的实现方式——「只查这个部门能看的文档」本身就是一次过滤。

  • 多向量的存储问题:走晚交互(ColBERT 那类),文档是 token 级向量,存储膨胀很多倍,要有量化和 token pooling 这些压缩手段。

  • 怎么选库

    定位
    FAISS库内嵌,轻量
    Chroma本地起手,开发友好
    pgvector已有 Postgres 直接能用
    Qdrant / Milvus上量、要分布式、要混合检索原生支持
    Weaviate自带混合检索、模块化

    何时必须换?三个信号:并发、持久化、混合检索的原生支持。部署一般从单机(Milvus Lite、Docker Compose)演进到云原生(Milvus 集群或托管)。

  • 增量更新与版本管理:新增文档不该每次全量重建,要支持增量入库。删除同步是最容易出问题的一环——文档下线、权限变了,索引里的旧块有没有跟着删,很多系统会漏。索引本身也要有版本方便回滚。质量检查(空块、重复块、超长块、元数据缺失)可以用代码断言自动化跑。

🧪 验收

10 万个块,要按部门权限过滤,每天更新,你怎么选库?索引怎么配?


九、一个反直觉的地方:重跑的代价不在计算,在连锁反应

前面说离线「做错了大不了重跑」,这话没错,但漏算了代价。

重跑的成本不在计算,在于连锁反应。假设你的切块策略有问题,很多句子被切断。你基于这套索引,花两周把在线侧的相似度阈值、候选数量、重排模型都调好了,指标看着还行——因为你在用一套有缺陷的块去迁就。这时候你回头改了切块策略,索引重建了,然后你会发现:下游那些调好的参数全部作废,得重新调一遍。 块粒度变了,最优候选数量变了,重排模型表现也变了。

🎯 正确心态:既不是「反正能重跑随便搞」,也不是「必须一次做到完美」,而是尽早把它做到能验证的程度,然后用评测集固定下来。之后再动它,就要有心理准备:下游要跟着重调。

这也解释了一件看起来奇怪的事——入库虽然排在最前面,却需要一个属于检索和生成环节的工具(评测集)来兜底。


十、总验收 + 知识地图

🧠 自测三连(评论区交作业)

  1. 一份 300 页手册从原始文件到能检索,中间经历了哪几次形态变化?每次得到什么、丢掉什么、后来在哪补回?
  2. 为什么 embedding 模型「离线建索引」和「在线算问题」必须用同一个?用不同模型会怎样?
  3. 10 万个块、要按部门权限过滤、每天更新——你选哪个向量库?索引怎么配?

🗺️ 知识地图(形态变化链)

原始文件 ──①解析──▶ 纯文本 ──②清洗──▶ 干净文本
   │                                  │
   │                         ③标元数据▶ 带出处的文本
   │                                  │
   └──────────────────④切块──────────▶ 上千个块
                                          │
                                  ⑤向量化▶ 上千个向量
                                          │
                              ⑥建索引+字面索引+过滤▶ 可检索索引
                                          │
                                  ⑦维护▶ 跟着资料变化更新

能顺着「原始文件 → 纯文本 → 带出处的块 → 向量 → 索引」这条线,把每一跳的得失说清楚,入库这条线就立住了。


写在最后

很多人以为 RAG 的难点在「检索和生成」,但真正的水,全在上线之前那行被一眼带过的「切块 → 向量化 → 存库」里。

解析抽散了表格、清洗打平了标题、切块切断了句子、embedding 选错了榜、索引建漏了过滤——随便哪一步偷懒,后面调再久的检索参数都是无头公案。

👍 觉得有用的话,点赞 + 收藏 + 关注 三连走起。 💬 评论区聊聊:你做 RAG 入库时,踩过最离谱的解析/切块坑是什么?下期想看「解析篇」硬核拆解,还是「检索篇」实战,留言告诉我。


下一篇预告:《解析篇——把格式还原成文字的第一道关》。