为什么需要多模态RAG?

传统RAG只能处理文字,但现实世界的信息是多维的。技术文档里穿插架构图、产品手册里有演示截图、研究报告里有数据图表——这些信息,纯文本RAG完全丢失了。

用户问"系统的整体架构是什么样的?",纯文本RAG只会返回一段文字描述,而无法返回架构图。这就是多模态RAG要解决的问题。

多模态RAG架构全景

┌─────────────────────────────────────────────────────────┐
│                  多模态RAG系统架构                        │
├─────────────────────────────────────────────────────────┤
│                                                          │
│  用户查询                                                 │
│     │                                                    │
│     ▼                                                    │
│  ┌─────────────┐                                        │
│  │ Query 分析   │ 判断用户需要图片、文字、还是图文         │
│  └──────┬──────┘                                        │
│         │                                                │
│    ┌────┴────┐                                           │
│    ▼         ▼                                           │
│ [纯文本查询]  [需要图片查询]                               │
│    │         │                                          │
│    ▼         ▼                                           │
│ 文本检索    多模态检索                                     │
│ (向量库)   (CLIP/图片向量)                                │
│    │         │                                          │
│    └────┬────┘                                           │
│         ▼                                                │
│  ┌─────────────┐                                        │
│  │ 结果融合     │ 图文结果融合排序                        │
│  └──────┬──────┘                                        │
│         │                                                │
│         ▼                                                │
│ 多模态LLM生成回答(可理解和描述图片)                       │
│                                                          │
└─────────────────────────────────────────────────────────┘

四种架构方案详解

方案1:独立双路检索(Easiest)

文本向量库     图片向量库
   │             │
   └──► 合并 ◄──┘
   多模态LLM生成

最简单也是最常见的方案:文本和图片分别Embedding,分别检索,然后合并结果交给多模态LLM生成回答。

from sentence_transformers import SentenceTransformer
from PIL import Image
import clip  # OpenAI CLIP

# 文本Embedding
text_model = SentenceTransformer("BAAI/bge-large-zh-v1.5")

# 图片Embedding(CLIP)
clip_model, preprocess = clip.load("ViT-L/14", device="cuda")

def retrieve_text(query, top_k=5):
    query_emb = text_model.encode(query)
    results = text_collection.search(query_emb, top_k)
    return [{"type": "text", "content": r["text"], "score": r["score"]} for r in results]

def retrieve_image(query, top_k=5):
    # 用CLIP将文本查询转为图片检索向量
    text_tokens = clip.tokenize([query]).to("cuda")
    with torch.no_grad():
        query_emb = clip_model.encode_text(text_tokens).cpu().numpy()
    
    results = image_collection.search(query_emb, top_k)
    return [{"type": "image", "url": r["url"], "caption": r["caption"], "score": r["score"]} for r in results]

def multimodal_rag(query):
    # 双路检索
    text_results = retrieve_text(query, top_k=3)
    image_results = retrieve_image(query, top_k=2)
    
    # 合并结果
    all_results = text_results + image_results
    
    # 按相关性排序
    all_results.sort(key=lambda x: x["score"], reverse=True)
    
    # 构建多模态上下文
    context = build_multimodal_context(all_results)
    
    # 多模态LLM生成
    response = multimodal_llm.generate(query, context)
    return response

优点:实现简单,各组件独立 缺点:文本和图片无法互相检索(文字查不到图片内容,图片查不到文字) 适用:图片内容简单(截图、照片),不需要跨模态理解

方案2:CLIP统一Embedding空间(Best Balance)

文本 ──► CLIP Encoder ──► 统一向量空间 ◄── CLIP Encoder ◄── 图片
              │                                      │
              └────────► 向量库 ◄────────────────────┘

CLIP的核心能力:将图片和文字映射到同一个向量空间。这意味着"一张猫的图片"和"猫"这个词在向量空间中距离很近。

import torch
import clip
from PIL import Image

# 加载CLIP模型
device = "cuda" if torch.cuda.is_available() else "cpu"
model, preprocess = clip.load("ViT-L/14", device=device)

def encode_text(texts):
    """用CLIP编码文本"""
    tokens = clip.tokenize(texts).to(device)
    with torch.no_grad():
        text_features = model.encode_text(tokens)
    return text_features.cpu().numpy()

