RAG 为什么总答不准?从切块到混合检索的全链路拆解

答不准,问题往往不在模型,而在喂进去的那几段资料。这篇文章把 RAG 这条流水线从里到外拆开:切块怎么定粒度、向量怎么存怎么找、召回怎么补漏、精排怎么收窄,最后用评测把「有没有变好」变成数字。全程围绕一个贯穿案例「公司年假政策是什么」。


一、什么是 RAG

RAG 的全称是 Retrieval-Augmented Generation,翻译过来就是「检索增强生成」。顾名思义,这个技术就是为了增强模型的生成能力而出现的。要理解它,得先理解大模型到底卡在哪里。

1.1 大模型的三个先天困境

困境一:知识有截止日期。

大模型的知识在训练结束的那一刻就冻结了,除非等到下一个版本发布,否则它压根不知道训练之后发生的事。你今天问它一件上周才发生的新闻,它要么答不上来,要么用旧知识硬答。

困境二:不懂私有知识。

它不知道企业内部的规则、奖罚制度、业务口径。这些信息在公开语料里根本不存在,模型自然无从学起。

困境三:幻觉。

当你问它一个它其实不懂的问题时,它并不会老老实实说「不知道」,而是会很自信地编:给你引一篇看起来很专业的论文、一个像模像样的编号,实际上全是假的。

不过要说明的是,这是最初大模型的问题。到今天,这个问题已经通过后训练缓解了不少。思路大概是通过惩罚机制:原本的训练是鼓励模型「像人一样回答」,把回答标准化、专业化,但未必准确;后续的训练就针对这一点做整改。模型在不知道信息时编造假信息会被扣分,而不知道就老实回答「不知道」反而会得高分。经此训练,幻觉有明显改善,但并没有根治。

而且更现实的是:很多时候我们并不需要「回答不知道」,我们需要的是回答真实的知识。 再加上领域内知识在模型原始训练权重里占比本来就不高,想补上这块知识,要么砸钱微调,要么让模型「开卷答题」。这就是 RAG 存在的必要性。

1.2 微调 vs RAG:两条注入知识的路

给模型补充知识,主流有两条路,理解它们的区别很关键。

微调(Fine-tuning) 是去调整模型的参数权重,你可以直观地理解为「内化知识」,把某个领域的内容专门喂给模型重新训练,训练完这个模型就带上了对应的领域知识。当然这是直观理解,本质上是让该领域内的那些词,在遇到用户在专业领域提问时会拥有更高的返回概率,于是回答更准。

RAG 则相反,它完全不碰模型的参数,只是把用户的原始提问包装了一下:

1
2
3
请根据以下参考资料回答用户的问题。
参考资料:[检索出的文本块]
用户问题:公司年假政策是什么?

答案其实已经躺在「参考资料」这段上下文里了,大模型能理解上下文之间的关系,于是顺理成章地返回对应的答案。

两者各有各的账本:

维度 微调(Fine-tuning) RAG(检索增强生成)
是否改参数 改,调整权重 不改,只包装 Prompt
本质 内化知识,抬高领域词的生成概率 开卷答题,把答案放进上下文
成本与周期 花费高、时间长 花费低、时间短,可做到小时级更新
知识更新 不能每日更新,要重训 更新向量库即可,配合自动化可以很实时
事实准确性 可能仍有幻觉(学到的是「模式」和「风格」) 对高度相关内容基于原文回答,幻觉很低
深度定制 强,可深度定制,效果很好 一般
语言风格 优势明显,能学出特定文风 靠 Prompt 调,效果一般

所以结论是:微调适合专业领域 + 高严谨性场景(比如法律、客服),而且通常要配上 RAG 一起用;其余大多数场景,RAG 就够了。

1.3 RAG 的整体流程:只有两个阶段

RAG 的整条流水线只有两个阶段。

  • 离线存储阶段(Indexing),把你的领域知识和企业内部业务知识存进向量数据库。这一步「一次做、长期用」,是整个检索质量的地基。
  • 在线检索阶段(Retrieval → Generation),用户提问,去库里把相关资料捞出来,拼成增强 Prompt,交给大模型生成。这一步「每次查询都要跑一遍」,是优化的主战场。

RAG 整体流程:离线存储与在线检索两大阶段


二、一个完整例子:从文档到回答

上一节把两个阶段列了出来,这一节用「公司年假政策」这个例子把每个环节具体走一遍,先建立整体印象,再进细节。

2.1 离线存储阶段

第一步,文档加载。 这一步是把企业里各种格式的文档加载进来。企业文档格式五花八门,PDF、Word 和 PPT,Excel、HTML 以及 Markdown,每种格式都有不同的解析方式。其中 PDF 尤其麻烦,表格和图片混排、分栏复杂的 PDF,解析起来很费劲。这一步的目标只有一个:把各种格式的文档统一转成纯文本。

第二步,文档切片。 为什么要切?文档知识是海量的,不可能一次性全塞给模型。原因有两个。

  • 一是注意力会被分散,上下文里无关内容越多,模型越抓不住重点;
  • 二是上下文有上限。现在模型的上下文即便到了 1M,放在海量文档面前也依然不算大,更何况还要留出很大一部分给 Agent 里的其他机制,比如记忆和工具,它们也要占上下文。

所以要把文档切片,把内容细化、分类,再转成对应的向量。这样后续检索出来的都是相关性高的知识。用户问冰箱退货策略,就绝不会捞出「怎么吃西瓜」这种毫不相干的内容,最多返回几千字相关知识,既不超上限,相关性又高,能大幅削弱幻觉和注意力分散。

