训练多模态Embedding模型:微信生态向量召回实践 打开微信搜一找输入“西湖边适合晚上去的小馆”你期望结果里有公众号推文、视频号探店视频还有小程序里的餐厅预订页面。问题是这些内容是三种完全不同的模态文字、画面还有带交互的页面结构。系统不可能真的去“看懂”视频再回答但它需要快速从海量内容里召回可能相关的候选。这个入口通常就是多模态 Embedding 模型——把文本、图像、视频帧全部映射成同一个向量空间中的点然后按向量距离找相似。我最近因为业务需要认真踩了一遍这条路下面把“如何训练一个能服务于微信生态的多模态 Embedding 模型”从场景、数据、结构、训练到部署完整拆一遍。这种大体量系统里肯定有大量定制细节不会对外公开。下面写的是我在类似业务场景里完整跑一遍的通用方法使用的数据和模型都是公开资源重点说清楚每个环节怎么选、为什么这么选。1. 业务预期先对齐搜一搜底下的多模态召回到底需要解决什么问题1.1 微信生态里的多模态数据到底长什么样微信生态远不止聊天。公众号里的每篇文章有主文案、封面图、标题还会插入大量图片视频号一个短视频里同时存在画面、语音、字幕、封面文字小程序里每个页面有文本介绍、截图、UI布局还可能涉及用户产生的评价图。如果只把这些数据堆在数据库里搜索和推荐都无从下手。具体到一次用户请求用户输入一段自然语言 query可能需要在一整天的图文库里找到能匹配的候选内容。图像没有分词视频没有倒排索引所以第一步是把不同模态都转成计算机能比较的向量。业内把这些向量叫 Embedding。多模态 Embedding 与普通文本 Embedding 最大的区别在于视觉向量空间和文本向量空间经过训练后是被拉齐的图像描述里的“西湖落日”和一张夕阳下的湖面照片会落得很近。在微信场景里这个向量会用在至少三个地方第一是搜一搜的图文/视频召回第二是信息流推荐和相似内容去重第三是同一内容跨模态关联比如用视频声音里的一句话去定位短视频中对应的画面片段。每一个任务的数据形式不太一样但模型训练的思路是一致的都要解决“不同模态如何可比”这件事。1.2 为什么是“训练模型”而不是直接套现成接口很多团队拿现成的第三方多模态大模型接口也能做出 Demo给一张图让大模型输出一句描述然后再把这句描述文本做 Embedding 搜索。这种“先把图像转成文字再走文本检索”的做法叫模态转换方案它在大模型时代很容易被想到。缺点是大模型可能漏掉画面里无法用一句话表达的细节而且生成延迟高、成本高线上搜一次要等好几秒显然不实际。真正适合线上召回的是另一个方向让图像编码器和文本编码器共享一个语义空间用户 query 直接作为文本向量候选内容封面图直接作为图像向量两边都只是过一遍轻量编码器然后到向量索引里做近邻检索。训练这种模型需要大量图文配对数据目的是让模型学到不同模态之间哪些特征是共通的。这也是近两年“多模态融合算法”在落地搜索和推荐时的主流做法和用生成式大模型的方式并不是一条路。看到“模型融合”这四个字也别混淆这里有两条不同的路线一是多个输入模态在特征层做融合比如把文本、图像、OCR 结果拼起来学一个表示二是在线服务里召回后接了一个精排模型做集成式的模型融合。这篇文章讨论的主要是前者——在训练时让视觉分支和文本分支通过对比学习互相“看”到对方的高维特征从而产出有判别力的 Embedding。2. 模型结构怎么搭双塔、融合和 Loss 的取舍2.1 双塔结构线上召回最重要的保底选择现在市面上的多模态 Embedding 基础结构大多是双塔也就是一个文本塔、一个图像塔。文本塔通常是一个 BERT/RoBERTa 式的 Transformer把句子编码成 token 序列再取 [CLS] 位置的输出过一个 projection layer 变成 d 维向量图像塔通常是一个 ViT 或 CNN把图片编码后做 pooling再接一个投影层。两路向量都做 L2 normalize使点积等于余弦相似度。训练时拿一个 batch 里所有图文 pair 做对比学习。为什么不用一个完整的 cross-attention 模型因为线上召回要面对百万甚至亿级候选如果每条候选都需要模型同时读 query 和 doc算力成本是 O(N×M) 的完全跑不动。双塔结构允许候选内容向量离线算好、提前建索引线上 query 只算一次向量然后去索引里查 topK实际延迟能控制在几十毫秒再配合粗排精排。这不是说双塔没有缺陷。双塔在向量空间只用了点积交互没有让文本 token 和图像 patch 互相 attend遇到复杂推理、细粒度指代之类的问题效果会弱一些。所以项目通常会做两级双塔负责第一轮快速召回召回结果再送进一个交互式的精排模型里做细筛。训练 Embedding 模型阶段我们要盯的是第一轮不要漏掉太多正样本。2.2 多模态融合的输入到底怎么处理微信场景里“多模态”不止图片和文字。拿视频号来说一条短视频有连续帧、有背景音乐、有口播字幕也可能有 OCR 出来的画面文字。业界习惯先把视频均匀抽帧比如每秒 1 帧或者均匀取 8 帧再把每帧送进图像编码器。如果文本来自 ASR 字幕就按字幕块和相邻帧配对。把各个模态的特征融合成一个向量最简单的是后融合图像编码器输出若干 patch token文本编码器输出若干 token把两个 [CLS] 向量拼在一起再过一层 MLP 做压缩。更进阶的做法是加一层轻量 transformer把视觉 token 和文本 token 的序列拼起来做几次 self-attention。训练时如果加 fusion layer模型会更懂内部关联但工程上要做到和双塔一致的推理速度需要把 fusion layer 只用在精排召回用的向量仍然由两塔分别输出。所以我在设计项目时召回模型和精排模型不共用一个版本而是从同一个 checkpoint 分别延展。共用预训练权重但导出方式不同。这个取舍一开始就要讲清楚否则后端同事会把召回候选直接拿去给精排用效果常常是灾难。2.3 多模态对比学习的主角InfoNCE 和温度系数多模态对比学习的核心 loss 是 InfoNCE典型来自 CLIP 的图文匹配。形式不复杂[ L -\log \frac{\exp(s(q_i, p_i)/\tau)}{\exp(s(q_i, p_i)/\tau) \sum_{j \neq i}\exp(s(q_i, n_j)/\tau)} ]符号说明q_i 是第 i 个 query 的向量p_i 是它的正样本配对内容向量n_j 是负样本向量τ 是温度系数s 是余弦相似度。直观理解就是让正样本对的相似度尽量大让错配的负样本对相似度尽量小。这个 loss 的神奇之处在于它用同一个 batch 里所有其他文本/图片作为负样本不需要额外标注很多“负例”训练效率很高。温度系数 τ 很关键。τ 太小模型会把绝大多数注意力放在最难的负样本上训练容易震荡τ 太大所有负样本的梯度几乎一样模型学不出细粒度差异。CLIP 论文里用的是 0.07后来很多工作把它改成可学习参数比如 SigLIP 进一步换成了 sigmoid loss。我在真实项目里一般初始设成 0.05 到 0.1然后专门做几组小实验最后看困难负样本集合上的 recall 决定。这个参数需要根据文本分布和业务难度调不能照抄。2.4 要不要加辅助 Loss对比 loss 只拉近了正样本对各自的距离但有时无法保证文本语义细节都被保留。比如把文本塔训练成只关注“菜名”而对“位置”不敏感用户搜“西湖边的小馆”时模型可能会把“北京朝阳的火锅店”也拉得很近。为了缓解这种问题我一般会在多模态训练流程里再加入一个轻量文本匹配 loss 或者图像自监督 loss。最常见的是让文本塔继续学习纯文本语义匹配任务用同一份文本句子构造正负样本以 query/document 形式做 Retriever 训练图像塔也可以用 SimCLR 风格做 self-supervised但成本高可以只在预训练阶段加。辅助 loss 不能喧宾夺主。加太多会把业务目标稀释掉训练时间变长。我的经验是先用纯多模态对比 loss 把模型跑通再看 badcase 分布如果出现某一模态内部语义混乱再针对性加辅助 loss。不要一上来就堆五个 loss调起来会非常痛苦。3. 决定 70% 效果的多模态数据管道清洗、配对和负样本3.1 微信生态里的配对数据从哪里来训练任何一个多模态 Embedding 模型第一步永远是数据。微信生态里可以当弱配对样本的来源很多公众号正文里的插图与邻近段落是天然匹配对视频号里的某个视频帧与当前时刻的 ASR 字幕存在弱对应朋友圈的一张照片和发布时的文案算强配小程序页面的标题文本与功能截图也可以构建配对。但这些配对都是“弱相关”质量参差不齐。一张红烧肉配图下面正文可能正在讲手机价格视频里正在演示画面字幕却在念昨天的股价波动。这些噪声样本会让向量空间发生漂移所以必须先用规则或现成小模型过滤。微信数据量巨大人工标注不可能全覆盖我常用的策略是先跑一个“粗筛模型”用预训练好的 CLIP 模型对每个图文 pair 计算相似度相似度低于阈值的直接丢弃高于阈值的保留。这一步能去掉大概四分之一到三分之一的明显噪声非常值得。3.2 构造负样本从 in-batch 到多粒度困难负例对比学习最方便的负样本就是 batch 内其他样本。假设 batch size 是 1024那么每个正样本对同时也有 1023 个负样本对几乎免费。但全部是 in-batch 负样本有个问题如果一个 batch 数据组织不够