说句实话,多模态检索这个方向我接手过几次之后,一直有个很头痛的问题:图片和文本各用一个向量模型,给用户出不了稳定结果。直到最近接触了 Qwen3-VL-Embedding & Qwen3-VL-Reranker 这套组合,才真正把图片、视频、文本统一到一个检索链路里。简单说,前者负责把不同模态的内容映射到同一个向量空间,后者负责对召回候选做精细打分。这篇文章就围绕这两个模型,拆一下原理、接入步骤、效果评测和一些我实际踩过去的坑,适合正在做多模态 RAG、图文搜索或视频库检索的工程师参考。
1. 统一表征为什么难:先看懂多模态检索的两段式架构
1.1 从“各管各的embedding”到一个向量空间
早期我做多模态检索,最常见的做法是:图片用某个视觉模型抽向量,文本用另外一个语言模型抽向量,视频再单独抽关键帧。等用户真正来搜的时候,query 是文本,图片库是图像向量,两边根本不互通。为了让他能搜到东西,我只能先做一层“翻译”,比如把文本 query 转成一组标签去匹配图片标签,或者反过来把图片转成文字描述再搜文本。这种方案的误差是叠加的,图像理解错一点,后面的检索就跟着错。
Qwen3-VL-Embedding 的核心价值,就是让文本 query 和图像/视频片段落在同一个特征空间里。它底层继承了一个很强的视觉语言模型,输入既有文本也有图像/视频,输出都是结构一致的稠密向量。你可以类比成以前出国要带好几张货币卡,刷卡之前先折算,现在直接用一张全币种卡,商家直接按一个汇率体系结算。对于检索系统来说,这就少掉了一整层“模态转换”的误差。
1.2 双塔召回与交叉编码精排,各管一段
统一向量空间解决了“去哪找”的问题,但不代表排序也能一步到位。向量召回本质上是双塔结构,query 和 document 各自编码为向量,再算相似度。这个结构适合海量候选的粗筛,响应快,但信息交互不足:两个向量只在一个点碰面,query 里的重点词,文档里的某些细节,没法充分做互相“看见”的匹配。
所以成熟检索系统通常会把召回和精排拆成两段。第一段用 Embedding 模型从全量库里拿回几百个候选;第二段用 Reranker 对每个候选和 query 做交叉编码,给一个更准确的排序分数。Qwen3-VL-Reranker 就是为第二段设计的多模态排序模型,它允许 query 和候选内容进行深度交互,而不是只靠一个 vector 距离。
打个比方:Embedding 像图书馆先按索书号把可能相关的 200 本书筛出来;Reranker 再一本一本翻目录和关键章节,判断哪本最切题。少了第一步,效率扛不住;少了第二步,精度上不去。这两个模型组合起来,恰好覆盖了完整的两段式检索链路。
1.3 为什么选择这套组合
市面上不是没有多模态向量模型,但很多还是偏单模态微调,视频就抽帧喂给图像模型,或者干脆把音频转写成文本再走文本检索。Qwen3-VL-Embedding 和 Qwen3-VL-Reranker 属于同一个模型族,Embedding 侧和排序侧训练时使用的是同一套视觉语言底座,query 和 document 里同时出现图片和文字时,它们的输入处理方式是一致的。
另一个让我选择它的原因是可控性。这套模型有不同参数量版本,不只是给你一个 7B 级别的“全家桶”,还有小体量版本可以跑在普通单卡甚至 CPU 推理场景。对做工程的人来说,“能用”和“能用得舒服”是两回事,后面我会专门聊选型。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Qwen3-VL-Embedding:图像、视频、文本怎么变成同一个向量
2.1 输入侧:把多模态内容拆成视觉语言模型能理解的 token
Qwen3-VL-Embedding 的输入形式比传统视觉模型要宽松得多。文本输入走标准分词器,图片输入会被编码器切成视觉 patch,视频输入会被抽帧并按时间顺序组织成序列。关键是,这些 token 最终都进入同一个 transformer 层叠起来,而不是像旧方案那样分成 image tower 和 text tower 两套硬拼。所以模型天然可以在一个句子里同时描述“图片里的红色书包”,也能把图片和这段文字放在同一个向量里参与匹配。
我自己在接入时最明显的感受是:一段视频,我可以传一个关键帧 thumbnail 加对应字幕文本,模型能综合理解画面和文字;反过来,如果用户上传的是一张商品图,query 本身是图片而不是文字,Embedding 也能直接产出图像侧的向量。这点对于“以图搜图”和“混合图文检索”都很关键。
2.2 向量输出逻辑:把“理解”压成一个稠密向量
模型经过多层处理后,输出的 hidden state 需要被池化成一个固定维度的向量。最常见的做法是取序列末尾的 EOS token 对应的 hidden state,或者对 token 表示做平均池化。Qwen3-VL-Embedding 提供的核心接口就是走这条路径:输入多模态序列,输出单个归一化过的向量。具体是取 EOS 还是 mean pooling,不同版本会有微调,保险的做法是从模型的配置文件里确认一下,不要凭经验拍脑袋。
这个向量的维度取决于模型配置。轻量版通常输出 1024 或 2048 维,大号版本可能更高。维度越高,表达能力越强,但存储和计算成本也线性上升。我一般建议先看自己的库规模:百万级以内用 1024/2048 维足够,没必要为了“大就是好”盲目上更高维度。
在推理时需要留意数值精度。向量做大规模相似度计算时,如果训练时用了 bfloat16,检索阶段最好保持一致的精读,否则可能出现精度漂移。实际项目里我习惯用 torch_dtype=bfloat16 加载,效果比较稳。
示例加载与生成向量的最小代码可以这样写:
python复制import torch
from transformers import AutoModel, AutoProcessor
model_id = "Qwen/Qwen3-VL-Embedding-0.6B"
processor = AutoProcessor.from_pretrained(model_id)
model = AutoModel.from_pretrained(model_id, torch_dtype=torch.bfloat16).eval()
text_query = "一个穿红色外套的人站在雪地里"
inputs = processor(text=text_query, return_tensors="pt")
with torch.no_grad():
embeddings = model(**inputs)["sentence_embedding"] # 具体字段名以仓库为准
embeddings = torch.nn.functional.normalize(embeddings, p=2, dim=-1)
如果 query 是图片,只需要把 images 参数传给 processor。视频输入会更复杂一点,一般建议先离线抽好帧,再把帧序列作为图像输入传进去。代码里 model(**inputs) 的输出字段可能因为版本不同而变化,比如有的版本直接返回 hidden state,有的封装好了 sentence_embedding,接入前看一眼 model 仓库说明即可。
2.3 建索引:向量化之后还要存和查
向量模型只负责生成向量,真正支撑检索的还需要一个向量库。我一般用 FAISS 或轻量级 ann 索引来建库。流程不复杂:
- 把库里的每篇文档/每张图/每个视频片段做切分;
- 分别调用 Embedding 生成向量;
- 统一做 L2 归一化;
- 写入索引;
- 检索时用同一个 Embedding 给用户 query 生成向量,再近似近邻召回。
这里有个容易忽略的细节:如果同一篇文档里既有文本又有图片,最好把它们拼成一个输入序列,生成一个向量,而不是分别生成两个再平均。因为模型在输入侧就看到图文对应关系,这样的语义是完整的。单独编码文本和图片再平均,相当于先丢掉关联,再试图用算术补回来,效果很难赶上联合编码。
3. Qwen3-VL-Reranker:比向量相似度更可靠的打分器
3.1 双塔 vs 交叉编码:为什么精排需要让 query 和文档正面相遇
向量召回阶段,query 和 document 各自过一遍 Encoder,交互只发生在最后两个向量的相似度计算里。这种设计是出于效率必须的,但代价是匹配粒度很粗。比如 query 是“白色帆布鞋 男生”,一个候选文档描述是“男士休闲帆布鞋,颜色为白色”,向量相似度可能不低;但如果文档里说的是“白色板鞋,男女同款,具体尺码见详情”,它没有“帆布”这个词,只有“板鞋”这个相关概念,普通双塔模型很难判断那是正例还是负例。
Reranker 采用交叉编码,把 query 和文档拼接成一个输入序列,让模型在内部逐 token 做注意力交互。这样一来,query 里的“帆布”可以跟文档里的“板鞋”“休闲鞋”产生深层匹配。它不需要把 query 和文档各自压成独立向量,而是输出一个交互后的相关性分数。
对多模态场景来说,这一点尤其重要。文档里如果有一张图,query 是一段文字,交叉编码器能看到图片 patch 和文字 token 之间的注意力关系;双塔模型则根本没这个机会。这也是为什么我建议多模态检索里 Reranker 不要省。
3.2 多模态候选怎么“拼接”给 Reranker
Qwen3-VL-Reranker 输入形式和 Embedding 模型很像,同样是接受多模态序列,但输出从“向量”换成了“相关性分数”。基本信息处理方式如下:
- 文本 query + 文本候选:正常拼接;
- 文本 query + 图片候选:图片编码为图像 token,和文本 token 拼接后进入排序模型;
- 图片 query + 视频候选:视频先抽帧,再作为多帧图像输入;
- 图文混合 query + 图文混合候选:把两边都各自编码,并在一起做排序。
实操时要注意候选文档不能太长。Reranker 相比双塔模型更慢,而且输入长度直接决定显存。如果候选文档是长文档,我一般会先按段落或窗口切分,分批精排之后再做分数聚合。视频候选也类似,先切出若干关键片段,再逐片段打分。
3.3 最小代码与分数解读
Reranker 的调用方式和 Embedding 没有本质区别,只是最后取的不是向量而是 logits / score。下面是一段最小示例:
python复制import torch
from transformers import AutoModel, AutoProcessor
ranker_id = "Qwen/Qwen3-VL-Reranker-0.6B"
processor = AutoProcessor.from_pretrained(ranker_id)
ranker = AutoModel.from_pretrained(ranker_id, torch_dtype=torch.bfloat16).eval()
query = "红色户外防风外套"
candidate_text = "一件红色冲锋衣,面料防风防水,适合户外徒步"
candidate_image = "path/to/outerwear.jpg"
inputs = processor(
text=query,
text_pair=candidate_text,
images=candidate_image,
return_tensors="pt",
)
with torch.no_grad():
score = ranker(**inputs).logits.squeeze().item()
输出的 score 通常是排序相关分数,范围不一定天然落在 0~1 之间。如果模型训练时用了对比学习或者 pointwise loss,可能需要过一层 sigmoid 才能当作概率用。如果模型直接输出 logits,那么全量候选只需要按分数从高到低排,不需要纠结绝对数值。只有当你希望设置统一阈值过滤无关结果时,才需要对分数分布做观察和校准。
3.4 组合成两段式管道
把 Embedding 和 Reranker 接在一起,管道大致是:
- 用户输入 query,可能是文本,也可能是图片;
- Embedding 模型生成 query 向量;
- 向量检索引擎召回 TopK 候选,我一般设 100~500;
- 将 query 和每个候选组装成 Reranker 输入;
- Reranker 打分排序,取 TopN 返回给用户。
这套思路和经典文本 RAG 一致,区别只是第一步和第四步换成了多模态模型。候选数量不要设得太大,否则 Reranker 的耗时会被成倍放大;也不要太小,否则召回阶段漏掉的相关内容没有补救机会。具体候选量要看你的库规模和时延预算,后面我会给一个参考值。
4. 把一个多模态 RAG 查全、查准:检索管道设计实战
4.1 场景定义:商品图 + 文字描述混合库
我拿一个模拟项目举例:库里有商品主图、商品详情文本、短视频展示片段,以及客服对话历史。用户提问方式五花八门,有人直接发一张截图找人找同款,有人用文字描述“上次那件蓝色立领外套”,有人甚至发一段小视频,希望找到视频里同类产品的购买页。
这种场景如果用传统文本检索,图片里的款式信息会全部丢失;如果用纯图片去匹配,文字里的尺码、面料、价格等结构化信息又用不上。用 Qwen3-VL-Embedding 统一建模后,query 和文档的“模态差”被拉平,不管是文字、图片还是混合输入,都能和库里任意模态的文档做相似度计算,再通过 Reranker 做最终排序。
4.2 数据预处理:切分文档、抽视频帧、生成描述
建库之前要做的预处理,比想象中的琐碎。我总结了几个必须做的点:
- 纯文本段落按语义切分,每段 200~500 字为宜,不要一整篇详情页直接丢进去,召回粒度太粗;
- 商品图片每张作为独立文档,如果图片包含多角度,可以每组角度生成一条 entry,而不是把所有图片塞进同一个向量;
- 视频先按场景或镜头切分,每个片段取 1~3 个关键帧,作为视频 entry;
- 长视频最好附带字幕或语音转写文本,与帧序列组合输入,这样 Reranker 能同时利用视觉和语言信息;
- 图片入库前统一处理分辨率,减少超大图带来的 token 开销。
这里的核心原则是:一个向量代表一个“可检索的最小语义单元”。大而无当的向量往往什么都想包含,结果什么都不像。宁可多切几条,也不要把整库压缩成几个长向量。
4.3 候选集数量和延迟预算
两段式管道需要平衡召回数量、延迟和精度。以下是我在普通单卡 A 级算力下用 0.6B 版模型做的参考配置:
| 库规模 | 召回候选数 | Reranker 精排数 | 单次检索延迟预期 |
|---|---|---|---|
| 10 万级 | 100 | 30 | 100~300ms |
| 100 万级 | 200 | 50 | 300~800ms |
| 千万级以上 | 500 | 100 | 1~2s |
这个延迟预期包含向量召回和 Reranker 排序,使用的是小体量模型。如果换大模型,Reranker 部分的耗时可能翻倍。正式上线前用真实业务分布压测最重要,不要盲信类似的参考值。候选数从 100 涨到 200,对 Reranker 意味着打分次数翻倍,这个增长是很直接的。
另外,对于“必须要高召回”的场景,比如知识库、档案检索,候选数可以适当调大;对于实时搜索场景,比如商品搜索、客服推荐,候选数宁可少一点,先保证返回速度。
4.4 效果对比:统一模型比手工堆模型强在哪
在同一批测试数据集上,我把三套方案跑了一遍:纯文本向量 + 图片标签匹配;图像向量 + 文本向量分开检索再合并;Qwen3-VL-Embedding + Qwen3-VL-Reranker 的两段式方案。结果在意料之中:混合模态 query 的召回率有显著提升,尤其是“图片 query + 视频候选”“文字 query + 图片候选”这两类,原先经常排在二十名开外的正确结果,现在能被排进前五。
这种改进不来自某个模型的“魔法”,而是来自架构上的修正:同一个特征空间,加上交叉编码精排。拿搜索场景类比,相当于以前是两个字段分开查,现在是把所有字段和附件内容放在一张全文索引里,并且拥有一套能理解图片语义的排序规则。对上线效果来说,用户明显感受到“发图也能搜到正确的东西”了。
5. 踩坑与调优:我在接入过程中遇到的问题
5.1 视频帧太多:显存和时间一起爆掉
第一次尝试把整段视频逐帧喂给模型时,我直接把训练卡搞到 OOM。视频不是静态图片,一个 10 秒的 30fps 视频就有 300 帧,全部作为输入 token 序列,计算量和显存是线性膨胀的。
后面改成“场景切分 + 关键帧采样”:先用镜头检测把视频切成几秒到十几秒的片段,再从每个片段中选 1~3 个最有代表性的帧。这样既保留了视频的时序信息,又把输入规模压到可控范围。如果视频镜头切换很快,比如运动类短视频,我还会额外增加采样密度;如果是讲解类视频,字幕文本权重大,帧数可以适当减少。
5.2 图片分辨率对向量质量的影响
另一个坑是直接对所有图片做统一缩放到 224×224 或 448×448。很多多模态模型训练时会做动态分辨率,强行把长图压扁会损失细节,尤其是小物体、文字水印、服装纹理这种信息。正确做法是保持长宽比,不足的补边或用模型的 processor 自动处理。
我一度为了保证批处理速度,把所有图强制 resize 到固定尺寸,结果检索“带 logo 的帆布包”时,正确结果全部排在后面。改成保留原始比例后再试,效果立刻恢复正常。如果你在接任何视觉模型,先检查 processor 是否内置动态分辨率处理,别在 DataLoader 里自己做 resize。
5.3 相似度分数分布偏移与 Reranker 分数校准
不同模态来源的向量,即使来自同一模型,分数分布也可能有明显差异。纯文本的相似度普遍偏集中,图像+文本的相似度可能动态范围更大。如果你直接用一个绝对阈值过滤,文本这边可能会误杀,图像那边可能会漏放。
Reranker 的分数也存在类似问题。为了避免这种偏移,我对“每个候选类型单独看分位数”,不做跨类型统一阈值。比如所有视频候选按 Reranker 分数排序后,取前 20 名;所有图片候选也各自取前 20 名,最后再合并去重。这样保证不同模态都有机会进入最终结果,而不是让某个高分模态霸屏。
5.4 后处理:合并重复结果
视频片段经常出现相邻几个片段同时返回同一内容,比如 00:01-00:03 和 00:03-00:05 两段其实都在讲同一件商品。直接展示给用户会造成重复信息,也浪费精排名额。
我的做法是在精排之后做一次时间戳维度的合并:如果候选的视频时间区间有重叠,就按最高分保留其中一个,并把相关区间标记为“覆盖”。文本候选也要做相似文章去重,用 SimHash 或者向量相似度阈值都行。这个后处理看起来不起眼,但对用户体验的提升非常直接。
6. 效果评测和选型建议:不是越大越好
6.1 评测集怎么搭:query-正例-负例三元组
多模态模型上线之前,我习惯搭一个“一套 query 对应多个正例和负例”的评测集。正例来自真实业务中确认正确的文档/图片/视频片段;负例要从检索结果里挖,不能偷懒随机采样,否则模型很容易在评测集上表现虚高。
困难负例尤其重要。比如用户搜“黑色双肩包”,库里一个“灰色双肩包”和一个“黑色单肩包”,后者才算困难负例,它和 query 有很强的字面或视觉重叠,但语义上不对。评测集里如果没有这类样本,Recall@k 和 NDCG 很难反映真实效果。
6.2 定量指标怎么读
我用三个指标比较多模态模型:
- Recall@k:正确结果是否出现在前 k 个召回结果里,反映“召回能否找得到”;
- MRR:第一个正确结果的排名倒数,反映“用户最早看到正确答案的位置”;
- NDCG@10:前 10 个结果的排序质量,反映“不只看有没有,还要看是否排前面”。
只刷 Recall@k 是不够的。如果一个检索结果永远把正确答案排在一百名之后,Reranker 就算把它拉到第 20 名,对用户来说还是不可用。所以我在项目里更看重 NDCG@10,它和用户感知的相关性更贴近。
6.3 0.6B / 4B / 8B 怎么选
同一个模型族不同参数量,直接决定部署成本和效果上限。我的经验是:
| 版本 | 适用场景 | 显存需求 | 推理速度 |
|---|---|---|---|
| 0.6B | 粗召回、实时搜索、CPU 兜底 | 2~4GB | 快 |
| 4B | 精排、常规 RAG、中等规模库 | 8~16GB | 中等 |
| 8B | 高质量排序、复杂图文关系 | 16GB+ | 慢 |
搜索场景里,我把 Embedding 用小体量版本跑召回,Reranker 用稍大的版本跑精排,这样成本和效果比较均衡。如果你对向量质量要求极高,且库规模不大,Embedding 也可以换大模型,毕竟建库是一次性成本,检索时只对 query 做一次编码,成本可控。Reranker 因为是逐候选打分,大模型的成本就非常明显了,必须谨慎。
6.4 Reranker 何时可以省略
每条业务都有自己的成本边界。如果候选集很小,比如业务限定在单机知识库,总共就几百条文档,向量召回后的结果相关性已经足够,Reranker 可以省掉,降低维护成本。如果实时性要求极高,比如搜索框每敲一个字就出结果,对多模态候选做逐条精排会明显拖慢响应,这时也不适合上重排序。
能接受部分精度损失时,还可以考虑“先少量召回 + 手动规则过滤”的轻量方案。把 Reranker 只用在最核心的流量场景,比如用户二次点击、高频搜索词、最终下单前的商品校验,而不是每一条低价值请求都做完整精排。这个取舍没有标准答案,完全取决于你的业务预算。
最后再分享一个我觉得特别实用的经验:永远把 Embedding 和 Reranker 当成一个组合来测试,而不是分开单独优化。只换 Embedding 不换 Reranker,或者只调 Reranker 不重新审视召回,效果都会卡在一个瓶颈上。真正有效的方式是先在离线评测集上让召回和排序一起跑,找到哪个环节贡献最大,再集中调整那个环节。多模态检索不是一道加法题,而是整个链路互相咬合的系统工程。
