Ida Silfverskiöld
2024-09-26
原文
---
title: "Embedding 实战:闭源 vs 开源"
author: Ida Silfverskiöld
source_url: https://towardsdatascience.com/working-with-embeddings-closed-versus-open-source-39491f0b95c2/
published_at: 2024-09-26
fetched_at: 2026-05-28
language: zh
translator: Hermes
---
# Embedding 实战:闭源 vs 开源
### 做检索这件事

_如果你不是会员但想读这篇,[这是免费阅读链接](https://medium.com/@ilsilfverskiold/39491f0b95c2?source=friends_link&sk=d94cfe5675e1c8810edb5ecfaa86ebc0)。_
embedding 是 NLP 的基石之一。它能干的事不少,但最常见的一件就是检索场景里的语义搜索。
虽然整个圈子最近都在聊知识图谱检索 pipeline,但传统的向量检索其实并没过时。
网上能找到一堆文章讲怎么把语义搜索里不相关的结果过滤掉——这篇也会聊这个,用聚类和重排(re-ranking)这两招。
不过这篇**真正想做的事**,是**把开源和闭源 embedding 模型放一起对比**——还要看不同体量。

我们会对比 9 个 embedding 模型,全是 MTEB [排行榜](https://huggingface.co/spaces/mteb/leaderboard)上排名靠前的。这样你大概能感觉到大模型 vs 小模型的差距,以及规模上去之后成本会怎么涨。
如果你用过 OpenAI 生成 embedding,估计也好奇过它到底竞争力够不够。
简单回顾一下 embedding——如果你刚接触:我们给每段文本生成 embedding(一组向量),就是把文本转成计算机能理解的东西。

具体到语义搜索,我们是把不同文本的 embedding 拿来比,看它们语义上有多像。这样查询就可以走"模糊搜索"——按关系找,不是按关键词精确匹配。

引言里我会讲 embedding 怎么工作,重点是怎么算语义相似度。
我每写一篇都喜欢拿一个具体场景练手,这次也不例外。这个想法来自一位咨询公司老板——他问我能不能做一个应用,把岗位描述自动匹配到 LinkedIn profile。
如果真做生产用,肯定要上百万真实 profile,但这篇只是演示,我用合成数据做了 6,900 份 LinkedIn profile。
数据集在[这里](https://huggingface.co/datasets/ilsilfverskiold/linkedin_profiles_synthetic)。
这场景算简单的:profile 不需要切块(一份就一段),领域也不复杂——一眼就能看出模型有没有抓对关系。
但思路是通用的,可以套到类似问题上。
如果你想直接跳过引言、用 LinkedIn 数据集动手玩不同模型,往下翻就行。
## 引言
这篇里 profile 数量不多,可以把聚类当成一种无监督的分类方法用在整个数据集上。
下面是聚类长什么样的示意图。

聚类还能让我们看出不同模型怎么理解事物之间的关联。
看模型表现,我们可以先把正确的那一组隔离出来,再在簇内做语义搜索。

这样能过滤掉不相关的结果,比如模型把 product manager 和 product marketing manager 搞混的情况。
最后还能加一步:用 LLM 做重排,确保最相关的结果排在最上面。
为了省事也省钱,我把每个待评测模型的 [embedding](https://huggingface.co/datasets/ilsilfverskiold/linkedin_recruitment_questions_embedded) 都已经预先算好放进[数据集](https://huggingface.co/datasets/ilsilfverskiold/linkedin_profiles_synthetic)里了,匿名岗位描述的 embedding 也算好了。
提醒一下:想直接动手可以跳到**用例**那一节,但成本那部分别跳过。
### Embedding
前面提过,embedding 是文本的数值表示,把含义编码下来——这样计算机就能处理和理解自然语言。

到了 transformer 这一代模型,它们能理解整句的 context,从而抓住一个词或一句话的多重含义——这事几年前还做不到。
我们其实可以把 embedding 画在图上,当成几何空间里的点。embedding 之间的语义关系,就体现为几何上的远近。

不同模型为不同任务训练,但稍大一些的模型一般都是通才,能干**检索**、**聚类**、**分类**这类活。
**语义搜索**用在检索里——它就是用图上的"近"来判断 query 应该匹到哪些 embedding,本质上是在算图上 embedding 之间的距离。
要在语义搜索里算 embedding 之间的相似度有好几种方法,但最常用的是余弦相似度。

你选哪个模型,直接决定语义搜索的结果——这归根结底取决于它是怎么训练的。
训练用的数据集、目标、架构都很关键,会直接影响模型对各种文本的理解和关联能力。
**聚类**则不太一样——它是把数据组织成一组组(簇),让同一簇内的相似度高于跨簇相似度。聚类更擅长把 embedding 之间的相似关系挖出来,让我们能干净地隔离出某一组。

这一步让我们能在做语义搜索之前先过滤掉不相关的匹配——相当于一道降噪工序。
起码思路是这样。
不是每个模型都能按我们想要的方式做聚类。有的擅长,有的拉胯,全看它怎么训出来的。
### Embedding 模型
那怎么知道选哪个?这时候 [MTEB](https://huggingface.co/spaces/mteb/leaderboard) 排行榜就该出场了——它按各种任务上的表现给 embedding 模型排名。
我从中挑了几个出来对比:OpenAI 那几款用得多的,再加一个 fine-tune 过的 Mistral-7B,还有更小更新的模型比如 Mixedbread AI 的 [Mxbai](https://huggingface.co/mixedbread-ai/mxbai-embed-large-v1)。
差不多都是过去两年里发布的。

如果你刚接触开源模型,可能会惊讶——好多开源模型其实排得很靠前。如果你已经玩过这些模型,看一眼哪个在这个任务上跑得最好也挺有意思。
下面这张表列了每个模型的体量、最大 token 数、以及检索和聚类的排名。

很多人都用过 OpenAI 的 [Ada-002](https://openai.com/index/new-and-improved-embedding-model/),它在我们的列表里垫底。不过 OpenAI 后来出了 text-embed-3 的 small 和 large 两版——表现更好,价格也更便宜。
那你可能会问:开源模型在排行榜上排得这么高,干嘛还有人用商业模型?
### 开源模型的成本账
用开源模型听起来挺好的,隐私上也是首选。排行榜上靠前的不少,但你得算一笔账:自己托管 vs 调 API,到底哪个划算。
我看了一下成本——把小一些(约 350M)和大一些(7B)的开源模型放 GPU 上自托管,对比按 token 付费的几款主流商业模型。

*前提是每段文本 400 token——这样 334M 模型在单张 L4 GPU 上每秒能处理 75–90 段,7B 模型在单张 A100 上每秒约 30–40 段(也可能更多)。*
你会发现:一旦要 embed 几百万段文本,text-embed-3-large 或 ada-002 这种模型成本就会飙——而且这还没算存储。
如果你是企业客户,又在看 Nvidia 的 embedding 模型比如 nv-embed-v1,他们的 [API](https://build.nvidia.com/nvidia/nv-embed-v1) 挺好用的——我用它测过几个模型。
不过用小模型(500M 以下)肯定是最稳的选择。能上小一点的开源模型就上——算力成本能直接砍掉 90%。
我也算了一下小模型和大模型放单张 GPU 上要跑多久。

调 API 也要花时间,而且有推理速率限制——所以不管选哪条路,都要算把整份数据集 embed 完要多久。
开源模型嘛,可以多堆几张 GPU。但这至少给你点感觉——选小一点的可能更省电。
不过测几个看看哪个适合你的任务总没错——接下来就这么干。
### 量化
上面看到了,7B 这种规模的大模型还是又贵又费电。我们只算了 250 万条 embedding 的成本——一旦再往上 scale,[量化](https://huggingface.co/blog/embedding-quantization)就值得看一看了。
量化是用更少的位数表示模型的数据,从而把模型压小。思路是:用 4-bit、8-bit 这类量化手段,让原本带不动大模型的硬件也能跑起来。
也有几个人测过量化对各种指标的影响——我最近看到的一篇说整体性能下降约 12%。
这个以后我得专门写一篇——这种"经济性 vs 性能损失"的话题我特别喜欢挖。
## 用例
我不知道你怎么样——反正我喜欢直接拿不同模型测一测,而不是只看指标。这样我能直观感受到:小模型和大模型在文本关系上理解力差多少。
合成 LinkedIn profile [数据集](https://huggingface.co/datasets/ilsilfverskiold/linkedin_recruitment_questions_embedded)在[这里](https://huggingface.co/datasets/ilsilfverskiold/linkedin_profiles_synthetic),要匹配的岗位描述数据集也在。
要用的 Colab notebook 在[这里](https://colab.research.google.com/gist/ilsilfverskiold/4214061d3ffd64479067e6e0dded3ffc/visualize_different_embedding_models.ipynb)。
### 导入数据
要跟着做就得打开那个 [notebook](https://colab.research.google.com/gist/ilsilfverskiold/4214061d3ffd64479067e6e0dded3ffc/visualize_different_embedding_models.ipynb)——打开后你会看到我们从 Hugging Face 拉了两个数据集。
```python
# 带 embedding 的合成 LinkedIn profile
dataset = load_dataset("ilsilfverskiold/linkedin_profiles_synthetic")
profiles = dataset['train']
# 带 embedding 的匿名岗位描述
dataset = load_dataset("ilsilfverskiold/linkedin_recruitment_questions_embedded")
applications = dataset['train']
```
这两个数据集让我们能在 6,900 份合成 LinkedIn profile 上对比不同 embedding 模型。
合成数据嘛,毕竟是合成的,要打折听。这批是 Llama 3.1 生成的——而且确实有点"过度对齐"的味道:profile 里反复出现 "results-driven"、"seasoned"、"dedicated" 这种词。
embedding 已经预先算好放进 `profiles` 里了——你点进去就能看到。
```python
# profiles 数据集
Dataset({
features: [...,'embeddings_nv-embed-v1', 'embeddings_nv-embedqa-e5-v5', 'embeddings_bge-m3', 'embeddings_arctic-embed-l', 'embeddings_mistral-7b-v2', 'embeddings_gte-large-en-v1.5', 'embeddings_text-embedding-ada-002', 'embeddings_text-embedding-3-small', 'embeddings_voyage-3', 'embeddings_mxbai-embed-large-v1 '],
num_rows: 6904
})
```
_P.S. embeddings_gte-large-en-v1.5 这个跑不通——我尝试过自己托管但没把所有 embedding 都跑齐,所以别用它。_
接下来你需要选一份要匹配的岗位描述。
看下面代码——我选了第 2 份,你可以换别的编号。
```python
application = applications[1] # 选第 2 份——一个 product marketing manager 岗位
application_text = application['natural_language']
print("application we're looking for: ", application_text)
```
懒得跑代码也行,直接在 Hugging Face 上看[数据集](https://huggingface.co/datasets/ilsilfverskiold/linkedin_recruitment_questions_embedded)。

这一步你可以挑想用的 embedding 模型。我大部分都试过了,这次用 `embeddings_mxbai-embed-large-v1`。
这是一个 334M 的开源模型——在前面那张表里它的检索和聚类排名都挺靠前。
想换别的模型只要改一下名字就行。看上面那个[数据集](https://huggingface.co/datasets/ilsilfverskiold/linkedin_profiles_synthetic)就知道有哪些选项。
```python
# 拿这个 embedding 模型对应的 query embedding——这里挑的是 mxbai-embed-large-v1
query_embedding_vector = np.array(application['embeddings_mxbai-embed-large-v1'])
embeddings_list = [np.array(emb) for emb in profiles['embeddings_mxbai-embed-large-v1 ']] # 注意这里多了个空格
texts = profiles['text']
```
### 语义搜索
我们先不上聚类,直接做一次语义搜索——看看裸跑能到什么效果。
要算 profile 跟 query(也就是岗位描述)之间的语义相似度,跑下面这段代码。
```python
# 先单独算一下余弦相似度(不带聚类)
def cosine_similarity(a, b):
a = np.array(a)
b = np.array(b)
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
similarities = []
for idx, emb in enumerate(embeddings_list):
sim = cosine_similarity(query_embedding_vector, emb)
similarities.append(sim)
```
然后按相似度从高到低排序,显示前 30 条。
```python
results = list(zip(range(1, len(texts) + 1), similarities, texts))
sorted_results = sorted(results, key=lambda x: x[1], reverse=True)
# 把结果打出来
print("\nSimilarity Results (sorted from highest to lowest):")
for idx, sim, text in sorted_results[:30]: # 想看更多就调这里
percentage = (sim + 1) / 2 * 100
text_preview = ' '.join(text.split()[:10])
print(f"Text {idx} similarity: {percentage:.2f}% - Preview: {text_preview}...")
```
结果大概长这样——具体看你选的是哪一份岗位描述。
```text
Similarity Results (sorted from highest to lowest):
Text 3615 similarity: 89.59% - Preview: Product Marketing Manager | Building Go-to-Market Strategies for Growth Results-driven...
Text 6299 similarity: 89.56% - Preview: Product Marketing Manager | Driving Growth & Customer Engagement Results-driven...
Text 3232 similarity: 89.09% - Preview: Product Marketing Manager | Driving Product Growth through Data-Driven Strategies...
Text 5959 similarity: 88.90% - Preview: Product Marketing Manager | Data-Driven Growth Expert Results-driven Product Marketing...
Text 5635 similarity: 88.84% - Preview: Product Marketing Manager | Driving Growth through Data-Driven Marketing Strategies...
Text 5835 similarity: 88.74% - Preview: Product Marketing Manager | Cloud-Based SaaS Results-driven Product Marketing Manager...
Text 139 similarity: 88.66% - Preview: Product Marketing Manager | Scaling Growth through Data-Driven Strategies Experienced...
Text 6688 similarity: 88.48% - Preview: Product Marketing Manager | Driving Business Growth through Data-Driven Insights...
Text 6405 similarity: 88.27% - Preview: Product Marketing Manager | Scaling SaaS Products for Global Markets...
Text 3439 similarity: 88.11% - Preview: Product Manager | Focused on delivering innovative products that drive...
Text 5958 similarity: 88.00% - Preview: Product Manager Office | Growth Driven by Customer Centricity Highly...
Text 5183 similarity: 87.86% - Preview: Product Marketing Manager | B2B SaaS Experienced Product Marketing Manager...
Text 1329 similarity: 87.81% - Preview: Product Marketing Manager | Scaling Growth for Emerging Tech Startups...
Text 130 similarity: 87.81% - Preview: Product Marketing Manager | Growth Strategies & Launches Results-driven Product...
Text 3423 similarity: 87.78% - Preview: Product Marketing Manager | Scaling B2B SaaS Solutions Experienced Product...
Text 4234 similarity: 87.72% - Preview: Product Manager | Leading Cross-Functional Teams to Drive Business Growth...
```
mxbai 这个模型——还有不少其他模型也一样——明显能看出它把 product marketing manager 跟 product manager 一起返回了,这不是我们想要的。
*看上面 88.11% – Preview: Product Manager 和 88.00% – Preview: Product Manager Office 这两条。*
那就上聚类——看看它能不能帮上忙。
### 聚类
第一步从 profile 的 embedding 里搭聚类——你得先决定要分多少簇。
我选了 10。
```python
embeddings_array = np.array(embeddings_list)
num_clusters = 10 # 这里数字可以换
kmeans = KMeans(n_clusters=num_clusters, random_state=42)
kmeans.fit(embeddings_array)
cluster_labels = kmeans.labels_
pca = PCA(n_components=2)
reduced_embeddings = pca.fit_transform(embeddings_array)
```
接下来要看 query(岗位描述)落在哪一簇。
```python
# 看 query 落到哪个簇里
query_embedding_array = np.array(query_embedding_vector).reshape(1, -1)
reduced_query_embedding = pca.transform(query_embedding_array)
# 预测 query 应该归到哪一簇
query_cluster_label = kmeans.predict(query_embedding_array)[0]
print(f"The query belongs to cluster {query_cluster_label}")
```
然后我们可以把簇画在 2 维图上——记得这是降维后的结果,所以有些簇可能叠在一起。

你可以鼠标悬停到不同 embedding 上看 profile 内容。

也可以把 query(图里的 X)单独标出来,看模型把它判到哪一簇。

可以清楚看到,模型把 marketing 相关的人都正确归到同一簇——SEO specialist 和 growth hacker 也在里面,但 Office Product Manager 和 Product Manager 都不在。
漂亮。
到这一步,我们可以把聚类和语义搜索两招拼起来用,效果会更好。
记得换不同模型试试看哪个效果更好——你会发现大模型在"把相似 profile 分到一组"这件事上按理说更强,但有些小模型也能跑得不错。
### 聚类 + 语义搜索
既然 query 能正确归到对的簇里,我们就可以把两招拼起来用。
```python
# 在正确的簇内做语义搜索
cluster_indices = np.where(cluster_labels == query_cluster_label)[0]
cluster_embeddings = embeddings_array[cluster_indices]
cluster_texts = [texts[i] for i in cluster_indices]
similarities_in_cluster = []
for idx, emb in zip(cluster_indices, cluster_embeddings):
sim = cosine_similarity(query_embedding_vector, emb)
similarities_in_cluster.append((idx, sim))
similarities_in_cluster.sort(key=lambda x: x[1], reverse=True)
top_n = 40 # 想看更多结果就调这个
top_matches = similarities_in_cluster[:top_n]
print(f"\nTop {top_n} similar texts in the same cluster as the query:")
for idx, sim in top_matches:
percentage = (sim + 1) / 2 * 100
text_preview = ' '.join(texts[idx].split()[:10])
print(f"Text {idx+1} similarity: {percentage:.2f}% - Preview: {text_preview}...")
```
跑完上面这段就能看到——结果里 Product Manager 不见了,换成了 Marketing Manager 这类更对路的人选。
```text
Top 40 similar texts in the same cluster as the query:
Text 3615 similarity: 89.59% - Preview: Product Marketing Manager | Building Go-to-Market Strategies for Growth Results-driven...
Text 3232 similarity: 89.09% - Preview: Product Marketing Manager | Driving Product Growth through Data-Driven Strategies...
Text 5959 similarity: 88.90% - Preview: Product Marketing Manager | Data-Driven Growth Expert Results-driven Product Marketing...
Text 5635 similarity: 88.84% - Preview: Product Marketing Manager | Driving Growth through Data-Driven Marketing Strategies...
Text 5835 similarity: 88.74% - Preview: Product Marketing Manager | Cloud-Based SaaS Results-driven Product Marketing Manager...
Text 139 similarity: 88.66% - Preview: Product Marketing Manager | Scaling Growth through Data-Driven Strategies Experienced...
Text 6688 similarity: 88.48% - Preview: Product Marketing Manager | Driving Business Growth through Data-Driven Insights...
Text 6405 similarity: 88.27% - Preview: Product Marketing Manager | Scaling SaaS Products for Global Markets...
Text 5183 similarity: 87.86% - Preview: Product Marketing Manager | B2B SaaS Experienced Product Marketing Manager...
Text 1329 similarity: 87.81% - Preview: Product Marketing Manager | Scaling Growth for Emerging Tech Startups...
Text 130 similarity: 87.81% - Preview: Product Marketing Manager | Growth Strategies & Launches Results-driven Product...
Text 3423 similarity: 87.78% - Preview: Product Marketing Manager | Scaling B2B SaaS Solutions Experienced Product...
Text 5945 similarity: 87.63% - Preview: Marketing Manager | Driving Growth through Data-Driven Strategies Results-driven marketing...
Text 2664 similarity: 87.59% - Preview: Product Marketing Manager | Driving Growth & Innovation Results-driven Product...
Text 3368 similarity: 87.54% - Preview: Product Marketing Manager | Scaling Growth through Data-Driven Strategies Highly...
Text 5794 similarity: 87.48% - Preview: Product Marketing Manager | Driving Growth through Data-Driven Insights Results-driven...
Text 5685 similarity: 86.71% - Preview: Performance Marketing Manager | Driving Business Growth through Data-Driven Strategies...
Text 5818 similarity: 86.37% - Preview: Digital Marketing Manager | Driving Business Growth through Data-Driven Strategies...
```
真实生产场景里,最好在做语义搜索之前先对数据做一遍过滤和分类。
这一节的核心就是让你自己对比不同模型——尤其是小模型 vs 大模型——看你愿意为更快更便宜的推理牺牲多少质量。
别只是为了上大模型而上大模型,除非你真的需要。
想继续评估这些模型,可以用 [RAGAs](https://docs.ragas.io/en/stable/) 看看不同模型下检索应用的表现。
### 关于模型表现的几点观察
要测性能总得挑个标准——我选的是看模型能不能把 product manager 和 product marketing manager 分清楚。
大模型在不上聚类的时候,更容易直接给出对的结果——但所有模型一开始都没把这两个职位分干净。
Ada-002 因为体量大不少,不上聚类的裸跑也表现不错;OpenAI 那个更小更新的 text-embed-3-small 反而差一些。

不过有些模型连聚类都聚不对——具体来说,fine-tune 过的 7B Mistral 和 E5 这次没跑好。这可能跟它们怎么训出来有关。
剩下那些在这个具体岗位上表现差不多。
让我意外的是 [mxbai](https://huggingface.co/mixedbread-ai/mxbai-embed-large-v1)——只有 335M 居然这么能打。这恰好说明:简单任务上,大模型可能就是杀鸡用牛刀。
这只是针对这一件小事的评测——你自己的任务该看什么指标得自己定。
不过从这里可以继续往前走——比如再叠一层重排,把最好的结果挑给 LLM 评。
### 重排(Re-Ranking)
RAG pipeline 里要修正不相关结果的招数有不少,重排是其中一种。
重排说白了就是把结果重新排序,让相关的排到最上面。一种常见做法是 [Pairwise Ranking(成对排序)](https://arxiv.org/pdf/2306.17563)。
具体怎么做:把一对 profile 喂给一个模型(可以是 LLM),让它根据岗位描述判断哪一个更合适。

具体到你的场景,得把几种方法组合起来才能跑得好。
如果你刚接触 embedding,希望这篇有点收获;如果不算新手,希望这次至少给你扒了点小模型 vs 大模型的成本账——开源闭源都聊到了。
至于更大的 LLM——闭源那边确实领先;但 embedding 模型这块不是这样。
就记一句:给小一些、算力更友好的模型一个机会。
❤