在日常开发中,我们常常会遇到这样的需求:用户希望通过上传一张图片并添加一些文字描述,就能精准找到与之匹配的目标图像。传统的图像搜索要么单纯依赖图像特征,要么仅基于文本关键词,很难同时理解图像内容和用户的文字意图。有没有一种方法能让机器像人类一样,结合图像和文字两种信息进行智能检索呢?今天,我们就来聊聊如何利用 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 密钥需要妥善保管,不要泄露在公开的代码中。

三、实践中的坑与解决:这些细节别忽略

在实际开发中,我们难免会遇到一些问题,这里分享几个常见的坑和解决方法:

  1. 模型加载失败:检查模型权重路径是否正确,确保下载的文件完整。如果使用不同的 BGE 模型,需要对应调整模型名称和权重路径。
  2. Milvus 连接问题:使用 Milvus Lite 时,确保路径没有中文或特殊字符;使用服务器版本时,检查网络连接和端口是否正确开放。
  3. 图像编码异常:部分图片可能因为格式错误或损坏导致无法生成嵌入,添加异常处理机制(如示例中的try-except),跳过有问题的图片。
  4. GPT-4o 响应不符合预期:调整提示信息的清晰度,明确告诉模型需要的输出格式和内容,必要时提供示例响应,引导模型生成正确的结果。

四、总结与建议:让技术为项目赋能

通过以上步骤,我们成功搭建了一个基于 Milvus、BGE 和 GPT-4o 的多模态 RAG 系统,实现了结合图像和文本的智能检索。这个系统的优势在于:

  • 多模态融合:通过 BGE 模型将图像和文本转化为统一的向量空间,让检索不再局限于单一模态。
  • 高效检索:Milvus 的向量检索能力能够快速处理海量数据,满足实时搜索需求。
  • 智能解释:GPT-4o 的重排和解释功能让检索结果更具可解释性,提升用户体验。

如果你想将这个系统应用到实际项目中,有几点建议:

  • 数据优化:根据项目需求扩展数据集,确保数据的多样性和代表性,提高模型的泛化能力。
  • 模型调优:尝试不同的 BGE 模型变体或其他多模态编码器,找到最适合项目场景的模型。
  • 性能优化:对于大规模数据,考虑使用 Milvus 的分布式部署方案,提升检索速度和系统稳定性。
  • 安全合规:在使用 OpenAI 等外部 API 时,注意数据隐私和合规性,对敏感信息进行脱敏处理。

希望这篇实践总结能为你在多模态检索领域的开发提供帮助。如果你在实践中遇到问题,欢迎在评论区留言交流。觉得有用的话,别忘了点击关注,后续会分享更多关于 Milvus 和多模态技术的实战经验,让我们一起在技术的道路上不断探索前行!

Logo

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

更多推荐