第三步,向量化。 把切好的块转成向量(Embedding),这一步方案很多,后面细讲。

第四步,存入向量数据库。 至此,全部文档知识就都存进向量库了。向量库的选型和底层,后面单独讲。

2.2 在线检索阶段

用户提问,比如「公司年假政策是什么」,然后:

第一步,把提问向量化。 注意,必须和当初切片向量化用的是同一个处理方式。因为向量化模型不同,得到的向量结果必然不同,一混用,整个查询向量就「偏移」了,检索结果全错。

第二步,相似度检索。 拿着问题的向量,去向量数据库里做相似度搜索,找到语义最相近的 Top-K 个文本块。比如可能捞出这么三块:

  • 「公司全职员工入职满一年后享有 10 天带薪年假」
  • 「年假可以拆分使用,每次不少于半天」
  • 「未使用的年假可以累积到下一年,但最多累积 5 天」

第三步,拼接增强 Prompt。 把捞出来的资料拼进提示词,大致长这样:

1
2
3
4
5
6
请根据以下参考资料回答用户的问题。
参考资料:
- 公司全职员工入职满一年后享有 10 天带薪年假
- 年假可以拆分使用,每次不少于半天
- 未使用的年假可以累积到下一年,但最多累积 5 天
用户问题:公司年假政策是什么?

第四步,交给大模型生成。 流程到此结束。

整体框架就是这些。接下来,我们把「存储细节」和「查询细节」逐个拆开。


三、离线存储阶段:细节

3.1 切块(Chunking):最容易被低估的一步

存的第一步就是对文本切片,而「怎么切」很有说法。

3.1.1 固定长度切分

最简单,按固定长度硬切,但它有个比较严重的问题,会丢掉句子的上下文。看这个例子:

公司全职员工入职满一年后享有 10 天带薪年假。年假可以拆分使用,每次不少于半天。

如果刚好在「一年后」这几个字后面被切成两半,就变成:

  • 块 A:「……公司全职员工入职满一年后」
  • 块 B:「享有 10 天带薪年假。年假可以拆分使用,每次不少于半天」

此时两块都不像话了。查「员工福利」这类信息时,块 A 只剩「入职满一年后」,跟福利在语义上差得远,检索不出来;块 B 虽然有「10 天带薪年假」,却丢了「入职满一年」这个前提,答不准。两个块各丢了一半信息,靠其中任何一块都答不全。

3.1.2 重叠(Overlap)

既然硬切会丢上下文,那就别直接切断,而是缓存一部分。比如缓存 50 个词:本来该在「一年后」后面切,那就从这里往后数 50 个字再切;后一句同理,要往前数 50 个字才是真正的切割点。这样确实保留了一部分上下文,但依然无法避免断句、断段,问题只是缓解,没有解决。

3.1.3 递归分隔符切分

这是目前最常用的方案,能最大程度保留一句或一段的语义。它维护一个分隔符优先级列表,比如:

1
["\n\n", "\n", "。", "!", "?", ";", ",", " "]

它会先尝试用双换行符(段落分隔)来切;如果切出来的块还是太大,就用单换行符切;还大,就用句号切……依次降级,直到块大小符合要求。

整理一本书时,先按「章」分,一章太大就按「节」分,一节太大就按「段」分。这样分出来的每块内容,都是相对完整和独立的。

3.1.4 结构感知切分

对特定文档类型效果更好。Markdown 可以按各级标题切;代码文件可以按函数、类切;HTML 可以按标签切;PDF 可以按页面切。好处是充分尊重文档结构,切出来的块语义完整;缺点是只适用于格式规范的文档,文档本身结构混乱、没有明显格式标记时,就不好用了。

3.1.5 语义切分

花费更高,但更贴语义。我们前面知道,可以通过算相邻句子的向量余弦值来判断相关性,那就逐句算,直到发现前后两部分的语义发生了明显变化(相似度显著变低),那里就是一个转折点,在此处切割,能最大程度保留语义。代价也大:每一句都要向量化并比较。

3.1.6 LLM/Agent 切分

先用上面的方法粗切,再让 LLM 评判每个块是否完整、是否需要和相邻块合并或拆分,智能编辑这些块。理论上效果最好,但成本最高。

3.1.7 父子块(Small-to-Big)

前面六种都在解决「怎么切」,父子块则是另一种思路,一种检索策略:先切出一个大一点的块,再把大块切成小块;小块负责向量化存储和精确检索,检索命中后顺着小块找回它所属的大块,把大块拼进模型。这样用小块保证检索精确,用大块保证喂给模型的上下文完整。

五种切块策略对比:固定长度、重叠、递归分隔符、语义切分、父子块

父子块(Small-to-Big):小块检索、大块喂模型

各策略的取舍汇总成下表。

策略 语义完整度 成本 适用场景
固定长度 低(易断句) 最低 快速原型、结构混乱的文本
固定长度 + 重叠 中(断句仍可能) 低 通用兜底
递归分隔符 较高(优先保段落/句子) 低 大多数文本的默认首选
结构感知 高(尊重结构) 低 Markdown、代码、HTML、规范 PDF
语义切分 高(贴语义) 高(逐句向量化) 高质量知识库、对召回精度要求高
LLM/Agent 切分 最高 最高 高价值语料,可接受高成本
父子块 高(检索 + 上下文兼顾) 中 长文档、上下文完整性要求高

3.2 向量化(Embedding):从 Word2Vec 到 BGE-M3

切完块,下一步就是编码成向量。这里必须先把一个隐含前提说清楚,不然后面全是空中楼阁。

为什么向量相近就代表语义相似?

