Текст-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 видят контекст целиком.
Как векторизовать изображение, чтобы ИИ его понял
Вот где большинство спотыкается. Нельзя просто взять картинку и запихнуть ее в эмбеддинг-модель. Нужна предобработка.
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 год:
- Ingestion pipeline: Принимает PDF/Word/PPT, извлекает текст и изображения
- Vision processor: Режет изображения на регионы, классифицирует их
- VLM описатель: Генерирует текстовые описания для каждого региона (с кэшированием)
- Векторизатор: Создает эмбеддинги для текста и описаний
- Гибридный поиск: Ищет по обеим коллекциям, объединяет результаты
- Reranker: Дополнительная ранжировка с помощью cross-encoder (опционально)
- Ответчик: LLM получает контекст и генерирует ответ с указанием источников
Что будет дальше?
К 2027 году мультимодальный RAG станет стандартом. Но уже сейчас появляются тренды:
- Нативные мультимодальные эмбеддинги: Модели типа Nemotron ColEmbed V2 от Nvidia — один эмбеддинг для текста и изображения одновременно
- 3D-визуализация поиска: Как в статье "Визуализация RAG в 3D" — видеть, как запрос находит похожие фрагменты
- Мультимодальные чанкеры: Умное разделение документов с сохранением контекста между текстом и изображениями
Главный совет: не пытайтесь сделать идеальную систему с первого раза. Начните с таблиц — они дают максимальный прирост точности. Добавьте графики. Потом уже общие изображения. Каждый шаг будет повышать accuracy на 10-15%.
И помните: пользователю все равно, какую модель вы используете. Ему важно получить ответ на вопрос "сколько продали в Q3" — даже если ответ был в графике, а не в тексте.