VLM驱动的搜索相关性度量:从文本匹配到跨模态理解
搜索相关性度量是搜索引擎里最容易被低估的环节。用户输入一个查询词,系统返回十条结果,看起来只是一次排序,但在排序背后,是检索团队对“什么才算相关”的持续定义与反复校准。过去十几年,这项工作的主力是文本语义模型,用 query 与 document 的向量表示判断匹配度。但今天的 Web 页面里,越来越多的信息密度集中在图片、视频、图表、商品图和多媒体元素上,纯文本相关性模型的瓶颈已经越来越明显。
视觉语言模型(VLM)正是在这个背景下进入搜索相关性度量的视野。它不只是在做“看图说话”,而是能够在同一套模型参数里同时建模文本和视觉信息,让相关性判断从“文本匹配”升级为“跨模态理解”。
这篇文章想给出一个明确判断:VLM 在 Web 规模搜索相关性度量中的价值,不是替代 BM25 或双塔召回,而是把相关性判断的边界从纯文本扩展到了视觉内容。真正的难点不在模型效果,而在工程成本、评测稳定性、以及线上线下的节奏控制。文章会从问题拆解、架构设计、示例代码、评估方法到工程踩坑逐层展开,适合正在做搜索、内容平台推荐或多模态检索的工程师阅读。
1. 搜索相关性度量的本质问题是什么
在搜索引擎和推荐系统里,相关性度量负责回答一个朴素的问题:给定一个查询,这条结果到底相不相关?这个判断结果会被用于排序、过滤、多样性控制、也包括搜索质量评测和离线评估。
传统做法是把它建模成一个“文本到文本”的相似度问题。query 是一个文本序列,document 的标题、正文、摘要也被切词后映射成向量,然后通过 BM25、TF-IDF 或者深度语义匹配模型算出分数。这套体系在过去二十年里非常有效,尤其对纯文本页面、新闻文章和问答型查询,效果足够稳定。
但问题在于:Web 页面并不是纯文本对象。一条商品结果里,主图、细节图、价格标签、评论图片可能比商品描述承载了更多决策信息;一条旅游攻略里,用户真正被吸引的往往是头图而不是几百字的景点介绍;一个视频搜索结果,没有视觉信号的情况下只能在 metadata 上猜相关性。如果相关性模型只能读文本,它就不能理解“查询里提到的某个物品长什么样”,也没法区分“页面里那张配图是真的相关图片,还是单纯的装饰图”。
这在实际项目中会带来几个很直观的麻烦:
- 纯视觉查询没法处理。用户上传一张商品图、一件衣服、一个地标,想找相似内容,文本侧根本拿不到 query 向量。
- 文本泛化能力弱。query 用的是口语化描述,document 的视觉主体才是真实对象,两者在字面上差异巨大,文本模型匹配困难。
- 评估和标注不一致。人工标注相关性时,人是能看到缩略图和排版布局的,但离线评估的模型只能看文本。这会导致标注标准与模型学到的标准出现系统性偏差。
从这个角度看,引入 VLM 并不是“赶多模态的热度”,而是要补上相关性度量中一直被忽略的视觉信息通道。
2. 视觉语言模型能为核心流程带来什么
简单说,VLM 是一类同时接受图像和文本输入、输出文本或表征的模型。它可以在一个模型结构里完成“看图 + 读字 + 跨模态推理”的组合任务。比如输入一张图片和一句文字,它可以回答图片里有没有这只猫、描述图片场景、或者判断这句话与图片内容是否相符。
在相关性度量场景里,我们关心的正是“相符”这件事。传统文本相关性模型判断的是 query 文本与 document 文本是否匹配,VLM 判断的是 query 文本与 document 视觉内容、或者 image query 与 document 文本之间的语义一致性。
用一句话概括 VLM 的相对性度量能力:它把搜索相关性从“文本信号是否存在”扩展到了“视觉内容是否语义对应”。
但这里有一个容易误解的地方:VLM 不是简单的“OCR + 文本匹配”的叠加。OCR 只能把图片里的文字抽出来,仍然只是文本侧的信息;VLM 则对视觉对象本身进行语义理解。例如一张没有文字的商品图,OCR 毫无作用,但 VLM 可以直接判断它是不是一双“防水越野跑鞋”,并且与 query 做跨模态匹配。
从实际应用经验来看,VLM 在相关性度量中最有价值的几个场景包括:
| 场景 | 传统文本模型表现 | VLM 作用 |
|---|---|---|
| 图片搜索 / 以图搜图 | 无法处理图片 query | 直接编码输入图片,做跨模态匹配 |
| 商品搜索 | 依赖标题和属性文本,噪音大 | 理解主图商品主体,过滤图文不一致 |
| 视频/短视频搜索 | 只能看标题、描述和标签 | 通过关键帧理解画面内容 |
| 新闻/图文页 | 正文主导,配图信息被浪费 | 结合配图判断图文是否与查询一致 |
| 搜索评测/标注 | 标注员看到图片,模型看不到 | 使模型与人工标注口径对齐 |
这些场景有一个共同点:视觉内容不是可选的附属信息,而是相关性判断的关键依据。越早意识到这一点,越容易判断 VLM 在自己的业务里应该放在哪个环节。
3. 从文本到跨模态:相关性判断链路的变化
把 VLM 引入相关性度量,不是换一个打分模型那么简单,这会改变整个相关性判断链路的结构。
3.1 相关性信号源的变化
传统链路中,相关性信号来自:
- 文本匹配信号:query 与 title、正文、anchor 的相似度
- 语义向量信号:双塔模型产出的 query 向量与 doc 向量内积
- 统计信号:点击率、停留时长、用户反馈
加入 VLM 后,新增了两类信号:
- 视觉语义信号:从 document 的图片、视频关键帧中抽取的语义表征
- 跨模态匹配信号:query 文本与视觉内容的匹配分数,或 image query 与文档文本的匹配分数
新增的信号不是叠加在旧模型之上,而是直接接入了相关性判断的输入端和输出端。在输入端,需要解析 document 的视觉内容;在输出端,需要融合文本相关性与视觉相关性。
3.2 在线链路的位置
如果按经典的搜索架构来分,VLM 可以放在不同位置:
- 粗排层:用轻量级 VLM 或者视觉表征模型过滤掉明显不相关的结果。这一层要求吞吐极高,通常不会跑完整的生成式 VLM,而是用视觉塔表征 + 近似最近邻检索。
- 精排层:对 Top 几十条结果做精细判断,这里可以使用 VLM 融合图文信息打分。
- 离线评测层:用 VLM 生成评测数据、构造伪标签、做相关性标注的辅助校验。
从目前行业讨论和公开资料来看,成本更可控的路线是先在离线评测层使用 VLM,等评估标准稳定、效果验证充分之后,再逐步放大到在线精排。这个节奏适合大多数搜索团队,因为相关性度量本身就是一个“先有稳定尺子,再优化排序”的领域。
3.3 与文本模型的关系
VLM 并不会淘汰文本相关性模型。实际工程中,文本模型在响应速度、可解释性和低成本推理上仍然有明显优势。
更合理的分工是:
- 文本模型负责基础文本语义匹配,覆盖面广、速度快。
- VLM 负责需要视觉理解的高价值场景,在文本模型分数基础上做修正,而不是全量替换。
- 线上精排时可以根据 query 类型路由,比如识别出商品类、图文类、视频类查询后,再决定要不要调用 VLM 打分。
这意味着团队需要的不是“一个 VLM 模型”,而是一套“文本模型打底 + VLM 做视觉信号增强”的混合架构。
4. VLM 相关性度量的核心流程拆解
一个完整的 VLM 相关性度量流程,通常包含查询解析、文档视觉信号抽取、相关性打分、结果后处理四个环节。下面按步骤拆开看。
4.1 查询解析
查询进入系统后,先做类型识别。这个查询是纯文本,还是包含图片?如果是纯文本,是否属于需要视觉信息的类目(如商品、地点、人物、品牌)?
这一步可以用一个轻量级分类器完成,也可以直接由规则触发。目标是决定后续要不要走视觉通道。
伪代码思路:
def parse_query(query: str, image_input=None): query_type = "text" if image_input is not None: query_type = "image_text" elif is_visual_intent(query): query_type = "text_visual" return { "query_text": query, "query_image": image_input, "query_type": query_type }这里is_visual_intent可以是规则、分类模型、也可以是 prompt 分类器。对于商品搜索这类垂直场景,通常直接根据业务线路由就行。
4.2 文档视觉信号抽取
Web 文档不是一张图片,而是一个包含多张图片、文字块、版式的复杂对象。抽取信号时一般分三步:
- 从 HTML 或数据源中抽出主图、视频封面、关键帧。
- 对图片做标准化处理:去重、裁剪、压缩、过滤低质量图。
- 送入 VLM 得到图片表征或图文匹配分数。
这一步常见的坑是图片加载失败、URL 过期、以及页面里大量与主题无关的广告图。务必要在特征抽取阶段加入质量过滤,而不是把脏数据直接塞给 VLM。
4.3 相关性打分
打分阶段是核心。一个通用做法是构造“查询 + 文档内容摘要 + 文档图片”的多模态输入,让 VLM 判断匹配程度并给出评分。
打分输出可以是:
- 离散标签:相关 / 部分相关 / 不相关
- 连续分数:0 到 100 的整数
- 排序序列:给定多个文档,让 VLM 给出相对排序
从实际使用角度看,连续分数更有利于排序融合,但离散标签更稳定、更容易评测。工程中可以先让模型输出离散标签加理由,再映射成分数。
4.4 后处理与融合
VLM 打分不能直接作为最终排序分。一般还会做三件事:
- 与文本相关性分数做加权融合,比例通过离线评测确定。
- 对分数做归一化和平滑,避免某个 query 的分数分布偏移。
- 增加兜底策略:VLM 超时、失败时自动降级到文本模型分数。
这套流程的核心设计原则是:视觉信号是增量,不是替代品;所有新增信号都必须能在离线评测中证明它提高了相关性指标,否则不要贸然上线。
5. 完整示例与代码实现
下面用一个最小可运行的示例来演示 VLM 相关性打分的整体思路。这里使用的属于示意代码,实际选型请根据团队基础设施决定,关键是理解流程。
5.1 示例 1:构造 VLM 相关性打分输入
假设我们需要判断一个查询与一条网页文档是否相关。文档带有标题、摘要和一张主图。
# 文件路径:feature_builder.py import json from typing import Dict, Optional def build_judgment_payload( query: str, doc_title: str, doc_summary: str, image_url: Optional[str] = None, ) -> Dict: """构造 VLM 打分输入。""" document_context = f"标题:{doc_title}\\n摘要:{doc_summary if doc_summary else '无'}" image_part = {"type": "image_url", "image_url": {"url": image_url}} if image_url else None text_part = { "type": "text", "text": ( "你是搜索相关性评估系统。" f"请判断查询与文档是否相关。\\n查询:{query}\\n文档信息:{document_context}\\n" "请按以下 JSON 格式返回:" '{"reason": "判断理由", "relevance_score": 0到100之间的整数}' ), } messages = [ { "role": "user", "content": [text_part] if image_part is None else [text_part, image_part], } ] return {"messages": messages} if __name__ == "__main__": payload = build_judgment_payload( query="防水越野跑鞋推荐", doc_title="2025 越野跑鞋选购指南", doc_summary="介绍多款越野跑鞋的防水性能和抓地力表现。", image_url="https://example.com/shoes.png", ) print(json.dumps(payload, ensure_ascii=False, indent=2))这里的关键不是 prompt 本身,而是把 query、文档文本、文档图片统一到一个多模态消息结构里。不同的 VLM 服务对消息格式有不同要求,但整体思路是一致的。
5.2 示例 2:调用 VLM 获取相关性分数
接下来需要一个客户端来调用模型。以 OpenAI 兼容接口为例,示意代码如下:
# 文件路径:vlm_judger.py import json import httpx from typing import Dict class VLMRelevanceJudger: def __init__(self, api_base: str, api_key: str, model_name: str): self.api_base = api_base self.api_key = api_key self.model_name = model_name self.headers = {"Authorization": f"Bearer {api_key}"} async def score(self, payload: Dict) -> float: request_body = { "model": self.model_name, "messages": payload["messages"], "temperature": 0.0, } async with httpx.AsyncClient(timeout=15) as client: resp = await client.post( f"{self.api_base}/chat/completions", headers=self.headers, json=request_body, ) resp.raise_for_status() data = resp.json() content = data["choices"][0]["message"]["content"] parsed = json.loads(content) return float(parsed["relevance_score"]) async def main(): judger = VLMRelevanceJudger( api_base="https://your-vlm-endpoint.example.com/v1", api_key="your-token", model_name="your-multimodal-model", ) payload = build_judgment_payload( query="防水越野跑鞋推荐", doc_title="越野跑鞋选购指南", doc_summary="介绍多款越野跑鞋的防水性能和抓地力表现。", image_url="https://example.com/shoes.png", ) score = await judger.score(payload) print(f"相关性分数:{score}") if __name__ == "__main__": import asyncio asyncio.run(main())这段代码有两点值得注意:
temperature必须设为 0 或尽量接近 0,保证相关性打分可复现。搜索相关性是需要稳定性的场景,不能一次跑一个分数。- 超时非常关键。Web 搜索链路对延迟敏感,判断逻辑不能因为单次模型调用超时拖垮整个请求。建议配合缓存和异步批处理来降低风险。
5.3 示例 3:离线评测指标计算
相关性强不强,不能只看几个 case。需要一个可量化的离线评测脚本。下面是一个极简的评测示例,计算 nDCG@k:
# 文件路径:evaluate.py import math from typing import List, Tuple def dcg_at_k(relevance_scores: List[float], k: int) -> float: relevance_scores = relevance_scores[:k] return sum(rel / math.log2(idx + 2) for idx, rel in enumerate(relevance_scores)) def ndcg_at_k( predictions: List[Tuple[str, float]], ground_truth: List[Tuple[str, float]], k: int = 5, ) -> float: """计算 nDCG@k。 predictions: [(doc_id, 预测分数)],已按预测分数降序 ground_truth: [(doc_id, 人工标注相关性)],分数越高越相关 """ pred_doc_ids = [doc_id for doc_id, _ in predictions[:k]] rel_scores = [] gt_dict = dict(ground_truth) for doc_id in pred_doc_ids: rel_scores.append(gt_dict.get(doc_id, 0.0)) dcg = dcg_at_k(rel_scores, k) ideal_scores = sorted([score for _, score in ground_truth], reverse=True) idcg = dcg_at_k(ideal_scores, k) if idcg == 0: return 0.0 return dcg / idcg if __name__ == "__main__": predictions = [("doc_1", 92.0), ("doc_2", 85.0), ("doc_3", 70.0), ("doc_4", 45.0)] ground_truth = [("doc_1", 3), ("doc_2", 2), ("doc_3", 1), ("doc_4", 0)] print(f"nDCG@4: {ndcg_at_k(predictions, ground_truth, k=4):.4f}")离线评测集的建设才是真正的难点。想让 VLM 在相关性上发挥作用,需要人工标注一批包含图文信息的 query-doc 对。如果团队暂时没有标注资源,可以用“让 VLM 标注,再做人工抽检”的方式启动,但要注意伪标签存在系统偏差。
5.4 示例 4:批量推断与结果缓存
Web 规模意味着即使只对 Top 结果做 VLM 打分,也可能面临每天千万级甚至亿级的请求。直接对每次搜索请求都实时调用大模型是不现实的。一个更可行的模式是离线批量打分 + 增量缓存。
# 文件路径:batch_infer.py import asyncio from dataclasses import dataclass from typing import Dict, List @dataclass class JudgmentInput: query: str doc_id: str doc_title: str doc_summary: str image_url: str class BatchJudger: def __init__(self, judger, cache: Dict[str, float]): self.judger = judger self.cache = cache def _key(self, query: str, doc_id: str) -> str: return f"{query}::{doc_id}" async def batch_score(self, inputs: List[JudgmentInput]) -> Dict[str, float]: results = {} pending = [] for item in inputs: key = self._key(item.query, item.doc_id) if key in self.cache: results[item.doc_id] = self.cache[key] continue payload = build_judgment_payload( query=item.query, doc_title=item.doc_title, doc_summary=item.doc_summary, image_url=item.image_url, ) pending.append((item, key, payload)) # 并发控制:防止一次性打爆模型服务 semaphore = asyncio.Semaphore(8) async def limited_judge(item, key, payload): async with semaphore: score = await self.judger.score(payload) self.cache[key] = score return item.doc_id, score if pending: done = await asyncio.gather( *(limited_judge(item, key, payload) for item, key, payload in pending) ) for doc_id, score in done: results[doc_id] = score return results这里引入并发信号量是为了控制对模型服务的压力。实际部署时还需要考虑重试、熔断、限流和结果回写。
6. 运行结果与效果验证
如果上面的批量打分流程运行成功,预期输出是一个 doc_id 到相关性分数的映射。例如:
{ "doc_1": 92.0, "doc_2": 85.0, "doc_3": 70.0, "doc_4": 45.0 }有了每一条 doc 的分数,离线评估要做的事情就很清晰了:
- 把模型打分结果写入一条评测表,字段包括 query、doc_id、VLM 分数、文本模型分数、人工标注分数。
- 计算 VLM 分数与人工标注的相关性。常用指标是 Spearman 相关系数、Pearson 相关系数、nDCG@k、以及人工标注一致性。
- 与纯文本基线对比。判断 VLM 的引入是否真正提升了相关性排序质量,而不是只在个别 case 上变好。
如果 VLM 分数与人工标注相关性低于文本模型基线,常见原因不是模型能力不够,而是输入设计出了问题。可能是文档图片没有抽到主图,也可能是 prompt 里的指令不够清晰,让模型把重点放在了不相关的内容上。
另外,用“理由”字段做案例分析非常重要。VLM 打分是黑盒,但输出的 reason 能帮你定位它到底在看什么。如果发现它总是围绕“图片清晰度”“图片美观度”打分,而不是围绕“是否相关”打分,就需要调整 prompt。
7. 常见问题与排查方法
在接入 VLM 做相关性度量的过程中,最容易遇到的几个问题如下:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 同一查询多次打分差异大 | 模型采样参数未固定 | 检查 temperature 是否设置为 0 | 固定采样参数,必要时固定随机种子 |
| 对含图文档打分明显偏低 | 文档主图抽取失败或选了广告图 | 可视化抽查输入图片 | 改进主图抽取逻辑,增加图片质量过滤 |
| VLM 分数与人工标注相关性低 | prompt 没有讲清“相关”标准 | 读取 reason 字段分析模型关注点 | 重写 prompt,给出正反例 |
| 实时调用超时严重 | 图片加载慢、模型推理慢 | 查看调用耗时分布 | 改为离线批量打分 + 缓存 |
| 图片无法访问 | 图片 URL 过期或存在防盗链 | 检查抓取时的图片状态码 | 存量图片做本地备份 |
| 业务点击率与 VLM 打分不一致 | 点击行为受位置、标题等因素影响 | 做位置归一化后再对比 | 用搜索评测集而非原始点击率作为效果指标 |
这里想特别提醒一点:VLM 打分需要和你业务里的“相关性真值”对齐。如果业务定义的相关性是“用户点击”,那么即使 VLM 在语义上判断得很合理,也未必会提升点击率。相关性度量是一个系统工程,模型输出只是其中一环,后面还有排序公式、业务策略和用户体验的持续调优。
8. Web 规模部署的工程建议
如果 VLM 相关性打分要通过测试,进入线上或大规模离线流程,以下几个工程问题必须提前规划。
8.1 成本控制
VLM 推理成本远高于文本模型。Web 规模下,全量 query 都走 VLM 精排不太现实。更推荐的做法是分层使用:
- 第一层:文本模型粗排,把候选集压到几十条。
- 第二层:VLM 只对 Top 结果打分,或者只对文本模型分数处于模糊区间的结果打分。
- 第三层:对特定 query 类型(图片查询、商品查询)触发 VLM 打分。
同时可以加入分桶策略,小流量上线对比效果,效果确认后再逐步扩大比例。
8.2 缓存与复用
搜索存在明显热点效应,同一 query 在短时间内会被大量重复查询。针对 query 与 doc 的匹配结果做缓存,能大幅降低 VLM 调用量。需要注意的是缓存应该有失效时间,因为网页内容会更新,图片可能被替换。
8.3 模型蒸馏替代直接推理
如果线上延迟和成本仍然难以接受,可以考虑用 VLM 在离线阶段生成大量“图文相关性”训练数据,然后蒸馏到一个轻量级多模态模型。蒸馏后的模型用于线上,VLM 用于离线迭代。这是目前很多团队更实际的落地路径。
8.4 监控与降级
新增 VLM 环节后,必须增加线上监控:
- 调用成功率、超时率、平均耗时
- 打分覆盖率:多少比例的 query-doc 对成功拿到了 VLM 分数
- 分数分布变化:分数均值是否出现系统性偏移
- 降级触发次数:VLM 不可用时,是否有回退到文本模型的逻辑
核心原则是 VLM 不能成为搜索链路的单点故障。在工程上要预设降级策略,任何异常都不能影响基础检索能力。
8.5 标注与评测流程
无论模型多强,评测数据仍然是相关性度量的地基。建议建设一套与业务目标对齐的标注指南,明确“相关”“部分相关”“不相关”的定义,并覆盖图文场景。标注员看到的内容应该尽量与线上模型看到的一致,这样才能缩小评测标准与线上信号之间的差异。
9. 趋势展望与后续学习建议
从搜索技术的演进看,相关性度量正在从纯文本语义走向多模态语义。这不只是给搜索引擎“加一个图片输入口”,而是会在查询理解、召回、排序、评测、标注等全链路产生影响。
接下来值得关注的方向有三个:
第一,轻量级多模态表征模型。VLM 的推理成本会持续下降,但短期内要真正做到 Web 规模全量使用,轻量级双塔多模态模型仍然是更现实的选择。
第二,评测自动化。如何用 VLM 辅助生成高质量的相关性评测集,减少人工标注成本,同时避免模型自我评价带来的偏差,这是一个很有价值的研究方向。
第三,搜索业务与多模态检索的深度融合。当视觉信号真正进入相关性判断,搜索系统会从“关键词到文本页”的匹配,演进为“语义意图到多媒体内容”的匹配,这对召回链路、索引结构、排序策略都会带来连锁变化。
对工程师来说,最快的入门路径不是直接训练一个大模型,而是先在现有搜索链路里找出那些“因为看不见图片而判断错误”的典型 case,然后用 VLM 对一个限定场景做相关性增强,跑通评估、监控、降级整套流程。这样既能快速产生业务价值,也能沉淀出团队对多模态搜索的系统理解。
这篇文章没有给出唯一的正确答案,原因很简单:不同业务里“相关性”的定义不同,适合的模型和服务也不一样。但判断链路的设计思路是通用的:先定义清楚相关性,再决定视觉信号在哪一层接入,最后用靠谱的评测体系持续迭代。方向已经明确,剩下的就是先用小成本跑通一个真实场景。