因为 Embedding 的本质,是把文本映射到一个高维向量空间里,让语义相近的文本,映射出来的向量在空间里也靠得近。也就是说,模型学到的是「什么样的文本该被放在空间的什么位置」。于是「距离近」就成了「语义像」的可计算代理,我们才能用余弦相似度这类几何量去衡量语义相关性。整个 RAG 检索的地基,就是这个前提。

搞清前提,再看 Embedding 技术是怎么一步步走到今天的。它大致经过了三个阶段。

3.2.1 第一代:Word2Vec(2013)

Word2Vec 是 Embedding 技术的奠基之作,由 Google 在 2013 年提出。它先立了一个核心假设:一个词的意思,由它周围的词决定。「我吃了一个苹果」里,「苹果」周围是「吃」「一个」,所以它是水果;「苹果发布了 iPhone」里,它周围是「发布」「手机」,所以它是公司。把这种搭配规律从海量文本里学下来,词义就被压进了向量。

训练方式有两种,是同一个思路的两个方向。

  • CBOW(连续词袋):用上下文预测中间的词。拿「今天天气__很好」,遮住中间,让模型用两边的词把它猜出来。要猜对,模型就必须「读懂」上下文,语义就是被这样逼出来的。
  • Skip-Gram(跳字模型):反过来,用中间的词预测上下文。给你「天气」,让它预测周围会出现哪些词。「用一词猜多词」比「用多词猜一词」更难,学出来的向量也往往更细。

训练完,每个词对应一个固定向量(比如 300 维)。最有意思的是向量还能做算术:「国王 − 男人 + 女人 ≈ 女王」。这不是巧合:国王和男人经常出现在相似的上下文里,两个向量的差「国王 − 男人」就在编码「王室」这个属性,把它加到「女人」上,离结果最近的词恰好是「女王」。这说明词与词的语义关系(性别、时态、单复数),真的被表示成了空间里的方向。

但 Word2Vec 有个先天缺陷:每个词只对应一个向量,训练完就固定了。「我吃了一个苹果」和「苹果发布了一代 iPhone」,两个「苹果」意思天差地别,向量却是同一个。原因也简单:它本质是「查表」,一个词一个向量,编码这个词的时候模型根本看不到它的上下文,多义词无法区分。

3.2.2 第二代:BERT(2018)

2018 年,Google 提出 BERT,把 Embedding 带入了全新阶段。它基于 Transformer 的自注意力机制,不再依赖循环神经网络,实现了真正的双向上下文理解。

于是「我去银行存钱」和「河岸边的风景很美」里的「银行」和「河岸」,BERT 会根据上下文给出不同的向量,多义词也就分得开了,正好补上了 Word2Vec 的短板。

它的训练任务有两个。

  • MLM(遮盖语言模型):随机遮盖 15% 的词,让模型靠上下文预测被遮住的词,强迫它学会理解上下文。
  • NSP(下一句预测):给模型两句话,让它判断第二句是不是第一句的下一句,帮助它理解句子间关系。

在 RAG 场景里,通常取 BERT 输出中 [CLS] 位置的向量(或者对所有 token 向量取平均)作为整段文本的向量表示。

但 BERT 也有局限:它的上下文窗口只有 512 tokens,处理长文档能力有限;而且它是通用的语言理解模型,并不是专门为「检索」这个任务设计的,所以在 RAG 检索场景下的效果还有提升空间。

3.2.3 第三代:BGE-M3 与 text-embedding(2024 年至今)

既然 BERT 窗口不够长、又不是为检索而生的,那就造一个窗口更长、专为检索训练的 Embedding 模型。这就是第三代要解决的。代表是智源研究院的 BGE 系列和 OpenAI 的 text-embedding 系列。

BGE-M3 是 2024 年初发布的里程碑模型,名字里的「M3」代表三个「多」:

  • Multi-Granularity(多粒度):句子、段落、篇章乃至整篇文档都能处理,最长支持 8192 tokens。
  • Multi-Functionality(多功能):一站式集成稠密检索、稀疏检索和多向量检索三种能力,一次推理就能同时得到三种输出。
  • Multi-Language(多语言):支持 100 多种语言。

它为什么能同时支持三种检索? Transformer 主干完全共享,不同的在输出端:在编码器的输出之上接了三个不同的投影头,一个头对应一种检索,训练时三个目标一起优化。于是一个模型就干完了三件事,大大节省花费。三种输出对应三种检索,逐个把机制说清楚。

3.2.4 稠密检索(Dense):语义相似

把整段文本压成一根固定长度的向量(比如 1024 维),相似度就是两根向量之间的余弦距离。

举例:查询「员工休假规定」和文档「年假与调休的使用办法」,两边没有几个字完全对得上——「休假」对「年假」「调休」,「规定」对「办法」——但它们语义相近,被压到了空间里相近的位置,余弦相似度照样很高。这是稠密检索的核心价值,同义改写也能命中。弱点同样来自「压缩」:整段语义被平均进一根向量,文档里「最多累积 5 天」这种具体细节,在向量里只剩一点微弱偏移,靠它做精确匹配不靠谱。

3.2.5 稀疏检索(Sparse):会打分的关键词匹配

输出的不再是一根向量,而是一份词权重表:给词表里的每个 token 打一个权重,用不到的位置几乎全是 0,所以叫稀疏。

举例:模型读完「未使用的年假可以累积到下一年,但最多累积 5 天」,输出类似 {「年假」: 2.3,「累积」: 1.8,「5」: 1.2,「未使用」: 0.9,「可以」: 0.05……}。检索时拿查询和文档的共同 token 对分:两边都有「年假」和「累积」,把重叠 token 的权重加起来就是匹配分。

