开放域检索里最难啃的骨头,多模态多跳检索绝对算一个。平时的检索任务,问一句、返回一堆文本段落,顶多再带几张图片;但要是碰到需要把文本、图像、实体关系全搅在一起的查询,传统那套"编码-召回-重排"就不太够用了。这篇 LILaC 的工作,标题全称是"Late Interacting in Layered Component Graph for Open-domain Multimodal Multihop Retrieval",把"晚交互"和"分层组件图"两件事焊在了一起,试图一次性解决多模态检索里的跨模态对齐和多跳推理问题。这篇文章我会从问题拆解、方法设计、工程实现和踩坑记录几个角度把它掰开讲,适合正在做检索、多模态问答或者想复现这类论文的同学参考。
1. 这个任务到底难在哪里
1.1 多跳查询的真实形态
先明确一下什么是"多跳"。普通检索是单跳的:比如搜"某某演员出演过哪些电影",模型把查询向量和文档向量做一次相似度计算,返回 Top-K 就完事。多跳检索的输入往往是组合问题,例如"图片里这个人主演的科幻片导演是谁"。要回答它,系统至少得走两步:先从图像中识别出人物,再把人物映射到电影实体,最后再找导演。每一步都依赖上一步的输出,中间任何一环断了,整个链路就垮了。
这种问题在真实场景里非常常见,新闻问答、购物助手、研究辅助系统都会遇到。难的不是某一跳本身,而是跨跳时的信息传递。文本里提到的实体,在图像里是以视觉特征出现的,两个模态的特征空间天然不对齐。你要么先在检索阶段把所有可能性都捞回来,靠后续的大模型硬推;要么就得设计一种结构,让不同模态的信息在中间显式地"碰面"。
1.2 现有方法的三种尴尬
我梳理了一下目前做多模态多跳检索的主流路子,基本逃不出三类。
第一类是"全量拼接 + 大模型硬看"。把所有检索到的文本和图像全部塞给多模态大模型,让它自己找答案。这样效果好,但代价极大,检索预算稍微一高,上下文就爆了,而且大模型容易产生幻觉,把不在证据里的事实也编出来。
第二类是"模块化流水线"。先用检测器抽人物、用OCR抽文字、用视觉语言模型做 caption,把图像变成文本,再跑传统文本多跳检索。这种做法的问题是信息损失太大,图像里那些无法用一句话描述的细节(物体的空间关系、细微的视觉差异)经过 caption 之后基本就废了。
第三类是"纯向量召回"。用 CLIP 这一类跨模态编码器把文本和图像分别编码,做最近邻检索。速度快,但过分粗粒度,查询里包含多个子问题和多个限定条件时,整体向量的内积计算会把细节全部抹平。
LILaC 走的是另一条路:不把查询当做一个整体向量,而是把它拆成一个个组件;也不把证据文档当做一个整体向量,而是也拆成组件;最后用"晚交互"在组件之间算分数。这样既保留了细粒度的匹配能力,又不像全量交叉编码那样贵到没法上路。
1.3 "晚交互"到底晚在哪里
搞过文本检索的同学对 ColBERT 应该不陌生。晚交互的核心思想是:查询和文档各自用编码器分开过一遍,不做双向 attention,等到最后算相似度时,再让两者每个 token 的向量两两交互,取最大相似度求和。这个"晚"字,指的是交互发生得很晚,身处编码阶段之后。
LILaC 把这个思想扩展到了组件图。它不比较 token,而是比较"组件"。组件的粒度比 token 大,比整个文档小,可能是文档里的一个句子、一个实体节点,或者图像里的一个区域。多跳推理中,第一跳往往只需要匹配查询里的某个子条件与证据中的某个局部区域,没必要把整个查询和整个文档做足全量交互。晚交互恰好给了系统这种"按需对齐"的能力。
2. 整体设计思想:为什么是"分层组件图"
2.1 组件图解决什么问题
多跳检索最核心的痛点是信息碎片化。一个查询里通常包含多个独立的语义单元:例如"这个人主演的科幻片",需要先锁定"人",再找"主演",再过滤"科幻片"。如果证据文档分散在文本段落和图像里,那么系统需要的其实是碎片之间的关联,而不是某个单一碎片的得分。
组件图就是一种显式的关联建模方式。图上每个节点是一个语义组件,每条边代表组件之间的一种关系。文本组件之间可以是共指关系,图像区域和文本实体之间可以是"图中人物与文本中提及一致"的关系,实体组件和描述组件之间可以是"修饰/被修饰"的关系。有了这些边,多跳搜索就变成了在图上做有导向的传播,而不是盲目的暴力组合。
2.2 "分层"的两个层面
图里的"分层"不是指神经网络层数,而是组件粒度的分层。粗粒度层是完整查询或完整证据文档的摘要级表示;中间层是句子级或子查询级的表示;细粒度层是实体、关键短语和图像区域表示。
分层的好处在于,多跳推理的不同阶段可以工作在合适的粒度上。早期召回阶段用粗粒度向量快速圈定候选池;中期重排阶段用句子级组件做精确匹配;后期推理阶段则依赖细粒度的实体和区域节点,去验证跨模态关系是否正确。这种设计非常像人找资料的思路:先大致定位到某几个页面,再细细看某一段,最后盯住某张图里的某个物体。
2.3 和早期融合、双编码器的对比
| 方法范式 | 交互方式 | 计算成本 | 细粒度匹配 | 适合多跳 |
|---|---|---|---|---|
| 双编码器 | 查询整体与文档整体做点积 | 极低 | 弱 | 差 |
| 早期融合 | 查询与文档 token 全交叉 attention | 极高 | 强 | 中 |
| 晚交互(文本Token级) | 查询 token 与文档 token 逐对 max-sim | 中 | 强 | 中 |
| LILaC(组件级晚交互) | 查询组件与文档组件逐对匹配 | 中低 | 强 | 强 |
一句话总结:LILaC 想拿双编码器的效率,做出近似早期融合的质量,再通过组件图把多跳信息显式地留存下来。
3. 分层组件图的构建细节
3.1 查询侧:先把问题拆成子组件
这一步是所有环节里最容易被低估的。查询拆得好不好,直接决定召回质量。实际操作中,核心是让大模型把查询拆成三个层次的组件:整体查询向量、子查询列表、以及关键限定词/实体集合。
举个例子,查询"上图中与红色汽车并排的那栋建筑的建筑师是谁",拆出来的子查询应该至少包括"图中有红色汽车与某建筑并排"、“该建筑的名称”、“该建筑的设计者是谁”三个部分。关键限定词则是"红色汽车"、“并排”、“建筑师”。这些组件最终构成了查询侧的图。
实操中我有一个非常重要的体会:拆解时一定要保留查询里的少量上下文结构词,比如"并排"、"位于"、“曾在……期间”。如果只是简单地把长句切成短句,丢掉修饰关系,后面图建得再漂亮也没用,因为跨模态的空间关系正是靠这些词维持的。
3.2 文档侧:文本与图像的组件抽取
证据文档的形式不光是纯文本,往往是图文混排的网页、PDF 或者多模态知识库条目。文本部分可以依赖现有的段落切分器和实体抽取器,直接得到段落节点和实体节点。图像部分的处理要谨慎得多。
典型做法是先对每张图做 region proposal,得到若干候选区域;再用一个大模型或者检测器给每个区域打标,生成一句话描述;接着通过某种跨模态对齐机制,把区域描述与文本中的实体链接起来。这一步本质上是把图像"半文本化",但不是完全丢弃视觉特征,而是保留了视觉向量和文本描述两套表示。
我在复现类似流程时发现,region proposal 的数量很关键。区域太多,图变得庞大,晚交互阶段的计算量直线上升;区域太少,容易漏掉关键证据。一般控制在每张图 10 到 20 个区域比较合适,先粗后细,重排阶段再适当扩张。
3.3 边怎么建:跨模态对齐的三板斧
组件有了,边是让图真正起作用的黏合剂。我总结了一下,建边主要靠三种信号:
- 语义相似度边:用 CLIP 或类似模型计算图像区域向量与文本片段向量的相似度,超过阈值就建边。这种方法鲁棒性尚可,但容易出现"看起来相似但实际不相关"的误连。
- 实体共指边:文本中的同一个命名实体出现多次时,把它们连起来。图像检测出来的 person id 若与某段文本中的人名对齐,也建跨模态共指边。
- 结构先验边:比如"修饰关系"、"包含关系"、“因果关联”这类知识图谱关系。如果证据源本身带有结构化信息,直接用,不要浪费。
有一个必须注意的坑:相似度边的阈值不要设得太死。跨模态语义相似度的分布方差很大,一个固定的 0.5 阈值很可能在某个领域过松、在另一个领域过紧。建议用分位数策略,比如取每张图中 top 20% 的相似度对来建边,而不是硬编码阈值。
4. 检索与重排流水线的实操实现
4.1 整体流程的五个阶段
一个完整的检索流程可以拆成五步:
- 查询组件化:解析输入查询,产出整体向量、子查询和关键实体。
- 候选召回:用组件化的粗粒度表示,从语料库中召回若干候选文档和候选图像区域。
- 图构建与扩展:为每个候选文档构建组件图,并把对应的图像区域和文本实体接进来。
- 晚交互打分:计算查询组件与候选图组件之间的匹配分数。
- 多跳迭代:如果当前证据不足以支撑完整答案,把已检索到的节点作为新的查询条件,重复步骤 2 到 4。
这个流程最妙的地方在于,迭代过程中查询图会越来越"胖"。每次新增的证据节点都会跟原始查询组件重新建一次边,相当于系统在内部维护了一个不断增长的推理工作区。
4.2 晚交互打分公式的设计思路
组件级别的晚交互打分,形式上可以这样理解:
score = sum(q_m in Q) max(d_n in D) sim(q_m, d_n)其中 Q 是查询组件集合,D 是文档组件集合。每个查询组件只跟自己最匹配的证据组件交互,其余全部忽略。这种 max 操作是对"证据碎片化"的天然适配:查询句子 A 只需要匹配文档里的段落 A',查询图像区域 B 只需要匹配文档里的图像区域 B',互不干扰。
实际实现时,sim(q_m, d_n) 我会用双塔编码器分别得到组件向量,然后计算余弦相似度。注意这里的编码器一定要是多模态统一编码器,输出空间必须一致,否则文本组件向量和图像组件向量没法直接比较。
4.3 多跳迭代什么时候停
迭代检索最怕两件事:停太早漏证据,停太晚引入噪声。我的经验是设置两个终止条件,满足任意一个就停。
第一个是覆盖度条件:查询组件的占比超过九成都能在证据图里找到相似度超过阈值的对应节点,说明证据基本齐了。第二个是收益递减条件:连续两轮新增的有价值节点数量很稀少,再迭代下去也只是往图里塞垃圾。
还有一个细节容易被忽略:每次迭代时,要对新检索到的节点做去重和冲突消解。同样的实体可能在不同来源里以不同的表面形式出现,如果不做节点合并,后面的打分会被重复证据拉偏。
5. 训练策略与工程落地心得
5.1 多任务损失怎么配
训练一个端到端的 LILaC 系统,单一损失肯定不够。至少需要三部分:
- 组件对比损失:让匹配的查询组件与证据组件向量靠近,不匹配的远离。这个损失是晚交互打分的根基。
- 图结构损失:预测两个组件之间是否有边,相当于把建边过程变成可微的。
- 多跳排序损失:综合考虑多条检索路径后,让包含完整答案的文档路径得分高于不完整路径。
三者的权重我建议不要平均分配。先以组件对比损失为主训练到收敛,再逐步加入图结构损失和多跳排序损失。如果一开始就三管齐下,模型会为了凑合结构预测任务而牺牲掉基本的语义对齐能力。
5.2 困难负样本的选择
晚交互模型的最大隐患是"部分匹配陷阱":一个查询组件明明只想匹配证据里的一个局部,模型却可能把证据整体都拉过来。比如查询组件问"红色汽车的颜色",证据里恰好有半句"红色汽车停在路边",那这个证据很容易被误判为高相关。
解决办法就是刻意构造困难负样本。我在实际流程里会从三处收集:一是全局召回阶段排名靠前但实际不匹配的样本;二是同一张图里不参与当前查询匹配的其他区域;三是经过一次多跳检索后,和真实证据路径结构相似但答案错误的路径。这三类负样本能有效压制"假阳性匹配"。
5.3 工程上三个值得关注的地方
第一,向量索引要用分片结构。证据组件数量通常远超查询组件,建 ANN 索引时最好按模态分开建,图像区域索引和文本段落索引互不干扰,查询组件各找各的,最后再合并候选。混在一起建索引,检索时会被不相关的模态噪声直接带偏。
第二,晚交互的打分阶段要批量矩阵化。朴素的循环遍历每个查询组件对每个文档组件做 max 操作,速度惨不忍睹。正确姿势是先一次性算出所有查询组件和所有文档组件的相似度矩阵,再在矩阵上做按行取 max 和求和。这个优化能带来几十倍的速度提升,我建议直接在代码里写成 PyTorch 的三维张量运算。
第三,缓存机制要分两级。查询侧的组件分解结果可以只算一次;文档侧的组件图对同一个文档是固定的,可以预计算做磁盘缓存。否则每次检索都重新跑一遍 region proposal 和实体抽取,延迟高到你怀疑人生。
6. 复现时常见的坑与调优建议
6.1 查询分解:大模型的稳定性和创造力
用大模型做查询分解,最大的麻烦不是效果差,而是不稳定。同一个查询跑十遍,能给你拆出八九种不同的组件结构。这会导致后续检索结果抖动。我的建议是:给大模型的 prompt 里加一个固定的 schema,要求输出必须包含"整体语义"、"子查询列表"、"实体提及"、"关系限定词"四个字段,少一个就重新生成,而不是让它自由发挥。
另外一个更隐蔽的问题是,大模型有时会把隐含的常识写进子查询里,产生不可检索的组件。比如查询"那个在月球上打高尔夫的人是谁",模型可能会拆出一个子查询"谁在月球上打高尔夫",这没问题;但如果它拆出"宇航员在低重力环境下的体育活动",检索系统就彻底懵了。拆解结果必须和原始查询保持词语层面的可逆性,不能随意改写。
6.2 图构建:错误传播比丢失更可怕
建错一条边比漏掉一条边更伤。漏边只是降低召回,建错边会把噪声海量引入后续所有跳次。我见过一个案例:某图像区域和错误的人名实体建了共指边,结果后续所有与这个人名相关的检索全部被该图像污染,最终答案完全跑偏。
建议在训练或离线阶段做一个"边质量审计":随机抽样一批边,人工标注正确率。如果发现某种建边信号(比如纯相似度阈值建边)的错误率超过一定比例,直接砍掉这种信号或者显著提高其置信度门槛。
6.3 打分异常:晚交互的分数会偏大
如果你在复现时发现所有查询的晚交互分数都高得离谱,先别慌。组件数量越多,max 求和的值天然越大。这不是 bug,但会导致跨查询比较失真。解决方式有两种:一是除以查询组件数量的平均值,二是做归一化时使用证据侧组件数量的 log 作为分母。
我遇到过另一个症状:图像组件的加入让整体分数系统性偏高。这是因为图像区域的监督信号往往更稀疏,模型对自己的预测过于自信。处理方式是在多模态打分融合阶段,给视觉模态的分数乘一个 0.8 到 0.9 的经验压低系数,然后再和文本分数相加。
6.4 调优优先级速查表
| 现象 | 首选排查方向 | 有效操作 |
|---|---|---|
| 召回率低 | 查询分解粒度 | 增加子查询数量,让限定词独立成组件 |
| 重排精度差 | 组件对齐质量 | 重建跨模态边,加入实体共指约束 |
| 多跳中途断掉 | 终止条件过严 | 放宽覆盖度阈值,多看一轮候选 |
| 图像证据被忽略 | 视觉组件权重 | 提高图像区域特征维度或减小压缩比例 |
| 训练不收敛 | 损失权重失衡 | 先只训练对比损失,再逐步加结构损失 |
写在最后的实操感悟
这套方法最值得借鉴的不是某个模块本身,而是"把晚交互从 token 级搬到组件级"这个抽象层次。之前做检索总觉得多模态麻烦,本质是缺少一个让视觉和文本在细粒度上平等对话的接口。LILaC 的组件图实际上就提供了这样一个接口,它让多跳检索从"猜组合"变成了"找路径"。
我个人在实际推导中还有一个体会:复现这类论文,花在数据预处理上的精力应该不少于写模型的时间。组件抽取、跨模态对齐、边构造这些环节,处处都是经验活。只要你在 query decomposition 的稳定性和 region proposal 的数量上多下功夫,整个系统的效果往往能比原论文在你们自己数据上还要好。先把这篇文章里的方法论吃透,再去动手,会少走很多弯路。