多模态 RAG 图文混合检索实战:把日调用 10 亿次的 WeMM-Embedding 搬进自己项目
关键词:多模态 Embedding、WeMM-Embedding、图文混合检索、RAG、向量检索、Matryoshka、FAISS
传统 RAG 只能检索文字:图片要么被丢进"OCR 文本"再检索,要么单独打标签,图文之间语义割裂。据微信 AI 官方公告,微信视觉团队 9 月初开源的 WeMM-Embedding(Apache-2.0)把文本、图片、视频、视觉文档映射进同一个向量空间------在 MMEB-v2 上 9B 版 80.6 分登顶、2B 版 77.9 分反超此前领先的 8B 开源模型,且已在朋友圈搜索、视频号推荐等场景日调用 10 亿次。本文用它搭一个最小可运行的图文混合检索系统,从装环境到 RRF 融合排序。
一、为什么图文必须进同一个向量空间
单模态 RAG 的结构是:文档 → 文本切片 → 文本向量;图片另走一条"标签/OCR 文本"的路。问题在于查询是跨模态的------用户想找"去年年会抽奖那张照片",若照片只有 OCR 文本,向量里根本没有"年会""抽奖"的语义。
多模态 Embedding 解决的不是"给图片补一层 OCR 文本",而是"同一把尺子量所有模态":模型对文本和图片各生成一个向量,两者直接算余弦相似度。于是检索可以双向进行:文字查图 ("红色连衣裙的产品图")、图查文(拿一张竞品图,召回库里的同类描述段落)。例如用户想找"去年年会抽奖那张照片",若照片只有 OCR 文本,向量里根本没有"年会""抽奖"的语义;进了同一向量空间后,这句话就能直接命中图片。WeMM-Embedding 额外支持"任意交错输入",意味着一个长文档里的"图 + 邻接文字"可以作为一个整体编码,避免图文配对信息被拆散。
二、最快跑通:sentence-transformers 一行加载
官方仓库(Tencent/WeMM-Embedding)推荐 transformers==5.2.0,也提供 sentence-transformers 推理脚本;SentenceTransformer.encode() 统一处理文本、图片、视频输入。
bash
pip install -U sentence-transformers faiss-cpu pillow
# 如需服务化:vllm serve 或 sglang(官方 scripts/serve_vllm.sh)
python
# embed_demo.py ------ 文本与图片编码到同一空间
from sentence_transformers import SentenceTransformer
from PIL import Image
import numpy as np
# 直接用 Hugging Face 模型 id;首次运行会自动下载权重
model = SentenceTransformer("tencent/WeMM-Embedding-2B")
texts = ["红色连衣裙的产品介绍", "年会抽奖环节照片"]
# 官方示例中文本/图片/视频统一走 encode();图片可传路径或 PIL Image
text_vecs = model.encode(texts, normalize_embeddings=True)
img_vecs = []
for p in ["dress.jpg", "annual_party.jpg"]:
v = model.encode(Image.open(p).convert("RGB"), normalize_embeddings=True)
img_vecs.append(v)
img_vecs = np.stack(img_vecs)
print("文本向量形状:", text_vecs.shape) # (2, 2048)
print("图片向量形状:", img_vecs.shape) # (2, 2048)
# 跨模态检索:用"红色连衣裙"的文本向量找最像的产品图
scores = text_vecs[0] @ img_vecs.T # L2 归一化后点积 = 余弦相似度
print("最匹配图片:", ["dress.jpg", "annual_party.jpg"][int(scores.argmax())])
Matryoshka(MRL)降维:模型支持 64~2048(2B 版)等截断维度,截断后需再次归一化。官方数据:2B 版在 256 维时保留全维度约 98.7% 的图文性能------内存和速度敏感的场景强烈建议先压到 256 维再谈放大。
python
def truncate_and_norm(vec: np.ndarray, dim: int = 256) -> np.ndarray:
v = vec[:dim]
return v / (np.linalg.norm(v) + 1e-9)
三、建图文混合索引:切片 + 图文配对 + 双路 FAISS
生产数据通常是"图文混排文档"。建索引策略:每张图与其邻近文本块配对成一条记录,图片向量进 idx_img,配对的文本与纯文本块进 idx_txt,记录共享同一个 doc_id。
python
# build_index.py
import json, numpy as np, faiss
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("tencent/WeMM-Embedding-2B")
DIM = 256 # MRL 截断维度
def norm(v: np.ndarray) -> np.ndarray:
v = v[:DIM]
return v / (np.linalg.norm(v) + 1e-9)
# 数据形态: [{doc_id, image: path|None, text: "相邻文本或整段"}]
docs = json.load(open("docs.json", encoding="utf-8"))
img_vecs, txt_vecs, img_ids, txt_ids = [], [], [], []
for d in docs:
if d["image"]:
v = model.encode(Image.open(d["image"]).convert("RGB"))
img_vecs.append(norm(np.asarray(v))); img_ids.append(d["doc_id"])
if d["text"]:
v = model.encode(d["text"])
txt_vecs.append(norm(np.asarray(v))); txt_ids.append(d["doc_id"])
idx_img = faiss.IndexFlatIP(DIM); idx_txt = faiss.IndexFlatIP(DIM)
if img_vecs: idx_img.add(np.stack(img_vecs).astype("float32"))
if txt_vecs: idx_txt.add(np.stack(txt_vecs).astype("float32"))
faiss.write_index(idx_img, "idx_img.faiss"); faiss.write_index(idx_txt, "idx_txt.faiss")
json.dump({"img_ids": img_ids, "txt_ids": txt_ids}, open("meta.json", "w"))
要点:IndexFlatIP(内积)+ 归一化向量 = 余弦检索,小数据量先暴力检索保证正确,再换 IndexIVFFlat/HNSW 上量。
四、混合查询:文查图 + 文查文 + RRF 融合
查询"找一张年会现场的照片",本质要同时命中"年会"文本与图片。做法:同一 query 向量分别查图库和文库,两路结果用 RRF(Reciprocal Rank Fusion)合并去重,兼顾相关性与模态多样性。
python
# query.py
import faiss, json, numpy as np
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("tencent/WeMM-Embedding-2B")
DIM = 256; TOPK = 20; K = 60 # RRF 常数
def norm(v):
v = np.asarray(v)[:DIM]
return v / (np.linalg.norm(v) + 1e-9)
def rrf_fuse(rank_lists, k=60):
score = {}
for rl in rank_lists:
for rank, doc_id in enumerate(rl):
score[doc_id] = score.get(doc_id, 0) + 1.0 / (k + rank + 1)
return sorted(score.items(), key=lambda x: -x[1])
idx_img = faiss.read_index("idx_img.faiss")
idx_txt = faiss.read_index("idx_txt.faiss")
meta = json.load(open("meta.json"))
qv = norm(model.encode("年会抽奖现场照片"))
_, I_img = idx_img.search(qv[None, :].astype("float32"), min(TOPK, idx_img.ntotal))
_, I_txt = idx_txt.search(qv[None, :].astype("float32"), min(TOPK, idx_txt.ntotal))
rank_img = [meta["img_ids"][i] for i in I_img[0]]
rank_txt = [meta["txt_ids"][i] for i in I_txt[0]]
for doc_id, s in rrf_fuse([rank_img, rank_txt], k=K)[:10]:
print(f"{doc_id}\t{s:.4f}")
小数据集上你会发现:纯文本向量那路常把"年会"相关段落排得很高,而图库那路能捞回"没被任何文字准确描述过"的图片------比如一张只有 LOGO 没有文字的现场海报,纯文本索引永远召不回,多模态索引却能靠视觉相似度命中。两路漏掉的东西不一样,融合后才完整。
五、工程取舍与选型
- 2B / 4B / 9B 怎么选:MMEB-v2 上 2B 77.9、4B 79.2、9B 80.6。差距主要落在视觉文档(VisDoc:2B 80.7 → 9B 83.3)。纯文本为主的场景 2B+256 维性价比最高;要处理复杂版式扫描件再上 9B。Qwen3-VL-Embedding-8B 为 77.8,2B 越级反超 0.1 分是选型的真实参照。
- 服务化 :量上来后别在业务进程里跑推理。官方给出 vLLM(
--runner pooling+ embedding chat template)与 SGLang 两条路,等价于起一个 OpenAI 兼容的 embedding 服务,与现有向量库链路无缝衔接。 - 召回与重排分离:多模态 Embedding 负责粗召回(要覆盖率高),精排交给重排模型或规则。别指望一个向量把"召回+排序"都做完。
- 增量更新:微信在 10 亿次日调用前做了 14 次线上 A/B------多模态模型换版会影响整个向量空间,升级后旧索引必须全量或批量重embed,建议索引带"模型版本"字段,避免新旧向量混算。
- 数据合规:2B 权重约数 GB 可自部署,Apache-2.0 商用友好;敏感数据不出域是选开源模型的核心理由。
但也要看到多模态 Embedding 的另一面:它不是万能的。榜单分数(MMEB-v2/v3)覆盖图像/视频/视觉文档/文本任务,但真实业务的长尾版式未必在评测集里,上线前务必用你自己的数据跑一遍 hit@k;模型升级会推倒整个向量空间,索引重建不是"跑个脚本"那么简单,需要纳入版本管理与灰度计划。这些隐性成本,往往比模型本身的授权费更高。
六、踩坑实录
- transformers 版本敏感 :官方推荐
transformers==5.2.0,版本过新可能导致预处理行为不一致、向量分布漂移------先锁版本再跑评测。 - 忘记截断后归一化:MRL 截断不重归一化,内积相似度会被维度截断放大/缩小,检索质量悄悄变差。
- 图片直接传路径与 PIL 的差异 :以官方
examples/sentence_transformers_inference.py为准,不同输入形态预处理路径不同,混用前先对齐。 - "Embedding 是
<embedding>token 位置的最后一层隐状态 + L2 归一化":自己魔改 pooling(如取 CLS 或 mean)会显著掉点,别自作聪明。 - 纯文本任务也要单独评测:MMEB-v3 把文本(NDCG@5)与 agent/MCMR 任务分开计分,别拿一个榜单总分当所有场景的保证。
- 视频输入成本:模型支持视频,但帧采样开销大,小项目先从"关键帧当图片"做起。
七、互动提问
- 你的 RAG 目前是纯文本还是已经带图?图片是靠 OCR 文本进索引,还是真·向量化?
- 2B 版 256 维能保住 98.7% 性能,你会用"小模型+低维"换吞吐,还是坚持全维度上 9B?
- 多模态 Embedding 换版后旧索引作废,你的生产链路打算怎么设计"模型版本 + 索引重建"?
欢迎在评论区聊聊你的图文检索落地场景和踩坑经历。
数据与事件来源:
- 微信视觉团队 WeMM-Embedding 技术报告与 GitHub 仓库(Tencent/WeMM-Embedding,Apache-2.0):模型族(2B/4B/9B)、MMEB-v2/v3 分数(据官方技术报告与 Hugging Face 模型卡)、
<embedding>token 池化与 L2 归一化、MRL 截断说明、sentence-transformers/vLLM/SGLang 用法。 - Hugging Face 模型卡:tencent/WeMM-Embedding-2B/4B/9B(Matryoshka 维度、Apache-2.0)。
- 微信 AI 官方公告及 IT之家/凤凰网科技(2026-09-04~08):模型已在朋友圈搜索、视频号推荐、公众号推荐、微信电商落地,日调用量达 10 亿次,上线前完成 14 次在线 A/B 测试。
- 官方技术报告 arXiv:2608.24053(MMEB-v2 78 数据集、MMEB-v3 190 任务评测口径)。
- 上述榜单分数、模型规格与调用方式,均对照官方技术报告、GitHub 仓库与 Hugging Face 模型卡交叉核实;落地规模经多家媒体报道相互印证,最终以微信官方口径为准。