完整的查询流程和向量检索一样:查询文本同样被编码成一份词权重表,然后对库里的每个文档算「重叠 token 的权重加和」,得到一个匹配分,最后按分数从高到低排,返回分数最高的 Top-K 条。

它和 BM25 的区别在权重是模型学出来的,不是数词频数出来的:BM25 里「年假」的权重只取决于它出现了几次,稀疏向量则能学到「这个 token 在这段话里有多重要」。代价是它仍按 token 匹配,查询里没出现的词帮不上忙。

3.2.6 多向量检索(Multi-Vector):每个 token 单独算分

稠密和稀疏最后都把整段「合」成了一个表示(一根向量或一份权重表),多向量不合并:文档里每个 token 各自保留一个向量,一个文档就是一组向量,几百根。

打分方法叫延迟交互(Late Interaction)。拿查询里的每个词,去和文档里的每一个词比相似度,这个查询词从中拿到一个最高分(MaxSim);再把查询里所有词的最高分加起来,就是这条文档的总分。(BGE-M3 的官方公式会再除以查询词数取平均,只影响分数刻度、不影响排序,这里按加总讲。)

拿「入职多久能休年假」对「入职满一年后享有 10 天带薪年假」走一遍:查询里每个词都去文档的词里找「最像的那一个」,「入职」直接对上「入职」,「多久」最像「满一年」,「年假」对上「年假」,「能休」找到「享有」,每个词各拿各的最高分,加总就是匹配分。它和稠密检索的关键差异就在这一步:稠密检索里「多久」的信息早被平均进整段向量糊掉了,多向量允许「一个词对一个词」地核对,细节匹配精度最高。

代价也直观:一个文档从一根向量变成几百根,存储涨几百倍、检索算力也涨。它对应 ColBERT 那一路思路,BGE-M3 把它一并集成进了同一个模型。

OpenAI 的 text-embedding 系列同样流行,从早期的 text-embedding-ada-002 到 2024 年的 text-embedding-3-small 和 text-embedding-3-large,以简单易用、效果好著称。其中 text-embedding-3-large 维度高达 3072,在 MTEB 榜单上表现优异。

顺带澄清一个容易混淆的点:BGE-M3 和 text-embedding 属于同一代,都是基于 Transformer 编码器、专门为检索训练的模型,区别只在使用方式,一个开源自部署,一个走 API。如今智源、OpenAI、阿里、Cohere 各家都有 Embedding 服务,选型时到 MTEB 榜单上按语言、维度和成本挑即可。

Embedding 三代演进:Word2Vec → BERT → BGE-M3 / text-embedding

3.3 向量数据库:海量向量里怎么快速找到近邻

存完向量,接下来要面对存储阶段最关键的一环,向量数据库。为什么它关键?因为切片会非常多。

假设你有 100 万个文本块,每块被转成 1536 维的向量。用户提问时,你要快速找到与问题向量最相近的 Top-10 个块。如果只用暴力检索(枚举全部向量逐个算一遍),每次查询都是 O(N·d) 的开销,又慢又贵。所以必须做索引优化,这就是向量库的核心。

但要注意:向量索引和 MySQL 的索引树不是一回事。B+ 树这类结构依赖数据有「全序」可比大小,而向量是高维点,没有这种天然顺序,用不了那套索引树。目前主流的向量索引有三条思路。

3.3.1 HNSW:分层导航小世界图

HNSW 的思路有点像跳表,把节点分成很多层:最底下那层包含全部向量点,越往上节点越少,顶层往往只剩几个点。每个点进到哪几层是建索引时按概率随机决定的(每往上一层按指数衰减,比如大约只剩六分之一),所以各层的规模是顶层几个点、往下一层几十个、再往下几百个,直到底层全量。

单层内怎么找:每一层的点都和自己的近邻连成一张图。查询从这一层的入口点出发,看它连着的邻居里谁离查询向量更近,就跳到谁,反复走;直到某一步所有邻居都比当前位置远,说明在这一层走到了「局部最近」,停下。

层与层之间怎么衔接:把这一层找到的点直接作为下一层的出发点继续贪心。上层点少,一步跨得远,负责把范围圈出来;下层点密,一步迈得小,负责精修。走到底层时,目标区域已经锁定,最后在候选队列里取 Top-N 就行。参数 efSearch 就是这个候选队列的长度:队列越长越不容易漏掉真最近邻,召回越高、速度越慢。

跳表靠上层链表「跳」过大量节点,快速定位区间。区别在于,跳表的最下层是有序链表,而 HNSW 的底层是近邻图,点连向彼此相近的邻居,它靠图上的边做贪心导航,没有链表指针可用。

HNSW 分层结构:上层稀疏、下层全量,逐层下探定位(三维示意)

上图是三维空间里的示意画法:每个点代表一个高维向量,点的位置由向量决定,越相近的点贴得越近。重点是「分层 + 逐层下探」这个结构。

3.3.2 IVF:先分桶,再桶内搜

IVF(倒排文件索引)走的是分区域的思路。好比快递分拣中心不会一个个翻包裹找,而是先把包裹按地区分成几堆(华东堆、华南堆和华北堆),要找上海来的包裹,只去「华东堆」里翻就行。

IVF 也是这样,分两步建、两步查。

建索引:跑 k-means 聚类,把全量向量聚成 nlist 个「桶」,记录每个桶的质心;每个向量归入离它最近的质心所在的桶,形成「质心表 + 每桶一个成员列表」的倒排结构。