def encode_image(image_paths):
    """用CLIP编码图片"""
    images = [preprocess(Image.open(p)).unsqueeze(0).to(device) for p in image_paths]
    images = torch.cat(images)
    with torch.no_grad():
        image_features = model.encode_image(images)
    return image_features.cpu().numpy()

# 索引构建:将所有内容(文本+图片)用CLIP编码存入同一向量库
def build_unified_index(documents, images):
    all_embeddings = []
    all_metadata = []
    
    # 文本编码
    for doc in documents:
        emb = encode_text([doc["text"]])[0]
        all_embeddings.append(emb)
        all_metadata.append({"type": "text", "content": doc["text"], "source": doc["source"]})
    
    # 图片编码
    for img in images:
        emb = encode_image([img["path"]])[0]
        all_embeddings.append(emb)
        all_metadata.append({"type": "image", "path": img["path"], "caption": img["caption"]})
    
    # 存入向量库(Milvus/Qdrant等)
    vector_store.add(all_embeddings, all_metadata)

# 检索:统一接口
def unified_retrieve(query, top_k=5):
    """用CLIP统一检索文本和图片"""
    query_emb = encode_text([query])[0]
    results = vector_store.search(query_emb, top_k=top_k)
    return results

优点:跨模态检索,文字可以查到相关图片,图片也可以查到相关文字 缺点:CLIP对长文本理解有限(只支持77个token) 适用:中等长度文本+图片的混合场景

方案3:图片Caption增强检索(Best for 图片为主)

图片 ──► 图片Captioning模型 ──► 文字描述 ──► 文本向量库
        │                                                     │
        │                                                     ▼
        └──► 原始图片 ──────────────────────────► 多模态LLM上下文

核心思路:先用图片Captioning模型(如BLIP-2)将图片转为文字描述,然后只维护一个文本向量库。检索时只用文字,但返回时带上原始图片。

from transformers import Blip2Processor, Blip2ForConditionalGeneration
from PIL import Image

# 图片Captioning
caption_processor = Blip2Processor.from_pretrained("Salesforce/blip2-opt-2.7b")
caption_model = Blip2ForConditionalGeneration.from_pretrained("Salesforce/blip2-opt-2.7b", torch_dtype=torch.float16).to("cuda")

def generate_caption(image_path):
    """为图片生成文字描述"""
    image = Image.open(image_path).convert("RGB")
    inputs = caption_processor(images=image, return_tensors="pt").to("cuda", torch.float16)
    generated_ids = caption_model.generate(**inputs, max_length=50)
    caption = caption_processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
    return caption

def build_image_augmented_index(images):
    """为图片生成描述后存入文本向量库"""
    for img in images:
        # 生成图片描述
        caption = generate_caption(img["path"])
        
        # 将描述和图片路径一起存入
        doc = {
            "text": caption,
            "metadata": {
                "type": "image_with_caption",
                "image_path": img["path"],
                "caption": caption,
            }
        }
        text_vector_store.add(doc)

def retrieve_with_images(query, top_k=5):
    """检索:返回文字结果和图片结果"""
    results = text_vector_store.search(query, top_k)
    
    formatted = []
    for r in results:
        if r["metadata"]["type"] == "image_with_caption":
            formatted.append({
                "type": "image",
                "image_path": r["metadata"]["image_path"],
                "caption": r["metadata"]["caption"],
                "score": r["score"],
            })
        else:
            formatted.append({
                "type": "text",
                "content": r["text"],
                "score": r["score"],
            })
    
    return formatted

优点:只用文本向量库,架构简单;图片有文字描述,可检索 缺点:Caption可能丢失图片细节;长文档中的图片不好处理 适用:图片为主的知识库,如图表库、截图库

方案4:多向量多索引(Most Flexible)

文档 ──► 分块 ──► 文本块 ──► 文本向量库
  │                    │
  │                    └──► 图片提取 ──► 图片向量库
  │                                         │
  └─────────────────────────────────────────┘
            结果融合 + 多模态LLM

最完整的方案:对文档同时维护文本索引和图片索引,用多路检索结果融合。

from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
import fitz  # PyMuPDF for image extraction

