微信生态多模态Embedding自研实践:数据、模型与上线 1. 为什么微信业务场景必须自研多模态 Embedding先说一个很多人容易忽略的点微信生态里的内容形态根本不是单一模态能搞定的。你在视频号看到一个美食教程用户点进去之后看到的画面、听到的背景音乐、理解的旁白文案、甚至左下角挂的小程序链接这四类信息分别属于图像、音频、文本、结构化行为数据。如果只用文本 Embedding 去召回这条视频标题写得模糊一点比如“三分钟搞定晚饭”检索效果会立刻崩塌因为“晚饭”对应的视觉特征向量压根没参与计算。我早期也在通用 Embedding 模型上踩过坑。拿开源的 CLIP 或者 Chinese-CLIP 直接套到微信内容场景里形式上行得通但一到真实业务就露馅通用模型对微信生态里高度口语化的表达、小程序封面图、带二维码的营销图、长图拼接的公众号头图语义理解基本等于零。更致命的是通用模型训练时用的是互联网图片-文本对和微信里“用户实际点击的内容”分布差异巨大导致召回的候选集里混入大量低质内容后续排序模型再怎么调也救不回来。所以正确的思路是自己训。不是从零训练一个视觉模型或者语言模型而是基于预训练好的单模态基座在一个大规模、微信生态内的多模态数据集上做对齐和微调。这样既保留了 base model 已有的语言能力和视觉能力又能把两种模态映射到同一语义空间让视频、图文、小程序这些异构内容可以用同一个向量体系去比较和检索。这套方案落地之后能解决的问题非常直接视频号搜索从“文本找视频”升级成“视觉文本联合找视频”朋友圈广告候选集可以用图片向量直接召回不依赖广告主填的关键词小程序推荐可以融合截图视觉信息和页面文本信息做统一向量化公众号文章可以被图像向量命中哪怕标题和正文跟搜索词没有一个字重叠。下面我把从数据处理、模型结构、训练策略到工程部署的完整路径拆开讲全程基于微信业务场景的真实约束来讨论不是实验室里的玩具方案。2. 微信生态内多模态训练数据的采集与清洗2.1 数据源识别哪些内容能构成训练对训练多模态 Embedding 模型第一件事不是选模型而是搞数据。微信生态里天然存在大量隐式的“图文对齐”信号关键看你有没有意识到这些信号的价值。视频号视频封面帧 视频标题/描述。这是最干净也最大量的数据源一个经过用户点击和完播验证的标题通常和视频画面语义高度相关。公众号文章文章头图 文章标题。头图经常是编辑精心挑选的和标题相关性很强但存在一部分头图是纯装饰、纯二维码的情况清洗时要过滤。朋友圈广告广告外层素材图 广告文案。这是商业数据点击率本身就包含质量信号高点击的图文对相当于有了人工标注。小程序页面截图 页面标题。小程序页面截图质量参差不齐需要额外的 OCR 过滤但胜在量大且覆盖很多长尾场景。聊天场景中的表情包表情图 表情名称/描述。这部分数据偏杂但可以用来扩充模型对“夸张、搞笑、情绪化表达”的理解。这里要强调一个原则数据对的质量 数据对的绝对数量。和很多团队一开始追求“几十亿对”不同我在实践中发现把数据量从 10 亿降到 2 亿把清洗规则做严格训练出来的模型在业务评测集上反而提升显著。因为 Embedding 模型本质上是度量学习脏数据里的错误对齐会让模型学到错误的距离关系这种错误是非常难通过增加训练步数来修正的。2.2 清洗链路不能直接拿“标题封面”就去训练如果直接把视频封面和标题扔进模型训练你会得到一堆让模型困惑的样本。微信生态内容有个显著特点夸张封面、标题党、营销图泛滥。封面上一行大字写着“震惊”标题其实是讲养生小知识视觉图和语义信息严重矛盾。模型反复看到这种对齐关系学出来的向量空间必然是扭曲的。我们的清洗链路分五步第一步低质图过滤。用 CLIP 的 image encoder 给图片计算一个“清晰度得分”配合传统的方差/边缘检测把模糊图、纯色图、带大面积水印的图直接筛掉。微信场景里还有大量截图型封面这类图信息密度高但视觉上很像垃圾图需要保留因为公众号头图大量是这个类型。第二步OCR 文字覆盖率检测。对图片做 OCR识别出的文字区域面积超过图片面积 30% 的大概率是文字海报图。这种图不是不能用而是它的语义和文字标题天然重复导致模型学到的视觉分支偏向把“文字识别”当成视觉特征而不是去理解构图、物体、场景。我们对这类图单独建桶处理不让它们污染主训练集。第三步文本侧去重。公众号标题、视频标题里大量存在“标题党模板”比如“不看后悔系列”“30岁以后一定要知道的事”“再忙也要看”。这些文本没有具体语义指向直接作为正样本会让模型把这些泛化短语和任意图片关联起来。对这类样本按模板聚类后降采样保留一部分增强泛化即可。第四步图文相关性粗筛。这里用一个小技巧先用一个现成的 CLIP 模型给图文对打分把得分极低的配对删掉。注意只是筛掉得分极低的那部分比如相似度低于阈值的样本而不是只保留高分样本否则会引入选择偏差训出来的模型只能处理“本来就高度相关”的内容对真实场景中低相关性内容的判别力会变差。第五步用户行为信号加权。清洗完的数据并非同等重要。视频号里完播率超过 60% 的图文对权重可以设为 2被用户主动收藏、转发的对权重设为 3只被曝光但滑走的对权重降到 0.5。这个按行为信号加权的样本采样策略在实践中比单纯堆数据量有效得多。2.3 组批次的艺术不要让训练数据“太简单”训练对比学习模型时batch 内部的负样本分布决定了下限。微信场景里的难点在于很多视频标题和封面看起来很像但其实是完全不同的内容。比如“北京美食探店”这个标题可能对应几百条不同餐厅探店的封面图如果它们恰好出现在同一个 batch 里模型会把它们当成互相的负样本这其实是合理的——因为图文对虽然标题相似但视觉特征应该区分开。但如果整个 batch 都是“标题相同、封面不同”的样本梯度就会在“拉近标题与封面”和“推开标题与不同类型封面”之间剧烈振荡导致 loss 波动大、收敛慢。我建议在组 batch 时做一层哈希约束保证同一个 batch 内相同标题模板的样本数量占比不超过 20%同时尽量让同一视频的不同截帧不要进入同一个 batch这些细节对训练稳定性帮助非常明显。3. 模型结构选型双塔与单塔融合方案的取舍3.1 微信场景的检索需求决定了结构多模态 Embedding 模型的结构选择首先不取决于你对多模态融合算法的偏好而取决于线上系统的工程约束。微信的召回链路无论是搜索、推荐还是广告都对延迟极其敏感。一次用户请求通常是毫秒级就要从千万甚至亿级候选池里捞出几百条候选。这就要求我们必须走双塔或者多塔结构文本塔负责把 query/标题编码成向量视觉塔负责把图片/视频帧编码成另一个向量两者都落到同一向量空间线上用向量检索完成召回。如果采用单塔交叉注意力结构比如把文本和图片 concat 后过一层跨模态 Transformer效果确实更好但推理时要对每个候选图文对单独过一遍模型这个计算量在微信的量级上完全不现实。所以我们的最终结构是文本塔基于一个 6 层左右的 BERT 变体微调自预训练中文 RoBERTa输出 768 维文本向量视觉塔基于 ViT-B/16 或者 Swin-T 结构输出也是 768 维向量投影层各自接一个 MLP把向量从 768 维压到 256 维做 L2 归一化后计算余弦相似度。压缩到 256 维是工程上的一个关键取舍。768 维向量做内积检索单机 QPS 大概只有 256 维的六成不到而 256 维在召回任务的精度损失基本可以控制在 1% 以内。对微信这种体量的场景来说用 5% 的召回精度换 40% 的性能提升完全划算。3.2 双塔的“不对称”设计细节很多人做双塔时犯的错是让两个塔完全对称这可能带来几个问题。我们踩坑之后做了一个不对称设计文本塔更宽hidden size 768视觉塔更窄hidden size 384。原因很简单微信生态里的文本尤其是搜索 query往往非常短且碎片化“烤肠机价格”“附近打印店”这种词语义信息的密度极低需要更宽的文本塔才能捕捉上下文关系和意图。而视觉塔对应的图片语义密度天然较高一个物体、一个场景、一种构图信息都在像素里不需要过宽的隐层就能提取到足够的判别性特征。另外文本塔和视觉塔的初始化方式也做了差异化文本塔完全用中文 RoBERTa 的权重初始化视觉塔则先用 CLIP 的 image encoder 权重做 5 个 epoch 的热启动再进入联合训练。这样做的理由是视觉特征的跨域迁移性比文本更强CLIP 在互联网图片上学到的通用视觉先验可以直接用起来而文本侧中文生态词表差异太大直接用开源权重继续预训练反而更好。3.3 为什么不直接上“三塔”融合微信生态里音频也很重要比如视频号的背景音乐、语音消息的转写文本。于是我们最初设想过三塔结构文本塔、图像塔、音频塔把三者的向量 concat 到一起作为最终表示。但实测下来发现收益极低音频塔的存在反而拉低了整体效果。问题出在模态对齐的复杂度上。图文对的语义关系是直接的而音频和文本、音频和图像之间的相关性在微信场景里非常弱。一条视频的背景音乐可能和内容毫无关联比如剪辑统一配的流行音乐这时候强行拉近音频向量和图像向量等于往模型里注入噪声。最后我们干脆放弃了音频塔只在视频场景用关键帧处理音频相关的特征单独走一个轻量级音频分类模型不参与多模态 Embedding 主链路。所以我的经验是多模态不是模态越多越好而是每一种模态都要有真实、稳定、可学习的对齐信号再去加塔。否则加一个模态的塔等于多一个噪声源。4. 训练策略对比学习的几个关键细节4.1 Loss 函数和困难样本挖掘我们在训练中采用的是 InfoNCE 对比损失这也是 CLIP 系列模型的标配。核心思路是一个 batch 里有 N 个图文对对每个文本把正确配对的图片当正样本把其他 N-1 张图片当负样本计算交叉熵损失反过来对每张图片也做同样的操作。整体 loss 是图文两个方向的平均。但纯 InfoNCE 有个问题batch 内的负样本通常是“简单负样本”模型学一阵子之后对这些样本的区分能力已经很强loss 下降开始变慢。微信业务场景里真正难判别的是那些“看起来很像但实际不同的内容”——比如两个不同品牌的手机评测视频封面都是手机局部特写标题都带“评测”字样这种 pair 模型最容易搞混。因此我们做了两件事第一在 batch 内增加人工构造的难负样本。对每条视频从同类别同一视频号博主的其他视频、相同话题标签的视频里随机抽取 3-5 条作为额外负样本加进 loss 计算中。这样模型被迫在一个更小的语义距离内做判别学出来的向量空间会更加紧凑。第二使用“困难负样本挖掘”的在线版本。每训练几步用当前模型对一小批候选样本做向量检索把相似度高但确实不是一对的样本挑出来作为难负样本加入下一轮的训练数据。这个策略一开始我们不敢用因为容易导致模型坍缩——在训练初期模型还比较弱选出来的“难负样本”可能本身就是对的配对。后来加了保护机制只在训练进行到 60% 之后开启难负样本挖掘并且选出的负样本加入后还需要人工抽检确认确实是不相关的内容才允许进入训练集。4.2 温度系数和 FLOPS 约束温度系数是对比学习里最敏感的超参数之一。温度设得太大模型对所有负样本一视同仁学不到细微差别温度设得太小模型只关注最难的那一个负样本训练不稳定loss 震荡剧烈。我们最终的设定是 0.05这个值在很多对比学习工作中都被验证过效果不错。训练初期可以先用 0.1 跑 2 个 epoch 进行预热让模型先收敛到一个合理的区域再调低温度做精细化对齐这个改动让最终评测指标提升了大约 2 个点。还有一个小技巧是 FLOPS 约束正则项。这个不是微信场景特有的但对我们的内容生态特别有用在计算 loss 时同时计算一个“负样本相似度均值”的正则项惩罚那些把全局所有向量都拉近的行为。因为公众号和视频号内容存在明显的头部效应爆款内容和长尾内容的特征分布差异巨大如果不加约束模型倾向于把所有向量都映射到一个很小的区域内反正 loss 也能降导致召回的多样性极差。加了这个正则之后至少保证头部内容和长尾内容在向量空间里能被分开。4.3 数据并行和梯度同步的几个坑双塔模型的训练和单模型训练不一样。两个塔各有独立的参数但 loss 是两者联合计算的这导致反向传播的时候两个塔的梯度其实是相互依赖的。如果用两个 GPU 分别放两个塔做梯度同步时必须保证同步的是“联合 loss 对各自参数的梯度”而不是各自 loss 的梯度。这个细节如果搞错训练出来的模型表现为“单塔效果正常但图文检索完全不可用”。我们最终采用的方案是单卡放双塔通过数据并行做多卡扩展。具体来说每张卡上都有一个完整的双塔副本各自处理一批数据前向传播结束后把所有卡上的特征向量 gather 到一起组成一个大 batch再统一计算 loss 和梯度。这样虽然显存开销更大每张卡都要存两份模型但逻辑简单训练稳定。实际用 8 张 A100 训练单卡 batch size 设为 512gather 之后等效 batch size 达到 4096效果已经足够好。这个环节我还想多说一句训练框架层面的问题往往比模型结构的问题更难排查。我们的模型在一次升级版本后文本塔和视觉塔的 BN 层行为异常排查了很久才发现是框架在数据并行时对 BN 的 sync 设置不对导致视觉塔的每卡 BN 统计量没有同步。如果你的训练过程中出现“loss 降了但评测不涨”的诡异情况优先检查归一化层的跨卡同步逻辑。5. 基于微信业务的评测体系设计5.1 通用的图文检索指标为什么“不够用”很多团队训完多模态 Embedding 模型随手用 Recall10、Recall100 这些通用指标评估一轮就上线了这个做法在微信场景里是不够严谨的。通用图文检索指标考察的是“给定一个 query 文本是否能从候选图集里召回正确图”但微信场景里的实际业务需求往往是反向的“给定一个候选内容图/视频/小程序是否能在用户搜索词中命中”以及更复杂的“给定一个用户的浏览行为序列能否召回相似内容用于推荐”。我们建了三个维度的评测集每个维度对应一条业务线第一搜索维度。从真实用户搜索日志中抽样 query用模型召回视频号和公众号的内容由标注员判断前 10 条结果是否相关。这块的评估指标用的是 Recall10 加人工相关性打分核心观察点是长尾 query 的召回质量。第二推荐维度。从视频号的推荐系统中抽取曝光但未点击、和曝光且点击的样本对用模型计算向量相似度看“点击正样本对”的相似度是否显著高于未点击负样本对。这里用 AUC 作为核心指标更贴近推荐的排序目标。第三去重维度。微信生态里大量存在“重复内容”同一个视频被不同博主剪辑重复发布、同一篇文章被多个公众号转载但改了头图。我们的模型一开始在这块表现很差因为它会把内容完全不同的向量映射得很接近。后来在训练数据里专门加了“重复内容对”作为额外负样本才逐步改善。这个维度的评测方式是抽一批已知重复内容对看模型给的相似度分数是否够高从而能否触发去重规则。5.2 人工评测集的构建与迭代闭环如果完全依赖自动指标很容易被“数据泄漏”欺骗。我们遇到过的情况是训练数据里有大量公众号文章标题和头图而评测数据也来自公众号导致模型在评测集上 Recall10 高达 90%但上线后真实搜索效果一塌糊涂。排查后发现模型根本没有学到图文语义的对齐而是死记硬背了“哪些标题对应哪些文章”这种 ID 级别的映射训练集里出现过的标题一旦换个图模型就认不出来了。解决方法是建一个“时间隔离评测集”。用每天新增的数据做评测训练数据只用过去 7 天前的内容保证评测集里的图文对从来没在训练时出现过。这样虽然评测难度更大、指标数字更难看好但更接近真实的线上分布。这个评测集不是一成不变的。我们每两周人工审一批模型犯错的 case把典型错误归纳成规则反馈到数据清洗和训练策略里。比如有一轮我们发现模型对“带超大文字的封面图”特别容易误判把几个完全不相关的标题都召回出来就是因为 OCR 过滤的阈值设得太狠导致模型没见过这种图理解不了图和文字的关系。后来把 OCR 过滤策略改成“文字面积大于 30% 但小于 50% 的图保留进训练集”模型的表现立刻得到提升。5.3 向量相似度的业务校准模型训完之后不能直接把余弦相似度得分当质量分用。微信场景里不同业务对“相似度得分”的校准需求完全不同搜索场景要求严格只有真正的强相关才能进前 10推荐场景要求宽松相关即可不要求强相关。同一个阈值不可能同时满足两条业务线。我们的做法是训练完之后额外用一个小型的温度缩放层针对不同业务线单独校准。具体实现是收集该业务线上的人工标注数据学习一个标量温度参数把向量相似度得分映射到 0-1 区间的“业务相关概率”。这一步成本极低只需要拟合一个参数但对业务效果提升非常明显。6. 向量检索上线与持续迭代的实战经验6.1 亿级向量索引选型从 FAISS 到 Milvus模型训练出来只是第一步真正难的是把它上线到召回链路里。微信生态的候选池是亿级甚至十亿级的要让模型产出的 256 维向量在这个规模下做毫秒级近邻检索工程上需要仔细打磨。早期我们直接用 FAISS 建 IVF-PQ 索引。IVF 负责把向量空间划分为若干个 cellPQ 负责把向量压缩成低比特表示。这套方案的优势是纯 CPU 内存就能跑部署简单但在亿级规模下的召回质量损耗比较明显。特别是在微信内容生态里长尾内容多PQ 压缩会显著降低长尾向量的区分度导致长尾内容召回率偏低。后来迁移到 Milvus使用 IVF_SQ8 和 HNSW 混合索引。策略是这样头部高频内容的向量大概是总量的 5%用 HNSW 索引保证召回精度长尾内容默认用 IVF_SQ8牺牲一点精度换取内存效率。这样既保证了推荐头部内容的效果也不至于让内存爆掉。这套混合索引跑下来的体感是单机 8 核 64G 内存可以承载 5000 万级别的向量单次查询延迟在 3-5ms召回质量相比纯 IVF-PQ 提升了大约 3 个百分点。6.2 向量版本管理与回滚机制多模态 Embedding 模型的迭代和常规模型的迭代有个很大区别向量一旦算完并导入索引如果模型要换版本是没法原地更新的。必须用新模型重新算一遍所有候选内容的向量再重新建索引。在微信的场景下全量重算一次亿级内容的向量大概需要 2000 卡时的算力和十几个小时的在线任务。我们为此建立了一套“双通道发布”机制。新模型训练完毕先在离线把全量向量算好、建好索引同时保持旧模型产出的索引在线服务。新索引在灰度环境先跑一周实时对比新旧两个索引的线上召回指标和业务指标。只有确认新索引在各项指标上都不低于旧版才切换线上流量实现平滑过渡。这个环节最常见的坑是“向量空间漂移”。新模型输出的向量分布和旧模型不一定一致就算同一条内容新旧模型的向量也存在偏差。直接对比新旧索引的召回结果意义不大因为分数体系都变了。我们的方法是保留一份固定的“锚定测试集”每次模型升级都用这份测试集重新计算新模型的向量和旧模型向量做对比确保新模型没有在特定子集上出现明显的定向偏移。6.3 持续迭代的数据回收机制最后说一下模型上线之后怎么持续迭代。很多团队训完模型就完事了后续只是在固定数据集上调参这其实是把训练好的模型白白浪费掉。更好的方式是建立“模型-业务-数据”的闭环从线上每一条用户反馈点击、完播、搜索无结果、举报中回收信号重新加工成训练样本进入下一轮模型的训练数据。微信生态里最有价值的数据回收信号是“搜索无结果/低点击”。用户搜索一个词召回出来的视频被大量滑走、不点击说明这一条内容虽然是检索命中了但语义上并不被用户认可。这类负反馈数据收集到一定量级加入训练集后模型对于这类模糊语义的辨识力会大幅提升。实测下来每引入 500 万级别的真实负反馈样本下一版模型的 R10 大约能提升 1.5-2 个百分点比单纯在原有数据集上加大训练步数划算得多。还有一个小技巧给训练数据打上“数据来源时间戳”。因为微信生态的内容偏好变化很快比如某个梗突然火了如果不加时间戳旧数据会持续干扰新模型的训练导致模型对“当前内容热点”的反应滞后。我们在训练时按时间衰减采样近 7 天的数据权重是 1.030 天前的数据权重衰减到 0.390 天前的数据直接不再进入训练集。这个改动让模型对热点内容的召回表现明显提升。7. 几个我踩过的坑和最终建议这套系统从最早的原型验证到稳定线上运行花了大半年时间中间踩过的坑比上面写到的还要多。挑几个对别人最有参考价值的再说一下。第一个坑是要不要自己训多模态模型。如果你在微信生态里做内容理解至少不要让通用模型直接对接业务用一个百万级的高质量业务图文对做微调成本不高但效果提升显著。如果连微调的算力都没有那至少要保证训练数据的质量比如用行为信号筛选出的高置信度样本而不是随便拿互联网开源的图文对凑数。第二个坑是千万不要把“模型效果提升”直接等同于“业务指标提升”。向量召回只是整个链路的开头后面的粗排、精排、重排同样重要。我们有一版模型在离线评测上 R10 提升了 5 个点看起来非常亮眼结果线上推荐业务指标基本没动因为召回的候选里真正能贡献 PV 的头部内容早就被旧模型稳定召回新增的 5% 长尾召回对线上大盘影响微乎其微。后来我们把评测目标改成“新增有效召回量”被下游排序接受且曝光点击的候选模型的迭代方向才终于对齐业务目标。第三个坑是团队协作问题。多模态 Embedding 模型不是纯算法团队能独立搞定的它需要数据团队提供用户反馈日志、搜索/推荐工程团队提供检索链路和 embedding 上线能力、评测团队提供标注平台和质量评估紧密配合。如果组织架构上这三拨人离得太远项目很容易在数据口径和线上接入上反复扯皮这个问题比任何技术难点都更拖慢节奏。最后说点方法论层面的建议。多模态 Embedding 模型本质上是在做“语义对齐”但微信生态的语义空间不是静态的它会随着内容供给、用户偏好和产品功能的变化持续移动。所以这个系统真正要建的不是一个模型而是一套能够持续吸收业务反馈、不断调整对齐方向的机制。模型结构、训练策略、工程架构都只是这套机制的执行器真正驱动整个系统迭代的是你对微信内容生态里“什么内容相似什么内容互斥”这个问题一版比一版更精准的理解。我自己的体会是多模态 Embedding 的建模难度不在某个单点技术上的突破而在于把所有环节——数据感知、清洗策略、模型训练、评测校准、检索部署——捏成一个不断螺旋上升的闭环。这个闭环一旦转起来模型的能力会随着每次数据回收和业务反馈变得越来越强这才是训练一个真正可用的多模态 Embedding 模型最核心的秘密。