查询:先算查询向量和全部 nlist 个质心的距离,挑出最近的 nprobe 个桶;然后只在这几个桶的成员里做精确的距离计算,取 Top-K。

用数字感受一下提速:100 万向量、nlist = 1024,每桶约 1000 个成员;查询时算 1024 次质心距离、挑 8 个桶,桶内只算约 8000 个向量。对比暴力检索的 100 万次,计算量缩了百倍以上。

代价就是只搜了部分桶,万一本该命中的向量偏偏落在没搜的桶里,就漏了。它有两个关键参数。

  • nlist:桶的数量,建索引时定,经验区间 4√N 到 16√N(N 为向量总数)。
  • nprobe:查询时实际搜索的桶数。nprobe 越大,召回越全、但越慢,是平衡「召回损失」和「查询速度」的旋钮。

IVF 分桶索引:聚类成桶 + 倒排,只搜最可能的几个桶

3.3.3 PQ:用压缩换内存

第三种思路叫 PQ(乘积量化,Product Quantization),核心是「压缩存储」。

向量太大、内存装不下时,PQ 的做法是把一个完整向量切成几段(子空间),每段单独量化压缩,就像把一张高清照片压成缩略图,细节丢了,关键特征还在。

整条流程分两半:库内向量量化成编号(建索引时一次做完),查询向量保持完整精度(每次查询现算)。拿数字例子走一遍,向量只有 8 维、切 4 段、每段 2 维,来了一条新向量:

1
[3.2, -1.5 | 0.8, 2.1 | -0.5, 1.9 | 2.4, 0.3]

建索引这一半(一次做完):

  1. 按段聚类出码本:把所有向量的第 1 段(各自的前 2 维)收集起来聚类,聚成 256 类,得到 256 个「2 维小向量」,编上 0~255 号,这就是第 1 段的码本;第 2、3、4 段同理各训一个。注意码本不是给每个向量配代表,而是所有向量的同一段共享同一个码本。
  2. 每段记最近质心的编号:这条新向量的第 1 段 [3.2, −1.5],去第 1 段码本里找距离最近的质心,假设是 77 号,于是只记 77;四段各找各的,整条向量最后存成 4 个编号 [77, 12, 203, 5]。8 维浮点向量原本要 32 字节,4 个编号只占 4 字节,压缩 8 倍;前文 128 维切 8 段,就是 512 字节压到 8 字节,压缩 64 倍。

查询这一半(每次查询现做):

  1. 查询向量保持精度,建查找表:查询向量不量化、不转编号,保持原始浮点数。查询的第 1 段去和「第 1 段码本里全部 256 个质心」算一遍距离,存成一张 256 格的表;4 个段就是 4 张表。这一步只对查询做一次,之后全部复用。
  2. 按编号查表求和,取最近:每条库内向量(如 [77, 12, 203, 5]),按编号查 4 次表:77 → 0.10,12 → 0.05,203 → 0.20,5 → 0.08,加起来 = 0.43,这就是它的近似距离;另一条 [88, 12, 40, 5] 算出 0.90 + 0.05 + 1.50 + 0.08 = 2.53。0.43 更小,前一条更近。所有库内向量都这么查一遍,最后取距离最小的 Top-K。

这个「库内向量量化、查询向量不量化」的设计,就叫 ADC(不对称距离计算,Asymmetric Distance Computation)——两边本就不对等:一边是精确的查询向量,一边是压缩过的库内向量。如果查询也量化成编号,两边都是粗粒度,误差叠加,检索质量会明显变差。ADC 把原本的向量运算变成了几次查表。

代价是有损:每段只记了「最近的那个小向量」,段内的精确数值丢了,算出的距离有误差,排序可能出小错。所以 PQ 用在内存紧张的场景,而且常和 IVF 组合成 IVF-PQ:先分桶再压缩,兼顾速度和内存。

3.3.4 相似度度量:余弦、内积、欧氏距离

向量库里衡量「像不像」,常用三种度量:余弦相似度(看夹角)、内积(看投影)、欧氏距离(看直线距离)。它们之间有个很实用的关系:向量都做了 L2 归一化(模长=1)之后,三者在排序上等价,余弦相似度就等于归一化后的内积,欧氏距离与余弦也单调对应。所以很多向量库默认对向量做归一化,再用内积加速计算。

3.4 向量数据库怎么选

市面上主流的向量库大致这几家。不用背参数,直接按「你的场景长什么样」对号入座。

产品 适合什么场景 关键理由 需要注意
Milvus 向量数亿级起步的企业级知识库 能扛十亿/百亿级,HNSW/IVF/PQ 索引齐全,支持 GPU 加速,Python/Java/Go SDK 完善 部署运维有门槛,通常要 Kubernetes 编排,团队没有 DevOps 经验会吃力
Pinecone 不想自建基础设施、团队小、要快速上线 全托管 SaaS,零运维,注册即用,自动扩缩容 付费且不便宜;数据存第三方,有合规(如数据出境)要求就不合适
Weaviate 中等规模、一开始就要混合检索 原生支持向量 + BM25 一起查,内置多种 Embedding 集成,可库内完成文本转向量 大规模数据下性能弱于 Milvus
Chroma 原型验证、个人项目、教学演示 Python 原生,pip 装完即用,API 极简 不支持分布式,安全与权限能力弱,别带进生产
Qdrant 自建部署、对延迟敏感、规模中等 Rust 编写,单机性能强,2025 年基准测试中 QPS 与延迟表现领先,过滤与混合检索丰富 生态相对新,超大规模仍需自建集群