def process_pdf_with_images(pdf_path):
    """处理PDF,同时提取文字和图片"""
    
    # 提取文字
    loader = PyPDFLoader(pdf_path)
    pages = loader.load()
    
    # 分块
    splitter = RecursiveCharacterTextSplitter(chunk_size=500, overlap=50)
    text_chunks = splitter.split_documents(pages)
    
    # 提取图片
    doc = fitz.open(pdf_path)
    images = []
    for page_num in range(len(doc)):
        for img_index, img in enumerate(doc.get_page_images(page_num)):
            xref = img[0]
            base_image = doc.extract_image(xref)
            image_bytes = base_image["image"]
            
            # 保存图片
            img_path = f"images/page{page_num}_img{img_index}.png"
            with open(img_path, "wb") as f:
                f.write(image_bytes)
            
            images.append({
                "path": img_path,
                "page": page_num,
                "parent_doc": pdf_path,
            })
    
    return text_chunks, images

def build_multivector_index(text_chunks, images):
    # 文本索引
    for chunk in text_chunks:
        text_store.add({
            "content": chunk.page_content,
            "metadata": chunk.metadata,
        })
    
    # 图片索引(用CLIP)
    for img in images:
        img_emb = encode_image([img["path"]])[0]
        image_store.add({
            "embedding": img_emb,
            "metadata": img,
        })

2026年多模态LLM选型

多模态RAG的最后一步是多模态LLM生成,它需要能理解和描述图片。

模型图片理解图片生成上下文窗口推荐场景
Claude 4 Sonnet⭐⭐⭐⭐⭐200K复杂图表分析
GPT-5⭐⭐⭐⭐⭐128K通用多模态
Gemini 2.5 Pro⭐⭐⭐⭐1M超长图文
Qwen-VL-Plus⭐⭐⭐⭐8K中文场景
LLaVA-1.6⭐⭐⭐4K本地部署

多模态生成示例

from anthropic import Anthropic

client = Anthropic(api_key="your-key")

def multimodal_generate(query, text_context, image_paths):
    """多模态LLM生成"""
    
    # 构建消息:文字+图片
    content = [{"type": "text", "text": f"参考资料:\n{text_context}\n\n问题:{query}"}]
    
    # 添加图片
    for img_path in image_paths:
        with open(img_path, "rb") as f:
            img_data = base64.b64encode(f.read()).decode()
        content.append({
            "type": "image",
            "source": {
                "type": "base64",
                "media_type": "image/png",
                "data": img_data,
            }
        })
    
    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=4096,
        messages=[{"role": "user", "content": content}],
    )
    return response.content[0].text

性能优化要点

1. 图片预筛选

不是所有图片都值得检索。先过滤掉:装饰性图片、重复图片、过小的图标。

def is_meaningful_image(img_path, min_size=100):
    """判断图片是否有信息价值"""
    img = Image.open(img_path)
    w, h = img.size
    
    # 过滤小图标
    if w < min_size or h < min_size:
        return False
    
    # 过滤纯色/装饰图片(通过图像复杂度判断)
    gray = img.convert("L")
    std = np.std(np.array(gray))
    if std < 10:  # 几乎纯色
        return False
    
    return True

2. 图文联合Embedding

对于"文字+配图"的文档,将文字和图片的Embedding拼接后进行检索:

def joint_embedding(text_emb, image_emb, fusion_method="concat"):
    """文字和图片的联合表示"""
    if fusion_method == "concat":
        return np.concatenate([text_emb, image_emb])
    elif fusion_method == "weighted_sum":
        return 0.7 * text_emb + 0.3 * image_emb  # 文字权重更高
    elif fusion_method == "attention":
        # 用Cross-Attention融合
        return cross_attention_fusion(text_emb, image_emb)

总结

多模态RAG的选型建议:

你的场景推荐方案
图片是辅助(截图、图标)方案1:独立双路
图文并重,需要跨模态检索方案2:CLIP统一空间
图片为主,需要描述方案3:Caption增强
复杂文档(PDF/PPT)方案4:多向量多索引

2026年的趋势是CLIP统一空间 + 多模态LLM生成成为标准配置。核心挑战不再是模型能力不足,而是如何高效处理海量图片的Embedding和更新

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。