目 录CONTENT

文章目录

Embedding 模型选型 2026:从BGE到Voyage的实战对比

PySuper
2025-11-15 / 0 评论 / 0 点赞 / 0 阅读 / 0 字
温馨提示:
本文最后更新于2026-05-22,若内容或图片失效,请留言反馈。 所有牛逼的人都有一段苦逼的岁月。 但是你只要像SB一样去坚持,终将牛逼!!! ✊✊✊

作者:PySuper | 来源:zhengxingtao.com

日期:2026-11-01

目录

  • 1. 2026 Embedding 模型格局

  • 2. 闭源阵营

  • 3. 开源阵营

  • 4. 八维度对比

  • 5. 实战1:RAG场景召回效果对比

  • 6. 实战2:长文档场景测试

  • 7. 混合检索:Dense + Sparse

  • 8. 选型决策树

  • 9. 本地部署方案

  • 10. 踩坑记录

  • 11. 总结

1. 2026 Embedding 模型格局

Embedding 模型是 AI 检索系统的地基——如果 Embedding 质量 mediocre,任何 reranking 或 prompt engineering 都救不回来检索质量。

2024 年大家默认选 OpenAI text-embedding-3-large,到 2026 年,格局已经完全不同了:

plaintext

┌────────────────────────────────────────────────────────────────┐
│  2026 Embedding 模型格局                                        │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│  闭源阵营 (API 调用)                                            │
│  ├─ OpenAI text-embedding-3-large/small  (行业默认,但不再领先)  │
│  ├─ Cohere embed-v4                      (多语言最强API)        │
│  ├─ Voyage voyage-3-large/code-3         (检索精度最高)         │
│  ├─ Google Gemini Embedding 2            (全模态,跨语言之王)   │
│  └─ Jina v4/v5                           (灵活度最高)          │
│                                                                │
│  开源阵营 (本地部署)                                            │
│  ├─ BGE-M3 (BAAI)    (多语言王者,混合检索标杆)                 │
│  ├─ Qwen3-Embedding-8B  (阿里,Apache 2.0)                    │
│  ├─ Microsoft Harrier-OSS-v1  (94语言SOTA)                    │
│  ├─ Jina v5-text       (蒸馏质量,sub-1B匹敌8B)                │
│  ├─ E5-Mistral-7B      (Decoder-only嵌入先驱)                  │
│  └─ nomic-embed-text   (轻量级长上下文)                        │
│                                                                │
│  新趋势                                                         │
│  ├─ 多模态 Embedding: 文本+图片+视频+音频共享向量空间            │
│  ├─ Matryoshka 表示: 一次生成,维度可裁剪                       │
│  ├─ 混合检索成为标配: Dense + Sparse (BM25-like)               │
│  ├─ 非对称嵌入: query 和 document 用不同编码策略                 │
│  └─ 领域专用模型: code/legal/finance 各有专精                   │
│                                                                │
└────────────────────────────────────────────────────────────────┘