一句话归纳选型思路:先看数据量和运维能力定「托管还是自建」,再看规模定「要不要 Milvus 这个量级」,最后看是否需要混合检索、延迟要求这些特性做收尾。

至此,离线存储阶段讲完。


四、在线检索阶段:细节与优化

4.1 朴素查询:Bi-Encoder 双编码器

正常的查询是这样的:把用户的提问用与存储时一样的 Embedding 模型转成向量,然后拿去数据库做相似度查询,检索到相似的文本。这套方案叫 Bi-Encoder(双编码器),把查询和文档分别编码成向量,再通过计算两者向量之间的余弦相似度来判断相关性。

它有个先天缺陷:因为查询和文档是分开、独立编码的,两者之间缺少交互,检索出来的只是「相似」的文本,并不一定「准确」。

举个例子。假设库里有两条都和「年假」高度相关:

  • 「年假未休完可以累积到下一年,最多累积 5 天」
  • 「年假未休完会被清零,不跨年结转」

用户问「年假没休完怎么办」。两条文档主题词高度重合,Bi-Encoder 分别编码后给出的相似度都很高,它分不出哪条才是公司真正的规定——「可以累积」和「会被清零」一字之差、含义相反,这种决定性的细节在各自编码时都被平均掉了。于是粗排可能把两条一起放行,把互相矛盾的答案喂给模型。

Bi-Encoder 双塔独立编码 vs Cross-Encoder 交叉编码

4.2 优化一:Rerank 精排(Cross-Encoder)

既然分开做不准确,那就把两者放在一起做。

Cross-Encoder(交叉编码器) 的做法是:先由 Bi-Encoder 检索出一批候选,然后把「查询」和「候选文档」拼接成一个序列送进模型:

1
[CLS] 用户的问题 [SEP] 检索到的文档内容 [SEP]

让模型对两者做交互分析,直接输出一个相关性分数。

Bi-Encoder 是「各自写好简历然后匹配」,Cross-Encoder 是「面对面面试」,面试官(查询)可以直接针对候选人(文档)的具体经历逐条追问,判断他是不是真的适合这个岗位。面试显然比看简历准得多。

但代价也高,不可能把库里所有文档都和查询做一遍匹配。所以工程上采用两级。

  1. 先用 Bi-Encoder 粗筛,快速捞一批候选;
  2. 再用 Cross-Encoder 对这批候选精筛,重排后取排名前几。

这个「粗排 + 精排」的动作,就叫 Rerank(重排序)。

实际落地时有几点经验。

  • 候选数量:先检索 50~100 个候选,再重排到 Top-5 或 Top-10,这个配置性价比最高。候选太多会拖慢 Rerank 耗时,候选太少又可能把真正相关的文档漏掉。
  • 不是所有查询都要 Rerank:简单的事实查询,Rerank 提升有限,可以设个条件触发,比如只在初排 Top-1 分数低于某个阈值时才启动重排;但复杂查询收益非常大,建议始终开启。

4.3 优化二:混合检索(多路召回)

精排是「对召回回来的数据重排」,那么上一步的粗排就应该尽可能多地返回候选,才不会漏。召回阶段本身也得优化,这个思路叫多路召回,也就是混合检索。

第一路是向量检索,就是前面讲的,用 Bi-Encoder 存进向量库并检索,负责语义相似。

第二路是关键词检索,最出名的就是 ES(Elasticsearch),默认的相关性打分就是 BM25。流程大致是先对文档分词,然后根据关键词构建倒排索引(词 → 出现该词的文档列表),查询时对词打分排序。它对关键词极其敏感,正好补向量检索的短板。

举个典型场景:查询某个具体编号的法律条文时,纯向量检索效果反而不好。因为向量看的是「两句话语义相不相似」,而一条完整法律条文和一个只提了编号的查询,语义相似度其实只落在那个编号上,整体算下来相似度不高,很可能检索不回来;关键词检索则能精准命中这个编号,一下就能捞到。

第三路可选,是其他召回,比如结构化字段过滤、多查询改写后的多路检索等。

多路召回的结果怎么合并?这一步叫融合(Fusion),常见的有两种做法。

  • RRF(倒数排名融合):只用「排名」,不用「原始分数」。对每个文档,把它在各路结果里的排名取倒数再加起来:

    1
    score(d) = Σ  1 / (k + rank_i(d))

    其中 rank_i(d) 是文档 d 在第 i 路结果里的名次,k 是个平滑常数(通常取 60)。谁在多路里都排得靠前,谁的综合分就高。RRF 的好处是天然省去了「分数归一化」的麻烦,因为向量检索的余弦分和 BM25 的分数量纲完全不同,直接比大小会翻车,而 RRF 只认名次,绕开了这个坑。

  • 加权分数融合:把各路分数先归一化到同一尺度(比如都压到 0~1),再按权重加权求和。这里最大的坑就是分数尺度不一致,BM25 的分数可能动辄两位数,余弦相似度却在 0~1 之间,不归一化直接加权,等于让 BM25 单方面碾压。

融合之后,再把结果一起交给 Cross-Encoder 做精排,最后取 Top-N 送给模型生成。

混合检索架构:向量路 + 关键词路 → 融合 → Cross-Encoder 精排 → 生成

预告:知识图谱检索(GraphRAG) 是另一条召回路线,它做的是关系检索,根据实体之间的关系把相关资料串起来。比如问「某药品和它相互作用的其他药品」,图谱能顺着「药品 A → 相互作用 → 药品 B」的关系边把整条链捞出来,这是纯向量和关键词都做不到的。它较为复杂,我放到下一篇单独讲,这里先理解「它靠关系召回」即可。

