site logo

Marico's space

你的 RAG Pipeline 可以很快但仍然错误:开发者嵌入评估指南

前端技术 2026-09-22 20:57:22 2

最近折腾 RAG 系统,被 Embedding 评估坑了几次,这篇把问题说清楚。

向量数据库 15 毫秒返回 20 个文档,RAG 系统照样给你一个烂答案。

检索的核心难点从来不是生成向量,而是判断向量有没有把对的文档排在查询附近。

从这个角度看,Embedding 评估本质上是个信息检索问题。一旦想通这点,三个指标就变得特别有用:

  • Recall@k:我们到底有没有检索到相关信息?
  • MRR:第一个相关结果多久出现的?
  • NDCG:最有用的结果有没有排到前面去?

然后还有第二个坑。

用户用印地语搜索英文文档怎么办?或者在印地语句子里用拉丁字母输入产品名?或者用了模型训练数据里根本不存在的内部术语?

最后还有个灵魂拷问:什么时候该微调 Embedding 模型,而不是改别的东西?

咱们从头捋一遍。

1. Embedding 模型本质上是个排序系统

很多人把 Embedding 理解成语义指纹——编码一段文本,得到一个向量:

"How do I rotate an API key?"

编码后得到:

[0.18, -0.07, 0.42, ...]

然后把语料库里每个文档块都编码存起来,检索时找最相似的向量。

相似度通常用余弦相似度:

cos(q, d) = (q . d) / (||q|| * ||d||)

向量归一化后,余弦相似度就等价于点积:

cos(q, d) = q . d

整个系统做的事用一句话就能说清:

query | v
embedding | v
rank documents by similarity | v
top-k documents

关键在 rank 这个词。

你的应用根本不在乎文档 A 的余弦相似度是 0.83 还是文档 B 的 0.79

它只在乎 A 是不是应该排在 B 前面。

这个区别在信息检索领域早就被研究透了。

2002 年,Kalervo Järvelin 和 Jaana Kekäläinen 提出了用分级相关性评估检索系统:一篇文档可以是无关的、有点用的、很有用的、直接回答问题的。动机很实际:检索系统返回的结果太多了,评估必须奖励那些把高度相关内容排在前面靠前的系统。

现代 Embedding 搜索遇到的是同一道题。

向量数据库只是实现手段。

你的系统真正在做的事是:

semantic query -> ranking -> useful context

所以你需要一个测试集。

2. 先建检索测试集,再动模型

假设你的文档系统有 10 万个文本块。

收集 500 条真实用户查询:

"how to rotate api keys"
"why does websocket authentication fail"
"can I run the agent without public ports"
"delete an organization"
...

对每条查询,标出哪些文档真正能回答它。

比如:

Query:
"how to rotate api keys" Relevant:
doc_1842
doc_7119
doc_9320

这样你就有了一个小规模检索 Benchmark。

现在可以用完全相同的查询来对比不同的 Embedding 模型了。

这比拍脑袋说"模型 A 看起来更语义化"有用一万倍。

一个能打的 Benchmark 应该包含各种刁钻情况:

  • 短查询
  • 模糊查询
  • 只有你的用户才懂的术语
  • 多个正确答案的查询
  • 正确答案藏在大文档深处的查询
  • 拼写错误
  • 多语言查询

还要留一部分测试集,专门用于调参时不动它。

一个简单的评估数据集长这样:

query_id: 17
query: "rotate api key"
relevant_docs: - doc_1842 - doc_7119 query_id: 18
query: "websocket auth failure"
relevant_docs: - doc_921

这样 Embedding 问题就变成可测量的了。

3. Recall@k 回答第一个问题:"找到了吗?"

假设某条查询有 5 个相关文档:

Relevant = {A, B, C, D, E}

Embedding 检索返回:

Top 5 = [X, A, Y, Z, B]

找到了 5 个中的 2 个。

所以:

Recall@5 = 2 / 5 = 0.40

通用公式:

Recall@k = relevant documents retrieved in top-k ------------------------------------- total relevant documents

