RAG 多模态检索方案:图片 + 文本的联合 Embedding 工程实践

📅 2026/7/15 22:47:57 👁️ 阅读次数 📝 编程学习
RAG 多模态检索方案:图片 + 文本的联合 Embedding 工程实践

RAG 多模态检索方案:图片 + 文本的联合 Embedding 工程实践

一、"用户上传了截图问'这个报错怎么解决',系统检索不到"

用户上传了一张报错截图并问"这个怎么解决"。RAG 系统试图对截图中的文字做 OCR 提取文本,但 OCR 准确率只有 80%,把NullPointerException识别成了NullPointerExceplion,向量检索自然找不到匹配的文档。

传统 RAG 只能处理文本。多模态 RAG 让系统能直接理解图片内容,而不依赖不完美的 OCR 中间层。核心思路:用多模态 Embedding 模型(如 CLIP)将图片和文本映射到同一个向量空间,实现图文混合检索。

二、多模态 RAG 的联合检索架构

flowchart TD Q[用户查询: 文字+图片] --> SPLIT{内容类型} SPLIT -->|文字| TE[文本 Embedding 模型] SPLIT -->|图片| IE[图像 Embedding 模型: CLIP] TE --> VEC_T[文本向量] IE --> VEC_I[图像向量] VEC_T --> FUSE[向量融合: 加权平均或拼接] VEC_I --> FUSE FUSE --> SEARCH[向量库检索] subgraph Index[多模态知识库索引] T_DOC[文本文档: 文本向量] I_DOC[截图/图片: 图像向量] T_DOC --> IDX[统一向量索引] I_DOC --> IDX end SEARCH --> IDX IDX --> RESULTS[Top-K 多模态结果] RESULTS --> CONTEXT[拼接: 文本+图片描述] CONTEXT --> LLM[多模态 LLM 推理] LLM --> ANSWER[回答]

关键技术点:CLIP 模型确保图文向量在同一个语义空间、向量融合策略(平均/加权/拼接)影响检索精度、知识库需要同时为图片和文本建立向量索引。

三、Python 实现:多模态检索