4.4 检索前的小优化

除了召回和精排,检索「之前」也还有几招。

  • 查询改写(Query Rewriting):检索前先用 LLM 把用户原始问题改写成适合检索的表述。用户的口语、指代和省略是检索的大敌,比如「你们公司加班多吗」这种问法,改写成「公司加班制度」会更贴近文档里的写法。这一步通常输出一个查询。
  • 多查询扩展(Multi-Query):把「一改一」变成「一改多」,用 LLM 生成多个不同角度的子查询,比如「公司加班制度」「加班时间规定」「加班补贴政策」,分别检索后合并结果,目的是提高召回。
  • HyDE(假设性文档嵌入):先让 LLM 根据用户问题生成一个「假设性的答案文档」,再用这个假设文档的向量去检索。因为假设文档在表述上更接近真实的答案文档(都是「答案口吻」),所以检索效果通常更好。

4.5 工业常用数值参考

把线上常见的经验区间汇总成一张表,方便起步(具体值要按你的数据和评测结果再调)。

环节 参数含义 常见取值
切块 切块大小(chunk size) QA 场景 200~500 tokens;通用 512~1024 tokens;中文约 200~500 字
切块 重叠长度(overlap) 约为切块大小的 10%~20%
向量召回 召回条数(Top-K) 20~100(配合精排常取 50~100)
关键词召回 召回条数(Top-K) 50~100
融合 RRF 平滑常数 k 60
精排 保留条数 3~10
HNSW M(每个点的最大连接数)/ efConstruction(建索引时候选队列长度)/ efSearch(查询时候选队列长度) 16~64 / 100~500 / 64~256
IVF nlist(桶数)/ nprobe(查询时搜索的桶数) ≈4√N / 8~64
融合权重 向量路 : 关键词路 0.7 : 0.3 起步(或 RRF 等权)

五、RAG 评测:把「效果好不好」量化出来

调 RAG 最怕一件事:改了半天不知道有没有变好。所以要评测,而且评测要分成两层,否则分不清到底是「没检索到」还是「检索到了但模型没用」。

5.1 第一层:检索质量评估

检索质量主要看两方面,这也是传统搜索沿用的评估方法。

  • 准确率:关注搜得准不准(搜回来的里面有多少是相关的),对应 Precision@K。
  • 全面率:关注搜得全不全(相关的里面有多少被搜回来了),对应 Recall@K。

除了这两个,还有一个综合指标 NDCG,用来看你排得对不对,即相关度排序是否合理。这也是最常用的综合指标。

NDCG 是怎么算的?

它由两个概念叠起来。

  1. DCG(折损累积增益):把每个位置上的「相关度 ÷ 位置折损」累加起来,折损就是除以 log2(位置+1)。意义很直白:排在前面的相关结果价值更大,排在后面的即使也相关,价值也被折损。公式是 DCG = Σ rel_i / log2(i+1),其中 rel_i 是第 i 个结果的相关度。
  2. IDCG(理想 DCG):假设排序完全理想(所有相关结果都排在最前面)时的 DCG。

于是 NDCG = DCG / IDCG,取值落在 0~1 之间。越接近 1,说明实际排序越接近理想排序,它同时兼顾了「有没有搜到相关的」和「把相关的排在了多前面」。

拿具体数字算一遍。搜回来 5 条,真实相关度分别是 3、2、0、1、0(3 最相关,0 不相关)。折损系数按位置算:第 1 位除以 log2(2)=1,第 2 位除以 log2(3)≈1.585,第 3 位除以 log2(4)=2,第 4 位除以 log2(5)≈2.322,第 5 位除以 log2(6)≈2.585。

  • 实际排序 [3, 2, 0, 1, 0],逐位算:3/1 + 2/1.585 + 0/2 + 1/2.322 + 0/2.585 = 3 + 1.26 + 0 + 0.43 + 0 ≈ 4.69,这就是 DCG。理想排序是 [3, 2, 1, 0, 0],同样逐位算:3/1 + 2/1.585 + 1/2 + 0 + 0 ≈ 4.76,这就是 IDCG。NDCG = 4.69 / 4.76 ≈ 0.99,相关的都搜回来了,而且排在了前面。
  • 同样 5 条,排成 [0, 1, 3, 2, 0]:0/1 + 1/1.585 + 3/2 + 2/2.322 + 0/2.585 = 0 + 0.63 + 1.50 + 0.86 + 0 ≈ 2.99。IDCG 不变还是 4.76,NDCG = 2.99 / 4.76 ≈ 0.63。结果一样,只是排乱了,分就掉下来。

5.2 第二层:生成质量评估

这一层看的是:检索回来的资料,有没有真正提升生成效果。核心是三个指标。

Faithfulness(忠实度)是 RAG 评估中最重要的指标,衡量的是大模型生成的回答是否忠实于检索到的文档,有没有编造文档里不存在的信息。

举个例子:检索出来的资料表明「苹果公司 2024–2025 年度营收为 3000 万亿美元(假设)」,此时你问苹果这几年的营收情况,如果模型答成「2025–2026 年度营收为 3000 万亿美元」,多加了资料里根本没有的年份,那就是不忠实,属于编造。

Answer Relevance(答案相关性)看的是生成的答案有没有回答用户的问题,有没有跑题。

举例:用户问「TCP 的三次握手是什么」,结果检索回来一堆网络协议的知识,模型最后大篇幅讲了网络协议的各种细节,反而把用户最关心的「三次握手」边缘化了,导致跑偏,这就是答案相关性出了问题。

