RAG с мультимодальными документами
ТЕХНИЧЕСКОЕ ЗАДАНИЕ: RAG с мультимодальными документами
1. Цель задачи
Разработать RAG-систему, способную обрабатывать документы, содержащие текст, таблицы и изображения. Научиться интегрировать CLIP (Contrastive Language-Image Pre-training) для эмбеддингов изображений, извлекать таблицы из PDF и объединять гетерогенные данные в единый индекс. Система должна отвечать на вопросы, используя информацию из всех трёх типов контента.
Ключевой результат Рабочий прототип RAG, который по запросу пользователя находит релевантные фрагменты (текст, таблицу, изображение) и генерирует ответ с участием LLM.
2. Исходные данные
| Что нужно | Откуда взять |
|---|---|
| PDF-документы с текстом, таблицами, изображениями | Открытые отчёты, статьи (например, arXiv PDF, финансовые отчёты) или сгенерировать самому (через LaTeX/Word → PDF) |
| Изображения (JPEG/PNG) | Вырезать из PDF или взять из датасетов (COCO, WikiArt) |
| Таблицы (CSV/Excel) | Из PDF или отдельные файлы (например, датасет TPC-H) |
| CLIP модель | Hugging Face: openai/clip-vit-base-patch32 |
| Текстовая embedding model | intfloat/multilingual-e5-small (поддержка русского) |
| Векторная БД | FAISS (CPU) или Qdrant (лёгкий запуск) |
| LLM (для генерации ответа) | OpenAI API (GPT-4o-mini) или локальный (Llama 3 8B через Ollama) |
Если нет реального инструмента — симулируем:
- PDF – создайте 2–3 простых PDF с помощью Python (reportlab) или LibreOffice. В каждый PDF добавьте: параграф текста, таблицу (2×3), одно изображение (любая картинка из интернета).
- Таблица – сохраните как CSV, если парсинг PDF не дал результатов.
- Изображение – сохраните отдельно, если в PDF оно плохо читается.
3. Технологический стек
| Компонент | Инструменты | Назначение |
|---|---|---|
| Парсинг PDF | PyMuPDF (fitz), pdfplumber, camelot-py | Извлечение текста, таблиц, изображений |
| Обработка изображений | PIL/Pillow, opencv-python | Препроцессинг (обрезание, нормализация) |
| CLIP эмбеддинги | transformers + torch | Векторизация изображений |
| Текстовые эмбеддинги | sentence-transformers | Векторизация текста и табличного представления |
| Векторное индексирование | FAISS (IndexFlatIP / IndexIVFFlat) | Поиск по ближайшим соседям |
| Роутинг / RAG-фреймворк | LangChain или LlamaIndex | Оркестрация retrieval + генерации |
| LLM | OpenAI API / Ollama + local model | Генерация ответа |
| Утилиты | pandas, numpy, json, click | Склеивание данных, конфигурация |
4. Этапы выполнения
Этап 1: Подготовка данных и парсинг (2–3 часа)
Действия
-
Соберите датасет – 3 документа PDF (каждый ~2–3 страницы) с текстом, таблицей и изображением.
Можно сгенерировать кодом:from reportlab.lib.pagesizes import A4 from reportlab.pdfgen import canvas from reportlab.lib.utils import ImageReader # ... создание PDF с текстом, таблицей (через drawString), изображением -
Напишите модуль
parser.py, который:- Извлекает текст постранично (
pdfplumberилиfitz.get_text()) - Находит таблицы (
pdfplumber.extract_tables()) - Извлекает изображения (
fitz.get_images()) и сохраняет их во временную папку - Возвращает список фрагментов:
{"type": "text"|"table"|"image", "content": ..., "page": ...}
- Извлекает текст постранично (
-
Для каждого фрагмента сгенерируйте уникальный ID и сохраните результаты в JSON:
documents.json.
Ожидаемый результат этапа Папка data/ с PDF-исходниками и файл documents.json со структурой:
[
{ "id": "doc1_text1", "type": "text", "content": "Текст абзаца...", "page": 1 },
{ "id": "doc1_table1", "type": "table", "content": [["A", "B"], ["1", "2"]], "page": 2 },
{ "id": "doc1_img1", "type": "image", "content": "path/to/img.jpg", "page": 2 }
]
Этап 2: Создание мультимодальных эмбеддингов (2 часа)
Действия
-
Загрузите CLIP и sentence-transformer:
from transformers import CLIPProcessor, CLIPModel model_clip = CLIPModel.from_pretrained("openai/clip-vit-base-patch32") processor_clip = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32") from sentence_transformers import SentenceTransformer model_text = SentenceTransformer("intfloat/multilingual-e5-small") -
Напишите функцию
embed_fragment(fragment):- Если
type == "text"— используйтеmodel_text.encode(fragment["content"]) - Если
type == "table"— преобразуйте таблицу в короткое текстовое описание (например,"Столбец A: 1, столбец B: 2"или черезpandas.to_markdown()) и эмбеддинг той же моделью. - Если
type == "image"— загрузите изображение, подайте в CLIP (черезprocessor(text=None, images=img, return_tensors="pt")) и получите image embedding (пул из последнего слоя).
- Если
-
Нормализуйте все эмбеддинги (L2) для косинусного поиска.
-
Сохраните эмбеддинги в
embeddings.npyи метаданные вmetadata.json.
Ожидаемый результат этапа Все фрагменты имеют эмбеддинги размерности 512 (E5) или 768 (CLIP). Матрица эмбеддингов shape (N, dim).
Этап 3: Индексирование (1 час)
Действия
-
Создайте индекс FAISS:
import faiss dim = 512 # или 768 – приведите все эмбеддинги к одной размерности через проекцию? # Проще: используйте только E5 для текста и таблиц, а CLIP эмбеддинги изображений тоже конвертируйте в то же пространство? # Лучше: создайте два индекса (один для текста/таблиц, второй для изображений), но для простоты – всё в один индекс с помощью `faiss.IndexFlatIP` (inner product = cosine similarity на нормализованных векторах). index = faiss.IndexFlatIP(dim) index.add(normalized_embeddings) -
Сохраните индекс:
faiss.write_index(index, "multimodal.index") -
Создайте
retriever.pyс функциейsearch(query, k=5):- Эмбеддинг запроса через
model_text.encode(query)(текстовый запрос) - Поиск в индексе, получение ID и расстояний
- Возврат
[ (fragment_id, score) ]
- Эмбеддинг запроса через
Ожидаемый результат этапа Индекс готов к поиску. Тестовый запрос возвращает релевантные фрагменты разных типов.
Этап 4: Retrieval и пост-процессинг (1 час)
Действия
-
Обогатите поиск – после получения списка ID выполните вторую стадию re-ranking:
- Используйте Cross-encoder (например,
cross-encoder/ms-marco-MiniLM-L-6-v2) для точной переранжировки топ-20 кандидатов. - Отфильтруйте фрагменты с низкой релевантностью (score < 0.5).
- Используйте Cross-encoder (например,
-
Сформируйте контекст для LLM:
- Из выбранных фрагментов соберите строки:
-
Реализуйте функцию
context_for_llm(ids)
Ожидаемый результат этапа Скрипт возвращает контекст, готовый для вставки в промпт.
Этап 5: Генерация ответа и интеграция (2 часа)
Действия
-
Соберите RAG-пайплайн в
main.py:def answer(query): # 1. Получить топ-k фрагментов # 2. Переранжировать # 3. Сформировать промпт: prompt = f"""Ответь на вопрос на основе предоставленного контекста. Контекст: {context_for_llm(ids)} Вопрос: {query} Ответ:""" # 4. Вызвать LLM (через openai или Ollama) # 5. Вернуть ответ -
Поддержка мультимодального LLM (опционально): если используете GPT-4o, передайте изображения из контекста как
image_url. -
Протестируйте на 5 запросах:
- "Какие данные в таблице на странице 2?" → должен найти таблицу.
- "Что изображено на рисунке?" → должен найти изображение и дать описание.
- "Какой основной вывод текста?" → должен найти текст.
- "Сравни цифры из таблицы и текст" → комбинированный.
- "Опиши содержание документа" → смешанный.
-
Документируйте код: README, requirements.txt, пример запуска.
Ожидаемый результат этапа Консольное приложение, работающее с тремя типами контента.
5. Критерии приемки (Definition of Done)
- Система парсит PDF и корректно извлекает текст, таблицы и изображения (минимум 1 документ проходит полностью).
- Эмбеддинги для текста, таблиц и изображений вычисляются и нормализуются без ошибок.
- Индекс FAISS создан, поиск по ключевым словам возвращает фрагменты соответствующих типов.
- Ответ LLM содержит информацию из контекста, корректно цитируя источник (текст, таблицу или изображение).
- Код организован модульно (parser.py, embedder.py, retriever.py, generator.py).
- Написан README с примером использования и списком зависимостей.
6. Ожидаемый результат
Основной артефакт GitHub-репозиторий со следующей структурой:
multimodal-rag/
├── data/ # сырые PDF и извлечённые файлы
├── parser.py # извлечение данных из PDF
├── embedder.py # создание эмбеддингов
├── retriever.py # поиск и re-ranking
├── generator.py # формирование промпта и вызов LLM
├── main.py # точка входа в CLI
├── requirements.txt
└── README.md
Дополнительные результаты
- Файл
documents.jsonс метаданными фрагментов. - Индекс
multimodal.index. - Лог тестовых запросов и ответов.
7. Возможные сложности и их решение
| Сложность | Решение |
|---|---|
| Низкое качество извлечения таблиц из PDF | Использовать camelot-py (Lattice/Stream) или tabula-py; если не получается — сохранять таблицы отдельными CSV и связывать с PDF через номер страницы |
| CLIP не даёт релевантных результатов для специфических изображений | Использовать более новую версию CLIP (ViT-L/14) или добавить текстовый caption как эмбеддинг вместе с визуальным (early fusion) |
| Размерность эмбеддингов разная (текст 512, изображение 768) | Привести к единой размерности через проекционную матрицу (linear layer) или использовать только CLIP text encoder для всех (768d) — качество может упасть |
| LLM не понимает таблицы | Преобразовывать таблицу в markdown-строку: ` |
| Ответ LLM галлюцинирует | Уменьшить top-k, добавить системный промпт с требованием отвечать только на основе контекста |
8. Бюджет времени (оценка)
| Этап | Время |
|---|---|
| Этап 1: Подготовка данных и парсинг | 2–3 ч |
| Этап 2: Создание мультимодальных эмбеддингов | 2 ч |
| Этап 3: Индексирование | 1 ч |
| Этап 4: Retrieval и пост-процессинг | 1 ч |
| Этап 5: Генерация ответа и интеграция | 2–3 ч |
| Итого | 8–10 ч |
При первом выполнении: заложить дополнительные 2 часа на отладку парсинга PDF и установку зависимостей.
9. Связанные вопросы из базы знаний
| Вопрос | Тема |
|---|---|
| 19 | Архитектура RAG: ингредиенты и пайплайн |
| 48 | CLIP: contrastive pre-training |
| 95 | Косинусное расстояние, FAISS |
| 134 | Мультимодальные эмбеддинги (текст+изображение) |
| 177 | Извлечение данных из PDF (PyMuPDF, pdfplumber) |
| 210 | Таблицы в RAG: обработка и индексирование |
| 288 | Sentence-Transformers: лучшие практики |
| 312 | Ранжирование (re-ranking) в retrieval |
| 401 | LLM с vision (GPT-4o, CLIP captioning) |
| 555 | Prompt engineering для RAG |
10. Чек-лист самопроверки
- Я проверил(а), что парсер извлекает все три типа контента хотя бы для одного PDF.
- Я убедился(ась), что эмбеддинги изображений нормализованы и лежат в том же пространстве, что и текстовые (или обработаны проекцией).
- Я протестировал(а) поиск с запросом, который очевидно относится к изображению, и убедился(ась), что индекс возвращает изображение.
- Я убедился(ась), что ответ LLM не галлюцинирует и ссылается на конкретные факты из контекста.
- Код проходит
python main.py --query "..."без ошибок и выводит осмысленный ответ.