跳转至

RAG Embedding

RAG Embedding 将查询和文档映射为向量,用于语义召回。查询与文档必须由同一个 embedding 模型映射到同一个向量空间,相似度才有意义;模型或归一化方式不一致,相似度得分会失真。作为向量检索的「原材料」,embedding 的模型选择、归一化与预处理直接决定召回质量的上限,检索算法只能在此基础上逼近。

快速开始

先固定三样东西:embedding 模型及其版本、向量是否归一化、以及索引版本。相似度度量上,两个向量 \(q\)(查询)与 \(d\)(文档)的余弦相似度为

\[ \cos(q,d)=\frac{q\cdot d}{\|q\|\,\|d\|} \]

内积则为 \(q\cdot d\)。当归一化(\(\|q\|=\|d\|=1\))后,余弦相似度与内积等价;不归一化则要明确用余弦还是内积,并保证建索引与查询时使用同一度量——度量混用是召回异常的常见来源。

这类「查询与文档分别编码后算相似度」的模型叫双编码器(bi-encoder),它与 交叉编码器(cross-encoder)的区别在于:双编码器可把文档向量预先算好建索引,查询时只编码 query 一次,适合大规模召回;交叉编码器把 query 与文档拼在一起编码,精度更高但每对都要单独算,只适合候选较少时精排。因此典型 RAG 用双编码器召回、交叉编码器重排。

开源双编码器常在 MTEB 上比较,该基准聚合了检索、聚类、分类等多项任务;常见的检索向模型包括 BGE 与 E5 系列。选型时应看与自身语言和领域匹配的子任务分数,而非只看总榜排名——总榜是多种任务的均值,未必代表「你的语料上的检索」表现。

用带标注的 query 集评估:标准片段应被召回到前 k。报告 Recall@k,同时记录向量维度、批量大小与并发,因为这些参数会影响索引构建与查询延迟。查询侧与文档侧要用完全相同的模型和预处理(分词、截断、前缀),否则向量不在同一空间——E5 这类模型还要求查询前加特定前缀(如 query:),漏加会让查询与文档落入不同的子空间。

from sentence_transformers import SentenceTransformer

model = SentenceTransformer("BAAI/bge-large-zh-v1.5")
queries = ["如何申请退款"]
docs = ["退款需在购买后 7 天内提出申请。"]
q_emb = model.encode(queries, normalize_embeddings=True)
d_emb = model.encode(docs, normalize_embeddings=True)
score = q_emb @ d_emb.T          # 归一化后内积 == 余弦相似度

任何一侧改动(换模型、改归一化、改预处理)都要重建索引,再灰度对比新旧索引在问题集上的召回,确认无回退后才全量切换。不要在新旧向量混用的索引上跑查询。

案例

团队决定更换 embedding 模型以提升召回。正确流程是:先用新模型对全量文档重新生成向量、重建一个新索引,保留旧索引;再用同一批标注 query 分别跑新旧索引,比较 Recall@k 与延迟,做灰度。

常见错误是只换查询侧模型、文档侧仍是旧向量:两套向量来自不同模型,相似度得分不可比,召回会明显变差且难以解释。因此换模型必须「文档侧 + 查询侧 + 索引」三者同时、一致地切换——向量空间的改变是全量的,不存在「只换一半」的中间态。

灰度确认新模型在召回和延迟上都无回退后,才把流量切到新索引,并把模型与索引版本一起写进配置,作为可回溯的发布单元。若新模型在某类 query 上召回下降,可保留旧索引做兜底或按场景分流——这正是「先建新、保留旧、灰度切」流程的价值:任何时刻都有可回退的旧版本。

相关主题