Context Relevance(上下文相关性)衡量捞回来的上下文本身和问题相不相关,归到底属于检索质量问题。

RAG 两层评测体系:检索层指标 ←→ 生成层指标

5.3 开源框架 RAGAS

上面这些指标如果每次都手写,很难标准化、复用。RAGAS 就是来解决这个问题的开源框架:给它「问题 + 检索到的上下文 + 模型回答」(部分指标还要标准答案),它自动把每项指标算成一个 0~1 的分数。五个核心指标,逐个看它是怎么算的。

  • Faithfulness(忠实度):把模型的回答拆成一条条事实断言,逐条让 LLM 判断「这条断言能不能被检索到的上下文支持」,被支持的比例就是得分。回答了三句话,两句有据可查、一句是编的,忠实度就是 2/3 ≈ 0.67。它不需要标准答案,是 RAG 评估里最常用的一项。
  • Answer Relevancy(答案相关性):反向验证。让 LLM 根据答案反推「这道题大概问的是什么」,生成几个候选问题,再算这些候选问题和原始问题的向量相似度。答案切题,反推的问题就和原问题很像,分高;答案跑题(问三次握手却答了一堆协议历史),反推的问题会偏到别处,分低。同样不需要标准答案。
  • Context Precision(上下文精确率):评检索结果的排序质量——相关块是不是排在了不相关块前面,思路和检索层的 NDCG 一致,只是「相关不相关」由 LLM 判定。
  • Context Recall(上下文召回率):把标准答案拆成断言,逐条检查「这一点能不能在检索到的上下文里找到依据」,找得到的比例就是得分。标准答案里的关键点在上下文里找不到,说明漏检了。
  • Answer Correctness(答案正确性):把回答和标准答案直接比对,按事实重合度和语义相似度加权,衡量最终回答对了多少。
指标 属于哪一层 需要标准答案吗
Faithfulness 生成层 不需要
Answer Relevancy 生成层 不需要
Context Precision 检索层 不需要(LLM 判定)
Context Recall 检索层 需要
Answer Correctness 生成层 需要

实践中一般先用免标注的 Faithfulness 和 Answer Relevancy 摸清整体水位,要深挖检索层时,再补上带标准答案的两项。

RAGAS 的价值在于,它让「调优有没有变好」变成可量化的数字,而不是凭感觉。


六、幻觉:来源、解决与不确定性量化

最后收个尾,聊聊 RAG 一直在对抗的那个老对手,幻觉。

幻觉从哪来? 根本原因是模型在被要求回答时,倾向于给出「看起来合理」的输出,而不是「事实正确」的输出。它的目标函数里,流畅、像样的回答更容易得高分。于是当它缺乏对应知识时,就会用训练中见过的模式去「填空」,编出论文、编号这类看着很专业、实际上全是假的内容。

怎么解决? 主要有几条路。

  • RAG:也就是本文全部内容,把真实知识先拼进 Prompt,让模型有据可依,从源头压住幻觉。
  • 微调:把领域知识内化进参数,但如前所述,它学到的更多是「模式」和「风格」,对具体事实的准确性不做保证。
  • 输出校验:对模型的输出再做一层事实核对。
  • 不确定量化:这个方法比较有意思,拿到模型输出那一句话里每个词的原始数值(本质就是概率),如果这些概率平均下来都很低,说明这个答案很有问题。可以这样理解:这句话是模型在「很多都不太行的候选里,挑了一个相对没那么烂的」。词级概率普遍偏低,就是模型自己心里没底,答案大概率不可信。

总结: RAG 的本质思路其实和限流一样,都是漏斗模型。前面用多路召回尽可能多捞(对应限流的「放行」),后面用精排和阈值层层收窄(对应限流的「拦截」)。再叠上评测和幻觉校验这类风控,才是一套完整的线上 RAG。这部分值得专门设计,而不是随便拼一个 Prompt 了事。


七、总结

按一篇文档从进库到被用上的完整顺序,把全流程串一遍。

离线这一侧,一篇《员工手册》进系统,先被解析成纯文本,然后切块。怎么切决定后面一切的上限:默认用递归分隔符保住句子完整,格式规范的文档上结构感知切分,对召回精度要求高就上语义切分或父子块。切好的块用同一个 Embedding 模型转成向量,入库时按规模和内存选索引:追求低延迟用 HNSW,量太大用 IVF,内存紧张再叠 PQ 压缩。

在线这一侧,用户问出「公司年假政策是什么」,先用同一个模型把问题转成向量,进入漏斗第一层——粗排。粗排要的不是准,是全:向量检索抓语义相近的,BM25 抓编号、型号这类精确关键词的,两路结果用 RRF 融合,宁可多捞几十条。第二层是精排,Cross-Encoder 把查询和每条候选放在一起逐条「面试」,把几十条收窄到三五条。有需要的话,检索之前还能用查询改写、HyDE、多查询扩展,把问题本身先修好。最后拼 Prompt,交给模型生成。

评测把两头闭环。改了切块粒度、换了融合权重、加了精排,效果到底变没变好,不靠感觉:检索层用 Precision@K、Recall@K、NDCG 判断「该捞的捞到没有、排得对不对」,定位检索的问题;生成层用 Faithfulness、Answer Relevancy 判断「模型有没有基于资料好好说话」,定位生成的问题。用 RAGAS 把这些分数自动化,每改一版跑一遍,数字说话。

一条主线贯穿始终:切块粒度决定召回噪声,噪声逼出精排,粗排的漏召逼出混合检索,而所有优化是否生效,靠评测验证。 RAG 的上限由检索质量决定,检索质量由切块和评测兜底。