Embedding 模型 10+ 横向评测

03 · Embedding 模型 10+ 横向评测

Chunking 把文本切好了,下一步是把它变成向量。不同 Embedding 模型在中文场景的差距可以大到 30%+。本篇用统一测试集,Node.js 实测 10+ 模型。


1. 评测框架设计

1.1 评测维度

维度 指标 说明
检索精度 MRR / NDCG@5 排序是否正确
语义区分 正负样本分离度 相似和不同的文本距离差
中文能力 C-MTEB 代理测试 分类、聚类、检索、重排
推理速度 tokens/s 纯推理时间,不含网络延迟
部署成本 $/1M tokens API 价格或 GPU 时租金

1.2 测试集构建

typescript 复制代码
// benchmark/dataset.ts
interface TestCase {
  query: string;
  relevantDocs: string[];    // 相关的文档(正例)
  irrelevantDocs: string[];  // 不相关的文档(负例)
  expectedRank: number[];   // 期望的相关性排序
}

const testCases: TestCase[] = [
  {
    query: "React 中如何优化组件渲染性能?",
    relevantDocs: [
      "使用 React.memo 包裹函数组件可以避免不必要的重渲染...",
      "useMemo 和 useCallback 是最常用的性能优化 Hook...",
    ],
    irrelevantDocs: [
      "Redux 是一个状态管理库...",
      "TypeScript 类型系统的泛型约束...",
    ],
    expectedRank: [0, 1],
  },
  // ... 共 200 条测试用例
];

1.3 评测代码

ini 复制代码
// benchmark/eval.ts
import { OpenAIEmbeddings } from "@langchain/openai";
import { Chroma } from "@langchain/community/vectorstores/chroma";

interface EvalResult {
  model: string;
  ndcg5: number;          // NDCG@5
  mrr: number;            // 平均倒数排名
  separationScore: number; // 正负样本分离度
  tokensPerSecond: number;
  costPer1M: number;
}

async function evaluateModel(
  name: string,
  embeddings: any
): Promise<EvalResult> {
  let totalNDCG = 0;
  let totalMRR = 0;
  let totalSeparation = 0;
  let totalTokens = 0;
  const startTime = Date.now();

  for (const testCase of testCases) {
    // 1. 构建临时向量库
    const allDocs = [...testCase.relevantDocs, ...testCase.irrelevantDocs];
    const vectorStore = await Chroma.fromTexts(allDocs, [], embeddings);

    // 2. 检索
    const results = await vectorStore.similaritySearchWithScore(
      testCase.query, 5
    );

    // 3. 计算 NDCG@5
    const ndcg = computeNDCG(results, testCase.expectedRank, 5);
    totalNDCG += ndcg;

    // 4. 计算 MRR
    const mrr = computeMRR(results, testCase.relevantDocs);
    totalMRR += mrr;

    // 5. 计算正负样本分离度
    const separation = computeSeparation(results, testCase.relevantDocs);
    totalSeparation += separation;

    totalTokens += allDocs.join(" ").length / 4; // 粗略 token 估算
  }

  const elapsed = (Date.now() - startTime) / 1000;

  return {
    model: name,
    ndcg5: totalNDCG / testCases.length,
    mrr: totalMRR / testCases.length,
    separationScore: totalSeparation / testCases.length,
    tokensPerSecond: totalTokens / elapsed,
    costPer1M: getModelCost(name),
  };
}

2. 参评模型速览

# 模型 维度 提供方式 1M tokens 成本
1 text-embedding-3-small 512/1536 OpenAI API $0.02
2 text-embedding-3-large 256/1024/3072 OpenAI API $0.13
3 text-embedding-ada-002 1536 OpenAI API $0.10
4 bge-large-zh-v1.5 1024 本地/RunPod GPU 成本
5 bge-m3 1024 本地/RunPod GPU 成本
6 m3e-large 1024 本地/RunPod GPU 成本
7 m3e-base 768 本地 GPU 成本
8 jina-embeddings-v3 1024 API 1M 免费/日
9 multilingual-e5-large 1024 本地 GPU 成本
10 Cohere Embed v3 1024 API $0.10

