候选模型先看硬指标
选 embedding 别只看排行榜总分,维度决定存储和内存、最大 token 决定要不要切长文档、训练和评测语料决定中文表现。下面是自建知识库里最常拿来比的几个,按 2026 年能直接下载到的版本列。
| 模型 | 维度 | 最大 token | 中文 | 部署 |
|---|---|---|---|---|
| BAAI/bge-m3 | 1024 | 8192 | 强,兼多语 | 本地 / API |
| bge-large-zh-v1.5 | 1024 | 512 | 短句强 | 本地 |
| m3e-base | 768 | 512 | 中 | 本地 |
| jina-embeddings-v3 | 1024 | 8192 | 好 | 本地 / API |
| 阿里 text-embedding-v3 | 1024 或 2048 | 8192 | 好 | 仅 API |
| text-embedding-3-small | 1536 | 8191 | 中 | 仅 API |
评测别只信 MTEB 总分,中文要看 C-MTEB 里 retrieval 的 nDCG@10 和 STS 的 Spearman 相关。更靠谱的是从自己业务日志里抽 20 到 50 条真实 query,人工标出应该命中的文档,算 recall@5,这个数和线上体感最贴合。
本地跑起来只要几行
Ollama 现在把 embedding 也纳入同一套服务,pull 之后走 /api/embed,省掉自己装 torch 的麻烦;需要精细控制或批量离线处理就用 sentence-transformers / FlagEmbedding。
ollama pull bge-m3
curl http://localhost:11434/api/embed -d '{
"model": "bge-m3",
"input": ["Ollama 的 num_ctx 设置", "向量检索的分块策略"]
}'
from sentence_transformers import SentenceTransformer
m = SentenceTransformer("BAAI/bge-large-zh-v1.5")
# 归一化后点积即余弦,存库前一定要做
v = m.encode(["上下文窗口与长文档处理"], normalize_embeddings=True)
print(v.shape)
取舍判断
- 中文为主、含长文档、中英混排:bge-m3。8192 token 能吃下整节文档,还同时输出稀疏向量,可以和关键词检索混排,长文档召回明显稳。
- 纯中文短句、显存小:bge-large-zh-v1.5 或 m3e-base,512 token 限制下先切块再编码,速度更快。
- 不想运维、能接受计费与数据合规评估:阿里 text-embedding-v3,维度可选,按 token 计费。
- 已有 OpenAI 链路:text-embedding-3-small 够用,中文略逊但省事。
这里有个几乎人人踩过的细节:bge-large-zh 和 bge-base 系列做非对称检索时,query 侧要加指令前缀"为这个句子生成表示以用于检索相关文章:",文档侧不加;bge-m3 不需要。忘记加前缀,相似度会整体偏低 0.1 到 0.2,表现成"明明相关却排不上去",很难查。对称任务(算两句相似度)则两边都不加。
QUERY_PREFIX = "为这个句子生成表示以用于检索相关文章:"
q_vec = m.encode([QUERY_PREFIX + query], normalize_embeddings=True)
d_vec = m.encode(documents, normalize_embeddings=True)
sims = d_vec @ q_vec[0]
最后一个硬约束:换 embedding 模型等于换了一套坐标系,必须全量重建索引。新旧向量混在一个库里,相似度数值没有可比性,阈值和排序全乱。把"模型名 + 维度 + 分块参数"写进 collection 名或元数据,是后面能做灰度迁移的前提。