据《Best Embedding Models 2025: MTEB Scores & Leaderboard》(https://app.ailog.fr/en/blog/guides/choosing-embedding-models),2026 年初四大发布彻底改写了格局:Gemini Embedding 2(多模态)、Microsoft Harrier-OSS-v1(多语言SOTA)、Voyage 4 家族(共享嵌入空间)、Jina v5-text(蒸馏质量)。

2. 闭源阵营

2.1 OpenAI text-embedding-3-large / 3-small

python

import openai

client = openai.OpenAI()

# text-embedding-3-large: 3072 维(可裁剪到 256/1024)
response = client.embeddings.create(
    model="text-embedding-3-large",
    input="这是一段需要嵌入的文本",
    dimensions=1024  # Matryoshka: 可选 256/512/1024/3072
)
embedding = response.data[0].embedding  # 1024 维向量

# text-embedding-3-small: 1536 维(更便宜)
response_small = client.embeddings.create(
    model="text-embedding-3-small",
    input="这是一段需要嵌入的文本"
)
embedding_small = response_small.data[0].embedding  # 1536 维向量

表格

指标

text-embedding-3-large

text-embedding-3-small

维度

3072(可裁剪)

1536(可裁剪)

MTEB 平均

64.6

62.3

上下文长度

8,191 tokens

8,191 tokens

价格

$0.13/M tokens

$0.02/M tokens

多语言

50+

50+

Matryoshka

据《Embeddings in Practice: Every Major Model Compared》(https://stochasticsandbox.com/posts/embeddings-in-practice-every-major-model-compared-2026-03-31/),OpenAI 的 Embedding 已经超过 2 年没有更新,在检索基准上不再是 SOTA。但它的生态惯性极强——每个向量数据库、框架、教程都默认用 OpenAI。

2.2 Cohere embed-v4

python

import cohere

co = cohere.Client()

# embed-v4: 支持非对称嵌入(query vs document 不同编码)
response = co.embed(
    texts=["这是查询文本"],
    model="embed-v4",
    input_type="search_query",     # 查询侧
    embedding_types=["float"]
)

response = co.embed(
    texts=["这是文档内容,需要被检索"],
    model="embed-v4",
    input_type="search_document",  # 文档侧
    embedding_types=["float"]
)

表格

指标

embed-v4

维度

1024(支持 256/512/1024/1536 Matryoshka)

MTEB 平均

66.2

MTEB 检索

58.4(高于 OpenAI 55.4)

上下文长度

128K tokens(最长!)

价格

$0.12/M tokens

多语言

100+

多模态

✅ 文本+图片

非对称嵌入

✅ search_query / search_document

Cohere 的最大优势是多语言和非对称嵌入。100+ 语言的原生支持,加上 query/document 分开编码的策略,在多语言检索场景中比 OpenAI 好 2-4%。

但注意: 512 token 的输入限制(v3),v4 扩展到了 128K,这是一个巨大进步。

2.3 Voyage voyage-3-large / voyage-code-3

python

import voyageai

vo = voyageai.Client()

# voyage-3-large: 通用检索精度最高
result = vo.embed(
    ["这是需要嵌入的文档内容"],
    model="voyage-3-large",
    input_type="document"    # 注意:query 和 document 要区分
)
embedding = result.embeddings[0]  # 1024 维

# 查询侧
query_result = vo.embed(
    ["搜索查询"],
    model="voyage-3-large",
    input_type="query"
)

# voyage-code-3: 代码专用
code_result = vo.embed(
    ["def calculate_embedding(text: str) -> list[float]: ..."],
    model="voyage-code-3",
    input_type="document"
)

表格

指标

voyage-3-large

voyage-3

voyage-code-3

维度

1024

1024

1024

MTEB 平均

67.1(最高)

65.8

N/A (代码专用)

NDCG@10 (BEIR)

0.568

0.552

0.714 (代码)

上下文长度

32K tokens

32K

32K

价格

$0.18/M

$0.06/M

$0.18/M

稀疏向量

✅ 原生混合检索

据《Voyage AI Review 2026》(https://llmversus.com/blog/voyage-ai-review),voyage-3-large 是 2026 年 3 月检索精度最高的商用 API。voyage-code-3 在 CodeSearchNet 上 NDCG@10 达到 71.4,比 OpenAI 高出 8 个点。

Voyage 的独特优势是领域专用模型:voyage-code-3(代码)、voyage-law-2(法律)、voyage-finance-2(金融)。领域专用模型比通用模型在该领域好 5-10%。

2.4 Google Gemini Embedding 2

python

import google.generativeai as genai

genai.configure(api_key="YOUR_KEY")

# Gemini Embedding 2: 首个全模态 Embedding
result = genai.embed_content(
    model="models/gemini-embedding-2",
    content="需要嵌入的文本",
    task_type="retrieval_document"  # 支持多种 task_type
)
embedding = result['embedding']  # 3072 维

# 也可以嵌入图片
image_result = genai.embed_content(
    model="models/gemini-embedding-2",
    content=image_data,
    task_type="retrieval_document"
)

表格

指标

Gemini Embedding 2

维度

3072(Matryoshka: 128/768/1536)

MTEB 检索

67.71

跨语言检索

0.997(最高)

代码检索 (MTEB Code)

84.0

模态

文本+图片+视频+音频+PDF

价格

0.20/Mtext,0.20/M text, 0.10/M batch

语言

100+

据 CSDN 实测(https://blog.csdn.net/m0_59235945/article/details/159425275),Gemini Embedding 2 在跨语言(0.997)和长文档大海捞针(1.000 满分)场景无对手,但 MRL 维度压缩能力垫底(ρ=0.668)。

3. 开源阵营

3.1 BGE-M3:多语言王者

python

from sentence_transformers import SentenceTransformer

# 加载模型(~567M 参数,单 GPU 可跑)
model = SentenceTransformer('BAAI/bge-m3')

# Dense 向量
embeddings = model.encode(
    ["这是需要嵌入的文本"],
    normalize_embeddings=True
)
# 输出: 1024 维向量

# 多功能模式:同时输出 dense + sparse + colbert
from FlagEmbedding import BGEM3FlagModel

model = BGEM3FlagModel('BAAI/bge-m3', use_fp16=True)

sentences = ["这是需要嵌入的文本", "这是另一个句子"]
output = model.encode(
    sentences,
    return_dense=True,
    return_sparse=True,
    return_colbert_vecs=True
)

dense_vecs = output['dense_vecs']       # 1024 维
sparse_vecs = output['lexical_weights'] # 稀疏表示(SPLADE-like)
colbert_vecs = output['colbert_vecs']   # ColBERT 多向量

表格

指标

BGE-M3

参数量

567M

维度

1024 dense + sparse + colbert

中文 MTEB

64.8(中文开源第一)

英文 MTEB

62.5

多语言

✅ 100+ (111种)

上下文长度

8192 tokens

混合检索

✅ Dense + Sparse + ColBERT

许可

MIT

内存

~1.2GB

GPU 需求

单卡消费级 GPU 即可

BGE-M3 的最大卖点是三种检索模式一体:Dense(语义检索)、Sparse(关键词检索)、ColBERT(精细匹配)。这让混合检索变得极其简单——一个模型搞定一切。

3.2 Qwen3-Embedding-8B

python

from sentence_transformers import SentenceTransformer

model = SentenceTransformer('Qwen/Qwen3-Embedding-8B')

embeddings = model.encode(
    ["这是需要嵌入的文本"],
    normalize_embeddings=True
)
# 输出: 4096 维向量

# 也支持指令式嵌入
embeddings = model.encode(
    ["查询:如何使用Python处理CSV文件"],
    prompt="Instruct: 检索与查询相关的文档\nQuery: ",
    normalize_embeddings=True
)

表格

指标

Qwen3-Embedding-8B

参数量

8B

维度

4096

多语言

✅ 100+

多模态变体

✅ Qwen3-VL-Embedding

许可

Apache 2.0

GPU 需求

16GB+ VRAM

3.3 Microsoft Harrier-OSS-v1

据《Best Embedding Models 2025》报告,Harrier-OSS-v1 在 2026 年 3 月发布,三个 MIT 许可模型:

表格

变体

参数量

维度

MTEB v2

VRAM 需求

270M

270M

640

66.5

~2GB

0.6B

600M

1024

69.0

~4GB

27B

27B

5376

74.3 (SOTA)

80GB+

94 种语言,32K 上下文。27B 版本是开源 SOTA,但需要 80GB+ VRAM,不是消费级硬件能跑的。

3.4 E5-Mistral-7B-Instruct

python

from sentence_transformers import SentenceTransformer

# 第一个证明 Decoder-only LLM 可以做 Embedding 的模型
model = SentenceTransformer('intfloat/e5-mistral-7b-instruct')

# 需要在文本前加指令前缀
embeddings = model.encode(
    ["Instruct: Retrieve relevant documents\nQuery: 如何部署Kubernetes"],
    normalize_embeddings=True
)
# 输出: 4096 维

E5-Mistral 证明了 Decoder-only 架构也能做高质量 Embedding,打破了之前"只有 Encoder-only(BERT 类)才能做 Embedding"的偏见。但 4096 维的输出确实偏大,存储成本高。

4. 八维度对比

4.1 总览表

表格

维度

OpenAI 3-large

Cohere v4

Voyage 3-large

BGE-M3

Qwen3-8B

Gemini 2

精度(MTEB)

64.6

66.2

67.1

62.5

~66.0

67.71*

延迟(p50)

~30ms

~32ms

~38ms

~52ms

~80ms

~35ms

成本($/M tok)

$0.13

$0.12

$0.18

免费**

免费**

$0.20

多语言

50+

100+

100+

111

100+

100+

长文档

8K

128K

32K

8K

32K

32K

部署方式

API only

API only

API only

本地/API

本地/API

API only

维度

3072

1024

1024

1024

4096

3072

许可

闭源

闭源

闭源

MIT

Apache2.0

闭源

* Gemini 2 的 67.71 是 MTEB 检索单项分,不是全任务平均

** 本地部署免费,但需要 GPU 成本

4.2 精度对比(检索场景 NDCG@10)

据《Embedding Models Benchmark 2026》(https://iotdigitaltwinplm.com/embedding-models-benchmark-openai-cohere-voyage-bge-2026/):

表格

模型

BEIR 平均 NDCG@10

Recall@100

代码 NDCG@10

Voyage-3-large

0.568

0.861

64.2

OpenAI 3-large

0.543

0.832

63.1

BGE-M3 (hybrid)

0.58-0.62

0.825

N/A

BGE-M3 (dense)

0.545

0.825

N/A

Cohere v4

0.558

0.845

61.8

Voyage-code-3

N/A

N/A

71.4

关键发现: BGE-M3 单纯 Dense 只有 0.545,但 Dense+Sparse 混合可以到 0.58-0.62,这是混合检索的威力。

4.3 成本对比

plaintext

┌────────────────────────────────────────────────────────────┐
│  每 100 万次查询的成本对比(假设平均 200 tokens/query)      │
├────────────────────────────────────────────────────────────┤
│                                                            │
│  API 模型(仅推理成本):                                    │
│  ├─ Voyage-3:        $12,000  ($0.06/M × 200 tok × 1M)   │
│  ├─ OpenAI 3-small:  $4,000   ($0.02/M × 200 tok × 1M)   │
│  ├─ OpenAI 3-large:  $26,000  ($0.13/M × 200 tok × 1M)   │
│  ├─ Cohere v4:       $24,000  ($0.12/M × 200 tok × 1M)   │
│  ├─ Voyage-3-large:  $36,000  ($0.18/M × 200 tok × 1M)   │
│  └─ Gemini 2:        $40,000  ($0.20/M × 200 tok × 1M)   │
│                                                            │
│  开源模型(GPU 成本,月租):                                │
│  ├─ BGE-M3 (A10G):   ~$500/月  (可跑 500K+ queries/月)   │
│  ├─ Qwen3-8B (A100): ~$1,500/月 (可跑 300K+ queries/月)  │
│  └─ Harrier 27B (H100): ~$3,000/月 (SOTA 精度)           │
│                                                            │
│  交叉点:                                                   │
│  ├─ < 50K queries/月 → API 更便宜                         │
│  ├─ 50K-500K → 看具体模型                                 │
│  └─ > 500K → 开源自部署更划算                              │
│                                                            │
└────────────────────────────────────────────────────────────┘

5. 实战1:RAG场景召回效果对比

5.1 测试设计

使用同一语料库(1000 篇技术文档),测试 5 个模型在 RAG 检索场景下的召回效果。

python

# benchmark_rag.py
"""
RAG 场景召回效果对比测试
5 个模型,同一语料库,统一评估指标
"""

import os
import json
import time
import numpy as np
from typing import List, Dict, Tuple
from dataclasses import dataclass


@dataclass
class BenchmarkResult:
    model_name: str
    recall_at_5: float
    recall_at_10: float
    recall_at_20: float
    mrr: float
    ndcg_at_10: float
    avg_latency_ms: float
    total_cost_usd: float


# --- 模型封装 ---

class OpenAIEmbedder:
    """OpenAI text-embedding-3-large"""
    
    def __init__(self, model: str = "text-embedding-3-large"):
        from openai import OpenAI
        self.client = OpenAI()
        self.model = model
    
    def embed(self, texts: List[str]) -> List[List[float]]:
        response = self.client.embeddings.create(
            model=self.model,
            input=texts,
            dimensions=1024
        )
        return [item.embedding for item in response.data]


class CohereEmbedder:
    """Cohere embed-v4"""
    
    def __init__(self):
        import cohere
        self.client = cohere.Client()
    
    def embed_documents(self, texts: List[str]) -> List[List[float]]:
        response = self.client.embed(
            texts=texts,
            model="embed-v4",
            input_type="search_document",
            embedding_types=["float"]
        )
        return response.embeddings.float
    
    def embed_queries(self, texts: List[str]) -> List[List[float]]:
        response = self.client.embed(
            texts=texts,
            model="embed-v4",
            input_type="search_query",
            embedding_types=["float"]
        )
        return response.embeddings.float


class VoyageEmbedder:
    """Voyage voyage-3-large"""
    
    def __init__(self, model: str = "voyage-3-large"):
        import voyageai
        self.client = voyageai.Client()
        self.model = model
    
    def embed_documents(self, texts: List[str]) -> List[List[float]]:
        result = self.client.embed(
            texts, model=self.model, input_type="document"
        )
        return result.embeddings
    
    def embed_queries(self, texts: List[str]) -> List[List[float]]:
        result = self.client.embed(
            texts, model=self.model, input_type="query"
        )
        return result.embeddings


class BGEM3Embedder:
    """BGE-M3 本地模型"""
    
    def __init__(self):
        from FlagEmbedding import BGEM3FlagModel
        self.model = BGEM3FlagModel('BAAI/bge-m3', use_fp16=True)
    
    def embed(self, texts: List[str]) -> List[List[float]]:
        output = self.model.encode(texts, return_dense=True)
        return output['dense_vecs'].tolist()


class Qwen3Embedder:
    """Qwen3-Embedding-8B 本地模型"""
    
    def __init__(self):
        from sentence_transformers import SentenceTransformer
        self.model = SentenceTransformer('Qwen/Qwen3-Embedding-8B')
    
    def embed(self, texts: List[str]) -> List[List[float]]:
        embeddings = self.model.encode(texts, normalize_embeddings=True)
        return embeddings.tolist()


# --- 评估函数 ---

def compute_cosine_similarity(
    query_vecs: np.ndarray,
    doc_vecs: np.ndarray
) -> np.ndarray:
    """计算余弦相似度矩阵"""
    query_norms = np.linalg.norm(query_vecs, axis=1, keepdims=True)
    doc_norms = np.linalg.norm(doc_vecs, axis=1, keepdims=True)
    return (query_vecs / query_norms) @ (doc_vecs / doc_norms).T


def recall_at_k(
    sim_matrix: np.ndarray,
    relevant_ids: List[List[int]],
    k: int
) -> float:
    """计算 Recall@K"""
    hits = 0
    total = 0
    for i, rel_ids in enumerate(relevant_ids):
        top_k = np.argsort(sim_matrix[i])[-k:][::-1]
        hits += len(set(top_k) & set(rel_ids))
        total += len(rel_ids)
    return hits / total if total > 0 else 0


def mrr(sim_matrix: np.ndarray, relevant_ids: List[List[int]]) -> float:
    """计算 MRR (Mean Reciprocal Rank)"""
    scores = []
    for i, rel_ids in enumerate(relevant_ids):
        ranked = np.argsort(sim_matrix[i])[::-1]
        for rank, doc_id in enumerate(ranked, 1):
            if doc_id in rel_ids:
                scores.append(1.0 / rank)
                break
        else:
            scores.append(0.0)
    return np.mean(scores)


def ndcg_at_k(
    sim_matrix: np.ndarray,
    relevant_ids: List[List[int]],
    k: int
) -> float:
    """计算 NDCG@K"""
    scores = []
    for i, rel_ids in enumerate(relevant_ids):
        ranked = np.argsort(sim_matrix[i])[::-1][:k]
        dcg = sum(
            1.0 / np.log2(rank + 1)
            for rank, doc_id in enumerate(ranked, 1)
            if doc_id in rel_ids
        )
        idcg = sum(
            1.0 / np.log2(rank + 1)
            for rank in range(1, min(len(rel_ids), k) + 1)
        )
        scores.append(dcg / idcg if idcg > 0 else 0)
    return np.mean(scores)


# --- 运行基准测试 ---

def run_benchmark(
    documents: List[str],
    queries: List[str],
    relevant_ids: List[List[int]],
    embedder,
    model_name: str,
    is_asymmetric: bool = False
) -> BenchmarkResult:
    """运行单个模型的基准测试"""
    
    print(f"\n{'='*60}")
    print(f"测试模型: {model_name}")
    print(f"{'='*60}")
    
    # 嵌入文档
    start = time.time()
    if is_asymmetric and hasattr(embedder, 'embed_documents'):
        doc_vecs = embedder.embed_documents(documents)
    else:
        doc_vecs = embedder.embed(documents)
    doc_time = time.time() - start
    
    # 嵌入查询
    start = time.time()
    if is_asymmetric and hasattr(embedder, 'embed_queries'):
        query_vecs = embedder.embed_queries(queries)
    else:
        query_vecs = embedder.embed(queries)
    query_time = time.time() - start
    
    # 计算相似度
    doc_vecs = np.array(doc_vecs)
    query_vecs = np.array(query_vecs)
    sim_matrix = compute_cosine_similarity(query_vecs, doc_vecs)
    
    # 计算指标
    r5 = recall_at_k(sim_matrix, relevant_ids, 5)
    r10 = recall_at_k(sim_matrix, relevant_ids, 10)
    r20 = recall_at_k(sim_matrix, relevant_ids, 20)
    mrr_score = mrr(sim_matrix, relevant_ids)
    ndcg10 = ndcg_at_k(sim_matrix, relevant_ids, 10)
    
    avg_latency = (doc_time + query_time) * 1000 / (len(documents) + len(queries))
    
    result = BenchmarkResult(
        model_name=model_name,
        recall_at_5=r5,
        recall_at_10=r10,
        recall_at_20=r20,
        mrr=mrr_score,
        ndcg_at_10=ndcg10,
        avg_latency_ms=avg_latency,
        total_cost_usd=0  # 需要根据实际 API 调用计算
    )
    
    print(f"  Recall@5:  {r5:.4f}")
    print(f"  Recall@10: {r10:.4f}")
    print(f"  Recall@20: {r20:.4f}")
    print(f"  MRR:       {mrr_score:.4f}")
    print(f"  NDCG@10:   {ndcg10:.4f}")
    print(f"  延迟:      {avg_latency:.1f}ms/请求")
    
    return result


# --- 主程序 ---

def main():
    # 加载测试数据
    # documents: 1000 篇技术文档
    # queries: 100 条检索查询
    # relevant_ids: 每条查询对应的正确文档 ID
    
    documents = load_test_documents()  # 你的文档加载函数
    queries = load_test_queries()
    relevant_ids = load_relevance_labels()
    
    results = []
    
    # 1. OpenAI text-embedding-3-large
    results.append(run_benchmark(
        documents, queries, relevant_ids,
        OpenAIEmbedder("text-embedding-3-large"),
        "OpenAI 3-large"
    ))
    
    # 2. Cohere embed-v4 (非对称)
    results.append(run_benchmark(
        documents, queries, relevant_ids,
        CohereEmbedder(),
        "Cohere v4",
        is_asymmetric=True
    ))
    
    # 3. Voyage-3-large (非对称)
    results.append(run_benchmark(
        documents, queries, relevant_ids,
        VoyageEmbedder("voyage-3-large"),
        "Voyage-3-large",
        is_asymmetric=True
    ))
    
    # 4. BGE-M3
    results.append(run_benchmark(
        documents, queries, relevant_ids,
        BGEM3Embedder(),
        "BGE-M3"
    ))
    
    # 5. Qwen3-8B
    results.append(run_benchmark(
        documents, queries, relevant_ids,
        Qwen3Embedder(),
        "Qwen3-8B"
    ))
    
    # 打印对比表
    print("\n" + "="*80)
    print("RAG 检索召回效果对比")
    print("="*80)
    print(f"{'模型':<20} {'R@5':<8} {'R@10':<8} {'R@20':<8} {'MRR':<8} {'NDCG@10':<8} {'延迟ms':<8}")
    print("-"*80)
    for r in sorted(results, key=lambda x: x.ndcg_at_10, reverse=True):
        print(f"{r.model_name:<20} {r.recall_at_5:<8.4f} {r.recall_at_10:<8.4f} "
              f"{r.recall_at_20:<8.4f} {r.mrr:<8.4f} {r.ndcg_at_10:<8.4f} {r.avg_latency_ms:<8.1f}")


if __name__ == "__main__":
    main()

5.2 预期结果(基于公开基准数据)

plaintext

================================================================================
RAG 检索召回效果对比
================================================================================
模型                 R@5      R@10     R@20     MRR      NDCG@10  延迟ms
--------------------------------------------------------------------------------
Voyage-3-large      0.6521   0.7834   0.8912   0.5823   0.5680   38.0
Cohere v4           0.6312   0.7645   0.8756   0.5612   0.5580   32.0
OpenAI 3-large      0.6102   0.7412   0.8534   0.5401   0.5430   30.0
Qwen3-8B            0.6189   0.7523   0.8621   0.5534   0.5520   80.0
BGE-M3 (dense)      0.5934   0.7234   0.8345   0.5189   0.5450   52.0
BGE-M3 (hybrid)     0.6612   0.7891   0.8978   0.5934   0.5950   65.0
--------------------------------------------------------------------------------

关键发现:

  1. Voyage-3-large 在纯 Dense 检索中精度最高

  2. BGE-M3 hybrid(Dense+Sparse)超越所有纯 Dense 模型

  3. OpenAI 3-large 不再是默认最优选择

  4. 非对称嵌入(Cohere/Voyage)比对称嵌入(OpenAI/BGE)效果好 2-4%

6. 实战2:长文档场景测试

6.1 测试设计

测试模型在 10K+ tokens 长文档场景下的表现。关键问题是:长文档需要分块吗?Voyage-3-large 的 32K 上下文能否不分块直接嵌入?

python

# benchmark_long_docs.py
"""
长文档 Embedding 测试
对比分块 vs 不分块在不同模型上的效果
"""

import numpy as np
from typing import List, Tuple
from dataclasses import dataclass


@dataclass
class LongDocResult:
    model_name: str
    strategy: str           # "chunked" or "full"
    recall_at_5: float
    ndcg_at_10: float
    avg_latency_ms: float
    avg_tokens_per_doc: int


def chunk_document(
    text: str,
    chunk_size: int = 512,
    overlap: int = 50
) -> List[str]:
    """将长文档分块"""
    # 简单的字符级分块(实际应用建议用语义分块)
    chunks = []
    start = 0
    while start < len(text):
        end = start + chunk_size
        chunks.append(text[start:end])
        start = end - overlap
    return chunks


def embed_chunked_document(
    text: str,
    embedder,
    chunk_size: int = 512
) -> np.ndarray:
    """分块嵌入:每个 chunk 单独嵌入,取平均"""
    chunks = chunk_document(text, chunk_size)
    
    if hasattr(embedder, 'embed_documents'):
        chunk_vecs = embedder.embed_documents(chunks)
    else:
        chunk_vecs = embedder.embed(chunks)
    
    # 取平均作为文档向量
    return np.mean(chunk_vecs, axis=0)


def embed_full_document(
    text: str,
    embedder
) -> np.ndarray:
    """全文档嵌入:不分块直接嵌入"""
    if hasattr(embedder, 'embed_documents'):
        vecs = embedder.embed_documents([text])
    else:
        vecs = embedder.embed([text])
    return np.array(vecs[0])


def run_long_doc_benchmark():
    """
    长文档场景测试
    - 文档长度:5K / 10K / 20K / 32K tokens
    - 策略:分块(512) vs 不分块
    """
    
    # 加载长文档测试集
    long_docs = load_long_documents()  # 你的加载函数
    queries = load_long_doc_queries()
    relevant_ids = load_long_doc_relevance()
    
    embedders = {
        "Voyage-3-large": VoyageEmbedder("voyage-3-large"),  # 32K 上下文
        "OpenAI 3-large": OpenAIEmbedder(),                   # 8K 上下文
        "Cohere v4": CohereEmbedder(),                        # 128K 上下文
        "BGE-M3": BGEM3Embedder(),                            # 8K 上下文
    }
    
    results = []
    
    for name, embedder in embedders.items():
        print(f"\n测试: {name}")
        
        # 策略1:分块
        doc_vecs_chunked = []
        for doc in long_docs:
            vec = embed_chunked_document(doc, embedder, chunk_size=512)
            doc_vecs_chunked.append(vec)
        
        # 策略2:不分块(如果模型支持)
        doc_vecs_full = []
        max_context = get_max_context(name)  # 获取模型最大上下文
        for doc in long_docs:
            doc_tokens = estimate_tokens(doc)
            if doc_tokens <= max_context:
                vec = embed_full_document(doc, embedder)
            else:
                # 超出上下文长度,截断
                truncated = doc[:max_context * 4]  # 粗略估计
                vec = embed_full_document(truncated, embedder)
            doc_vecs_full.append(vec)
        
        # 嵌入查询
        if hasattr(embedder, 'embed_queries'):
            query_vecs = embedder.embed_queries(queries)
        else:
            query_vecs = embedder.embed(queries)
        
        # 计算相似度
        sim_chunked = compute_cosine_similarity(
            np.array(query_vecs), np.array(doc_vecs_chunked)
        )
        sim_full = compute_cosine_similarity(
            np.array(query_vecs), np.array(doc_vecs_full)
        )
        
        # 计算指标
        r5_chunked = recall_at_k(sim_chunked, relevant_ids, 5)
        r5_full = recall_at_k(sim_full, relevant_ids, 5)
        ndcg_chunked = ndcg_at_k(sim_chunked, relevant_ids, 10)
        ndcg_full = ndcg_at_k(sim_full, relevant_ids, 10)
        
        print(f"  分块 - R@5: {r5_chunked:.4f}, NDCG@10: {ndcg_chunked:.4f}")
        print(f"  不分块 - R@5: {r5_full:.4f}, NDCG@10: {ndcg_full:.4f}")
        
        results.append(LongDocResult(
            model_name=name, strategy="chunked",
            recall_at_5=r5_chunked, ndcg_at_10=ndcg_chunked,
            avg_latency_ms=0, avg_tokens_per_doc=10000
        ))
        results.append(LongDocResult(
            model_name=name, strategy="full_doc",
            recall_at_5=r5_full, ndcg_at_10=ndcg_full,
            avg_latency_ms=0, avg_tokens_per_doc=10000
        ))
    
    return results

6.2 关键发现

plaintext

┌──────────────────────────────────────────────────────────────┐
│  长文档场景:分块 vs 不分块                                    │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  模型上下文限制:                                             │
│  ├─ OpenAI 3-large: 8K tokens  → 10K 文档必须分块           │
│  ├─ BGE-M3:         8K tokens  → 10K 文档必须分块           │
│  ├─ Voyage-3-large: 32K tokens → 10K 文档可以不分块         │
│  └─ Cohere v4:      128K tokens → 10K 文档完全没问题        │
│                                                              │
│  不分块的利弊:                                               │
│  优点:                                                      │
│  ├─ 保留完整文档语义,不会因分块丢失上下文                     │
│  ├─ 简化 pipeline,不需要分块策略                             │
│  └─ 对整体文档相似性更准确                                    │
│                                                              │
│  缺点:                                                      │
│  ├─ 长文档压缩到单个向量,局部细节被稀释                      │
│  ├─ RAG 场景中,用户通常查询文档的特定部分                    │
│  │   全文档向量不如 chunk 向量精准                            │
│  └─ 向量维度固定,信息密度下降                                │
│                                                              │
│  结论:                                                      │
│  ├─ 文档相似性/聚类 → 不分块更好                              │
│  ├─ RAG 精准检索 → 分块更好                                  │
│  ├─ 法律/专利文档对比 → 不分块(Voyage/Cohore的长上下文)     │
│  └─ 技术文档QA → 分块 + 重排序                               │
│                                                              │
└──────────────────────────────────────────────────────────────┘

7. 混合检索:Dense + Sparse

7.1 为什么混合检索是标配

plaintext

┌──────────────────────────────────────────────────────────────┐
│  Dense 检索 vs Sparse 检索 vs 混合检索                        │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  Dense 检索(语义匹配)                                       │
│  ├─ 优点:理解语义,"如何部署" 能匹配 "安装指南"               │
│  ├─ 缺点:专有名词、产品名、错误码容易匹配不到                │
│  └─ 例子:查 "K8s" 可能匹配不到 "Kubernetes" 的某些文档      │
│                                                              │
│  Sparse 检索(关键词匹配,BM25-like)                         │
│  ├─ 优点:精确匹配关键词、专有名词、错误码                    │
│  ├─ 缺点:不理解语义同义词                                    │
│  └─ 例子:查 "部署" 匹配不到 "安装" 的文档                   │
│                                                              │
│  混合检索(Dense + Sparse)                                   │
│  ├─ 同时做语义匹配和关键词匹配                                │
│  ├─ 用 RRF (Reciprocal Rank Fusion) 融合结果                 │
│  ├─ BGE-M3 混合模式 NDCG 比 Dense 单独高 3-5%               │
│  └─ 2026 年生产 RAG 系统的标配                               │
│                                                              │
└──────────────────────────────────────────────────────────────┘

7.2 用 BGE-M3 实现混合检索

python

# hybrid_search.py
"""
BGE-M3 混合检索实现
Dense + Sparse 同时使用
"""

import numpy as np
from FlagEmbedding import BGEM3FlagModel
from typing import List, Dict, Tuple


class HybridSearchEngine:
    """基于 BGE-M3 的混合检索引擎"""
    
    def __init__(self, model_name: str = 'BAAI/bge-m3'):
        self.model = BGEM3FlagModel(model_name, use_fp16=True)
        self.documents = []
        self.dense_vecs = None
        self.sparse_vecs = None
    
    def index(self, documents: List[str]):
        """索引文档"""
        self.documents = documents
        
        # 一次性生成 Dense + Sparse 向量
        output = self.model.encode(
            documents,
            return_dense=True,
            return_sparse=True
        )
        
        self.dense_vecs = output['dense_vecs']       # (N, 1024)
        self.sparse_vecs = output['lexical_weights']  # List[Dict[str, float]]
    
    def search(
        self,
        query: str,
        top_k: int = 10,
        dense_weight: float = 0.7,
        sparse_weight: float = 0.3
    ) -> List[Tuple[int, float]]:
        """混合检索"""
        
        # 编码查询
        query_output = self.model.encode(
            [query],
            return_dense=True,
            return_sparse=True
        )
        
        query_dense = query_output['dense_vecs'][0]        # (1024,)
        query_sparse = query_output['lexical_weights'][0]   # Dict
        
        # 1. Dense 检索
        dense_scores = self._dense_search(query_dense)
        
        # 2. Sparse 检索
        sparse_scores = self._sparse_search(query_sparse)
        
        # 3. RRF 融合
        fused_scores = self._rrf_fusion(
            dense_scores, sparse_scores,
            dense_weight, sparse_weight
        )
        
        # 返回 top_k 结果
        ranked = sorted(fused_scores.items(), key=lambda x: x[1], reverse=True)
        return ranked[:top_k]
    
    def _dense_search(self, query_vec: np.ndarray) -> Dict[int, float]:
        """Dense 向量检索"""
        # 余弦相似度
        scores = np.dot(self.dense_vecs, query_vec)
        norms = np.linalg.norm(self.dense_vecs, axis=1) * np.linalg.norm(query_vec)
        scores = scores / norms
        return {i: float(s) for i, s in enumerate(scores)}
    
    def _sparse_search(self, query_sparse: Dict[str, float]) -> Dict[int, float]:
        """Sparse 向量检索(类似 SPLADE)"""
        scores = {}
        for i, doc_sparse in enumerate(self.sparse_vecs):
            # 计算查询和文档的稀疏向量点积
            score = 0.0
            for token, weight in query_sparse.items():
                if token in doc_sparse:
                    score += weight * doc_sparse[token]
            scores[i] = score
        return scores
    
    def _rrf_fusion(
        self,
        dense_scores: Dict[int, float],
        sparse_scores: Dict[int, float],
        dense_weight: float,
        sparse_weight: float,
        k: int = 60
    ) -> Dict[int, float]:
        """Reciprocal Rank Fusion"""
        # Dense 排名
        dense_ranked = sorted(dense_scores.items(), key=lambda x: x[1], reverse=True)
        dense_ranks = {doc_id: rank + 1 for rank, (doc_id, _) in enumerate(dense_ranked)}
        
        # Sparse 排名
        sparse_ranked = sorted(sparse_scores.items(), key=lambda x: x[1], reverse=True)
        sparse_ranks = {doc_id: rank + 1 for rank, (doc_id, _) in enumerate(sparse_ranked)}
        
        # RRF 融合
        all_doc_ids = set(dense_ranks.keys()) | set(sparse_ranks.keys())
        fused = {}
        
        for doc_id in all_doc_ids:
            dense_rrf = dense_weight / (k + dense_ranks.get(doc_id, len(dense_ranks)))
            sparse_rrf = sparse_weight / (k + sparse_ranks.get(doc_id, len(sparse_ranks)))
            fused[doc_id] = dense_rrf + sparse_rrf
        
        return fused


# --- 使用示例 ---

def main():
    # 创建搜索引擎
    engine = HybridSearchEngine()
    
    # 索引文档
    documents = [
        "Kubernetes 部署指南:使用 kubectl apply 命令部署应用",
        "Docker 容器化最佳实践:多阶段构建优化镜像大小",
        "Python FastAPI 教程:构建高性能 REST API",
        "K8s 集群监控:使用 Prometheus 和 Grafana 监控节点",
        "微服务架构设计:服务拆分与 API Gateway 模式",
        "错误码 ERR-5001:数据库连接超时,请检查网络配置",
        "错误码 ERR-6002:认证 Token 过期,需要重新登录",
        "Kubernetes Pod 调度策略:亲和性、反亲和性和污点容忍",
    ]
    
    engine.index(documents)
    
    # 测试检索
    queries = [
        "如何部署 K8s 应用",          # 语义+关键词混合
        "ERR-5001 错误怎么处理",      # 精确错误码
        "容器化部署方案",              # 语义查询
    ]
    
    for query in queries:
        print(f"\n查询: {query}")
        results = engine.search(query, top_k=3)
        for doc_id, score in results:
            print(f"  [{score:.4f}] {documents[doc_id][:60]}...")


if __name__ == "__main__":
    main()

7.3 不同模型对混合检索的支持

表格

模型

Dense

Sparse

ColBERT

混合模式

BGE-M3

原生三合一

Voyage-3-large

原生 Dense+Sparse

Cohere v4

需搭配 BM25

OpenAI 3-large

需搭配 BM25

Jina v3

原生 Dense+ColBERT

Qwen3-8B

需搭配 BM25

8. 选型决策树

plaintext

你的需求是什么?
│
├─ 需要本地部署?(合规/数据隐私/成本)
│  ├─ 是 → 主要语言是中文?
│  │  ├─ 是 → BGE-M3(中文开源第一,MIT 许可)
│  │  └─ 否 → 需要最高精度?
│  │     ├─ 是 → Harrier-27B(MTEB SOTA,需 80GB VRAM)
│  │     └─ 否 → BGE-M3 或 Qwen3-8B(Apache 2.0)
│  └─ 否 → 继续 ↓
│
├─ 代码检索场景?
│  ├─ 是 → Voyage-code-3(代码 NDCG 71.4,比通用模型高 8+ 点)
│  └─ 否 → 继续 ↓
│
├─ 多语言/跨语言场景?
│  ├─ 是 → Gemini Embedding 2(跨语言 0.997)或 Cohere v4
│  └─ 否 → 继续 ↓
│
├─ 长文档(>8K tokens)需要不分块?
│  ├─ 是 → Cohere v4(128K)或 Voyage-3-large(32K)
│  └─ 否 → 继续 ↓
│
├─ 混合检索(Dense+Sparse)?
│  ├─ 是 → BGE-M3(原生三合一)或 Voyage-3-large
│  └─ 否 → 继续 ↓
│
├─ 预算敏感?
│  ├─ 是 → Voyage-3($0.06/M,性价比之王)
│  │       或 OpenAI 3-small($0.02/M,最便宜API)
│  └─ 否 → 继续 ↓
│
├─ 需要最高检索精度?
│  ├─ 是 → Voyage-3-large(NDCG 0.568,商用最高)
│  └─ 否 → OpenAI 3-large(生态最广,够用)
│
└─ 不确定?→ BGE-M3(开源免费,混合检索,多语言,不会选错)

快速选择表

表格

场景

首选

备选

原因

中文 RAG

BGE-M3

Qwen3-8B

中文开源第一,混合检索

代码搜索

Voyage-code-3

BGE-M3

代码 NDCG 领先 8+ 点

多语言企业搜索

Cohere v4

Gemini 2

100+语言+非对称嵌入

法律/长文档对比

Cohere v4

Voyage-3-large

128K/32K 长上下文

预算有限 API

Voyage-3

OpenAI 3-small

$0.06/M 性价比最高

预算有限自部署

BGE-M3

Harrier-0.6B

MIT 免费,1.2GB 内存

多模态(文本+图片)

Gemini 2

Jina v4

5种模态共享空间

现有 OpenAI 生态

OpenAI 3-large

Voyage-3-large

零迁移成本

9. 本地部署方案

9.1 用 Ollama 跑 Embedding 模型

bash

# 安装 Ollama
curl -fsSL https://ollama.ai/install.sh | sh

# 拉取 BGE-M3
ollama pull bge-m3

# 用 API 调用
curl http://localhost:11434/api/embeddings \
  -d '{
    "model": "bge-m3",
    "prompt": "这是需要嵌入的文本"
  }'

python

# Python 调用 Ollama Embedding
import requests

def embed_with_ollama(text: str, model: str = "bge-m3") -> list:
    """使用 Ollama 本地 Embedding"""
    response = requests.post(
        "http://localhost:11434/api/embeddings",
        json={"model": model, "prompt": text}
    )
    return response.json()["embedding"]

# 批量嵌入
def batch_embed(texts: list, model: str = "bge-m3") -> list:
    embeddings = []
    for text in texts:
        emb = embed_with_ollama(text, model)
        embeddings.append(emb)
    return embeddings

9.2 用 vLLM 部署高吞吐 Embedding 服务

bash

# 安装 vLLM
pip install vllm

# 启动 BGE-M3 Embedding 服务
python -m vllm.entrypoints.openai.api_server \
  --model BAAI/bge-m3 \
  --task embedding \
  --host 0.0.0.0 \
  --port 8080 \
  --trust-remote-code \
  --dtype float16 \
  --max-model-len 8192 \
  --gpu-memory-utilization 0.9

python

# 调用 vLLM Embedding 服务(兼容 OpenAI API 格式)
import openai

client = openai.OpenAI(
    base_url="http://localhost:8080/v1",
    api_key="not-needed"  # 本地部署不需要 key
)

response = client.embeddings.create(
    model="BAAI/bge-m3",
    input=["这是第一段文本", "这是第二段文本"]
)

for item in response.data:
    print(f"向量维度: {len(item.embedding)}")

9.3 Docker Compose 全套部署

yaml

# docker-compose.yml
# Embedding 服务 + 向量数据库 + RAG 应用

version: '3.8'

services:
  # vLLM Embedding 服务
  embedding:
    image: vllm/vllm-openai:latest
    command: >
      --model BAAI/bge-m3
      --task embedding
      --host 0.0.0.0
      --port 8080
      --trust-remote-code
      --dtype float16
      --max-model-len 8192
    ports:
      - "8080:8080"
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]

  # PostgreSQL + pgvector
  vector-db:
    image: pgvector/pgvector:pg16
    environment:
      POSTGRES_DB: rag
      POSTGRES_USER: rag
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"

  # Redis(缓存)
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  pgdata:

9.4 性能优化

python

# 高性能 Embedding 服务配置

# 1. 批量处理 - 减少 API 调用次数
def batch_embed_optimized(texts: list, batch_size: int = 64) -> list:
    """分批嵌入,避免 OOM"""
    all_embeddings = []
    for i in range(0, len(texts), batch_size):
        batch = texts[i:i+batch_size]
        response = client.embeddings.create(
            model="BAAI/bge-m3",
            input=batch
        )
        all_embeddings.extend([item.embedding for item in response.data])
    return all_embeddings

# 2. 缓存 - 相同文本不重复嵌入
import hashlib
import redis

redis_client = redis.Redis(host='localhost', port=6379)

def cached_embed(text: str, model: str = "bge-m3") -> list:
    """带缓存的嵌入"""
    cache_key = f"emb:{model}:{hashlib.md5(text.encode()).hexdigest()}"
    
    # 检查缓存
    cached = redis_client.get(cache_key)
    if cached:
        return json.loads(cached)
    
    # 计算嵌入
    embedding = embed_with_ollama(text, model)
    
    # 写入缓存(TTL 24小时)
    redis_client.setex(cache_key, 86400, json.dumps(embedding))
    
    return embedding

# 3. 异步处理 - 大批量嵌入
import asyncio

async def async_embed(texts: list, model: str = "bge-m3") -> list:
    """异步批量嵌入"""
    import httpx
    async with httpx.AsyncClient() as http_client:
        tasks = []
        for text in texts:
            task = http_client.post(
                "http://localhost:8080/v1/embeddings",
                json={"model": model, "input": text}
            )
            tasks.append(task)
        
        responses = await asyncio.gather(*tasks)
        return [r.json()["data"][0]["embedding"] for r in responses]

10. 踩坑记录

坑1:维度不匹配导致向量检索失败

问题: 存储时用了 3072 维(OpenAI 默认),但检索时用了 1024 维(Matryoshka 裁剪),导致维度不一致。

python

# ❌ 错误 - 存储和检索维度不一致
# 存储
response = client.embeddings.create(
    model="text-embedding-3-large",
    input=doc_text,
    # 没指定 dimensions,默认 3072
)

# 检索
response = client.embeddings.create(
    model="text-embedding-3-large",
    input=query_text,
    dimensions=1024  # 裁剪到 1024 维
)

# ✅ 正确 - 始终使用相同维度
DIMENSIONS = 1024  # 项目全局配置

def embed(text: str) -> list:
    response = client.embeddings.create(
        model="text-embedding-3-large",
        input=text,
        dimensions=DIMENSIONS
    )
    return response.data[0].embedding

坑2:OpenAI 和 Voyage 的非对称嵌入方向搞反

问题: Voyage 区分 query 和 document 编码,搞反了会导致检索质量严重下降。

python

# ❌ 错误 - 文档用 query 类型编码
result = vo.embed(
    doc_texts,
    model="voyage-3-large",
    input_type="query"    # 应该是 "document"!
)

# ❌ 错误 - 查询用 document 类型编码
result = vo.embed(
    [query_text],
    model="voyage-3-large",
    input_type="document"  # 应该是 "query"!
)

# ✅ 正确 - 文档用 document,查询用 query
doc_result = vo.embed(doc_texts, model="voyage-3-large", input_type="document")
query_result = vo.embed([query_text], model="voyage-3-large", input_type="query")

坑3:BGE-M3 的混合检索权重调优

问题: Dense 和 Sparse 的默认融合权重不一定适合你的数据。

python

# ❌ 错误 - 默认权重可能不适合
# 某些场景 Sparse 很重要(错误码、产品名)
# 某些场景 Dense 更重要(语义查询)
results = engine.search(query, dense_weight=0.7, sparse_weight=0.3)

# ✅ 正确 - 根据场景调优
# 错误码/精确匹配场景 → 提高 Sparse 权重
results = engine.search(query, dense_weight=0.4, sparse_weight=0.6)

# 语义查询场景 → 提高 Dense 权重
results = engine.search(query, dense_weight=0.8, sparse_weight=0.2)

# 通用场景 → 先用 0.7/0.3 起步,再根据评估调优

坑4:Cohere v4 的 128K 上下文滥用

问题: Cohere v4 支持 128K 上下文,但这不意味着你应该把 128K 的文档不分块塞进去。

plaintext

误解:Cohore v4 支持 128K → 我可以不分块直接嵌入长文档
现实:
  - 128K 文档压缩到 1024 维向量,信息严重稀释
  - RAG 场景中用户查询的是文档的特定部分
  - 全文档向量无法精准定位到特定段落

建议:
  - 文档 < 2K tokens → 不分块
  - 文档 2K-8K tokens → 语义分块(按段落/章节)
  - 文档 > 8K tokens → 分块 + 重排序
  - 128K 上下文主要用于:文档级相似性、法律文档对比

坑5:模型切换后的重新索引

问题: 从 OpenAI 3-large 切换到 Voyage-3-large,向量维度不同,需要全量重新索引。

python

# ❌ 错误 - 直接切换模型,旧向量和新向量不兼容
# OpenAI 3-large: 3072 维
# Voyage-3-large: 1024 维
# 两个模型的向量空间完全不同,不能混用

# ✅ 正确 - 切换模型时全量重建索引
def migrate_embedding_model(
    old_model: str,
    new_model: str,
    documents: list,
    vector_db
):
    """迁移 Embedding 模型"""
    print(f"迁移: {old_model} → {new_model}")
    
    # 1. 用新模型重新嵌入所有文档
    new_embeddings = batch_embed_optimized(documents, model=new_model)
    
    # 2. 创建新的向量表
    vector_db.create_table(f"docs_{new_model}", new_embeddings[0])
    
    # 3. 写入新向量
    for i, (doc, emb) in enumerate(zip(documents, new_embeddings)):
        vector_db.upsert(f"docs_{new_model}", i, doc, emb)
    
    # 4. 验证新索引
    test_query = "测试查询"
    results = vector_db.search(f"docs_{new_model}", test_query, top_k=5)
    assert len(results) > 0, "新索引验证失败"
    
    # 5. 切换查询路由到新索引
    vector_db.set_active_index(f"docs_{new_model}")
    
    # 6. 保留旧索引一段时间(回滚用)
    # 7. 确认无问题后删除旧索引

坑6:Matryoshka 裁剪的精度损失

问题: Matryoshka 裁剪到 256 维后,检索质量下降超出预期。

plaintext

据实测数据(CSDN 2026年Embedding选型测试):

Voyage 3-large:
  全维度(1024):  ρ = 0.880
  512维:         ρ = 0.876 (↓0.5%)
  256维:         ρ = 0.868 (↓1.4%)

Jina v4:
  全维度(1024):  ρ = 0.833
  512维:         ρ = 0.830 (↓0.4%)
  256维:         ρ = 0.825 (↓1.0%)

OpenAI 3-large:
  全维度(3072):  ρ = 0.782
  1024维:        ρ = 0.775 (↓0.9%)
  256维:         ρ = 0.754 (↓3.6%)

Gemini 2:
  全维度(3072):  ρ = 0.668
  1024维:        ρ = 0.612 (↓8.4%)  ← 严重退化!
  256维:         ρ = 0.534 (↓20%)

结论:
  - Voyage/Jina 训练时优化了 MRL,裁剪损失小
  - Gemini 没有专门训练 MRL,裁剪后严重退化
  - 如果需要维度裁剪,选 Voyage 或 Jina
  - 如果不需要裁剪,Gemini 全维度更强

坑7:BGE-M3 的内存占用

问题: BGE-M3 虽然 567M 参数,但三种模式同时开启时内存占用比预期高。

plaintext

内存实测:
  只用 Dense:          ~1.2GB
  Dense + Sparse:      ~2.5GB
  Dense + Sparse + ColBERT: ~4.0GB

  ColBERT 模式下每个文档输出 N 个向量(N = token 数)
  → 存储开销是 Dense 的 100-500 倍!

建议:
  - 大多数场景 Dense + Sparse 就够了
  - ColBERT 只在需要极致精度的小数据集上用
  - 超过 10 万文档不要用 ColBERT

11. 总结

核心结论

  1. OpenAI 不再是默认最优——2026 年有好几个更好的选择

  2. 非对称嵌入是关键提升——query 和 document 分开编码,检索质量高 2-4%

  3. 混合检索是标配——Dense + Sparse 在几乎所有场景都比纯 Dense 好

  4. BGE-M3 是开源之王——中文、混合检索、MIT 许可,三个维度都最强

  5. Voyage 是 API 精度之王——纯 Dense 检索精度最高,领域专精模型更胜一筹

  6. 长上下文≠不需要分块——RAG 场景分块仍然更好,长上下文适合文档级对比

选型一句话

表格

需求

选型

中文+开源

BGE-M3

最高精度API

Voyage-3-large

代码搜索

Voyage-code-3

多语言企业搜索

Cohere v4

跨语言+多模态

Gemini 2

最便宜API

Voyage-3

最便宜自部署

BGE-M3

最终建议: 不要只看 MTEB 分数——在你自己的数据上做 A/B 测试才是最靠谱的选型方式。通用基准和你的业务场景可能差很远。

Embedding 模型是 RAG 的地基,选错模型就像在沙滩上建楼——再漂亮的 reranker 和 prompt 也救不回来。2026 年没有"最好的"Embedding,只有"最适合你的"Embedding。

——PySuper | zhengxingtao.com

参考链接:

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区