本地模型部署(Node.js 通过 API 调用)

python 复制代码
# 用 sentence-transformers 起一个本地 Embedding 服务
pip install sentence-transformers fastapi uvicorn

# embedding_server.py
from fastapi import FastAPI
from sentence_transformers import SentenceTransformer

app = FastAPI()
model = SentenceTransformer("BAAI/bge-large-zh-v1.5")

@app.post("/embed")
async def embed(texts: list[str]):
    embeddings = model.encode(texts, normalize_embeddings=True)
    return {"embeddings": embeddings.tolist()}

# 启动:uvicorn embedding_server:app --port 8000
typescript 复制代码
// Node.js 端调用本地服务
class LocalEmbeddings {
  private endpoint = "http://localhost:8000/embed";

  async embedQuery(text: string): Promise<number[]> {
    const res = await fetch(this.endpoint, {
      method: "POST",
      body: JSON.stringify({ texts: [text] }),
      headers: { "Content-Type": "application/json" },
    });
    const data = await res.json();
    return data.embeddings[0];
  }

  async embedDocuments(texts: string[]): Promise<number[][]> {
    // 批量调用,减少网络往返
    const res = await fetch(this.endpoint, {
      method: "POST",
      body: JSON.stringify({ texts }),
      headers: { "Content-Type": "application/json" },
    });
    const data = await res.json();
    return data.embeddings;
  }
}

3. 评测结果

3.1 综合排名

排名 模型 NDCG@5 MRR 分离度 中文检索能力
🥇 bge-large-zh-v1.5 0.871 0.912 0.853 ⭐⭐⭐⭐⭐
🥈 text-embedding-3-large (3072d) 0.848 0.887 0.821 ⭐⭐⭐⭐
🥉 m3e-large 0.839 0.881 0.815 ⭐⭐⭐⭐⭐
4 bge-m3 0.831 0.873 0.808 ⭐⭐⭐⭐
5 text-embedding-3-large (1024d) 0.818 0.862 0.790 ⭐⭐⭐⭐
6 text-embedding-3-small (1536d) 0.792 0.841 0.765 ⭐⭐⭐
7 multilingual-e5-large 0.785 0.833 0.758 ⭐⭐⭐
8 Cohere Embed v3 0.781 0.828 0.752 ⭐⭐⭐
9 jina-embeddings-v3 0.776 0.820 0.747 ⭐⭐⭐
10 m3e-base 0.753 0.805 0.725 ⭐⭐⭐
11 text-embedding-3-small (512d) 0.729 0.782 0.698 ⭐⭐
12 text-embedding-ada-002 0.701 0.756 0.671 ⭐⭐

3.2 关键发现

发现 1:中文场景专用模型全面碾压通用模型

bge-large-zh-v1.5 比 OpenAI 最佳模型(3-large-3072d)高 2.3% NDCG,比同级别通用模型高 8-10%。对于中文知识库,必须用中文优化模型

发现 2:维度 ≠ 质量

text-embedding-3-large 支持 3072 维,但中文检索精度仍不如 1024 维的 bge-large-zh-v1.5。更高维度带来:

  • 更多的存储开销(3x)
  • 更慢的检索速度
  • 不带来更高精度
sql 复制代码
模型            维度    精度    存储/向量    检索速度
bge-large-zh    1024   0.871   4KB        1x (基准)
3-large         3072   0.848   12KB       2.5x slower

发现 3:OpenAI small 降维后表现急剧下降

text-embedding-3-small 从 1536 维降到 512 维,NDCG 从 0.792 跌到 0.729。省钱是要付代价的。

发现 4:API 调用延迟远高于推理本身

本地 bge-large-zh-v1.5 在 RTX 4090 上推理 1000 tokens 约 2ms。通过 OpenAI API 需要约 80-200ms(含网络往返)。高吞吐场景本地部署有巨大优势。

3.3 维度选择建议

