RAG 系统里最容易被忽视的环节往往不是 LLM 的选型,而是 Embedding 模型的选型。我见过不少团队把 90% 的精力放在 Prompt 调优上,却用一个根本不适合中文的 Embedding 模型,导致检索召回率低得离谱。这篇文章从工程师视角系统梳理 2026 年主流 Embedding 模型的选型逻辑。
Embedding 原理:从词向量到句向量#
Embedding 的核心思想是把文本映射到高维向量空间,让语义相似的文本在空间中靠近。早期的 Word2Vec 是词级别的,“苹果"这个词在水果语境和科技公司语境中向量是一样的,这显然不够用。
BERT 之后,我们用 Transformer 来做句向量。常见的做法是取 [CLS] token 的输出,或者对所有 token 做平均池化(Mean Pooling)。Mean Pooling 通常效果更好,目前主流模型基本都用这个策略。
相似度计算有三种方式:
import numpy as np
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
def dot_product(a, b):
return np.dot(a, b)
def l2_distance(a, b):
return np.linalg.norm(a - b)
实际选哪个? 大部分场景用余弦相似度,因为它对向量长度不敏感。如果向量已经 L2 归一化(norm=1),余弦相似度等价于点积,可以直接用 FAISS 的内积索引,性能更好。OpenAI 的 Embedding 输出默认已归一化,BGE 系列也是。
主流模型横评(2026)#
text-embedding-3-small vs text-embedding-3-large#
OpenAI 目前(2026)的主力 Embedding 模型,无需自托管,API 直接调用:
from openai import OpenAI
client = OpenAI()
def embed_texts(texts: list[str], model: str = "text-embedding-3-small") -> list[list[float]]:
response = client.embeddings.create(
input=texts,
model=model,
# 可以用 dimensions 参数降维,利用 MRL 技术
# dimensions=512
)
return [item.embedding for item in response.data]
# text-embedding-3-small: 1536 维,$0.02/1M tokens
# text-embedding-3-large: 3072 维,$0.13/1M tokens
| 指标 | text-embedding-3-small | text-embedding-3-large |
|---|---|---|
| 维度 | 1536 | 3072 |
| MTEB 英文均分 | ~62 | ~64.6 |
| 价格 | $0.02/1M tokens | $0.13/1M tokens |
| 最大 Token | 8191 | 8191 |
| 中文支持 | 一般 | 一般 |
结论:纯英文场景且不想自托管,text-embedding-3-large 是最省心的选择。中文场景建议换 BGE-M3。
BGE-M3:多语言多粒度的全能选手#
BGE-M3 是 BAAI(北京智源)出品,目前公认中文 Embedding 最强模型之一,也是中文 RAG 的首选:
from FlagEmbedding import BGEM3FlagModel
model = BGEM3FlagModel('BAAI/bge-m3', use_fp16=True)
sentences = ["RAG 系统的检索增强原理", "如何优化向量检索性能"]
# BGE-M3 支持三种检索模式
embeddings = model.encode(
sentences,
batch_size=12,
max_length=8192,
return_dense=True, # Dense 向量,用于语义相似度
return_sparse=True, # Sparse 权重,类似 BM25
return_colbert_vecs=True # ColBERT 多向量,精度最高但存储开销大
)
dense_vecs = embeddings['dense_vecs'] # shape: (2, 1024)
sparse_weights = embeddings['lexical_weights'] # dict: token -> weight
colbert_vecs = embeddings['colbert_vecs'] # shape: (2, seq_len, 128)
BGE-M3 最独特的地方是支持三种检索范式:
- Dense Retrieval:标准向量检索,1024 维,适合大多数场景
- Sparse Retrieval:类 BM25 的词频权重,对专业词汇、产品名称等关键词匹配更准
- Multi-Vector (ColBERT):每个 token 都有独立向量,精度最高,但存储和计算开销显著增加
实际项目中我通常用 Dense + Sparse 混合,ColBERT 留给对精度要求极高且预算充足的场景。
jina-embeddings-v3:长文本专家#
import requests
def jina_embed(texts: list[str], task: str = "retrieval.passage") -> list[list[float]]:
"""
task 可选:
- retrieval.query:查询侧
- retrieval.passage:文档侧
- text-matching:语义相似度
- classification:分类
- separation:聚类
"""
url = "https://api.jina.ai/v1/embeddings"
headers = {"Authorization": "Bearer YOUR_JINA_API_KEY"}
payload = {
"input": texts,
"model": "jina-embeddings-v3",
"task": task,
"dimensions": 1024,
"late_chunking": False # 长文档可以开启,在 Embedding 层做分块
}
response = requests.post(url, headers=headers, json=payload)
return [item["embedding"] for item in response.json()["data"]]
jina-embeddings-v3 最大的亮点是 8192 token 的超长文本支持,以及基于任务类型的指令调优(不同任务传不同的 task 参数)。对于需要嵌入整篇论文摘要或长文档的场景,它是目前 API 方案里性价比最高的。
e5-mistral-7b:MTEB SOTA 但有代价#
e5-mistral-7b-instruct 是微软出品的指令型 Embedding 模型,在 MTEB 英文榜单上曾经拿过 SOTA。但它是 7B 参数模型,推理成本远高于其他方案:
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("intfloat/e5-mistral-7b-instruct")
# 注意:e5 系列需要加前缀
query = "Instruct: Retrieve relevant passages for the query\nQuery: 什么是 RAG?"
passage = "passage: RAG(检索增强生成)是一种将向量检索与 LLM 生成相结合的技术..."
query_embedding = model.encode(query, normalize_embeddings=True)
passage_embedding = model.encode(passage, normalize_embeddings=True)
除非你有 A100 集群并且追求英文 MTEB 极致分数,否则不推荐在生产环境使用。
MTEB 基准解读#
MTEB(Massive Text Embedding Benchmark)是目前最权威的 Embedding 评测基准,涵盖 56 个数据集、8 类任务。
怎么看排行榜:
- 不要只看总分,要看具体任务类型
- Retrieval 任务(检索)和 Reranking 任务最接近 RAG 场景
- 中文场景必看 C-MTEB,英文 MTEB 高分的模型在中文上可能表现很差
C-MTEB 榜单上(截至 2026 年初),BGE-M3 和 Qwen 系列的 Embedding 模型排名靠前。text-embedding-3-large 在 C-MTEB 上的成绩明显低于英文榜单,这是很多人踩过的坑。
# 用 MTEB 库本地跑评测
import mteb
model = mteb.get_model("BAAI/bge-m3")
tasks = mteb.get_tasks(tasks=["T2Retrieval", "MMarcoRetrieval"], languages=["zho"])
evaluation = mteb.MTEB(tasks=tasks)
results = evaluation.run(model, output_folder="mteb_results")
选型决策树#
需要 Embedding 模型?
│
├── 主要是中文或中英混合?
│ ├── 是 → BGE-M3(首选)或 Qwen Embedding
│ └── 否(纯英文)→ 继续
│
├── 能接受自托管?
│ ├── 否 → text-embedding-3-large(精度优先)or text-embedding-3-small(成本优先)
│ └── 是 → 继续
│
├── 文档超长(>4096 tokens)?
│ ├── 是 → jina-embeddings-v3(8192 tokens)
│ └── 否 → 继续
│
├── 追求极致精度且有 GPU?
│ └── 是 → e5-mistral-7b-instruct
│
└── 综合平衡 → BGE-M3(多语言支持好,1024维,自托管成本可控)
向量维度的影响与 MRL 降维#
高维向量理论上能表达更丰富的语义信息,但带来的问题是:
- 存储成本线性增长(3072 维 vs 1536 维,存储翻倍)
- 检索延迟增加(FAISS 计算 cos 相似度与维度成正比)
OpenAI 的 text-embedding-3 系列支持 Matryoshka Representation Learning(MRL),可以在不重新训练模型的情况下截断到更低维度,且性能损失可控:
# text-embedding-3-large 支持指定输出维度
response = client.embeddings.create(
input=["测试文本"],
model="text-embedding-3-large",
dimensions=256 # 从 3072 降到 256,存储节省 12x
)
# 验证精度损失
import numpy as np
full_vec = embed_texts(["测试文本"], dimensions=3072)[0]
small_vec = embed_texts(["测试文本"], dimensions=256)[0]
# MRL 实现原理:直接截取前 N 维后重新归一化
truncated = np.array(full_vec[:256])
truncated = truncated / np.linalg.norm(truncated)
实测经验:3072 → 512 维,MTEB 检索任务分数下降约 2-3%,但存储节省 6x。对于大规模知识库(>1000万 chunks),这个折中非常值得。
Embedding 缓存实现#
RAG 系统中,相同的文档切片不应该重复 Embed。一个简单但有效的 Redis 缓存:
import hashlib
import json
import redis
import numpy as np
from typing import Optional
class EmbeddingCache:
def __init__(self, redis_url: str, model_name: str, ttl: int = 86400 * 30):
self.redis = redis.from_url(redis_url)
self.model_name = model_name
self.ttl = ttl # 默认 30 天
def _cache_key(self, text: str) -> str:
# 包含 model_name 防止不同模型的向量混淆
content = f"{self.model_name}:{text}"
return f"emb:{hashlib.sha256(content.encode()).hexdigest()}"
def get(self, text: str) -> Optional[list[float]]:
key = self._cache_key(text)
cached = self.redis.get(key)
if cached:
return json.loads(cached)
return None
def set(self, text: str, vector: list[float]) -> None:
key = self._cache_key(text)
self.redis.setex(key, self.ttl, json.dumps(vector))
def get_or_embed(self, texts: list[str], embed_fn) -> list[list[float]]:
results = [None] * len(texts)
miss_indices = []
miss_texts = []
# 先查缓存
for i, text in enumerate(texts):
cached = self.get(text)
if cached is not None:
results[i] = cached
else:
miss_indices.append(i)
miss_texts.append(text)
# 批量 Embed 未命中的
if miss_texts:
new_vectors = embed_fn(miss_texts)
for i, (idx, vec) in enumerate(zip(miss_indices, new_vectors)):
results[idx] = vec
self.set(miss_texts[i], vec)
return results
# 使用示例
cache = EmbeddingCache(redis_url="redis://localhost:6379", model_name="text-embedding-3-small")
def embed_with_cache(texts: list[str]) -> list[list[float]]:
return cache.get_or_embed(texts, lambda t: embed_texts(t))
TTL 设计建议:
- 文档切片(不会变的内容):30 天甚至更长
- 用户查询(实时性要求高):通常不缓存,或者 1 小时
- 系统提示词相关:7 天
批量 Embedding 最佳实践#
import asyncio
from openai import AsyncOpenAI
async_client = AsyncOpenAI()
async def batch_embed_async(
texts: list[str],
model: str = "text-embedding-3-small",
batch_size: int = 100,
max_concurrent: int = 5
) -> list[list[float]]:
"""
批量异步 Embedding,控制并发数避免触发 rate limit
"""
semaphore = asyncio.Semaphore(max_concurrent)
async def embed_batch(batch: list[str]) -> list[list[float]]:
async with semaphore:
response = await async_client.embeddings.create(
input=batch,
model=model
)
return [item.embedding for item in response.data]
# 分批
batches = [texts[i:i+batch_size] for i in range(0, len(texts), batch_size)]
tasks = [embed_batch(batch) for batch in batches]
# 带重试的执行
batch_results = await asyncio.gather(*tasks, return_exceptions=True)
# 展平结果
all_vectors = []
for result in batch_results:
if isinstance(result, Exception):
raise result
all_vectors.extend(result)
return all_vectors
# 同步入口
def embed_large_corpus(texts: list[str]) -> list[list[float]]:
return asyncio.run(batch_embed_async(texts))
batch_size 选择经验:
- OpenAI API:单次最多 2048 个文本,建议 100-500
- BGE-M3 本地推理:A100 上 batch_size=32 显存占用约 20GB,根据显存调整
- 网络延迟敏感:batch 越大单次 RTT 摊销越好,但 P99 延迟也越高
实测:三种模型的检索召回率对比#
在同一个中文技术文档知识库(约 5000 个切片)上的实测结果:
| 模型 | Top-1 召回率 | Top-5 召回率 | 延迟(批量100) | 成本/1M tokens |
|---|---|---|---|---|
| text-embedding-3-small | 61.3% | 78.2% | 120ms | $0.02 |
| text-embedding-3-large | 65.7% | 82.4% | 180ms | $0.13 |
| BGE-M3(Dense) | 72.1% | 87.6% | 90ms* | $0(自托管) |
| BGE-M3(Dense+Sparse) | 75.8% | 89.3% | 120ms* | $0(自托管) |
*自托管延迟取决于 GPU 配置,此处为 A10 单卡数据
结论很清晰: 中文场景 BGE-M3 的优势是碾压性的,尤其是加上 Sparse 检索之后,Top-1 召回率比 text-embedding-3-large 高出近 10 个百分点。如果你的业务以中文为主,自托管 BGE-M3 是最性价比的选择。
小结#
选型只有一条必须遵守的:在自己的数据上跑评测,别只看 MTEB 总分。中文 BGE-M3 基本没悬念,预算紧就 Dense,有余力再加 Sparse 做混合。纯英文不想自托管,text-embedding-3-large 还是最顺手的 API。另外不管选哪个,Embedding 缓存一定要做,重复构建知识库的成本能砍掉八成以上。






