面试官问:“两句毫不相干的话,embedding 相似度是 0 吗?“
面试官问:“两句毫不相干的话,embedding 相似度是 0 吗?”
简历:“3 年 RAG 开发经验,负责企业知识库检索系统,熟悉主流 embedding 模型选型与向量数据库调优。”
看到这份简历,我问了个不需要任何准备的问题:随便找两句毫不相干的话,把它们的 embedding 算个余弦相似度,你猜是多少?
候选人想了两秒,说应该接近 0 吧,不相关嘛。
这个答案在 Qwen3-Embedding 上勉强算对,在 BGE-M3 上偏了一倍,在 multilingual-e5 上错得离谱。三个数字都不是我测的,全写在这些模型自己的 HuggingFace 主页上,就在示例代码下面第一行输出里。
做向量检索的人不少,认真看过自己那个模型分数长什么样的人不多。这场面试的五个问题,全都绕着同一个数打转。
Round 1:不相干的两句话,分数真是 0 吗
面试官:“余弦相似度的取值范围是 -1 到 1。两段完全没关系的文本,你估计落在哪?”
候选人:“应该在 0 附近吧,高维空间里随机两个向量差不多是正交的。”
正解:
随机向量确实接近正交,但 embedding 模型吐出来的不是随机向量。三个主流模型对同一件事给出的答案分别是 0.68、0.35 和 0.08。
全部出自官方 model card 的 Usage 示例,作者自己跑出来贴上去的,谁去跑都能复现。
先看multilingual-e5-large-instruct,它的 model card 用两个 query 和两个 passage 演示,打印出的分数矩阵是[[91.93, 67.58], [70.38, 92.13]](乘了 100)。对角线上匹配的那两对是 0.92,非对角线上完全不匹配的两对是 0.68 和 0.70。
换成BAAI/bge-m3,dense 检索的示例矩阵是[[0.6265, 0.3477], [0.3499, 0.678]],匹配的 0.63,不匹配的 0.35。
再看Qwen3-Embedding-8B,示例里匹配的两对是 0.75 和 0.63,不匹配的两对是 0.08 和 0.09。
三个模型,同样是毫不相干,一个说 0.68,一个说 0.35,一个说 0.08,差了将近一个数量级。
为什么不会归零:主流 embedding 输出前都做了 L2 归一化,所有向量落在单位球面上,余弦相似度退化成两点的夹角。OpenAI 的文档写得很直白,它家的 embedding 归一化到长度 1,所以余弦相似度可以直接用点积算,欧氏距离排出来的顺序也完全一样。关键在于这些向量在球面上不是均匀铺开的,训练完之后它们挤在一个相对狭窄的锥形区域里。SimCSE 那篇论文(arXiv 2104.08821)就是冲这个去的,核心贡献之一是用对比学习目标把预训练嵌入的各向异性空间正则化得更均匀。挤在一个锥里的两个点,夹角天然小于 90 度,余弦值天然大于 0。
30 秒自己验证,别信我也别信直觉:
`from sentence_transformers import SentenceTransformer
m = SentenceTransformer(“BAAI/bge-m3”) # 换成你线上正在用的那个
a = m.encode([“今天下午三点开产品评审会”], normalize_embeddings=True)
b = m.encode([“这道菜的辣椒放多了”], normalize_embeddings=True)
print(a @ b.T)`
嫌装库麻烦的话有个更快的办法:打开你线上那个模型的 HuggingFace 页面,往下翻到 Usage 那段示例代码,作者已经把输出分数贴在注释里了。
要点速记
- 官方示例里的不相关分数:e5-large-instruct 0.68,BGE-M3 0.35,Qwen3-Embedding 0.08
- 主流 embedding 都做了 L2 归一化,cosine 等价于点积,欧氏距离排序结果一致
- 分数不归零的直接原因是向量集中在球面的一个窄锥内,随机两点夹角小于 90 度
- 验证成本:5 行代码,或者翻一下 model card 的 Usage 段
Round 2:同样判定相关,凭什么一个给 0.92 一个给 0.63
面试官:“e5 判定相关给 0.92,BGE-M3 判定相关只给 0.63。哪个模型更有把握?”
候选人:“那肯定是 e5 吧,0.92 的置信度比 0.63 高多了。”
正解:
这两个数没有可比性,它们量的不是同一把尺子。分数分布的宽窄是训练时的温度参数直接压出来的,和模型判得准不准是两件事。
bi-encoder 的训练目标基本都是 InfoNCE:
L = -log[ exp(s⁺/τ) / Σ exp(sᵢ/τ) ]
其中s是 query 和 candidate 的余弦相似度,s⁺是正样本那一对,分母跑遍 batch 内所有负样本,τ 是温度。
E5 论文(arXiv 2212.03533)里明确写了默认 τ 取 0.01。把这个值代进去看会发生什么:相似度差 0.01,除以 τ 之后在 logit 层面就是 1.0 的差距。正负样本只要拉开 0.24 的余弦差距,进 softmax 前的 logit 差就是 24,指数化之后正样本概率无限逼近 1,loss 趋近于 0,梯度基本消失。
训练到此为止。模型没有任何动力再把正负样本推得更开,从 loss 的角度看它已经满分了。0.92 配 0.68 这个组合,就是 τ=0.01 训练收敛后的自然产物。E5 的 model card 自己也承认了这点,说分数通常落在 0.7 到 1.0 之间,这是低温度 InfoNCE 的预期行为。
换个温度,比如 τ 取 0.05,同样 0.24 的余弦差距只能换来 4.8 的 logit 差,softmax 之后正样本概率还没饱和,梯度还在,模型会继续把负样本往外推,最后收敛出来的分布就宽得多。Qwen3-Embedding 那个 0.75 对 0.08 的分布,宽度是 e5 的两倍多。
温度是主要旋钮,但不是唯一的。负样本的采样策略(batch 内随机负样本还是挖过的 hard negative)、有没有加 hard negative 的额外损失项、蒸馏时对齐的是分数还是排序,都会改变最终分数落在哪个区间。共同点是这些全属于训练侧的工程选择,没有一个跟这两句话在语义上有多像直接挂钩。
BGE 的作者干脆把这件事写进了 model card 的 FAQ,原文是:
a similarity score greater than 0.5 does not indicate that the two sentences are similar
what matters is the relative order of the scores, not the absolute value
分数超过 0.5 不代表这两句话像;有意义的是分数之间的相对顺序,不是绝对值。
OpenAI 的 embedding 文档也一样,通篇推荐用余弦相似度,从头到尾没给过任何分数范围参考或者阈值建议。这不是文档写漏了。
要点速记
- InfoNCE 的 τ 直接决定分数分布宽窄,E5 论文默认 τ=0.01,分数被压在 0.7-1.0
- τ 越小,同样的余弦差被放大越多,饱和越早,分布越窄
- 影响分布的还有负样本采样、hard negative 损失项、蒸馏对齐目标
- BGE model card 原文:分数 > 0.5 不代表相似,有意义的是相对顺序
- OpenAI embedding 文档从未给出任何推荐阈值
Round 3:支持和不支持,向量分得开吗
面试官:“知识库里有两段话,一段说本产品支持离线部署,一段说本产品不支持离线部署。用户问是否支持离线部署,你的向量检索能把正确那段排前面吗?”
候选人:“这两段相似度肯定都很高,都在讲离线部署。不过 top-k 都召回了的话,让大模型自己判断应该问题不大。”
正解:
bi-encoder 在这类场景下的表现不是差一点,是稳定地低于瞎猜。
EACL 2024 的 NevIR(arXiv 2305.07614)专门测了这件事。构造方法很干净:一对文档只在否定上有区别,两个 query 分别对应其中一个,要求模型两个方向都排对才算通过。随机排序的期望通过率是 25%。
实测结果(论文 Table 2):
| 模型类型 | 代表模型 | pairwise accuracy |
|---|---|---|
| 随机基线 | — | 25% |
| Cross-encoder | MonoT5-3b | 50.6% |
| Cross-encoder | MonoT5-base | 34.9% |
| Late-interaction | ColBERTv1 | 19.7% |
| Late-interaction | ColBERTv2 | 13.0% |
| Bi-encoder(向量检索) | DPR | 6.8% |
| Bi-encoder(向量检索) | multi-qa-mpnet-base-dot-v1 | 11.1%(该类最高) |
| Sparse | SPLADEv2 | 8.0% / 8.7% |
| Sparse | TF-IDF | 2.0% |
论文的结论句是 nearly all models perform worse than randomly ranking,几乎所有模型都比随机排序更差。
低于随机这件事有结构性原因,错误方向是固定的。bi-encoder 把整段文本压成一个定长向量,池化那一步会把每个 token 的贡献摊平。一个否定词在几十上百个 token 里只占一份权重,压缩之后基本被主题信息淹没了。而对比学习的训练语料里几乎不存在只差一个否定词的正负样本对,模型从来没被要求过要把它们分开,自然也没学会。
排序稳定地错还有个更细的原因:带否定的那条文档比不带的多了一个词,主题重心被这个词轻微拉偏,于是它和不含否定的 query 的距离反而近了一点。这个偏移在整个语料上方向一致,所以错误是可复现的,不是随机抖动。
候选人说的兜底方案也不太成立。指望 top-k 全召回再让大模型判断,前提是两条都进了 top-k。真实知识库里同一主题下有几十上百个 chunk,两条只差一个否定词的文档,排序上很可能被一堆同主题内容隔开,真正拿到答案的只有排错的那条。
更麻烦的是这类错误在评测里几乎查不出来。跑 faithfulness 指标,大模型完全忠实于检索到的内容,指标是满分;跑 answer relevancy,答案确实在回答用户的问题,指标也漂亮。一个自信、流畅、和检索内容严格一致的错误答案,整条评测链路上没有一个环节会报警。
架构层面能救一部分。Cross-encoder 把 query 和 document 拼在一起过一遍完整的 attention,否定词能和主题词直接交互,MonoT5-3b 做到了 50.6%。这个数字比 bi-encoder 高了四五倍,但也只是刚过及格线的一半,加了 reranker 不等于这个问题解决了。
要点速记
- NevIR(EACL 2024):pairwise accuracy 随机基线 25%,bi-encoder 全部落在 6.8%-11.1%
- ColBERTv2 13.0%,SPLADEv2 8.0%,TF-IDF 2.0%,最好的 MonoT5-3b 也只有 50.6%
- 根因:池化把否定词权重摊平,训练语料里没有只差否定词的 hard negative
- 这类错误在 faithfulness / answer relevancy 评测里全是满分,监控不到
- 加 cross-encoder reranker 能提升四五倍,仍然只到 50% 左右
Round 4:那阈值到底怎么定
面试官:“你们线上过滤低质量召回,阈值设的多少?”
候选人:“0.8,低于 0.8 的直接丢掉,避免噪声进 prompt。”
面试官:“这个 0.8 怎么来的?换模型的时候动过吗?”
候选人:“……应该是当时试出来的,后面一直没改。”
正解:
把 Round 1 那三组官方分数和 0.8 这个阈值放一起,结果一目了然。
| 模型 | 官方示例·相关对 | 官方示例·不相关对 | 阈值 0.5 | 阈值 0.8 |
|---|---|---|---|---|
| multilingual-e5-large-instruct | 0.92 / 0.92 | 0.68 / 0.70 | 全放行,等于没设 | 工作正常 |
| BGE-M3(dense) | 0.63 / 0.68 | 0.35 / 0.35 | 工作正常 | 全拦截,返回空 |
| Qwen3-Embedding-8B | 0.75 / 0.63 | 0.08 / 0.09 | 工作正常 | 全拦截,返回空 |
在 e5 上精心调出来的 0.8,搬到 Qwen3-Embedding 上,检索接口开始稳定返回空列表。Qwen3-Embedding-8B 在 MTEB 多语言榜上排第一(截至 2025 年 6 月 5 日,得分 70.58,C-MTEB 73.84),换过去是升级不是降级,但阈值不动的话,线上表现是知识库突然全空。反过来在 e5 上设 0.5,等于什么都没过滤,它连不相关的都给 0.68。
能用的做法是分位数校准,把阈值当成需要标定的量而不是常量:
`import numpy as np
rel / irr: 人工标注后,相关对与不相关对各自的分数数组
抽 200-500 条真实线上 query,各标 top-10 就够
thr = np.percentile(irr, 95) # 让 95% 的不相关召回被挡住
kept = (rel >= thr).mean()
print(f"阈值 {thr:.3f},相关文档还能留下 {kept:.1%}")`
标注量不用很大,200 条 query 各标 top-10 是 2000 个判断,两个人一天能干完。跑出来的阈值是这个模型 + 这批语料 + 这类 query 分布三者的联合产物,三个里任何一个变了都得重新标。换 embedding 模型要重标,知识库从产品文档扩到工单记录要重标,产品从 to B 转 to C 导致提问方式变了也要重标。
有个坑会让离线标好的阈值在线上直接失效:非对称前缀。
| 模型 | query 侧要求 | document 侧 | 漏掉的代价 |
|---|---|---|---|
| multilingual-e5-large-instruct | Instruct: {task}\nQuery: {q} | 不加 | model card 原文:会看到性能退化 |
| Qwen3-Embedding | Instruct: {task}\nQuery: {q} | 不加 | 官方说明:掉 1%-5% |
| bge-large-zh-v1.5 | 可加为这个句子生成表示以用于检索相关文章: | 不加 | v1.5 优化过,不加只有轻微下降 |
| BGE-M3 | 不需要 | 不加 | — |
这几个模型对前缀的要求各不相同,而且都只加在 query 一侧,document 侧保持原文。真正出事的场景是这样:离线批量灌库的脚本和线上查询的服务是两个人写的,一边加了前缀一边没加,模型照样跑得出向量,检索照样返回结果,没有任何报错。唯一的症状是整批分数集体偏移,辛苦标定的阈值全部对不上号。
另一个坑是归一化。OpenAI 的向量出厂就归一化到长度 1,点积和余弦完全等价。自己训的模型、或者某些开源模型的原始输出没做归一化,这时候用点积算相似度,向量的模长会直接进结果。文本越长模长通常越大,长 chunk 就天然拿高分。这类问题的典型症状是检索结果总是偏爱长文档,而排查方向往往被引到分块策略上去。
要点速记
- 同一个 0.8:e5 上工作正常,BGE-M3 和 Qwen3-Embedding 上全部返回空
- 阈值是 模型 × 语料 × query 分布 的联合产物,任一变化都要重标
- 校准成本:200-500 条真实 query 标 top-10,
np.percentile(irr, 95)起步- E5 / Qwen3 只在 query 侧加 Instruct 前缀,document 侧不加,两边不一致会整体偏移分数
- Qwen3-Embedding 官方数据:不加 instruction 掉 1%-5%
- 未归一化的向量用点积,模长进结果,长文档天然分数高
Round 5:怎么判断知识库里根本没有
面试官:“用户问了一个知识库里完全没有的问题,你怎么让系统承认自己不知道,而不是硬凑三条最像的?”
候选人:“那就得提升召回质量,比如上混合检索、加 reranker、优化分块……”
正解:
问的是怎么识别没有答案,答的是怎么把答案找得更准,这两件事方向是反的。
绝对阈值做不了这件事,Round 4 已经说清楚了。三个可以落地的替代方案:
方案一:看落差,不看绝对值。记录 top1 与 top5 的分数差。库里确实有答案时,top1 会明显甩开后面一截;库里没有的时候,top1 到 top10 是一条几乎水平的线,一堆同样不相关的文档挤在一起,谁排第一基本靠抖动。这个信号跨模型稳定,因为它量的是相对量,不受温度压出来的分布宽窄影响。落差阈值同样需要标定,但它对换模型的鲁棒性远好于绝对阈值。
方案二:cross-encoder 二次判定。reranker 把 query 和候选拼在一起过完整 attention,输出的相关性分数比 bi-encoder 更接近可解释。代价是延迟,一次 rerank 通常在几十到几百毫秒量级,取决于候选数和模型大小。Round 3 的数据提醒了这个方案的天花板:在否定这类场景上,最好的 cross-encoder 也就 50.6%。
方案三:把判断权交给大模型。在 prompt 里明确写清楚,如果提供的资料里没有相关信息就直接回答不知道。大模型读的是文本,能看懂否定、能看懂答非所问,而这两件事恰恰是余弦相似度做不到的。成本是多进 prompt 的那几千 token,比误答的代价便宜得多。
四种做法放在一起对比:
| 方案 | 判定信号 | 换 embedding 模型后 | 额外延迟 | 主要局限 |
|---|---|---|---|---|
| 绝对阈值 | top1 分数超过 T | 必须重新标定 | 0 | 跨模型直接失效,见 Round 4 |
| 分数落差 | top1 减 top5 超过 D | 建议复核一遍 | 0 | 库里有多条同样相关的文档时落差也小 |
| cross-encoder | rerank 相关性分数 | 建议复核一遍 | 几十到几百毫秒 | 否定场景上限 50.6% |
| 交给大模型 | 模型自己回答不知道 | 不用动 | 一次生成的时间 | 多进几千 token |
线上通常是后三种叠着用:落差先做粗筛,reranker 收一遍,最后靠 prompt 兜底。绝对阈值那一列唯一的优点是实现起来只要一行 if。
生产上还要加一条监控:每天记录线上 query 的 top1 分数的 p50 和 p90,画成时间序列。这两条线正常情况下是平的,一旦整体平移,说明三件事之一发生了:embedding 模型版本变了、灌库脚本的前缀逻辑改了、或者用户的提问方式变了。分数分布漂移是这几类问题里最早出现也最容易被忽略的信号,等到用户投诉搜不出东西的时候,通常已经漂了好几天。
要点速记
- 绝对阈值判定不了无答案,改用 top1 与 top5 的分数落差,跨模型稳定
- cross-encoder 二次判定更可解释,但否定场景上限只有 50.6%
- 最可靠的是在 prompt 里写清楚资料里没有就回答不知道,成本是几千 token
- 监控 top1 分数的 p50/p90 时间序列,整体平移意味着模型换了、前缀漏了或用户变了
面试官点评
这位候选人的问题不在于不懂向量检索,混合检索、reranker、分块策略他都能说得头头是道。问题出在他把一个训练副产品当成了置信度在用,而且用了三年没有怀疑过。
余弦相似度是模型内部的一个刻度,刻度间距由训练温度决定,零点在哪没人定义过。拿它做绝对判断,相当于拿一把没有起点的尺子量长度,量出来的数字换一把尺子就全变了。
三条建议:
- 打开你线上那个 embedding 模型的 model card,翻到 Usage 段,看一眼示例输出的分数是多少,这是成本最低、信息量最大的一步
- 花一天标 200 条真实 query,把阈值从试出来的常量变成标定出来的分位数,顺便你会第一次看清自己系统的分数分布长什么样
- 检查灌库脚本和查询服务,确认 query 前缀的处理逻辑一致。这个 bug 不报错、不崩溃,只是悄悄让检索质量差一截
做了几年向量检索,天天看着日志里那一列 similarity 分数做判断,却从来没打开过自己那个模型的 model card 看一眼作者写在 FAQ 里的第一句提醒。
BGE 的作者把分数大于 0.5 不代表相似写进了文档,OpenAI 的 embedding 指南一个阈值都不敢给。真正读到这两句话的人,比用这两个模型的人少太多了。
学AI大模型的正确顺序,千万不要搞错了
🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!
有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!
就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋
📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇
学习路线:
✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经
以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!
我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~
