English translation is not available yet. Showing Russian content.

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 modelintfloat/multilingual-e5-small (поддержка русского)
Векторная БДFAISS (CPU) или Qdrant (лёгкий запуск)
LLM (для генерации ответа)OpenAI API (GPT-4o-mini) или локальный (Llama 3 8B через Ollama)

Если нет реального инструмента — симулируем:

  1. PDF – создайте 2–3 простых PDF с помощью Python (reportlab) или LibreOffice. В каждый PDF добавьте: параграф текста, таблицу (2×3), одно изображение (любая картинка из интернета).
  2. Таблица – сохраните как CSV, если парсинг PDF не дал результатов.
  3. Изображение – сохраните отдельно, если в PDF оно плохо читается.

3. Технологический стек

КомпонентИнструментыНазначение
Парсинг PDFPyMuPDF (fitz), pdfplumber, camelot-pyИзвлечение текста, таблиц, изображений
Обработка изображенийPIL/Pillow, opencv-pythonПрепроцессинг (обрезание, нормализация)
CLIP эмбеддингиtransformers + torchВекторизация изображений
Текстовые эмбеддингиsentence-transformersВекторизация текста и табличного представления
Векторное индексированиеFAISS (IndexFlatIP / IndexIVFFlat)Поиск по ближайшим соседям
Роутинг / RAG-фреймворкLangChain или LlamaIndexОркестрация retrieval + генерации
LLMOpenAI API / Ollama + local modelГенерация ответа
Утилитыpandas, numpy, json, clickСклеивание данных, конфигурация

4. Этапы выполнения

Этап 1: Подготовка данных и парсинг (2–3 часа)

Действия

  1. Соберите датасет – 3 документа PDF (каждый ~2–3 страницы) с текстом, таблицей и изображением.
    Можно сгенерировать кодом:

    from reportlab.lib.pagesizes import A4
    from reportlab.pdfgen import canvas
    from reportlab.lib.utils import ImageReader
    # ... создание PDF с текстом, таблицей (через drawString), изображением
    
  2. Напишите модуль parser.py, который:

    • Извлекает текст постранично (pdfplumber или fitz.get_text())
    • Находит таблицы (pdfplumber.extract_tables())
    • Извлекает изображения (fitz.get_images()) и сохраняет их во временную папку
    • Возвращает список фрагментов: {"type": "text"|"table"|"image", "content": ..., "page": ...}
  3. Для каждого фрагмента сгенерируйте уникальный 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 часа)

Действия

  1. Загрузите 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")
    
  2. Напишите функцию 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 (пул из последнего слоя).
  3. Нормализуйте все эмбеддинги (L2) для косинусного поиска.

  4. Сохраните эмбеддинги в embeddings.npy и метаданные в metadata.json.

Ожидаемый результат этапа Все фрагменты имеют эмбеддинги размерности 512 (E5) или 768 (CLIP). Матрица эмбеддингов shape (N, dim).

Этап 3: Индексирование (1 час)

Действия

  1. Создайте индекс FAISS:

    import faiss
    dim = 512  # или 768 – приведите все эмбеддинги к одной размерности через проекцию?
    # Проще: используйте только E5 для текста и таблиц, а CLIP эмбеддинги изображений тоже конвертируйте в то же пространство? 
    # Лучше: создайте два индекса (один для текста/таблиц, второй для изображений), но для простоты – всё в один индекс с помощью `faiss.IndexFlatIP` (inner product = cosine similarity на нормализованных векторах).
    index = faiss.IndexFlatIP(dim)
    index.add(normalized_embeddings)
    
  2. Сохраните индекс: faiss.write_index(index, "multimodal.index")

  3. Создайте retriever.py с функцией search(query, k=5):

    • Эмбеддинг запроса через model_text.encode(query) (текстовый запрос)
    • Поиск в индексе, получение ID и расстояний
    • Возврат [ (fragment_id, score) ]

Ожидаемый результат этапа Индекс готов к поиску. Тестовый запрос возвращает релевантные фрагменты разных типов.

Этап 4: Retrieval и пост-процессинг (1 час)

Действия

  1. Обогатите поиск – после получения списка ID выполните вторую стадию re-ranking:

    • Используйте Cross-encoder (например, cross-encoder/ms-marco-MiniLM-L-6-v2) для точной переранжировки топ-20 кандидатов.
    • Отфильтруйте фрагменты с низкой релевантностью (score < 0.5).
  2. Сформируйте контекст для LLM:

    • Из выбранных фрагментов соберите строки:
      • Текст: сам текст.
      • Таблица: представьте в виде CSV или markdown-таблицы.
      • Изображение: если LLM мультимодальная (GPT-4 с vision), передайте base64; иначе опишите изображение через CLIP captioning (можно использовать BLIP или просто "Изображение с заголовком: " + filename)
  3. Реализуйте функцию context_for_llm(ids)

Ожидаемый результат этапа Скрипт возвращает контекст, готовый для вставки в промпт.

Этап 5: Генерация ответа и интеграция (2 часа)

Действия

  1. Соберите RAG-пайплайн в main.py:

    def answer(query):
        # 1. Получить топ-k фрагментов
        # 2. Переранжировать
        # 3. Сформировать промпт:
        prompt = f"""Ответь на вопрос на основе предоставленного контекста.
        Контекст:
        {context_for_llm(ids)}
    
        Вопрос: {query}
        Ответ:"""
        # 4. Вызвать LLM (через openai или Ollama)
        # 5. Вернуть ответ
    
  2. Поддержка мультимодального LLM (опционально): если используете GPT-4o, передайте изображения из контекста как image_url.

  3. Протестируйте на 5 запросах:

    • "Какие данные в таблице на странице 2?" → должен найти таблицу.
    • "Что изображено на рисунке?" → должен найти изображение и дать описание.
    • "Какой основной вывод текста?" → должен найти текст.
    • "Сравни цифры из таблицы и текст" → комбинированный.
    • "Опиши содержание документа" → смешанный.
  4. Документируйте код: 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: ингредиенты и пайплайн
48CLIP: contrastive pre-training
95Косинусное расстояние, FAISS
134Мультимодальные эмбеддинги (текст+изображение)
177Извлечение данных из PDF (PyMuPDF, pdfplumber)
210Таблицы в RAG: обработка и индексирование
288Sentence-Transformers: лучшие практики
312Ранжирование (re-ranking) в retrieval
401LLM с vision (GPT-4o, CLIP captioning)
555Prompt engineering для RAG

10. Чек-лист самопроверки

  • Я проверил(а), что парсер извлекает все три типа контента хотя бы для одного PDF.
  • Я убедился(ась), что эмбеддинги изображений нормализованы и лежат в том же пространстве, что и текстовые (или обработаны проекцией).
  • Я протестировал(а) поиск с запросом, который очевидно относится к изображению, и убедился(ась), что индекс возвращает изображение.
  • Я убедился(ась), что ответ LLM не галлюцинирует и ссылается на конкретные факты из контекста.
  • Код проходит python main.py --query "..." без ошибок и выводит осмысленный ответ.