多模态 RAG 实践:用 Milvus+BGE+GPT-4o 构建智能图像检索系统
通过以上步骤,我们成功搭建了一个基于 Milvus、BGE 和 GPT-4o 的多模态 RAG 系统,实现了结合图像和文本的智能检索。多模态融合:通过 BGE 模型将图像和文本转化为统一的向量空间,让检索不再局限于单一模态。高效检索:Milvus 的向量检索能力能够快速处理海量数据,满足实时搜索需求。智能解释:GPT-4o 的重排和解释功能让检索结果更具可解释性,提升用户体验。数据优化:根据项目需
在日常开发中,我们常常会遇到这样的需求:用户希望通过上传一张图片并添加一些文字描述,就能精准找到与之匹配的目标图像。传统的图像搜索要么单纯依赖图像特征,要么仅基于文本关键词,很难同时理解图像内容和用户的文字意图。有没有一种方法能让机器像人类一样,结合图像和文字两种信息进行智能检索呢?今天,我们就来聊聊如何利用 Milvus、BGE 模型和 GPT-4o 搭建一个多模态 RAG 系统,实现更智能的图像搜索。
一、核心技术栈:让检索更智能的 “三驾马车”
在开始实践之前,我们先了解一下这个系统的核心组件,它们各自承担着不同的关键任务:
- Milvus:作为高性能的向量数据库,它就像一个庞大的 “向量图书馆”,能够快速存储和检索海量的向量数据。我们会把图像和文本转化为向量存入其中,当需要搜索时,它能在毫秒级时间内找到最相似的向量,为高效检索奠定基础。
- BGE 模型(bge-visualized-base-en-v1.5):这是一个强大的多模态编码器,它的作用是将图像和文本转化为计算机能够理解的向量表示。它可以同时处理图像像素和文本语义,生成包含两者信息的多模态嵌入向量,让机器能够 “看懂” 图像内容并 “理解” 文本指令。
- GPT-4o:作为多模态大语言模型,它在系统中扮演着 “智能筛选员” 的角色。当 Milvus 返回一批候选图像后,GPT-4o 会根据用户的原始查询意图,对这些候选图像进行重新排序,并给出选择最佳结果的详细解释,让检索结果更贴合用户需求。
二、从 0 到 1 搭建系统:手把手教你实践
接下来,我们按照实际开发的流程,一步步搭建这个多模态 RAG 系统。为了方便演示,我们使用 Amazon Reviews 2023 的子集作为示例数据,包含约 900 张来自不同类别的图片,以及一张豹子图片作为查询示例。
1. 环境准备:搭建开发基础
首先,我们需要安装必要的依赖库,这些库是我们开发的基础工具:
bash
# 安装核心依赖
pip install --upgrade pymilvus openai datasets opencv-python timm einops ftfy peft tqdm
# 克隆并安装BGE模型库
git clone https://github.com/FlagOpen/FlagEmbedding.git
pip install -e FlagEmbedding
这里需要注意的是,如果使用 Google Colab 环境,安装完依赖后可能需要重启运行时,才能让新安装的库生效。可以通过点击屏幕上方的 “运行时” 菜单,选择 “重新启动会话” 来完成。
2. 数据准备:下载并处理示例数据
接下来,下载示例数据并解压到指定文件夹:
bash
# 下载数据压缩包
wget https://github.com/milvus-io/bootcamp/releases/download/data/amazon_reviews_2023_subset.tar.gz
# 解压到images_folder目录
tar -xzf amazon_reviews_2023_subset.tar.gz
解压后,我们会得到一个包含 900 张图片的数据集和一张用于查询的豹子图片。这些图片将作为我们的数据库数据和查询示例。
3. 模型加载:让 BGE 模型为我们工作
BGE 模型是处理多模态数据的核心,我们需要下载它的权重文件并构建编码器:
python
运行
# 下载模型权重
wget https://huggingface.co/BAAI/bge-visualized/resolve/main/Visualized_base_en_v1.5.pth
# 构建编码器类
import torch
from FlagEmbedding.visual.modeling import Visualized_BGE
class Encoder:
def __init__(self, model_name: str, model_path: str):
# 初始化BGE模型,加载预训练权重
self.model = Visualized_BGE(model_name_bge=model_name, model_weight=model_path)
self.model.eval() # 设置模型为评估模式
def encode_query(self, image_path: str, text: str) -> list[float]:
"""
生成包含图像和文本信息的查询向量
:param image_path: 图像路径
:param text: 文本描述
:return: 多模态查询向量
"""
with torch.no_grad(): # 关闭梯度计算,提高推理速度
# 调用模型的encode方法,传入图像路径和文本
query_emb = self.model.encode(image=image_path, text=text)
return query_emb.tolist()[0] # 将张量转换为列表并返回
def encode_image(self, image_path: str) -> list[float]:
"""
生成图像的单模态向量
:param image_path: 图像路径
:return: 图像向量
"""
with torch.no_grad():
query_emb = self.model.encode(image=image_path)
return query_emb.tolist()[0]
# 初始化编码器,指定模型名称和权重路径
model_name = "BAAI/bge-base-en-v1.5"
model_path = "./Visualized_base_en_v1.5.pth"
encoder = Encoder(model_name, model_path)
这里定义的编码器类有两个主要方法,encode_query
用于生成包含图像和文本信息的多模态查询向量,encode_image
用于生成图像的单模态向量,方便后续处理。
4. 数据入库:将图像向量存入 Milvus
我们需要将处理好的图像向量存入 Milvus 数据库,以便后续进行高效检索:
python
运行
import os
from tqdm import tqdm
from glob import glob
data_dir = "./images_folder"
image_list = glob(os.path.join(data_dir, "images", "*.jpg")) # 筛选出所有jpg格式的图片
image_dict = {}
# 生成图像嵌入向量
for image_path in tqdm(image_list, desc="Generating image embeddings: "):
try:
# 使用编码器生成图像向量
image_dict[image_path] = encoder.encode_image(image_path)
except Exception as e:
print(f"Failed to generate embedding for {image_path}. Skipped.")
continue
print(f"Number of encoded images: {len(image_dict)}") # 输出成功编码的图像数量
在生成嵌入向量的过程中,我们使用了tqdm
库来显示进度条,让我们能直观地看到处理进度。如果遇到无法生成嵌入的图像,会打印错误信息并跳过,保证程序的健壮性。
接下来,连接 Milvus 并创建集合,将图像路径和向量存入其中:
python
运行
from pymilvus import MilvusClient
dim = len(list(image_dict.values())[0]) # 获取向量维度
collection_name = "multimodal_rag_demo"
# 连接Milvus,这里使用本地文件存储(Milvus Lite),方便演示
milvus_client = MilvusClient(uri="./milvus_demo.db")
# 创建集合,指定维度等参数
milvus_client.create_collection(
collection_name=collection_name,
auto_id=True,
dimension=dim,
enable_dynamic_field=True,
)
# 插入数据,将图像路径和向量作为实体存入
milvus_client.insert(
collection_name=collection_name,
data=[{"image_path": k, "vector": v} for k, v in image_dict.items()],
)
print(f"Inserted {len(image_dict)} images into Milvus.")
这里使用 Milvus Lite,通过本地文件存储数据,无需复杂的服务器配置,非常适合开发和测试阶段。如果数据规模较大,可以切换到 Docker 或 Kubernetes 上的 Milvus 服务器,或者使用 Zilliz Cloud 全托管服务,只需调整连接参数即可。
5. 多模态搜索:结合图像和文本进行查询
现在,我们准备用示例中的豹子图片和文本 “phone case with this image theme” 进行查询,看看系统能否找到合适的手机壳图片:
python
运行
query_image = os.path.join(data_dir, "leopard.jpg") # 查询图像路径
query_text = "phone case with this image theme" # 查询文本
# 生成多模态查询向量
query_vec = encoder.encode_query(image_path=query_image, text=query_text)
# 在Milvus中搜索,返回最多9个结果,使用余弦相似度度量
search_results = milvus_client.search(
collection_name=collection_name,
data=[query_vec],
output_fields=["image_path"],
limit=9,
search_params={"metric_type": "COSINE", "params": {}},
)[0]
# 提取检索到的图像路径
retrieved_images = [hit.get("entity").get("image_path") for hit in search_results]
print("Retrieved images count:", len(retrieved_images))
这里的关键是encode_query
方法,它将图像和文本结合起来生成查询向量,使得搜索不再局限于单一模态,而是同时考虑图像内容和文本意图。Milvus 使用余弦相似度来查找最相似的向量,快速返回候选图像。
6. 结果重排:让 GPT-4o 给出最佳选择
Milvus 返回的候选图像可能有多个,我们需要让 GPT-4o 根据用户意图进行重新排序,并给出解释。首先,我们需要将查询图像和检索到的图像组合成一个全景图,方便 GPT-4o 理解:
python
运行
import numpy as np
import cv2
from PIL import Image
img_height = 300
img_width = 300
row_count = 3
def create_panoramic_view(query_image_path: str, retrieved_images: list) -> np.ndarray:
"""
创建包含查询图像和检索结果的全景图
:param query_image_path: 查询图像路径
:param retrieved_images: 检索到的图像列表
:return: 全景图数组
"""
# 初始化全景图,背景为白色
panoramic_width = img_width * row_count
panoramic_height = img_height * row_count
panoramic_image = np.full((panoramic_height, panoramic_width, 3), 255, dtype=np.uint8)
# 处理查询图像,添加蓝色边框并放置在左下角
query_image = Image.open(query_image_path).convert("RGB")
query_array = np.array(query_image)[:, :, ::-1] # 转换为BGR格式
resized_query = cv2.resize(query_array, (img_width, img_height))
bordered_query = cv2.copyMakeBorder(resized_query, 10, 10, 10, 10, cv2.BORDER_CONSTANT, value=(255, 0, 0)) # 蓝色边框
# 将查询图像放置在全景图的下方左侧
query_image_null = np.full((panoramic_height, img_width, 3), 255, dtype=np.uint8)
query_image_null[img_height*2:img_height*3, 0:img_width] = cv2.resize(bordered_query, (img_width, img_height))
# 添加“query”文本说明
cv2.putText(query_image_null, "query", (10, img_height*3 + 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 2)
# 处理检索到的图像,添加黑色边框和红色索引
retrieved_imgs = [np.array(Image.open(img).convert("RGB"))[:, :, ::-1] for img in retrieved_images]
for i, image in enumerate(retrieved_imgs):
resized_img = cv2.resize(image, (img_width - 4, img_height - 4))
bordered_img = cv2.copyMakeBorder(resized_img, 2, 2, 2, 2, cv2.BORDER_CONSTANT, value=(0, 0, 0)) # 黑色边框
row = i // row_count
col = i % row_count
start_row = row * img_height
start_col = col * img_width
panoramic_image[start_row:start_row+img_height, start_col:start_col+img_width] = bordered_img
# 添加红色索引数字
cv2.putText(panoramic_image, str(i), (start_col + 10, start_row + 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
# 合并查询图像和检索结果图像
panoramic_image = np.hstack([query_image_null, panoramic_image])
return panoramic_image
# 生成全景图并保存
combined_image_path = os.path.join(data_dir, "combined_image.jpg")
panoramic_image = create_panoramic_view(query_image, retrieved_images)
cv2.imwrite(combined_image_path, panoramic_image)
然后,使用 GPT-4o 进行重排和解释,这里需要替换成你自己的 OpenAI API 密钥:
python
运行
import requests
import base64
openai_api_key = "[API 密钥]" # 请替换为你的实际API密钥
def generate_ranking_explanation(combined_image_path: str, caption: str) -> tuple[list[int], str]:
"""
使用GPT-4o对检索结果进行重排并生成解释
:param combined_image_path: 全景图路径
:param caption: 用户查询文本
:return: 重排后的索引列表和最佳结果解释
"""
with open(combined_image_path, "rb") as image_file:
base64_image = base64.b64encode(image_file.read()).decode("utf-8")
# 构建提示信息,引导GPT-4o进行重排和解释
prompt = (
"你需要对图像检索结果进行排序。用户通过带有‘指令’的查询图像表达检索意图,例如:如果用户查询一辆红色汽车并附带指令‘将这辆车改为蓝色’,那么蓝色的类似车型会在结果中排名更高。\n"
f"当前用户指令:{caption}\n"
"查询图像带有蓝色边框,每个检索结果图像的左上角有红色索引编号(从0开始)。请根据用户指令对这些结果进行重新排序,从最适合到最不适合,并仅对排名第一的结果给出解释。\n"
"响应格式必须为:‘Ranked list: [索引列表]’,后跟‘Reasons: 解释内容’。"
)
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {openai_api_key}",
}
payload = {
"model": "gpt-4o",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{base64_image}"},
},
],
}
],
"max_tokens": 300,
}
response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload)
result = response.json()["choices"][0]["message"]["content"]
# 解析响应,提取重排索引和解释
start_idx = result.find("[")
end_idx = result.find("]")
ranked_indices = [int(index.strip()) for index in result[start_idx+1:end_idx].split(",")]
explanation = result[end_idx+1:].strip()
return ranked_indices, explanation
# 调用函数获取重排结果和解释
ranked_indices, explanation = generate_ranking_explanation(combined_image_path, query_text)
print("Ranked indices:", ranked_indices)
print("Explanation:", explanation)
# 显示最佳结果
best_index = ranked_indices[0]
best_img = Image.open(retrieved_images[best_index])
best_img.show()
在这个过程中,我们通过构建详细的提示信息,让 GPT-4o 理解用户的查询意图和图像的排列方式,从而做出合理的重排和解释。需要注意的是,OpenAI API 密钥需要妥善保管,不要泄露在公开的代码中。
三、实践中的坑与解决:这些细节别忽略
在实际开发中,我们难免会遇到一些问题,这里分享几个常见的坑和解决方法:
- 模型加载失败:检查模型权重路径是否正确,确保下载的文件完整。如果使用不同的 BGE 模型,需要对应调整模型名称和权重路径。
- Milvus 连接问题:使用 Milvus Lite 时,确保路径没有中文或特殊字符;使用服务器版本时,检查网络连接和端口是否正确开放。
- 图像编码异常:部分图片可能因为格式错误或损坏导致无法生成嵌入,添加异常处理机制(如示例中的
try-except
),跳过有问题的图片。 - GPT-4o 响应不符合预期:调整提示信息的清晰度,明确告诉模型需要的输出格式和内容,必要时提供示例响应,引导模型生成正确的结果。
四、总结与建议:让技术为项目赋能
通过以上步骤,我们成功搭建了一个基于 Milvus、BGE 和 GPT-4o 的多模态 RAG 系统,实现了结合图像和文本的智能检索。这个系统的优势在于:
- 多模态融合:通过 BGE 模型将图像和文本转化为统一的向量空间,让检索不再局限于单一模态。
- 高效检索:Milvus 的向量检索能力能够快速处理海量数据,满足实时搜索需求。
- 智能解释:GPT-4o 的重排和解释功能让检索结果更具可解释性,提升用户体验。
如果你想将这个系统应用到实际项目中,有几点建议:
- 数据优化:根据项目需求扩展数据集,确保数据的多样性和代表性,提高模型的泛化能力。
- 模型调优:尝试不同的 BGE 模型变体或其他多模态编码器,找到最适合项目场景的模型。
- 性能优化:对于大规模数据,考虑使用 Milvus 的分布式部署方案,提升检索速度和系统稳定性。
- 安全合规:在使用 OpenAI 等外部 API 时,注意数据隐私和合规性,对敏感信息进行脱敏处理。
希望这篇实践总结能为你在多模态检索领域的开发提供帮助。如果你在实践中遇到问题,欢迎在评论区留言交流。觉得有用的话,别忘了点击关注,后续会分享更多关于 Milvus 和多模态技术的实战经验,让我们一起在技术的道路上不断探索前行!

GitCode 天启AI是一款由 GitCode 团队打造的智能助手,基于先进的LLM(大语言模型)与多智能体 Agent 技术构建,致力于为用户提供高效、智能、多模态的创作与开发支持。它不仅支持自然语言对话,还具备处理文件、生成 PPT、撰写分析报告、开发 Web 应用等多项能力,真正做到“一句话,让 Al帮你完成复杂任务”。
更多推荐
所有评论(0)