搜索相关性度量这件事,看起来简单,实际很麻烦。难点不在“文本匹配”,而在于 Query 和网页之间的相关性判断,往往要“看得见图”才能做对。一个典型例子:用户搜“白色帆布鞋 男款”,商品页标题写的是“新款休闲鞋”,正文没有任何“白色”“帆布”字样,但首图就是一双白色帆布鞋。纯文本模型大概率漏判,人类标注员扫一眼图就知道相关。这正是视觉语言模型(Vision-Language Model,VLM)进入相关性度量的核心动机:把图片、版式、视觉语义一起纳入打分,而不是只靠文字。
这篇不是某个一键启动的整合包教程,而是梳理 VLM 做 Web 规模搜索相关性度量这条技术路线:它解决了什么、工程上要拆成几个阶段、模型怎么选、服务怎么搭、批量评估怎么做、显存和延迟成本大概是什么量级。文章会给出可落地的 API 服务模板、批量任务脚本和排查清单。适合做搜索、推荐、广告相关性评估的工程师,以及准备把多模态模型接入线上打分链路的同学。
先给结论。这套方案的核心特点可以概括为:文本信号做兜底,对比式 VLM 做召回和粗排,生成式多模态大模型做精排和理由生成;支持批量离线评估,也可以封装成 REST API 接进自己的评估平台;对硬件有明确要求但不夸张,8GB 显存能跑对比式模型,7B 级别多模态大模型建议 16GB 显存以上。下面的章节会围绕这些点逐个展开。
1. 核心能力速览
先把这条技术路线的主要参数列出来。以下硬件建议属于通用工程经验,不是某个模型的官方最低要求,实际以你选用的模型卡和量化方式为准。
| 能力项 | 说明 |
|---|---|
| 技术路线 | 对比式 VLM 做召回/粗排 + 生成式多模态大模型做精排/解释 |
| 核心任务 | Query-Document 相关性打分、Query-Image 相关性判断、相关性二分类与理由生成 |
| 输入数据 | 查询文本、网页标题、网页正文、网页首图/商品图/网页截图,可选 OCR 文本 |
| 输出数据 | 相关性分数(0-1 或 1-5)、相关性标签、判定理由 |
| 推荐硬件 | 独立显卡,8GB 显存起;7B 级别多模态大模型建议 16GB 以上 |
| 支持平台 | Linux 优先;Windows 可用于调试,生产环境不建议 |
| 启动方式 | Python 服务(FastAPI)+ 离线批处理脚本 + 评估指标计算 |
| API 能力 | 可封装 REST 接口,支持单条打分和批量打分 |
| 批量任务 | 支持,建议配合任务队列、断点续跑和失败重试 |
| 主要场景 | Web 搜索、电商搜索、广告相关性、图文检索效果评估 |
为什么要分“对比式”和“生成式”两类 VLM?因为 Web 规模搜索的打分链路对延迟和成本极其敏感。对比式 VLM 比如 CLIP、SigLIP 这类,把文本和图片编码到同一向量空间,算余弦相似度,速度快、显存占用低,适合在第一阶段从海量候选中筛出 top-K。生成式多模态大模型比如 Qwen-VL、InternVL 这类,能同时看图和文字,输出带理由的相关性判定,质量高但推理慢、成本高,只适合对 top-K 做精排,或者离线构建评估集时使用。两条路线不是替代关系,而是流水线上下游。
2. 为什么相关性度量需要 VLM
2.1 文本模型的天花板很明显
传统相关性度量工具链大致经历了三个阶段:BM25、TF-IDF 这类词法匹配,到 Sentence-BERT 这类稠密向量检索,再到 BERT/RoBERTa 这类 Cross-Encoder 精排。它们对纯文本语料有效,但对真实网页有一个共同缺陷:看不见图片。
网页相关性判断里,图片信息的占比比很多人想象的高。电商场景中,标题可能刻意堆关键词或不堆关键词,正文摘要经常是模板生成,真正决定用户是否满意的,是首图能不能清楚展示商品的款式、颜色、角度。新闻场景中,配图能帮助确认事件主体和场景。本地生活场景中,店铺头图直接决定了“这家店卖什么”。这些信号全部落在图片里,文本模型面对的是信息残缺的输入。
2.2 Web 页面的多模态特征必须利用
一个真实的网页,至少包含四类可被相关性模型利用的信号:标题文本、正文文本、页面图片或截图、以及图片的 alt 文本和周边文字。传统做法是只取前两类,后两类要么丢弃,要么用 OCR 转成文本后再进模型。OCR 会丢信息,比如颜色、形状、构图、物体相对位置,这些视觉语义很难用文字完整还原。
VLM 直接把图像像素作为输入,在模型内部完成视觉编码和文本推理。它不需要你先把图片“翻译”成文字,而是让模型自己判断“这张图里有什么、和 Query 是否匹配”。这是本质区别。
2.3 VLM 带来的三个具体能力
第一,图文统一表征。对比式 VLM 把 Query 文本和图片投到同一个向量空间,可以直接算相似度,这为召回阶段提供了可用的多模态检索信号。第二,细粒度视觉推理。生成式多模态大模型能处理“左图是红色款,右图是黑色款,Query 要红色款所以右图不相关”这类需要视觉细读的样本。第三,可解释性。生成式模型可以输出判定理由,这在构建评估集、审核错误样本、向业务方解释打分结果时非常有用。
2.4 适用场景与边界
适合的场景包括:Web 搜索相关性评估、电商搜索、广告创意与 Query 相关性、图文检索的质量衡量、内容平台搜索的多样性评估。不适合的场景也要说清楚:如果语料本身就是纯文本,比如代码仓库、论文库、合同库,VLM 带来的收益有限,反而增加成本和延迟;如果图片质量极差、大量缺图,VLM 的优势也发挥不出来。另外,任何涉及图像数据的使用都必须确认数据来源合法,网页图片通常有版权,评估数据集的构建需要遵守来源网站的授权条款和 robots 协议。
3. 技术路线与数据流设计
3.1 两阶段架构:召回 + 精排
Web 规模搜索面临的核心约束是数据量级。线上索引可能有数十亿网页,不可能对每个候选都跑一次生成式多模态大模型。因此工程上必须拆成两阶段。
第一阶段是召回和粗排。用对比式 VLM 为网页图片生成 embedding,离线存入向量索引;线上拿到 Query 后,用同一个模型编码 Query 文本,在向量索引里做 ANN 检索,取出 top-K 候选。如果网页有正文文本,也可以叠加文本向量召回,做分数融合。这一阶段的目标不是精确,而是用很低成本把相关候选捞回来。
第二阶段是精排。对 top-K 候选,把 Query、标题、正文摘要、页面图片或截图一起送入生成式多模态大模型,让模型输出相关性分数和理由,再按分数重排。这一阶段只处理几十到几百个候选,成本可控,质量和可解释性都有保障。
3.2 相关性信号设计
具体打分时,可以拆成几个可独立验证的信号,最后再融合:
- S1 文本语义相似度:Query 与标题、正文、alt 文本之间的文本相似度,用文本 embedding 模型或 VLM 的文本编码器计算。
- S2 图文匹配度:Query 与页面首图之间的图文相似度,用对比式 VLM 的图文匹配头计算。
- S3 生成式判定:把 Query、标题、正文、图片整体交给多模态大模型,输出综合相关性判断。这个信号最贵,也最接近人类标注员的行为。
融合方式可以是加权求和,也可以是级联阈值。一个稳妥的做法是:先用 S1 和 S2 过滤掉明显不相关的候选,再对边界样本调 S3,这样既控制成本又保证精度。
3.3 模型选型参考
对比式 VLM 可以选 OpenAI 开源的 CLIP 系列、open_clip 复现的多种权重,或者 SigLIP。如果业务以中文为主,可以找中文语料训练的 CLIP 变体,这类模型在中文图文检索任务上通常比英文原版更稳。生成式多模态大模型可以选 Qwen-VL、InternVL 等支持图片输入的开源模型,具体用什么规模和量化,取决于你的显存和延迟预算。
选型时要同时看三个指标:相关性判别能力、推理延迟、显存占用。不要只看刷分结果,实际业务里延迟和成本往往比几个点的准确率提升更重要。
3.4 标注集与评估指标
做相关性度量,必须先有一套人工标注的评估集。标注规范要写清楚:什么算相关,什么算部分相关,什么算不相关,边界情况怎么处理。评估指标以 NDCG@k 和 MRR 为主,配合人工标注与模型打分的一致性分析。每批模型迭代,都要在固定评估集上重跑,避免个别 prompt 修改后“顾此失彼”。
4. 环境准备与前置条件
4.1 硬件与系统
建议使用 Linux 服务器,原因不是 Windows 跑不了,而是生产环境的进程管理、GPU 驱动、多卡推理和 Docker 部署都更省事。显卡方面,跑对比式 VLM 的 base 规模模型,8GB 显存基本够用;跑 7B 级别生成式多模态大模型,建议 16GB 以上,开启 FP16 或 INT8 量化后更稳。磁盘至少要预留模型文件、评估数据集、输出结果三部分空间,7B 模型权重大概 14GB 到 15GB,量化后更小。
4.2 Python 环境与依赖
推荐 Python 3.10 以上,用 conda 或 venv 隔离环境。基础依赖是 PyTorch、transformers、PIL、FastAPI、uvicorn、pydantic。如果做向量检索,还需要一个 ANN 库,比如 faiss 或 hnswlib。下面给一套通用的环境创建命令,版本号请按你本机情况调整。
conda create -n vlm-search python=3.10 -y conda activate vlm-search # PyTorch 安装命令请参考 pytorch.org 的 CUDA 版本号 pip install torch torchvision pip install transformers pillow fastapi uvicorn pydantic pip install faiss-cpu httpx如果你的 GPU 驱动和 CUDA 版本不确定,先用nvidia-smi查看驱动支持的 CUDA 版本,再选择对应的 PyTorch 版本,避免装完跑不起来。
4.3 数据目录规划
建议把输入、模型、输出分成三个目录,避免一批任务跑完目录乱掉。下面是一个参考结构:
data/ queries.jsonl # 查询文本 docs/ # 网页正文或摘要 images/ # 页面图片或截图 labels.tsv # 人工标注结果 models/ clip-model/ # 对比式 VLM 权重 vlm-7b/ # 生成式多模态大模型权重 outputs/ batch-run-20250101/ # 每天/每次任务独立输出目录模型文件、输入素材、输出结果分开管理,是后续排查问题和复现结果的基础。
5. 模型加载与本地推理
5.1 对比式 VLM:图文相似度计算
先用 CLIP 系列模型做图文相似度。下面的代码给出从加载模型到输出相似度的完整流程,模型名按你实际下载的权重替换。
import torch from transformers import CLIPModel, CLIPProcessor from PIL import Image model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32") processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32") query = "白色帆布鞋 男款" image = Image.open("data/images/product_001.jpg") inputs = processor(text=[query], images=image, return_tensors="pt", padding=True) with torch.no_grad(): outputs = model(**inputs) # 图文余弦相似度,范围约为 0 到 1 similarity = outputs.logits_per_image.item() print("image-text similarity:", similarity)这一步适合验证“Query 文本和页面图片是否匹配”。要注意 CLIP 对中文的支持取决于训练数据,英文原版权重对中文文本效果不稳定,中文场景优先换中文或多语言版本。
5.2 生成式大模型:相关性判定与理由输出
生成式多模态大模型负责输出带理由的判定。下面是一个通用模板,具体模型类名、对话模板和图片输入方式,以你选用的模型库为准,不要直接照抄。
import base64 import json import torch from PIL import Image # 假设你已经加载了支持图片输入的多模态模型 processor 和 model # 以 transformers 生态为例,实际类名需要按模型卡片调整 # from transformers import AutoModelForVision2Seq, AutoProcessor # processor = AutoProcessor.from_pretrained("your-vlm-model", trust_remote_code=True) # model = AutoModelForVision2Seq.from_pretrained("your-vlm-model", torch_dtype=torch.float16, device_map="auto") def judge_relevance(query, title, body, image_path): image = Image.open(image_path).convert("RGB") prompt = ( "你是搜索相关性评估员。请判断 Query 与网页是否相关。\n" f"Query: {query}\n" f"网页标题: {title}\n" f"网页正文摘要: {body}\n" "请结合图片内容判断,只输出 JSON:\n" '{"relevance": 0 或 1, "score": 1-5, "reason": "简要说明"}' ) # 把你的 inputs 构造方式替换到这里 # inputs = processor(text=prompt, images=image, return_tensors="pt") # outputs = model.generate(**inputs, max_new_tokens=128, do_sample=False) # answer = processor.decode(outputs[0], skip_special_tokens=True) # return json.loads(answer) raise NotImplementedError("请替换为所选模型的实际推理代码")这段代码的关键点:生成时把do_sample设成False,也就是 greedy decoding,避免同一输入多次打分波动;输出格式强制 JSON,方便后续解析和落库;如果模型对 JSON 格式不稳,可以在 prompt 末尾加一句“不要输出任何多余内容”。
5.3 输出解析与分数归一
生成式模型输出的 score 是 1 到 5 的离散分,对比式 VLM 输出的是 0 到 1 的余弦相似度,两者不能直接混用。建议在融合前各自做归一化:生成式分数除以 5,对比式分数按 min-max 统计映射到 0-1。归一化的统计量要从评估集计算,不要随便拍一个固定值。
6. 搭建相关性打分 API 服务
6.1 FastAPI 单条打分接口
把模型推理封装成 REST API,方便接入标注平台、评测脚本或线上降级链路。下面是一个 FastAPI 服务模板,模型初始化放在启动阶段,避免每个请求重复加载。
# app.py from fastapi import FastAPI, HTTPException from pydantic import BaseModel import base64 from io import BytesIO from PIL import Image app = FastAPI() class ScoreRequest(BaseModel): query: str title: str = "" body: str = "" image_base64: str = "" # 可选,Base64 编码后的图片 @app.on_event("startup") def load_models(): # 在这里加载对比式 VLM 和生成式大模型,保存到全局变量 pass @app.post("/score") def score(req: ScoreRequest): if not req.query: raise HTTPException(status_code=400, detail="query is required") image = None if req.image_base64: image = Image.open(BytesIO(base64.b64decode(req.image_base64))).convert("RGB") # 实际逻辑: # 1. 计算 S1 文本相似度 # 2. 计算 S2 图文相似度 # 3. 对边界样本调用生成式大模型,得到 S3 与 reason # 4. 融合分数 return { "query": req.query, "score": 0.0, "relevant": False, "reason": "" }启动命令:
uvicorn app:app --host 127.0.0.1 --port 8000注意--host在生产环境不要暴露到公网,建议绑定内网地址,并在前面加一层鉴权或网关。
6.2 curl 调用示例
curl -X POST http://127.0.0.1:8000/score \ -H "Content-Type: application/json" \ -d '{ "query": "白色帆布鞋 男款", "title": "新款休闲鞋", "body": "轻便透气,适合日常穿搭", "image_base64": "" }'图片以 Base64 传输时,单张图可能很大,建议服务端限制请求体大小,或者改用上传图片后返回图片 ID、再在打分请求里引用 ID 的方式,减少重复传输。
6.3 Python 批量调用
import requests def score_one(query, title, body, image_base64=""): resp = requests.post( "http://127.0.0.1:8000/score", json={ "query": query, "title": title, "body": body, "image_base64": image_base64, }, timeout=60, ) resp.raise_for_status() return resp.json() result = score_one("白色帆布鞋 男款", "新款休闲鞋", "轻便透气") print(result)7. 批量评估任务与指标计算
7.1 输入格式与批处理脚本
批量评估时,把待测样本写成 JSONL,每行一条,包含 id、query、title、body、image_path 和可选的 human_label。逐条读取、逐条打分、逐条写结果,这样中途失败也不会丢已完成的数据。
import json from concurrent.futures import ThreadPoolExecutor def process_line(line): # 调用 /score 接口或本地推理 # 这里需要替换成你的实际打分函数 return { "id": line["id"], "pred_score": 0.0, "pred_relevant": False, } def run_batch(input_path, output_path, max_workers=4): with open(input_path, "r", encoding="utf-8") as f: items = [json.loads(l) for l in f if l.strip()] with ThreadPoolExecutor(max_workers=max_workers) as pool: results = list(pool.map(process_line, items)) with open(output_path, "w", encoding="utf-8") as f: for item, result in zip(items, results): merged = {**item, **result} f.write(json.dumps(merged, ensure_ascii=False) + "\n") if __name__ == "__main__": run_batch("data/queries.jsonl", "outputs/pred_results.jsonl", max_workers=4)并发数要按 GPU 显存调整。生成式大模型并发太高会 OOM,稳妥做法是先跑一个 4 并发的小样本,观察显存峰值再放开。
7.2 NDCG 与一致性计算
有 human_label 后,可以用 NDCG@k 评估排序质量。下面是标准的 NDCG 实现,可以直接抄进脚本。
import math def ndcg_at_k(relevances, k=10): # relevances: 按模型排序后的人工标注分列表,0 表示不相关,越大越相关 dcg = sum( (2 ** rel - 1) / math.log2(i + 2) for i, rel in enumerate(relevances[:k]) if rel > 0 ) ideal = sorted(relevances, reverse=True)[:k] idcg = sum( (2 ** rel - 1) / math.log2(i + 2) for i, rel in enumerate(ideal) if rel > 0 ) return dcg / idcg if idcg > 0 else 0.0除了 NDCG,还要看模型打分和人工标注的相关系数,比如 Spearman。相关系数低说明模型排序和人的直觉不一致,即使 NDCG 还行,也要检查是不是分数分布太集中。
7.3 判断是否成功的标准
一次批量评估跑完,至少回答四个问题:模型打分和人工标注在易分样本上是否一致;边界样本集中在哪些类型;每个 Query 的打分方差是否过大;单条样本平均延迟和 P99 延迟是多少。如果延迟超预算,即使准确率再好也不能上线,需要先做量化或模型蒸馏。
8. 资源占用与性能观察
8.1 显存与延迟观察方法
推理过程中用nvidia-smi或nvidia-smi -l 2实时查看显存占用。重点看两个值:显存峰值和 GPU 利用率。显存峰值决定服务能跑多大的并发,GPU 利用率决定资源有没有吃满。生成式大模型推理时,如果利用率和显存都不稳定,可能是显存碎片或并发设置不合理。
nvidia-smi -l 2更细的指标可以用 PyTorch 的 profiler 或者 Nsight Systems 查看每层的耗时,定位瓶颈在视觉编码、文本解码还是图像预处理。
8.2 降低显存占用的常规手段
第一,FP16 推理,大部分消费级显卡和服务器显卡都支持,显存占用直接减半。第二,INT8 或 4-bit 量化,7B 模型可以压到 6GB 到 8GB 左右,但量化后打分稳定性需要用评估集验证。第三,用小 batch,生成式模型对 batch 不敏感,batch 大显存容易爆,收益却不大。第四,用 vLLM 这类推理框架做连续批处理,吞吐比原生 transformers 的 generate 高很多,适合批量任务。
8.3 Web 规模下的成本控制思路
Web 规模搜索不可能对全量候选跑生成式大模型,这是硬约束。务实做法是分层:对比式 VLM 处理全量候选,做粗排;生成式大模型只处理 top 200;线上只部署对比式模型,把生成式大模型用于离线评估、训练蒸馏模型和审核边界样本。这种设计下,真实线上延迟主要由对比式 VLM 的编码和向量检索决定,7B 大模型不在请求关键路径上。
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 模型加载时 OOM | 显存不足或未开启量化 | nvidia-smi 查看显存占用 | 换小模型、开启 FP16/INT8、使用 device_map="auto" |
| 中文文本乱码 | 编码或 tokenizer 配置问题 | 打印输入输出日志,检查字符编码 | 统一 UTF-8,检查 processor 的 tokenizer 是否匹配 |
| 图片无法加载 | 路径错误、图片损坏或格式不支持 | 单测一张图,打印异常堆栈 | 增加重试、格式校验和损坏图片跳过逻辑 |
| 打分结果不稳定 | 提示词扰动或生成参数未固定 | 同一输入多次调用,看分数方差 | 固定 prompt,do_sample=False,temperature=0 |
| 生成式模型输出非法 JSON | 模型没按格式输出 | 查看原始输出文本 | 在 prompt 中强调“只输出 JSON”,加解析容错 |
| API 请求超时 | 生成式模型单条推理太慢 | 查看服务日志,统计单条耗时 | 增大超时时间,改用批量接口或者更小的模型 |
| 批量任务卡住 | 某个样本长期不结束或 worker 异常 | 看 worker 日志和任务进度 | 增加单任务超时、失败重试、断点续跑 |
| 显存逐步上涨直至 OOM | 推理框架显存缓存未释放 | 观察长时间运行的显存曲线 | 重启服务,或改用 vLLM 等带显存管理的框架 |
最值得注意的是打分稳定性问题。很多人第一次跑生成式 VLM 打分,会惊讶于同一输入两次结果不同。原因通常是采样参数没关,或者 prompt 里夹带了随机感较强的表述。排查时先把do_sample=False、temperature=0,再跑三遍相同输入验证。
10. 最佳实践与合规建议
第一次搭建不要直接上全量数据。建议先用 200 到 500 条样本,跑通“数据准备 -> 模型打分 -> 指标计算 -> 人工复核”全流程,确认每个环节的输出格式和错误处理都正确,再逐步扩大数据量。模型文件、输入素材、输出结果分目录管理,每条结果带模型版本、prompt 版本和数据版本,否则后面很难复现“上周那个分数是哪次跑出来的”。
批量任务必须加日志、超时和失败重试。推荐每个任务写一条 JSON 日志,记录样本 id、耗时、返回码、原始输出和解析后结果,排查时能直接定位到具体样本。API 服务要限制访问范围,至少绑定内网地址,加简单的 token 鉴权,不给公网裸奔。
合规方面有几个红线。网页图片通常有版权,搭建评估数据集时要遵守来源网站的授权条款、robots 协议和平台规则,不能随意抓取、打包、分发。涉及个人数据的 Query 或页面内容,要去标识化处理,并在测试环境验证。任何使用 VLM 做相关性判断的系统,都不能把模型输出当作最终裁决,尤其在高影响的场景里需要人工复核。多模态模型同样存在幻觉和偏见,对敏感内容、争议内容、版权素材的判断,必须做额外的安全过滤和人工审核。
11. 总结与下一步
VLM 做相关性度量,最值得尝试的点是让打分链路第一次拥有了“看图”能力。最先应该验证的功能是:找 50 个文本模型易错但看图就能判对的样本,用对比式 VLM 和生成式大模型分别打分,看能不能把这类样本捞回来。最容易踩的坑有两个:一是直接对全量候选跑生成式大模型,成本和延迟直接失控;二是生成参数没固定,导致打分不稳定,后续所有指标都不可信。
下一步的扩展方向可以分三条。第一条是把生成式大模型当标注器,用它的输出训练一个小的蒸馏模型,部署到线上打分链路。第二条是引入网页截图,让模型直接看页面整体版式,而不是只看首图,这对信息流和本地生活场景帮助更大。第三条是把相关性度量和用户点击行为做联合建模,用 VLM 的输出作为特征,而不是替代整个排序模型。
这套方案不适合追求“极致精度但不管成本”的场景,也不适合纯文本语料。如果你的业务是电商、本地生活、信息流这类图片强相关的搜索场景,建议先按本文的流程搭一个离线评估集,跑完 500 条样本的对比实验,再决定要不要进入线上链路。