
📰 科技要闻
• 苹果起诉OpenAI窃取商业机密,AI巨头间的专利与人才战进一步升级。
• SK海力士CEO称史上最大存储短缺将在明年到来,考虑推出"内存即服务"模式应对算力扩张需求。
• Lorde吐槽Ray-Ban Meta AI眼镜"不够性感",AI硬件的审美争议还在继续。
上一篇聊完 Chunking,我以为知识库的检索质量问题总算翻篇了。结果上线两周后,我做了一件"聪明"的事——把 Embedding 模型从 text-embedding-ada-002 换成了新出的 text-embedding-3-large,想着"新模型肯定更强,直接切过去"。切完之后线上检索直接报错,Milvus 里几十万条旧向量全部对不上新的相似度分布,召回结果全乱了。
我当时就纳闷了,模型不是越新越强吗?后来才搞明白:Embedding 模型换了,向量空间就变了,旧向量和新向量根本不在一个坐标系里,相似度计算完全没有意义。打个比方,这就像原来用厘米量的一堆数据,突然全部换成英寸,你拿老数据跟新数据去比大小,数字看着都在,但压根不是一个度量体系,比出来的结果全是垂圾。这玩意儿坑了我整整一个周末——重新跑全量向量化,1400万字的文档库,光是调 API 就烧了小一千块钱。这一篇就把 Embedding 选型这件事彻底讲清楚:怎么选、怎么部署、怎么微调、怎么省钱。
MTEB排行榜怎么看:榜是榜,你的场景是你的场景
选 Embedding 模型第一反应肯定是去查 MTEB Leaderboard。但说实话,我最开始也信了榜单排名,结果被坑得不轻。MTEB 是个综合了检索、聚类、分类、重排等 8 大类任务的评测集,排名靠前的模型未必在"中文企业文档检索"这一个具体场景里表现最好。
几个容易被忽略的坑:
1. 榜单里的检索任务(Retrieval)大多是英文语料(MS MARCO、NQ 等),中文场景应该单独看 C-MTEB 这个中文子榜,混着看会误判。
2. 综合分数是多个任务的平均值,你的业务只关心检索,别被聚类、分类任务的高分带偏。
3. 榜单不体现推理成本和延迟,一个 1024 维的模型和一个 3072 维的模型分数差 2 个点,但存储和计算成本可能差 3 倍。
我的做法是:先用 C-MTEB 的 Retrieval 子榜筛出前 5~8 个候选,再拿自己业务的真实文档(哪怕就 200 条人工标注的 query-doc 对)跑一次小规模评测,最终决策看自己的数据,不看榜。
三个模型的中文表现实测对比
OpenAI text-embedding-3 vs BGE-M3 vs M3E
我们知识库项目里同时试过这三个:OpenAI 的 text-embedding-3-large、智源的 BGE-M3,以及早期国产开源里很火的 M3E-base。拿公司内部 300 条真实客服问答对做的召回率测试,结果如下:
模型 | 维度 | Top-5召回率 | 部署方式 |
|---|---|---|---|
text-embedding-3-large | 3072(可截断) | 91.3% | API调用 |
BGE-M3 | 1024 | 89.7% | 私有化部署 |
M3E-base | 768 | 78.4% | 私有化部署 |
数字差距不算离谱,但用起来的体感差很多。M3E 是 2023 年那批国产开源模型的代表,训练语料偏通用网络文本,遇到我们业务里大量的技术术语(比如"灰度发布"、"熔断降级"这类词)经常召回不准,说白了就是"老了",已经不推荐新项目再选它。
真正值得纠结的是前两个。text-embedding-3-large 效果最好,但有两个硬伤:一是数据出境合规问题,企业文档跑去调用境外 API,法务那关很难过;二是成本,我们每天新增几万条文档要向量化,按 token 计费长期跑下来不便宜。BGE-M3 差距在可接受范围内,还有个隐藏优势——它是"三合一"模型,同时支持稠密检索(Dense)、稀疏检索(Sparse,类似 BM25)和多向量检索(Multi-Vector),一个模型能顶三种召回方式,这对做混合检索(下一篇会讲)特别友好。
我的判断:企业内部知识库、数据合规敏感、需要私有化部署,选 BGE-M3;对外 C 端产品、不在乎境外调用、追求极致效果,选 OpenAI 的模型;纯粹图便宜图省事的小项目玩具级验证,M3E 也够用,但别带进生产。
私有化部署:HuggingFace TEI 实战
选定 BGE-M3 之后,部署方案我对比了三个:HuggingFace 的 TEI(Text Embeddings Inference)、vLLM、以及 NVIDIA 的 Triton Inference Server。
三者怎么选
部署Embedding服务
↓
主要目的是什么?
↓
专做Embedding → TEI,部署最简单, 支持动态batching,Rust写的推理层, CPU上也能跑,小团队首选
已有vLLM跑LLM → vLLM也支持 embedding,一套基建搞两件事, 省运维成本
多模型多框架混部 → Triton, 支持TensorRT/ONNX/PyTorch混合, 但配置复杂度陡增,小团队别碰
我们团队规模不大,运维精力有限,最后选了 TEI,部署过程比想象中简单:
# 拉取TEI镜像并启动服务
docker run --gpus all -p 8080:80 \
-v $PWD/models:/data \
ghcr.io/huggingface/
text-embeddings-inference:1.5 \
--model-id BAAI/bge-m3 \
--max-batch-tokens 16384启动之后就是一个标准的 HTTP 服务,调用方式跟 OpenAI 的接口很像,迁移成本很低:
import httpxasync def embed(texts: list[str]):
async with httpx.AsyncClient() as c:
resp = await c.post(
"http://tei:8080/embed",
json={"inputs": texts},
timeout=10
)
return resp.json()实测下来,单张 A10 显卡跑 BGE-M3,动态 batching 打满的情况下吞吐能到每秒 800+ 条短文本,比我们最初预估的高不少。TEI 的动态 batching 是真的省心,不用自己写请求合并逻辑,这也是我为什么最终没选自己拿 FastAPI 裸写一个推理服务的原因——省下的开发时间远比省下的那点灵活性值钱。
Fine-tuning Embedding:用业务数据微调
BGE-M3 通用效果已经不错,但真正让我们的召回率从 89.7% 干到 94% 以上的,是拿业务数据做了一次微调。这一步很多团队会跳过,觉得"通用模型够用了",但如果你的领域术语密度高(比如法律、医疗、内部技术文档),微调的收益非常明显。
思路很简单:用 sentence-transformers 库,构造 (query, positive_doc, negative_doc) 三元组,用对比学习(Contrastive Learning)让模型把 query 和它对应的正确文档在向量空间里拉近,跟错误文档推远。
from sentence_transformers import (
SentenceTransformer, losses
)
from sentence_transformers import (
InputExample
)model = SentenceTransformer("BAAI/bge-m3")# 三元组:问题, 正确答案, 错误答案
examples = [
InputExample(texts=[
"灰度发布怎么配置",
"分批放量文档...",
"全量回滚文档..."
]),
]train_loss = losses.TripletLoss(model)
model.fit(
train_objectives=[
(examples, train_loss)
],
epochs=3,
warmup_steps=100
)这里有个坑我踩过:负样本(negative_doc)不能随便找一个不相关的文档,效果会很差,因为模型很容易区分"完全不相关"的东西,学不到细粒度的区分能力。真正有效的是"难负样本"(Hard Negative)——找那些语义上跟正确答案很像,但其实答非所问的文档。这就跟考试出干扰选项一个道理,出一个明显搞笑的错误选项没有任何区分度,真正拉分的是那种"看起来很像正确答案,但差了关键一步"的选项。比如问"灰度发布怎么配置",难负样本应该是"蓝绿部署怎么配置",而不是随便一篇产品介绍。挖掘难负样本最简单的办法是先用未微调的模型跑一遍检索,取 Top-10 里排除掉真正正确答案的那几条,就是天然的难负样本。
多语言与跨模态Embedding的前沿进展
简单提两个值得关注的方向。一是多语言统一 Embedding,BGE-M3 本身已经支持 100+ 语言共享同一个向量空间,这意味着中文 query 可以直接检索到英文文档,对我们这种经常要检索英文技术文档的场景很有用,不用再维护"中文库+英文库"两套索引。
二是跨模态 Embedding,像 CLIP 系列以及国产的 Jina CLIP,能把图片和文字映射到同一个向量空间,用文字搜图片、用图片搜文字。我们的知识库里有大量架构图、流程截图,之前这些图片素材完全没法被检索到,纯靠人工打标签。跨模态 Embedding 理论上能解决这个问题,但目前实测中文场景下跨模态检索精度还不够稳定,我们先放进了 backlog,等模型再成熟一点再上生产。
Embedding缓存策略:避免重复计算
最后说一个容易被忽视但收益很直接的优化:Embedding 缓存。用户的搜索 query 天然有重复性——"怎么申请权限"这种问题一天可能被问几十次,每次都重新跑一遍向量化模型,纯粹是浪费算力,就跟你常去的那家餐厅,服务员早就记住你的口味,不用你每次都重新点一遍菜。
我们的做法是拿 query 文本做 hash 当 key,向量结果存 Redis,加个合理的过期时间(我们设了 7 天,因为模型不会突然变化):
import hashlib
import jsonasync def embed_cached(text: str):
key = f"emb:{hashlib.md5(
text.encode()
).hexdigest()}"
cached = await redis.get(key)
if cached:
return json.loads(cached)vec = await embed([text])
await redis.setex(
key, 604800, json.dumps(vec)
)
return vec上线之后我们统计了一下,高频问题的缓存命中率能到 35% 左右,直接省下了三分之一的推理算力,这个投入产出比是我们这次改造里最高的一项——半天写完,效果立竿见影。
Embedding 缓存唯一要注意的坑:如果你后续会做微调或者换模型,一定要在缓存 key 里加上模型版本号,不然模型更新后缓存里全是"过期"的旧向量,比没有缓存还危险——这也是我文章开头那次踩坑的根源之一。
这一篇把 Embedding 从选型到部署到微调到缓存讲完了,回头看,最大的教训是:换模型这件事,代价远比想象中大,一定要提前规划好版本管理和迁移方案,别像我一样临时起意周末加班重跑全量向量化。下一篇打算讲讲 ElasticSearch 全文检索——纯向量检索有它的短板,遇到精确匹配的场景(比如查一个具体的错误码),还是关键词检索更靠谱,混合检索才是生产级 RAG 的正确姿势。