直觉含义:

Recall@k 衡量你把多少答案空间纳入了视野。

想一下:如果你的 RAG 系统检索 10 个块,但答案所在的那个块从来没出现在 top 10 里,LLM 就是巧妇难为无米之炊。

更强的 LLM 不会神奇地修复缺失的上下文。

为什么 RAG 要关注 Recall@k

假设有 1000 条评估查询,平均每条 4 个相关块。

大约有:

1,000 * 4 = 4,000 个相关块

如果 Recall@20 是 0.80,检索器大约召回了:

4,000 * 0.80 = 3,200 个相关块

大约 800 个相关块漏掉了。

这是一个具体的失败预算。

Recall@k 有一个重要局限

它把所有相关文档一视同仁。

假设相关集合是:

[A, B, C]

两个系统返回:

System 1: [A, X, Y, Z, B]
System 2: [X, A, B, Y, Z]

k=5 时,两者的 Recall 完全一样。

但 System 1 把最强结果排在了第一位。

Recall 告诉你有没有把有用材料纳入候选集。

它对排序质量几乎说不出什么。

这就是 MRR 和 NDCG 登场的地方。

4. MRR 问:"多快找到有用的东西?"

MRR 是 Mean Reciprocal Rank(平均倒数排名)。

单条查询:

reciprocal rank = 1 / rank_of_first_relevant_result

所以如果第一个相关结果出现在:

rank 1 -> 1.00
rank 2 -> 0.50
rank 3 -> 0.33
rank 10 -> 0.10

多条查询取平均。

例子:

Query 1 -> first relevant at rank 1 -> 1.00
Query 2 -> first relevant at rank 2 -> 0.50
Query 3 -> first relevant at rank 4 -> 0.25
Query 4 -> no relevant result -> 0.00

那么:

MRR = (1.00 + 0.50 + 0.25 + 0.00) / 4 = 0.4375

MRR 特别适合那种通常只有一个正确答案的查询。

比如:

"what is the default timeout?"
"where is the config file?"
"how do I reset my password?"
"what command starts the server?"

这类问题,把正确答案排在第 1 位远比排到第 15 位好得多。

但 MRR 完全忽略第一个相关文档之后的所有内容。

考虑:

System A: [A, X, X, X, X]
System B: [A, B, C, D, E]

只要 A 是相关的,两个系统的:

RR = 1.0

MRR 觉得它们完全一样。

但对于很多 RAG 应用来说这显然不对——如果你的答案需要多个证据片段,你肯定关心整个排序。

这正是 NDCG 的用武之地。

5. NDCG 问更实际的问题:"有用的东西排对了吗?"

NDCG 是 Normalized Discounted Cumulative Gain(归一化折损累计增益)。

核心思想比名字简单得多。

给每个检索结果打一个相关性分:

比如:

0 = irrelevant
1 = somewhat useful
2 = useful
3 = directly answers the question

假设你的排序是:

rank 1 -> relevance 3
rank 2 -> relevance 2
rank 3 -> relevance 0
rank 4 -> relevance 1
rank 5 -> relevance 0

NDCG 给排在前面靠前的高相关文档更多分数。

底层 DCG 公式:

DCG@k = sum from i=1 to k of (2^rel_i - 1) / log2(i + 1)

各部分含义:

2^rel_i - 1

让相关性 3 比相关性 1 价值高很多。

而:

log2(i + 1)

对排名靠后的结果打折。

所以排名第 1 位的结果比排名第 10 位的权重高很多。

然后用理想排序做归一化:

NDCG@k = DCG@k / ideal_DCG@k

因此:

NDCG@k = 1.0

意味着你的排序和理想排序完全一致。

一个具体例子

假设理想相关性评分是:

[3, 2, 2, 1, 0]

你的系统返回:

[1, 0, 3, 2, 0]

系统仍然召回了相关文档。

Recall 看起来可能还行。

MRR 可能也还行,因为一个相关文档出现得很早。

但 NDCG 崩了,因为相关性 3 的答案被推到了第 3 位。