import io import base64 import numpy as np from dataclasses import dataclass from typing import Optional, Union from PIL import Image import openai # ========== 多模态文档 ========== @dataclass class MultimodalDocument: """多模态文档:文本 + 可选图片""" doc_id: str text_content: str image_base64: Optional[str] = None # 截图 text_embedding: Optional[np.ndarray] = None image_embedding: Optional[np.ndarray] = None modality: str = "text" # text, image, mixed # ========== 多模态 Embedding 编码器 ========== class MultimodalEncoder: """多模态编码器:文本 + 图片统一向量化""" def __init__(self, clip_model: str = "ViT-B/32"): self.text_dim = 1536 # text-embedding-ada-002 self.image_dim = 512 # CLIP ViT-B/32 self.fused_dim = self.text_dim + self.image_dim def encode_text(self, text: str) -> np.ndarray: """文本向量化""" response = openai.Embedding.create( model="text-embedding-ada-002", input=text, ) vec = np.array(response["data"][0]["embedding"]) return vec / np.linalg.norm(vec) # L2 归一化 def encode_image(self, image_base64: str) -> np.ndarray: """图像向量化(使用 CLIP)""" # 实际项目中使用 open_clip 或 transformers # import clip # model, preprocess = clip.load("ViT-B/32") # image = preprocess(Image.open(io.BytesIO(base64.b64decode(image_base64)))) # with torch.no_grad(): # vec = model.encode_image(image.unsqueeze(0)) # return vec.numpy().flatten() # 简化模拟 vec = np.random.randn(self.image_dim) return vec / np.linalg.norm(vec) def fuse_embeddings( self, text_vec: np.ndarray, image_vec: Optional[np.ndarray] = None, strategy: str = "concat", text_weight: float = 0.6, image_weight: float = 0.4, ) -> np.ndarray: """融合文本和图像向量 strategy: concat: 拼接 [text_vec, image_vec](保留全部信息) avg: 加权平均(维度对齐) """ if image_vec is None: return text_vec if strategy == "concat": # 拼接后归一化 fused = np.concatenate([text_vec * text_weight, image_vec * image_weight]) return fused / np.linalg.norm(fused) elif strategy == "avg": # 加权平均 fused = text_vec * text_weight + image_vec * image_weight return fused / np.linalg.norm(fused) return text_vec # ========== 多模态索引 ========== class MultimodalIndex: """多模态向量索引""" def __init__(self): self.vectors: list[np.ndarray] = [] self.documents: dict[int, MultimodalDocument] = {} def add_document(self, doc: MultimodalDocument, fused_vec: np.ndarray): """添加文档到索引""" idx = len(self.vectors) self.vectors.append(fused_vec) self.documents[idx] = doc return idx def search( self, query_vec: np.ndarray, top_k: int = 5, ) -> list[tuple[MultimodalDocument, float]]: """检索最相似的文档""" if not self.vectors: return [] # 批量计算余弦相似度 vectors_matrix = np.stack(self.vectors) similarities = np.dot(vectors_matrix, query_vec) # Top-K top_indices = np.argsort(similarities)[::-1][:top_k] results = [] for idx in top_indices: doc = self.documents[int(idx)] score = float(similarities[int(idx)]) results.append((doc, score)) return results # ========== 多模态 RAG 引擎 ========== class MultimodalRAGEngine: """多模态 RAG 引擎""" def __init__(self): self.encoder = MultimodalEncoder() self.index = MultimodalIndex() def index_document(self, doc: MultimodalDocument): """索引文档""" # 计算文本向量 text_vec = None if doc.text_content: text_vec = self.encoder.encode_text(doc.text_content) doc.text_embedding = text_vec # 计算图像向量 image_vec = None if doc.image_base64: image_vec = self.encoder.encode_image(doc.image_base64) doc.image_embedding = image_vec # 融合向量 if image_vec is not None: fused = self.encoder.fuse_embeddings(text_vec, image_vec, "concat") else: fused = text_vec if fused is not None: self.index.add_document(doc, fused) def query( self, text_query: str, image_query: Optional[str] = None, top_k: int = 5, ) -> list[dict]: """多模态查询""" # 编码查询 text_vec = self.encoder.encode_text(text_query) image_vec = None if image_query: image_vec = self.encoder.encode_image(image_query) # 融合查询向量 query_vec = self.encoder.fuse_embeddings(text_vec, image_vec, "concat") # 检索 results = self.index.search(query_vec, top_k) return [ { "doc_id": doc.doc_id, "text": doc.text_content[:200], "has_image": doc.image_base64 is not None, "score": score, "modality": doc.modality, } for doc, score in results ] # ========== 图片预处理 ========== class ImagePreprocessor: """图片预处理工具""" @staticmethod def preprocess_for_embedding(image_path: str) -> str: """预处理图片,返回 base64""" img = Image.open(image_path) # 1. 调整尺寸(CLIP 要求 224x224) img = img.resize((224, 224), Image.Resampling.LANCZOS) # 2. 转 RGB(处理 PNG 透明度等) if img.mode != "RGB": img = img.convert("RGB") # 3. 转 base64 buffer = io.BytesIO() img.save(buffer, format="JPEG", quality=85) return base64.b64encode(buffer.getvalue()).decode() @staticmethod def preprocess_for_display(image_path: str, max_size: int = 1024) -> str: """预处理用于模型输入的图片(大图压缩)""" img = Image.open(image_path) # 保持宽高比缩放到 max_size w, h = img.size if max(w, h) > max_size: ratio = max_size / max(w, h) img = img.resize((int(w*ratio), int(h*ratio)), Image.Resampling.LANCZOS) buffer = io.BytesIO() img.save(buffer, format="JPEG", quality=80) return base64.b64encode(buffer.getvalue()).decode()

四、多模态 RAG 的边界与挑战

CLIP 的 Vector Space 不完美。CLIP 的图文对齐是基于互联网图文对训练的,对于专业领域图片(如架构图、流程图、代码截图)理解能力有限。微调 CLIP 或使用领域专用的多模态模型是必要的。

向量融合的策略影响大。Concat 保留全部信息但维度翻倍,检索速度变慢;Average 维度不变但信息有损。建议从 Average 开始,如果检索精度不够再换 Concat。

图片存储成本。知识库中存储原始图片(如截图)会显著增加存储开销。在送入 Embedding 模型之前压缩图片(224x224),原始大图只在需要展示时才读取。

多模态 LLM 的推理成本。图文混合输入比纯文本消耗更多 Token。在检索阶段用轻量级的 CLIP Embedding,只在最后推理阶段才把图片送给多模态 LLM,避免重复的图片编码开销。

五、总结

多模态 RAG 的核心是把图片和文本映射到同一个向量空间。关键步骤:用 CLIP 做图像 Embedding + 与文本向量做融合 + 在统一索引中检索。实现路径:先验证 CLIP 在你的领域图片上的检索精度,确认可用后再建立完整的图文索引。图片预处理是性价比最高的优化——合适的尺寸和压缩比可以降低 50% 的存储和传输开销。