Мультимодальный RAG 2026: Векторизация изображений и таблиц с VLM | AiManual
AiManual Logo Ai / Manual.
18 Фев 2026 Гайд

Мультимодальный RAG: Как заставить ИИ понимать картинки, а не просто текст

Полное руководство по созданию мультимодального RAG с Vision-Language Models. Векторизация графиков, таблиц, изображений. Практические примеры на Python.

Текст-only RAG убивает 80% информации

Представьте: у вас есть техническая документация с графиками продаж, медицинский отчет со снимками МРТ, финансовый отчет с таблицами. Классический RAG берет только текст, а все визуальные данные выбрасывает. Результат? Система отвечает "данные не найдены" на вопросы про тренды на графике или значения в таблице.

Типичная ошибка 2025 года: разработчики берут PDF, вытаскивают текст через PyPDF2, векторизуют его и думают, что сделали RAG. Графики, схемы, таблицы — все это теряется. Пользователь спрашивает "покажи динамику продаж по кварталам", а система молчит — потому что динамика была на картинке.

Зачем нужны VLM в RAG?

Vision-Language Models (VLM) — это модели, которые понимают и текст, и изображения одновременно. В отличие от старых подходов (отдельно OCR + отдельно LLM), современные VLM типа GPT-4V, Claude 3.5 Sonnet Vision или открытые Qwen2.5-VL-32B видят контекст целиком.

💡
На 18.02.2026 актуальны: GPT-4V (самый точный, но дорогой), Claude 3.5 Sonnet Vision (лучший баланс цена/качество), Qwen2.5-VL-32B (лучший open-source), LLaVA-NeXT-34B (хорош для локалки). Для production я бы взял Claude 3.5 — он дешевле GPT-4V в 3 раза с почти такой же точностью.

Как векторизовать изображение, чтобы ИИ его понял

Вот где большинство спотыкается. Нельзя просто взять картинку и запихнуть ее в эмбеддинг-модель. Нужна предобработка.

1 Layout analysis и обрезка

В документах часто смешаны текст, таблицы, графики. Если скормить всю страницу VLM, модель запутается. Нужно резать.

import cv2
import numpy as np
from PIL import Image

def extract_regions(image_path):
    """Выделяет логические регионы на странице"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # Применяем adaptive threshold для выделения текста
    thresh = cv2.adaptiveThreshold(gray, 255, 
                                   cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                                   cv2.THRESH_BINARY_INV, 11, 2)
    
    # Находим контуры
    contours, _ = cv2.findContours(thresh, 
                                   cv2.RETR_EXTERNAL,
                                   cv2.CHAIN_APPROX_SIMPLE)
    
    regions = []
    for contour in contours:
        x, y, w, h = cv2.boundingRect(contour)
        if w * h > 1000:  # Фильтруем мелкий шум
            region = img[y:y+h, x:x+w]
            regions.append({
                'bbox': (x, y, w, h),
                'image': region,
                'type': classify_region(region)  # Определяем тип
            })
    
    return regions

def classify_region(region_img):
    """Определяем тип региона: текст, таблица, график"""
    # Простая эвристика по соотношению сторон
    h, w = region_img.shape[:2]
    aspect_ratio = w / h if h > 0 else 1
    
    if 0.8 < aspect_ratio < 1.2:
        return 'graph/chart'  # Квадратные — обычно графики
    elif aspect_ratio > 3:
        return 'table'  # Широкие — таблицы
    else:
        return 'text'  # Остальное — текст

Это упрощенный пример. В реальности используйте библиотеки типа Layout Parser или Detectron2.

2 Генерация описаний через VLM

Теперь каждый регион нужно описать текстом. Но не просто "это график", а семантически.

import openai
from openai import OpenAI

def describe_with_vlm(image_region, region_type):
    """Генерируем текстовое описание для региона"""
    client = OpenAI(api_key="your-api-key")
    
    # Сохраняем регион во временный файл
    temp_path = f"temp_region_{hash(str(image_region))}.png"
    cv2.imwrite(temp_path, image_region)
    
    # Промпт в зависимости от типа региона
    if region_type == 'graph/chart':
        prompt = """Опиши этот график подробно. Укажи:
1. Тип графика (линейный, столбчатый, круговая диаграмма)
2. Что на оси X и Y (если есть подписи)
3. Основные тренды и значения
4. Выводы, которые можно сделать из графика"""
    elif region_type == 'table':
        prompt = """Опиши эту таблицу. Укажи:
