多模态RAG并不是简单地把图片文件路径塞进上下文窗口。如果检索系统只对文本块建立向量索引,那么当用户问“这张架构图里的数据流经过哪些组件”时,系统既无法理解图片内容,也不能找到对应的文本描述。要实现图文混合检索,第一步是让模型能够同时理解文本和图像,并把它们映射到同一个可计算的向量空间。这个过程通常依赖对比学习训练的多模态嵌入模型,例如CLIP、SigLIP或BLIP系列,它们通过大量图文对学习联合表示,使得语义相近的图片和文字在向量空间中距离更近。

多模态RAG的核心挑战:文本与图像的对齐
传统RAG的基础组件是文本嵌入模型,比如BGE、Sentence-BERT或OpenAI的text-embedding系列。这些模型只能接收字符串输入,图片必须经过OCR或被人工描述成文字后才能进入索引。然而,OCR会丢失版面结构、颜色关系、图表中的视觉隐喻等信息。例如一张流程图里箭头方向可能代表数据流向,但OCR只输出“组件A”和“组件B”两个词,无法体现它们之间的连接关系。更麻烦的是,即使你用某种方式把图片转成了一段文字描述,那个描述所用的嵌入空间和原始查询的嵌入空间仍然可能不一致,导致相似度计算失真。
解决这个问题的关键是用多模态编码器统一文本与图像的表示。CLIP是最具代表性的模型之一,它采用对比学习策略:在一个批次里,每张图片对应一条正确文本描述,模型不断拉近匹配图文对的向量,同时推远不匹配图文对。训练完成后,文本编码器和图像编码器各自输出向量,这些向量位于同一个语义空间,可以直接用余弦相似度或内积衡量相关性。正因为如此,一个纯文本查询“包含登录按钮的界面截图”可以和一张实际的登录界面图片在向量距离上非常接近。
下面这段代码展示了如何用Hugging Face Transformers加载CLIP模型,对一张本地图片和几条候选文本分别编码,并输出相似度概率。注意在实际项目中,图片路径应替换成你自己的文件路径。
import torch
from PIL import Image
from transformers import CLIPProcessor, CLIPModel
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
image = Image.open("architecture.png")
texts = ["系统架构图", "数据流图", "用户登录流程图"]
inputs = processor(text=texts, images=image, return_tensors="pt", padding=True)
outputs = model(**inputs)
logits_per_image = outputs.logits_per_image
probs = logits_per_image.softmax(dim=1)
print(probs)
运行后会得到一个概率分布,最高的那一项说明模型认为图片内容和哪条文本最匹配。这种方式不仅适用于图文匹配,还能直接用于以文搜图:把图片全部离线编码成向量,查询时把问题编码成文本向量,再做最近邻搜索即可。
图文混合索引的构建方案
构建图文混合索引通常有两条路线。第一条是统一向量空间方案,把文本和图像都通过同一个多模态编码器转成向量,然后全部写入同一个向量集合。这样做的好处是实现简单,检索时只需要跑一次查询,不需要维护两套索引。缺点是如果某个模态的嵌入质量不够好,会拖累整体效果。第二条是分离向量空间方案,为文本和图像分别建立独立的索引,查询时各自召回再融合排序。这种方式灵活度更高,可以给两种模态配置不同的嵌入模型和距离度量,但代价是增加了一个融合排序层,工程复杂度更高。
对于大多数团队来说,初期建议先采用统一向量空间方案。如果使用FAISS作为本地向量索引,下面的代码演示了如何把图像向量和文本向量合并写入同一个索引,并执行一次文本查询。
import faiss
import numpy as np
import torch
from transformers import CLIPProcessor, CLIPModel
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
# 假设已经准备好一批图片和对应文本
image_paths = ["img1.png", "img2.png"]
texts = ["登录界面截图", "数据库表关系图"]
all_vecs = []
# 图像向量
for path in image_paths:
image = Image.open(path)
inputs = processor(images=image, return_tensors="pt")
vec = model.get_image_features(**inputs).detach().numpy()
all_vecs.append(vec)
# 文本向量
inputs = processor(text=texts, return_tensors="pt", padding=True)
text_vecs = model.get_text_features(**inputs).detach().numpy()
all_vecs.extend(text_vecs)
all_vecs = np.vstack(all_vecs).astype("float32")
dim = all_vecs.shape[1]
index = faiss.IndexFlatIP(dim) # 内积相似度
index.add(all_vecs)
# 查询文本
query = "包含用户名和密码输入框的页面"
query_inputs = processor(text=[query], return_tensors="pt", padding=True)
query_vec = model.get_text_features(**query_inputs).detach().numpy()
D, I = index.search(query_vec, k=3)
print("top indices:", I[0])
除了向量本身,实际系统还需要为每个向量关联元数据,例如来源类型(image或text)、文件路径、所在文档页码、所属章节等。这些信息在检索命中后非常重要,因为下游大模型需要知道命中的到底是一段文字还是一张图片,以及从哪里加载内容。Milvus、Qdrant、Weaviate等向量数据库都支持标量过滤,可以在查询时只搜索特定类型或特定来源的数据。
另外还有一个容易被忽视的细节:图片上传后是否要做预处理。大尺寸图片会拖慢编码速度,建议在入库前统一缩放或裁剪,并去除EXIF信息。对于扫描件,还可以先做旋转校正和去噪,这些都会直接影响后续嵌入质量。
检索策略与重排序:如何提升图文混合检索效果
如果查询只涉及文本,直接返回和查询向量最接近的top_k项通常就够用了。但图文混合场景下,用户的问题可能同时需要文本证据和图像证据。比如问“这个API的鉴权流程是怎么设计的”,理想的召回结果里既要有描述鉴权流程的段落,也要有相关的时序图或架构图。单一的距离排序很难保证两种类型的证据都能进入最终的上下文窗口。因此,多路召回加融合排序是更实用的策略。
具体做法是:分别从文本索引和图像索引中召回top_k条结果,然后根据可配置的权重把两路分数合并。文本相关性通常更重要,可以给文本路召回更高权重;但如果查询本身包含“截图”“图片”“流程图”等视觉信号词,就可以动态提高图像路召回的权重。下面是一个简单的合并函数示例。
def merge_results(text_hits, image_hits, alpha=0.7):
merged = {}
for item in text_hits:
merged[item["id"]] = merged.get(item["id"], 0.0) + alpha * item["score"]
for item in image_hits:
merged[item["id"]] = merged.get(item["id"], 0.0) + (1 - alpha) * item["score"]
rank_list = sorted(merged.items(), key=lambda x: x[1], reverse=True)
return rank_list
多路召回之后,如果对精度要求较高,还可以引入重排序模型。Cross-Encoder类模型会把查询和候选文档拼在一起做一次深度交互计算,精度远高于双塔模型的向量内积,但推理延迟也高得多。实际工程中通常只对召回的前20到50条做重排,再由大模型生成最终答案。对于包含图片的候选,可以把图片先转成文本描述或缩略图输入多模态重排器,判断图文相关性。
还有一种值得关注的方案是ColPali,它直接对文档页面的截图进行嵌入,把版面视觉信息和文字信息同时编码到向量里,在PDF这类富格式文档检索上表现很好。这类模型跳过了传统的OCR加版面分析流程,减少了信息损失,但计算成本也更高。
实际落地中的工程考量与优化
把图文混合检索落地到生产环境,首先要考虑向量数据库的选型。如果数据量在百万级以内,FAISS或轻量级向量数据库足够应付;如果数据量达到千万甚至上亿,就需要Milvus、Qdrant这类支持分布式部署和持久化的方案。同时,图像嵌入模型的推理速度比文本慢不少,建议把图片编码任务放到离线批处理流程中,而不是在用户查询时实时编码。新图片入库时,可以用消息队列异步触发嵌入任务,避免阻塞主流程。
缓存也是提升用户体验的重要手段。很多查询存在明显热点,比如“登录页面长什么样”这类问题会反复出现。可以对查询文本做一次哈希,把前50条召回结果缓存到Redis,命中缓存后直接返回,省去向量检索和多模态编码的时间。对于图片去重,可以用感知哈希或图像指纹,避免同一张图被反复缩放或另存后重复入库。
安全性方面,不建议把图片转成base64字符串直接塞进大模型的上下文窗口,因为一张高清截图可能占用数万个token,导致成本飙升且上下文超限。更稳妥的做法是:给大模型提供命中的文件路径、OCR摘要或图片的文本描述,由下游服务按需加载图片再执行视觉理解。如果必须让大模型直接看图片,可以先把图片压缩到合理尺寸,并确保只传命中的少数几张。
最后别忘了评估。图文混合检索的效果很难用单一的文本相似度指标衡量,需要构建包含图文对的人评基准,同时观察端到端问答的准确率、响应延迟和索引更新延迟。可以先从小规模数据开始验证架构,再逐步扩大图片量和文档量,这样才能在可控的复杂度下获得稳定的检索质量。