跳过正文
Embedding 模型选型与优化实战:从 BGE 到 OpenAI Embedding

Embedding 模型选型与优化实战:从 BGE 到 OpenAI Embedding

·940 字·5 分钟·
目录
AI 工程化实践路径 - 这篇文章属于一个选集。
§ : 本文

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-smalltext-embedding-3-large
维度15363072
MTEB 英文均分~62~64.6
价格$0.02/1M tokens$0.13/1M tokens
最大 Token81918191
中文支持一般一般

结论:纯英文场景且不想自托管,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 最独特的地方是支持三种检索范式:

  1. Dense Retrieval:标准向量检索,1024 维,适合大多数场景
  2. Sparse Retrieval:类 BM25 的词频权重,对专业词汇、产品名称等关键词匹配更准
  3. 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-small61.3%78.2%120ms$0.02
text-embedding-3-large65.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 缓存一定要做,重复构建知识库的成本能砍掉八成以上。

Wenzhuo Huang
作者
Wenzhuo Huang
搞运维的工程师,写代码的运维人。专注 Kubernetes、AWS、GitOps 与基础设施可靠性。这个博客既是我的技术笔记本,也是我踩过的坑的受害者档案。
AI 工程化实践路径 - 这篇文章属于一个选集。
§ : 本文

相关文章