多模态检索新范式:CoCo-IR上下文组合图像检索技术与实践 📅 发布时间:2026/8/27 9:29:37 👁 浏览次数: 最近在梳理多模态检索相关工作时有一个问题让我印象很深很多团队已经把“组合图像检索”Composed Image Retrieval, CIR做到了上线用户上传一张图片再输入一句修改描述系统能返回符合预期的结果。但一旦把交互从单轮变成多轮情况就完全变了。用户先发一张沙发图说“帮我找类似款式但尺寸更小的”系统返回结果后用户又说“第三个的颜色不太合适换浅灰色继续找”最后补一句“顺便看看有没有同系列的茶几”。这种带连续上下文的搜索传统 CIR 完全接不住因为每一轮都被当作独立任务前面的约束、否定和用户偏好全部丢失。CoCo-IR 要解决的正是这一类问题上下文组合图像检索。这篇文章我会从任务定义、方法体系、数据构造、模型实现到工程落地把 Contextual Composed Image Retrieval 这条技术线的完整逻辑讲清楚。如果你正在做多模态搜索、电商场景的以图搜图或者打算把多轮对话能力接进检索系统这篇文章能帮你少踩很多坑。1. 这篇文章真正要解决的问题先说一个判断组合图像检索从“单轮”走向“多轮上下文”不是做加法而是做重构。单轮 CIR 的技术路线已经相对成熟。经典做法是训练一个双塔模型左边输入“参考图 修改文本”右边输入候选图通过对比学习拉近匹配对的距离。典型场景是用户上传一张纯色卫衣图片输入“把条纹改成格子”模型去检索同款格子卫衣。但真实产品中的搜索几乎不会停在单轮。用户会连续比价、修正偏好、补充约束甚至带着上一轮的选择进入下一轮。举个例子第 1 轮用户上传一张客厅图说“我想要类似风格的电视柜”第 2 轮系统推荐后用户说“高度矮一点不要玻璃门”第 3 轮用户又说“这款木质纹路太深换浅色橡木”如果检索系统没有“记忆”第 2 轮和第 3 轮就必须让用户重新描述所有条件。这种体验在 C 端产品里基本不可用。CoCo-IR 的核心贡献就是把上下文Context正式变成组合检索的输入维度。它不再是“图 文本”的一次性匹配而是“图 文本 历史上下文”的联合检索。这篇文章适合以下几类读者正在做电商搜索、图片检索、多模态种草推荐的研发工程师对多模态大模型应用落地感兴趣但还不清楚 CIR 与 CCIR 边界的产品/算法同学想了解上下文建模如何在多模态检索中生效的在校学生和研究者。读完你会得到三样东西一是清晰的概念边界知道 CCIR 和传统 CIR、多模态对话、视觉问答有什么区别二是可复现的模型设计思路包括目标函数、网络结构、训练数据构造方式三是一套工程落地的注意事项包括批量构建、评估方法和线上部署时的典型坑。2. 基础概念图像检索、组合图像检索与上下文组合图像检索2.1 三个检索任务的边界在展开 CoCo-IR 之前有必要先把几个容易混淆的概念理清楚。**图像检索Image Retrieval**是最基础的任务。输入一张查询图基于视觉特征从数据库中召回相似图像。它只使用图像信息没有任何语言交互。典型实现是提取 CNN/ViT 特征用余弦相似度做最近邻检索。**组合图像检索Composed Image Retrieval, CIR**在查询图中加入一段修改文本。输入是“参考图 修改文本”输出是符合组合描述的目标图。比如用户提供一张方桌照片文本是“改成圆角”系统就应该返回圆角方桌。CIR 的关键是让图像特征和文本特征在联合空间里对齐经典数据集包括 Fashion-IQ、CIRR 等。**上下文组合图像检索Contextual Composed Image Retrieval, CCIR**是 CoCo-IR 所属的任务类别。它在 CIR 的基础上加入了多轮对话上下文输入的每一轮都是一个“参考图 修改文本”且每一轮都要考虑之前所有轮的交互信息。用户可能在同一轮中同时给出两个约束也可能在后续轮次中否定之前的结果这要求模型具备跨轮的信息聚合能力。任务输入输出是否跨轮传统图像检索参考图候选图否组合图像检索 CIR参考图 修改文本候选图否上下文组合图像检索 CCIR多轮 (参考图 修改文本)当前轮候选图是多模态对话 VQA图像 多轮文本文本答案是但输出是文本从表格可以看出CCIR 和视觉问答VQA最大的区别在于输出模态。VQA 输出的是文本答案而 CCIR 输出的是“最匹配当前上下文语义”的图像列表。2.2 组合的含义不只是“拼接”很多初学者会把“组合”误解为把图像特征和文本特征简单拼在一起。实际上组合检索的核心是在联合嵌入空间中对齐跨模态语义。以 CIR 为例假设参考图是一条“蓝色牛仔裤”修改文本是“改成黑色”那组合后的特征应该既保留牛仔裤的版型又替换掉颜色属性。这需要模型具有强大的属性解耦和重组合能力而不是简单的向量相加。到了 CCIR组合的对象还多了历史轮次的语义。模型需要判断当前轮的修改文本是针对哪张参考图说的它是否覆盖了上一轮的某个条件用户是否在否定之前的候选结果这种推理能力远超出“图 文本”的静态匹配。2.3 CoCo-IR 的定位如果把 CIR 比作“根据一句话修改一张图去搜索”那 CoCo-IR 就是“根据一整段对话去搜索每一轮都在前文基础上精修”。它更像一个具备记忆能力的多模态检索器。CoCo-IR 这个名字可以拆成两部分CoCo 代表 Contextual CompositionIR 是 Image Retrieval。它强调的正是“上下文感知的组合式检索”。从工程角度理解它要同时解决三层问题表示层如何把多轮图像和文本编码成统一的上下文感知特征交互层如何让当前轮的查询语义和历史轮次的约束有效交互检索层如何在大规模候选集上高效计算相似度并返回可解释的结果。后面几节我会围绕这三层逐一展开。3. 上下文信息为什么让检索难度陡增如果只看表面很容易以为 CCIR 只是给 CIR 的输入前面加一段历史拼接。一旦深入实现会发现难度提升是数量级的。3.1 组合空间爆炸单轮 CIR 的输入空间是“图像 × 文本”。多轮 CCIR 的输入空间是“图像 × 文本^ 轮次”。每一轮都会引入新的约束词、属性词和否定词组合空间呈指数增长。举例来说第 1 轮输入是“棕色皮沙发”第 2 轮输入是“要三人位”第 3 轮输入是“不要贵妃榻”。模型需要把“棕色”、“皮质”、“三人位”、“不要贵妃榻”这四个条件同时融合到最终检索表示中任何一轮的信息丢失都会导致结果偏移。这要求模型不能用简单的平均池化把多轮向量压成一个固定向量因为不同轮次对最终结果的贡献权重是不等的。3.2 意图漂移与条件覆盖多轮交互中用户经常“边聊边想”。一开始说“想要中古风的书架”看到搜索结果后改口“其实原木色也可以”。这种意图漂移在单轮 CIR 中不存在但在 CCIR 中非常常见。模型面临的难点是当前轮的文本是增量修改还是全面覆盖“其实换成原木色”属于覆盖它替代了之前的颜色条件“宽度再窄一点”属于增量它在保留“原木色”的基础上进一步增加尺寸约束。如果模型把全文无条件拼在一起前几轮被覆盖的条件就会变成噪音。所以在模型设计时不能只依赖一个全局编码器还要引入条件合并、冲突消解之类的机制。3.3 多模态对齐的长期依赖多轮建模还有一个经典问题长期依赖。用户在第 1 轮提到的某个属性可能直到第 5 轮才真正影响检索结果。而 Transformer 对超长上下文的建模无论是显存占用还是注意力计算量都会急剧上升。这也解释了为什么 CCIR 的工程实现不能简单照搬 LLM 的长上下文方案。结合这些难点可以得出一个结论上下文组合检索的模型设计重心应该在“如何建模上下文”上而不是“如何编码单轮查询”上。4. CoCo-IR 的模型设计思路目标函数与网络结构这一节进入方法层面。CoCo-IR 的整体设计遵循“多轮编码 → 上下文融合 → 检索比对”三段式框架。4.1 总体架构一个典型的 CoCo-IR 模型包含四个核心模块视觉编码器提取参考图的视觉特征常用 CLIP 的 ViT 或 ResNet 系列文本编码器提取修改文本的语义特征常用 CLIP 的 Text Transformer 或 BERT 系列上下文融合模块把所有轮次的组合表示聚合成一个上下文向量检索头把上下文向量与候选图特征映射到同一度量空间计算相似度。这里需要注意视觉编码器和文本编码器可以复用预训练多模态模型的权重但上下文融合模块通常需要专门训练。原因是预训练模型没有见过“多轮组合”这种输入格式直接冻结使用会导致上下文信息无法有效流动。4.2 一个关键选择早融合还是晚融合上下文融合的位置决定了整个模型的复杂度和效果边界。晚融合的做法是先分别编码每一轮的参考图和文本得到每轮的单轮融合向量再把这些向量输入上下文编码器得到最终的上下文向量。这种做法的优势是模块清晰、训练稳定每一轮都可以复用成熟的 CIR 模型。早融合的做法是把所有轮次的信息拼成长序列直接输入一个多模态 Transformer让它在连续交互中建模依赖关系。这种做法的表达能力更强但训练成本和显存开销也更高。从工程落地的稳妥性看我更推荐先做晚融合。先让每个单轮的“图 文本”融合到一个可用的联合空间再在轮次维度上建模上下文这样每一层的问题可以被单独验证和调试。4.3 目标函数与损失设计CoCo-IR 的主流训练方式是对比学习。核心思想是对于一个查询上下文它对应的目标图像是正样本批次中其他图像是负样本通过 InfoNCE 风格的损失拉近正样本对、推开负样本对。这里有一个容易被忽视的细节**负样本的构造方式。**在单轮 CIR 中负样本只需要从候选库中随机采样。但在 CCIR 中视频或对话中很常见的“语义相近但属性不同”的样本才是真正的 hard negative。比如参考图都是同款沙发区别只在颜色这种负样本如果缺失模型训练出来的检索能力会非常差。4.4 实现层面的损失函数用 PyTorch 风格写一个简化的对比损失示例如下# 文件路径coco_ir/loss.py import torch import torch.nn as nn import torch.nn.functional as F class CCIRContrastiveLoss(nn.Module): def __init__(self, temperature0.07): super().__init__() self.temperature temperature def forward(self, query_embeds, target_embeds): # query_embeds: [batch, hidden_dim] 上下文组合查询向量 # target_embeds: [batch, hidden_dim] 目标图像向量 logits (query_embeds target_embeds.T) / self.temperature labels torch.arange(query_embeds.size(0), devicequery_embeds.device) loss F.cross_entropy(logits, labels) return loss这段代码的意义在于说明CCIR 的损失函数本质上和单轮 CIR 是一样的变化的不是损失而是query_embeds的生成方式。5. 数据准备与训练流程指南5.1 需要什么样的数据CCIR 训练数据必须具备“多轮”结构。一条完整的训练样本应该包含多轮对话记录每轮包含参考图、修改文本、目标图可能还有用户当前轮的最终检索意图标注可选的负样本标注或 hard negative 图。目前公开的纯 CCIR 数据集数量和规模都还比较有限。实际落地时更常见的做法是从单轮 CIR 数据中自构建多轮样本。比如把一个包含 5 张图像、多个属性标签的数据集按属性顺序拆成多轮修改每轮逐步修改颜色、材质、尺寸等属性。这种方法能快速生成大量带上下文依赖的训练数据。5.2 数据格式建议推荐使用 JSONL 格式存储便于流式读取和分布式训练{dialogue_id: sample_001, rounds: [ {image_id: img_101, text: 类似这种款式的餐桌, target: img_201}, {image_id: img_201, text: 桌面改成长方形, target: img_202}, {image_id: img_202, text: 桌腿换成金属材质, target: img_203} ]}读取时每一轮都可以变成一条训练样本模型看到前若干轮的“参考图 修改文本”目标是对应当前轮的target图像。这样一条三轮回合可以拆成三条训练样本显著提高数据利用率。5.3 训练流程拆解CCIR 的训练流程可以分成四步数据预处理下载并清洗图像去掉重复图、损坏图统一图像尺寸把文本转成 token。构建批次每个批次内保证有足够的 hard negative多轮样本要按对话 ID 组织避免同一个对话的轮次流落到不同 batch。模型训练先加载预训练的 CLIP 权重作为视觉和文本编码器初始化冻结一部分参数只训练上下文融合模块和部分可训练参数。先用小学习率预热再逐步放开。验证与调优在验证集上按轮次拆解评估分别观察第 1 轮、第 2 轮、第 3 轮以后的检索指标判断模型是否真的学到了跨轮依赖。环境方面建议使用 PyTorch 2.x配合 CUDA 和 fp16 混合精度训练。具体的版本号请以实际项目的依赖为准本文不过度锁定版本。6. 核心代码实现与推理示例这一节给出一套可参考的最小实现框架。完整项目还会涉及数据加载、评估脚本和部署服务但核心逻辑集中在模型和推理部分。6.1 定义上下文感知的组合检索模型# 文件路径coco_ir/modeling.py import torch import torch.nn as nn import torch.nn.functional as F class RoundEncoder(nn.Module): 单轮图 文本融合编码器 def __init__(self, vision_dim768, text_dim512, hidden_dim512): super().__init__() self.fusion nn.Sequential( nn.Linear(vision_dim text_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), ) def forward(self, image_feat, text_feat): concat_feat torch.cat([image_feat, text_feat], dim-1) return self.fusion(concat_feat) class ContextEncoder(nn.Module): 跨轮上下文编码器 def __init__(self, hidden_dim512, num_layers2): super().__init__() self.gru nn.GRU(hidden_dim, hidden_dim, num_layersnum_layers, batch_firstTrue) self.layer_norm nn.LayerNorm(hidden_dim) def forward(self, round_states): # round_states: [batch, rounds, hidden_dim] out, _ self.gru(round_states) last_state out[:, -1, :] return self.layer_norm(last_state) class CoCoIRModel(nn.Module): def __init__(self, vision_encoder, text_encoder, hidden_dim512): super().__init__() self.vision_encoder vision_encoder self.text_encoder text_encoder self.round_encoder RoundEncoder(hidden_dimhidden_dim) self.context_encoder ContextEncoder(hidden_dimhidden_dim) def encode_round(self, image, text): img_feat self.vision_encoder(image) # [batch, vision_dim] txt_feat self.text_encoder(text) # [batch, text_dim] return self.round_encoder(img_feat, txt_feat) def forward(self, images, texts, maskNone): # images: [batch, rounds, 3, H, W] # texts: [batch, rounds] batch_size, num_rounds images.size(0), images.size(1) round_states [] for t in range(num_rounds): round_feat self.encode_round(images[:, t], texts[:, t]) round_states.append(round_feat) round_states torch.stack(round_states, dim1) context_vector self.context_encoder(round_states) return context_vector核心逻辑是先把每一轮的图像特征和文本特征拼起来过一层 MLP 得到单轮融合向量再把所有轮次向量喂给 GRU取最后一轮隐状态作为上下文向量。这里使用 GRU 而不是 Transformer是为了在小规模数据上更容易收敛也方便做实时拼接增量上下文。6.2 训练入口# 文件路径tools/train.py import torch from torch.utils.data import DataLoader from coco_ir.modeling import CoCoIRModel from coco_ir.loss import CCIRContrastiveLoss # 以实际项目加载方式为准 train_loader DataLoader(ccir_dataset, batch_size64, shuffleTrue) model CoCoIRModel(vision_encodervision_encoder, text_encodertext_encoder) optimizer torch.optim.AdamW(model.parameters(), lr1e-4) criterion CCIRContrastiveLoss(temperature0.07) for step, batch in enumerate(train_loader): images batch[images] # [batch, rounds, 3, H, W] texts batch[texts] # [batch, rounds] target_images batch[targets] # [batch, 3, H, W] query_embeds model(images, texts) target_embeds vision_encoder(target_images) loss criterion(query_embeds, target_embeds) optimizer.zero_grad() loss.backward() optimizer.step() if step % 100 0: print(fstep{step}, loss{loss.item():.4f})训练循环本身并不复杂真正的复杂度在于批次组织。必须确保同一个dialogue_id的多轮样本不会被打散。建议自定义collate_fn按对话 ID 分组后再截断或补齐到统一轮数。6.3 推理与检索流程推理阶段可以把候选图像库中的所有图像特征预先提取并建索引。线上请求到达时只需要编码当前对话的上下文向量然后在索引中做最近邻检索# 文件路径coco_ir/inference.py import torch import torch.nn.functional as F def retrieval(model, image_index, query_images, query_texts, top_k10): # query_images: [1, rounds, 3, H, W] # query_texts: [1, rounds] model.eval() with torch.no_grad(): query_embeds model(query_images, query_texts) # [1, hidden_dim] query_embeds F.normalize(query_embeds, dim-1) # image_index: [num_candidates, hidden_dim]预先归一化 scores query_embeds image_index.T topk_scores, topk_indices torch.topk(scores, ktop_k, dim-1) return topk_indices[0].tolist(), topk_scores[0].tolist()注意推理前要对上下文向量和候选图向量做 L2 归一化否则余弦相似度计算会被向量模长干扰。7. 评估指标与实验验证CCIR 的评估不能只看整体指标必须拆解到不同轮次和不同上下文类型上。7.1 主要指标组合图像检索最常用的指标是RecallK含义是前 K 个检索结果中包含正确目标图的概率。对多轮任务还要额外关注MRR平均倒数排名和NDCGK归一化折损累积增益因为它们能反映正确结果排名的靠前程度。R1第一个结果就是正确图的概率。R10前十个结果里命中正确图的概率更贴近实际产品召回评估。MRR正确答案排名的倒数平均值越大说明排序越靠前。7.2 按轮次拆解的评估方法多轮检索最大的风险是“越往后越差”。因为前面轮次引入的错误条件会被累积放大。所以验证阶段一定要按轮次统计指标轮次R10MRR说明第 1 轮0.720.35退化成单轮 CIR作为基线第 2 轮0.680.31上下文增益开始体现第 3 轮及以后0.650.29如果下降明显说明长程依赖建模不足如果第 3 轮以后指标明显下滑优先检查上下文编码器尤其是 GRU/Transformer 的输入长度是否超过有效建模范围。另一个常见问题是训练数据的轮次分布不均匀导致模型见过大量 2 轮样本却很少见 5 轮样本。7.3 消融实验的设计在做技术选型时建议至少设计三组消融对比无上下文基线每轮独立 CIR完全丢弃历史信息简单拼接把历史文本直接拼进当前文本图像只保留当前轮参考图完整 CoCo-IR使用图像 文本 上下文融合模块。通过这三组对比可以清楚看到上下文融合模块带来的收益也能判断当前数据量是否足以支撑更复杂的上下文建模。8. 常见问题与排查方法在多轮上下文检索的实际开发中下面这些问题出现频率最高问题现象可能原因排查方式解决方案第 1 轮效果好多轮后指标骤降上下文编码器轮次建模能力不足分别统计各轮次 R10查看轮次曲线增加上下文编码器层数或改用 Transformer 编码器模型无视历史约束训练时轮次样本随机打散上下文丢失检查 batch 中是否出现同一 dialogue 跨批次按对话 ID 重排数据确保上下文完整性训练 loss 不下降正负样本构造不合理难负样本过少检查 batch 内负样本分布增加属性级 hard negative推理速度太慢每轮实时编码整段上下文打印各模块耗时对历史轮次特征做缓存增量更新上下文向量查询文本中带否定词时结果反了文本编码器对否定语义不敏感单独构建否定样本测试在损失函数中加大否定样本权重或做数据增强图像库规模大后召回变差特征索引没有更新或向量量化损失严重校验索引库特征版本使用更精确的索引参数定期全量重建一个值得特别强调的坑是**训练和线上推理的轮次分布不一致。**如果训练时所有样本都固定为 5 轮但线上用户经常只聊到第 2 轮就搜索模型会表现得很不稳定。建议训练时对轮次数量做随机截断让模型在不同长度的上下文中都能正常工作。9. 最佳实践与工程落地建议结合前面的分析和工程经验这里整理几条 CCIR 落地的高优先级建议。9.1 先解决数据问题再优化模型结构多轮检索的模型结构并不神秘真正决定上限的是训练数据的质量。建议优先投入资源做两件事一是把现有单轮 CIR 数据改造成多轮结构二是从真实线上日志中收集多轮交互轨迹然后清洗成训练样本。真实数据的价值远大于人工构造的数据因为真实用户会使用否定、含糊表达和指代词这些在人工数据中很难模拟。9.2 注意上下文压缩策略随着轮次增加直接把全部历史图像特征塞进上下文编码器显存和延迟都会线性上涨。更稳妥的做法是设置一个上下文窗口比如只保留最近 4 到 6 轮。如果历史信息需要长期保留可以使用“记忆摘要”机制把早前的条件压缩成向量后再参与后续计算。9.3 特征索引与版本管理线上检索依赖预先构建的图像特征索引。每次更新模型权重之后必须同步重建索引否则会出现“模型改了、索引没改”的线上事故。建议在 CI/CD 流程中把“模型发布 索引重建 回滚脚本”打包成一个发布单元。任何一步失败都要能快速回滚到上一版本。9.4 灰度与评估方法新模型上线前建议用离线评估集跑一次完整评估再选择小流量灰度。灰度期间除了看检索点击率、转化率还要单独看多轮场景的转化漏斗因为多轮检索对最终成交的贡献往往分散在多次交互中只看首轮效果会低估新模型的价值。9.5 可解释性与兜底策略多轮检索系统一定会遇到没有匹配结果的场景。建议在线上增加“条件解析”功能把每一轮的约束条件以标签形式展示给用户比如“3 人位、皮质、浅色、不要贵妃榻”。这样用户能理解系统为什么返回这些结果也更容易在无结果时修改约束。这一步虽然不是核心算法但往往决定了用户是否愿意继续第二轮搜索。10. 总结与后续学习方向CoCo-IR 和上下文组合图像检索本质上是在回答一个问题当用户连续多轮表达需求时检索系统如何把每一轮的增量信息都转化为更精确的搜索结果。这篇文章讲清楚了几个关键点CCIR 与单轮 CIR 的概念边界上下文建模带来的三重技术挑战从模型结构、数据构造到训练评估的完整实现路径以及工程落地中容易踩坑的细节。如果你准备动手实践建议按这个顺序推进先找一个开源的 CIR 数据集比如 Fashion-IQ理解单轮组合检索的基本流程用第 6 节的最小模型框架把单轮数据改造成多轮样本跑通训练和推理增加上下文编码器和对比损失之外的 hard negative 样本观察多轮指标变化再根据你的业务场景补充真实对话日志和评估体系。后续值得深入的方向还包括用多模态大模型做上下文解析与条件提炼、在更大的图文数据上做端到端预训练、以及把检索结果融入生成式推荐话术。对于大多数团队来说不要一上来就追求最复杂的模型先把“多轮上下文丢得一干二净”这个基本问题解决就已经能带来明显的产品体验提升。