这往往正是 RAG Pipeline 中你想惩罚的情况。

三个指标在说不同的事

把它们想象成三个不同的问题:

Recall@k -> 我们检索到足够多的答案了吗? MRR -> 我们快速找到答案了吗? NDCG -> 我们把有用的答案排对了吗?

一个靠谱的检索 Benchmark 通常三个都要用。

简单 Python 实现把区别说得很清楚:

import math def recall_at_k(ranked_ids, relevant_ids, k): relevant_ids = set(relevant_ids) retrieved = set(ranked_ids[:k]) return len(retrieved & relevant_ids) / len(relevant_ids) def reciprocal_rank(ranked_ids, relevant_ids): relevant_ids = set(relevant_ids) for rank, doc_id in enumerate(ranked_ids, start=1): if doc_id in relevant_ids: return 1.0 / rank return 0.0 def ndcg_at_k(relevances, k): def dcg(values): return sum( (2 ** rel - 1) / math.log2(i + 2) for i, rel in enumerate(values[:k]) ) actual = dcg(relevances) ideal = dcg(sorted(relevances, reverse=True)) return actual / ideal if ideal else 0.0

注意一个重要细节:

NDCG 需要分级评判。

如果每个文档只标记"相关"或"不相关",NDCG 也能跑,但你扔掉了有用信息。

很多生产系统的实用评判方案:

0 = 不能回答查询
1 = 相关但不够充分
2 = 有用的证据
3 = 直接回答查询

这样通常就足够让排序问题暴露出来了。

6. 多语言检索改变了问题本身

现在假设你的文档是多语言的。

用户问:

"How do I reset my password?"

相关文档是中文的:

"忘记密码时,您可以通过以下步骤重置密码..."

一个真正支持多语言的 Embedding 模型应该在向量空间里把这两段文本放得很近。

这比普通单语言相似度计算难多了,因为模型要学习语言无关的语义表示。

这在 LLM 时代之前就是个重要的研究方向了。

比如 Mikel Artetxe 和 Holger Schwenk 开发的 LASER,支持 93 种语言的多语言句子嵌入。他们的工作明确评估了跨语言相似度,证明了共享嵌入空间可以支持跨语言语义搜索。

但"多语言"不等于"在所有语言对上表现一样好"。

你自己得测。

比如你的评估矩阵可能是:

Query Document Recall@10
英文 英文 0.91
中文 中文 0.87
英文 中文 0.64
中文 英文 0.59
泰语 英文 0.52
中英混合 英文 0.47

这些数字是举例,但测试结构本身很重要。

聚合分数可能掩盖严重问题。

假设:

英文-英文: 0.92
英文-中文: 0.61
中文-英文: 0.58

而 90% 的 Benchmark 查询都是英文-英文。

你的整体 Recall 可能看起来很漂亮。

你的中文用户实际体验完全不同。

什么容易搞砸多语言检索

有几个反复出现的坑:

不同文字体系

英文: "insurance claim"
中文: "保险理赔"

音译

"保险"
"baoxian"
"insurance"

代码混合

"UPI 的交易失败了"

专有名词

公司名、API 名、药品名、地名或产品名可能在各语言间保持不变。

领域词汇

你的用户可能在自然语言中混入术语:

OAuth
JWT
webhook
protobuf
nginx

这些既不是纯英文也不是纯中文。

多语言检索要按语言对、按查询类型分别评估,而不是看一个全局分数。

更宽泛的教训来自后来的 Benchmark 研究。MTEB 对 Embedding 模型在大量任务和语言上做了评估,发现没有任何单一 Embedding 方法在所有任务上称霸。Embedding 质量是任务相关的。

这个发现非常重要——当有人说:

"我们用的是最好的 Embedding 模型。"

最好追问一句:最好是哪种?

7. 什么时候该微调 Embedding 模型

微调很诱人,因为它看起来像直接解法:

retrieval is bad | v
fine-tune embedding model | v
retrieval gets better

有时候这确实是对的。

但很多时候这是 premature(过早)的