1. Заголовки столбцов и строк
2. Ключевые числовые значения
3. Паттерны в данных (максимум, минимум, среднее)
4. Что эти данные означают"""
    else:
        prompt = "Извлеки текст с этого изображения и кратко перескажи содержание"
    
    response = client.chat.completions.create(
        model="gpt-4-vision-preview",
        messages=[
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": prompt},
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": f"data:image/png;base64,{encode_image(temp_path)}"
                        }
                    }
                ]
            }
        ],
        max_tokens=500
    )
    
    return response.choices[0].message.content

def encode_image(image_path):
    """Кодируем изображение в base64"""
    import base64
    with open(image_path, "rb") as image_file:
        return base64.b64encode(image_file.read()).decode('utf-8')

Не делайте так: "Опиши эту картинку". VLM начнет описывать цвета, фон, стиль. Вам нужны данные, а не эстетика. Промпт должен быть специфичным: "Какие данные представлены на графике?", "Какие значения в таблице?".

3 Векторизация мультимодальных описаний

Теперь у нас есть текстовое описание каждого визуального элемента. Но как его векторизовать вместе с текстом документа?

Есть три подхода:

Подход Плюсы Минусы Когда использовать
Единое пространство (CLIP) Просто, быстро, естественное сравнение Теряет детали, плохо с таблицами Для простых изображений и текста
Гибридный поиск Максимальная точность, гибкость Сложная архитектура, дорого Для критичных систем (медицина, финансы)
Текст + метаданные Дешево, работает с любой БД Нет семантического поиска по визуалу Для MVP или ограниченного бюджета

Я рекомендую гибридный подход для production:

from sentence_transformers import SentenceTransformer
import chromadb
from chromadb.utils import embedding_functions

class MultimodalRAGIndexer:
    def __init__(self):
        # Для текста используем современную модель
        self.text_encoder = SentenceTransformer('intfloat/multilingual-e5-large-v2')
        
        # Для изображений — CLIP или аналоги
        self.image_encoder = SentenceTransformer('clip-ViT-L-14')
        
        # Инициализируем ChromaDB с двумя коллекциями
        self.chroma_client = chromadb.PersistentClient(path="./chroma_db")
        
        # Коллекция для текстовых эмбеддингов
        self.text_collection = self.chroma_client.create_collection(
            name="text_embeddings",
            embedding_function=embedding_functions.SentenceTransformerEmbeddingFunction(
                model_name="intfloat/multilingual-e5-large-v2"
            )
        )
        
        # Коллекция для визуальных описаний
        self.visual_collection = self.chroma_client.create_collection(
            name="visual_embeddings",
            embedding_function=embedding_functions.SentenceTransformerEmbeddingFunction(
                model_name="intfloat/multilingual-e5-large-v2"
            )
        )
    
    def index_document(self, document_id, text_chunks, visual_descriptions):
        """Индексируем текст и визуальные описания"""
        
        # Индексируем текстовые чанки
        for i, chunk in enumerate(text_chunks):
            self.text_collection.add(
                documents=[chunk],
                metadatas=[{"doc_id": document_id, "chunk_id": i, "type": "text"}],
                ids=[f"{document_id}_text_{i}"]
            )
        
        # Индексируем визуальные описания
        for j, desc in enumerate(visual_descriptions):
            self.visual_collection.add(
                documents=[desc["description"]],
                metadatas=[{
                    "doc_id": document_id,
                    "vis_id": j,
                    "type": desc["region_type"],
                    "bbox": str(desc["bbox"]),
                    "image_path": desc["image_path"]
                }],
                ids=[f"{document_id}_visual_{j}"]
            )
    
    def hybrid_search(self, query, top_k=5):
        """Гибридный поиск по тексту и визуальным данным"""
        
        # Ищем в текстовой коллекции
        text_results = self.text_collection.query(
            query_texts=[query],
            n_results=top_k
        )
        
        # Ищем в визуальной коллекции
        visual_results = self.visual_collection.query(
            query_texts=[query],
            n_results=top_k
        )
        
        # Объединяем результаты с весами
        combined = []
        
        # Текстовые результаты получают вес 1.0
        for i in range(len(text_results['documents'][0])):
            combined.append({
                'content': text_results['documents'][0][i],
                'metadata': text_results['metadatas'][0][i],
                'distance': text_results['distances'][0][i],
                'weighted_score': 1.0 / (1.0 + text_results['distances'][0][i]),
                'type': 'text'
            })
        
        # Визуальные результаты получают вес 0.8 (немного ниже приоритет)
        for i in range(len(visual_results['documents'][0])):
            combined.append({
                'content': visual_results['documents'][0][i],
                'metadata': visual_results['metadatas'][0][i],
                'distance': visual_results['distances'][0][i],
                'weighted_score': 0.8 / (1.0 + visual_results['distances'][0][i]),
                'type': 'visual'
            })
        
        # Сортируем по взвешенному скорингу
        combined.sort(key=lambda x: x['weighted_score'], reverse=True)
        
        return combined[:top_k]

Особые случаи: таблицы и графики

Таблицы — больное место VLM. Модели часто путают строки и столбцы, пропускают данные. Решение: двухэтапный подход.

Первый этап: извлечение структуры

Используйте специализированные инструменты для таблиц:

  • Table Transformer от Microsoft — детектирует таблицы на изображениях
  • PaddleOCR с табличной моделью — извлекает табличную структуру
  • Camelot для PDF — работает с векторными таблицами

Второй этап: семантическое описание

После извлечения данных нужно их описать:

def describe_table_dataframe(df):
    """Генерируем семантическое описание таблицы"""
    description = f"Таблица с {len(df)} строками и {len(df.columns)} столбцами.\n"
    
    # Описываем столбцы
    description += "Столбцы: " + ", ".join(df.columns.tolist()) + "\n"
    
    # Ключевые статистики для числовых столбцов
    numeric_cols = df.select_dtypes(include=[np.number]).columns
    for col in numeric_cols[:3]:  # Первые 3 числовых столбца
        if not df[col].empty:
            description += f"Столбец '{col}': "
            description += f"от {df[col].min():.2f} до {df[col].max():.2f}, "
            description += f"среднее {df[col].mean():.2f}\n"
    
    # Важные строки (первые, последние, с экстремальными значениями)
    if len(df) > 0:
        description += f"Первая строка: {df.iloc[0].to_dict()}\n"
        if len(df) > 1:
            description += f"Последняя строка: {df.iloc[-1].to_dict()}\n"
    
    return description

Где это ломается и как чинить

Проблема 1: VLM не видит текст на сканах. Решение из статьи "VLM сломались на ваших сканах" — предобработка с увеличением контраста и разрешения.

Проблема 2: Конфликт источников. Когда текст говорит одно, а график показывает другое. Подробнее в "Когда свежие SQL-данные проигрывают старым векторам" — нужно добавлять метаданные о приоритете источника.

Проблема 3: Дороговизна. GPT-4V стоит $0.01-0.03 за изображение. Для 1000 документов счет будет $300+. Решение: использовать открытые модели типа Qwen2.5-VL или кэшировать описания.

Архитектура production-системы

Вот как выглядит полная система на 2026 год:

  1. Ingestion pipeline: Принимает PDF/Word/PPT, извлекает текст и изображения
  2. Vision processor: Режет изображения на регионы, классифицирует их
  3. VLM описатель: Генерирует текстовые описания для каждого региона (с кэшированием)
  4. Векторизатор: Создает эмбеддинги для текста и описаний
  5. Гибридный поиск: Ищет по обеим коллекциям, объединяет результаты
  6. Reranker: Дополнительная ранжировка с помощью cross-encoder (опционально)
  7. Ответчик: LLM получает контекст и генерирует ответ с указанием источников

Что будет дальше?

К 2027 году мультимодальный RAG станет стандартом. Но уже сейчас появляются тренды:

  • Нативные мультимодальные эмбеддинги: Модели типа Nemotron ColEmbed V2 от Nvidia — один эмбеддинг для текста и изображения одновременно
  • 3D-визуализация поиска: Как в статье "Визуализация RAG в 3D" — видеть, как запрос находит похожие фрагменты
  • Мультимодальные чанкеры: Умное разделение документов с сохранением контекста между текстом и изображениями

Главный совет: не пытайтесь сделать идеальную систему с первого раза. Начните с таблиц — они дают максимальный прирост точности. Добавьте графики. Потом уже общие изображения. Каждый шаг будет повышать accuracy на 10-15%.

И помните: пользователю все равно, какую модель вы используете. Ему важно получить ответ на вопрос "сколько продали в Q3" — даже если ответ был в графике, а не в тексте.