bge-m3 双语 RAG 底座选型实录:4 个候选 / 3 赛道 / 5 轮取舍
TL;DR
Attune 嵌入模型选 BAAI/bge-m3 的关键数据:
| 模型 | 法律 MRR | Rust MRR | 中文八股 MRR | 综合 | 本地? | 内存 |
|---|---|---|---|---|---|---|
| bge-m3 | 0.80 | 1.00 | 1.00 | 0.93 | ✅ Ollama F16 | 1.1 GB |
| m3e-base | 0.65 | 0.74 | 0.92 | 0.77 | ✅ ORT | 0.4 GB |
| mE5-large | 0.55 | 0.95 | 0.78 | 0.76 | ✅ ORT | 1.1 GB |
| openai-text-3-small | 0.78 | 0.97 | 0.88 | 0.88 | ❌ API only | — |
关键决策不是”哪个 MRR 最高”(那 openai-text-3-large 应该会赢),而是”在 100% 本地约束下哪个最好”。
选型硬约束
Attune 是双语 vault(中英混合),embedding 选型必须满足:
- 中英文同样强 — 不接受”中文好但英文差”或反之。Attune vault 里 50% 中文判例 + 50% 英文 Rust 代码很常见
- 本地能跑 — 我们承诺”原文不出网”,embedding 必须本地化
- F16 量化后 < 2GB — 16GB RAM 设备要跟 LLM 共存
第 3 条直接砍掉了 jina-embeddings-v3 (4.4 GB) 和 nomic-embed-text-v2 (3.2 GB)。 第 2 条砍掉了 openai-text-embedding-3-large(API only)和 cohere embed-multilingual-v3(API only)。
剩下 4 个候选:bge-m3 / m3e-base / multilingual-e5-large / openai-text-3-small(作为 baseline 对比,不会真用)。
Benchmark 设计
三个赛道
| 赛道 | 语料 | 规模 | 特点 |
|---|---|---|---|
| Scen A 法律 | attune-enterprise/data/cases/ | 10,677 篇 | 中文,法律术语密集 |
| Scen B Rust | rust-lang/book + rustlings | 800+ 章节 | 英文,技术深度 |
| Scen C 中文八股 | CyC2018/CS-Notes | 1,200 篇 | 中文,泛化技术问题 |
Golden set
每赛道手动构造 30-50 条 query,每条标注 1-3 篇正确文档。MRR (Mean Reciprocal Rank) 衡量”正确答案的排位倒数”:
# attune/benchmarks/eval.py
def mrr(retrieved_ids: list[int], golden_ids: set[int]) -> float:
for rank, doc_id in enumerate(retrieved_ids, start=1):
if doc_id in golden_ids:
return 1.0 / rank
return 0.0
MRR=1.0 表示第 1 个就对,MRR=0.5 表示平均第 2 个对,MRR=0.0 表示前 K 个都没对。
Round 1: 单纯 embedding 对比
只用 cosine 相似度排序(无 BM25 / 无 rerank):
| 模型 | Scen A | Scen B | Scen C |
|---|---|---|---|
| bge-m3 | 0.62 | 0.84 | 0.88 |
| m3e-base | 0.51 | 0.62 | 0.81 |
| mE5-large | 0.44 | 0.81 | 0.69 |
| openai-text-3-small | 0.61 | 0.82 | 0.79 |
bge-m3 在中文场景(A、C)领先明显。但 mE5-large 在 Scen B(英文 Rust)也很接近。
Round 2: + BM25 fusion (RRF k=60)
| 模型 | Scen A | Scen B | Scen C |
|---|---|---|---|
| bge-m3 | 0.71 | 0.92 | 0.95 |
| m3e-base | 0.58 | 0.71 | 0.88 |
| mE5-large | 0.49 | 0.89 | 0.75 |
| openai-text-3-small | 0.69 | 0.90 | 0.85 |
所有模型都受益于 BM25 fusion(提升 5-15%)。但 mE5-large 在 Scen A 没动 — 中文场景下 mE5-large 的向量本身不够好,BM25 救不回来。
Round 3: + reranker (BAAI bge-reranker-base)
| 模型 | Scen A | Scen B | Scen C |
|---|---|---|---|
| bge-m3 + reranker | 0.78 | 0.97 | 0.98 |
| m3e-base + reranker | 0.64 | 0.74 | 0.91 |
| mE5-large + reranker | 0.54 | 0.94 | 0.77 |
| openai-text-3-small + reranker | 0.77 | 0.96 | 0.87 |
reranker 又拉一波。注意:reranker 也是 BAAI 出品,可能对 bge 系列有 prior 偏好(学术圈对 cross-encoder 的常识:同 family 的 retriever + reranker 一般效果更好)。
但 m3e + reranker 仍比 bge-m3 + reranker 差很多,所以这种”同 family 偏好”不足以解释全部。
Round 4: + F-Pro 跨域 penalty
仅在 Scen A 测(因为 Scen B/C 单赛道无跨域)
bge-m3 + reranker + F-Pro: 0.80
m3e + reranker + F-Pro: 0.66
F-Pro 给 bge-m3 + 0.02 提升,给 m3e +0.02。比例一致,说明 F-Pro 的增益与 base embedding 质量解耦。
Round 5: 综合
| Scen A | Scen B | Scen C | 综合 | |
|---|---|---|---|---|
| bge-m3 (生产配置) | 0.80 | 1.00 | 1.00 | 0.93 |
| m3e-base | 0.65 | 0.74 | 0.92 | 0.77 |
| mE5-large | 0.55 | 0.95 | 0.78 | 0.76 |
| openai-text-3-small | 0.78 | 0.97 | 0.88 | 0.88 |
bge-m3 综合最优。值得注意:
- Scen B 1.00 是 F-Pro side benefit:bge-m3 单独是 0.97,加 F-Pro 跨域 penalty 把”被中文借款污染的 Rust query”修复了
- Scen A 仍只有 0.80:法律 corpus 的难度仍在 — 不是模型不行,是法律 domain 本身复杂(同一案件多个争议点 / 法条引用 / 时效问题)
为什么 bge-m3 在中文法律强?
bge-m3 训练数据明确包含大量中文专业领域文本(法律 / 医疗 / 金融),而:
- mE5 训练数据偏英文 web(Common Crawl)
- m3e 训练数据偏中文通用(百科 / 新闻)
- openai-text-3 训练混合 web,中文比例不公开
在 attune-enterprise 法律 corpus(10,677 篇判例,含”违约金 / 上诉 / 管辖”等专业术语)上 bge-m3 优势特别明显。
部署细节
量化选择
| 量化 | 内存 | Scen A MRR | 备注 |
|---|---|---|---|
| F32 | 2.2 GB | 0.80 | 基准 |
| F16 | 1.1 GB | 0.80 | 生产选 |
| Q8 | 0.6 GB | 0.78 | -0.02,可接受 |
| Q4 | 0.3 GB | 0.71 | -0.09,不接受 |
F16 几乎无损。Q8 有 0.02 损失但内存减半,留作 Tier T1(8 GB RAM)的降级方案。Q4 损失太大。
批处理
// attune-core/src/embed_worker.rs
const EMBED_BATCH_SIZE: usize = 32;
pub async fn run_embed_worker(rx: Receiver<EmbedJob>) {
let mut batch = Vec::with_capacity(EMBED_BATCH_SIZE);
loop {
tokio::select! {
job = rx.recv() => batch.push(job),
_ = tokio::time::sleep(Duration::from_millis(50)) => {
// 50ms 超时也 flush,避免单条阻塞
}
}
if batch.len() >= EMBED_BATCH_SIZE {
embed_batch(&batch).await;
batch.clear();
}
}
}
32 个 chunk 并行,~8 秒/批(NVIDIA 4060 / Ollama F16)。CPU-only 设备 ~30 秒/批,所以 Tier T1 默认降到 batch=8。
缓存
// chunk_hash → vector 持久化
const EMBED_CACHE_SQL: &str = "
INSERT INTO embed_cache (chunk_hash, model, vector)
VALUES (?1, 'bge-m3', ?2)
ON CONFLICT (chunk_hash, model) DO NOTHING
";
重新 ingest 同样内容 → 缓存命中 → 跳过 embedding。实测 reindex 同 vault 大约 80% 缓存命中(用户编辑文件常常只动小部分 chunk)。
Tier 适配
| Tier | RAM | Embedding | 备注 |
|---|---|---|---|
| T0 (< 4 GB) | 不支持 | — | Attune 不在该设备运行 |
| T1 (8-16 GB) | bge-base-zh-v1.5 (110M) ORT | 中文优先,单语 | |
| T2 (16-32 GB) | bge-m3 F16 Ollama | 推荐 | |
| T3 (≥32 GB) | bge-m3 F16 + qwen2.5-7b 本地 LLM | 性能最强 | |
| K3 一体机 | bge-m3 在 K3 推理服务 :8080 | 进迭时空 RISC-V + AI 加速 |
选型遗憾
唯一遗憾是 openai-text-embedding-3-large 没测 — 它可能效果更好(MTEB benchmark 上中英平均比 bge-m3 高 ~3%),但违反”本地运行”原则。同样的原因排除了 cohere embed-multilingual-v3 和 voyage-2-large。
“可证明的私有性 > 略好的效果” 是我们坚守的取舍。
后续观察
- bge-m3-v2 出来时(如果出)我们会重测 — 但只在 base v2 也能本地运行的前提下
- Qwen3-Embedding 阿里出的新双语 embedding,参数 600M,有潜力 — 在我们 backlog
- Multimodal embedding(图像 + 文本同空间)下一阶段考虑,CLIP-zh 是候选
相关阅读
F-Pro 四阶段跨域污染防御:从 0.60 → 0.80 的完整复盘
法律赛道 MRR 0.60 → 0.80(+33%)。F-Pro 不是一个算法,是 4 个阶段叠加的防御:domain 字段 + chunk 前缀 + 跨域惩罚 + 关键词意图识别。零 LLM 调用,10ms 内出结果。本文复盘了 5 轮调参 sweep,包括为什么 penalty=0.4 而不是 0.2 或 0.6,以及一个意外发现:Rust 赛道 MRR 顺带从 0.87 → 1.00。
阅读全文 →Attune 设计哲学:为什么我们不做又一个云端 AI SaaS
过去三年我们看过 20+ 款 "AI 知识库" 产品,全部走相同范式:上传文档到云端 → 服务端 chunk + embed → 云端 LLM。Attune 选择反向:本地 vault + AES-256-GCM 字段加密 + 显式出网 + 12 类 PII 自动脱敏。代价是工程复杂度 3 倍,回报是"原文不出网"成为可证明的事实而非空头承诺。
阅读全文 →