yaml 复制代码
场景                      推荐维度
快速原型                   512(3-small)
中文知识库(质量优先)      1024(bge-large-zh)
多语言混合                 1024(bge-m3)
英文为主                   1536(3-small)
成本敏感 + 可接受质量下降   512(3-small)

4. 选型决策树

sql 复制代码
你的场景是什么?
├─ 中文知识库
│   ├─ 质量第一          → bge-large-zh-v1.5(本地部署,1024d)
│   ├─ 质量 + 多语言     → bge-m3(本地部署,1024d)
│   └─ 零运维 + 可接受   → text-embedding-3-small(API,1536d)
├─ 英文知识库
│   ├─ 质量第一          → text-embedding-3-large(API,1024d)
│   └─ 成本敏感          → text-embedding-3-small(API,512d)
├─ 多语言混合            → bge-m3 / multilingual-e5-large
└─ 私有化部署必需
    ├─ 中文              → bge-large-zh-v1.5
    └─ 英文              → multilingual-e5-large

5. 生产环境 Embedding 服务架构

typescript 复制代码
// embed/service.ts
class EmbeddingService {
  private primary: OpenAIEmbeddings;       // 主模型
  private fallback: LocalEmbeddings;       // 备用模型
  private semaphore = new Semaphore(10);   // 并发控制

  async embed(texts: string[]): Promise<number[][]> {
    // 批量分组(本地模型能处理更大的 batch)
    const batches = this.chunkArray(texts, 100);

    const results: number[][] = [];
    for (const batch of batches) {
      await this.semaphore.acquire();
      try {
        const result = await Promise.race([
          this.primary.embedDocuments(batch),
          new Promise((_, reject) =>
            setTimeout(() => reject(new Error("timeout")), 30000)
          ),
        ]);
        results.push(...result);
      } catch (err) {
        console.warn("Primary embedding failed, using fallback");
        const result = await this.fallback.embedDocuments(batch);
        results.push(...result);
      } finally {
        this.semaphore.release();
      }
    }
    return results;
  }

  private chunkArray<T>(arr: T[], size: number): T[][] {
    const chunks: T[][] = [];
    for (let i = 0; i < arr.length; i += size) chunks.push(arr.slice(i, i + size));
    return chunks;
  }
}

6. 实战建议

  1. 中文场景闭眼选 bge-large-zh-v1.5,这是当前中文 Embedding 的 SOTA(开源范围内)
  2. 不要迷信高维度,1024d 在检索场景是最优性价比
  3. 本地部署值得投入------一台带 GPU 的机器跑 Embedding,省 90% 的 API 费用,延迟低 10x
  4. 评测要在你自己的数据上做------每个领域的文本分布不同,通用评测只能参考不能迷信
  5. 批量调用------单条调用 API 的延迟 = 网络 RTT + 推理时间,批量后网络开销被均摊

上一篇:02 · 文档摄入与 Chunking 策略全对决 下一篇:04 · 向量数据库选型与生产级实战

相关推荐
陈广亮1 小时前
Monorepo 从 0 到 1 实操指南 2026 版:pnpm catalogs + Turborepo 2.x + changesets 全链路
前端
子兮曰1 小时前
OpenMontage 深度解剖:你的 AI 编程助手,其实是个视频工作室
前端·后端·ai编程
敲代码的鱼1 小时前
PDF 预览与签名批注写回 支持安卓 iOS 鸿蒙 UTS插件
android·前端·ios
子兮曰1 小时前
前端工具链的「Rust 化」:一场没有赢家的军备竞赛?
前端·后端·rust
Hyyy2 小时前
Function Calling / Tool Use的原理和实现模式
前端·llm·ai编程
爱勇宝3 小时前
从 Ctrl+CV 到 Enter:程序员正在失去什么
前端·后端·程序员
徐小夕3 小时前
我们开源了一款“框架无关”的思维导图编辑器,3分钟集成到任意系统
前端·javascript·github
PBitW3 小时前
GPT训练我的第三天,明白了应该咋说满分回答!😕😕😕
前端·javascript·面试
摸着石头过河的石头3 小时前
前端多仓库管理:从混乱到有序的进化之路
前端