Недавно получил задачу сделать автоматизированную оцифровку характеристик из паспортов товаров в БД, а не изменение параметров вручную в ERP. Я подумал, было бы здорово поделиться, как я это сделал, с вами на Хабре!
Базовые задачи:
Нужно, чтобы это все работало локально
Система должна принимать разные форматы (.doc, .pdf, .png)
Возможность создавать динамические таблицы, куда ИИ будет заполнять сама информацию, а не хардкодить для каждой категории паспорта свои отчеты
Желательно, чтобы все работало на одной видеокарте (в моем случае 3090 на 24GB VRAM)
Для реализации задачи, я решил развернуть 2 ИИ модели — deepseek-ocr-7b и Qwen-1.5. Первая будет считывать текст из изображений, а вторая преобразовывать сырой текст в json, который, затем, будет распределяться по полям таблицы.
Скачать весь проект вы можете тут — https://github.com/Chashchin-Dmitry/llm-ocr-handmade
Шаг 1. Подготовка системы
1.1 Проверка драйвера NVIDIA
Для начала, нам нужно установить драйвер NVIDIA. В моем случае, это 576.66 серия, но если у вас другая видеокарта, то введите свою модель и серию тут.

Перезагружаем комп/сервер и пишем команду:
nvidia-smi
Если команда показала что-то с Nvidia и номер модели, значит мы удачно скачали драйверы.
1.2 Установка Docker с GPU поддержкой
Windows:
-Docker Desktop + включить WSL2 backend
-В настройках Docker Desktop включить "Use WSL 2 based engine"
Linux:
# NVIDIA Container Toolkit distribution=$(. /etc/os-release;echo $ID$VERSION_ID) curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add - curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit sudo systemctl restart docker
1.3 Проверка GPU в Docker
docker run --rm --gpus all nvidia/cuda:12.1-base nvidia-smi
Должна показать твою видеокарту.
Шаг 2: Выбор версии vLLM
Очень важно подобрать под себя верную версию vLLM под вашу видеокарту. Немаловажно, она должна быть совместима с вашей версией драйвера.
Для DeepSeek-OCR и Qwen-1.5, я выбрал vLLM v0.11.2 с CUDA 12.9 (с версией ниже, у вас ничего не запустится)
# Скачивание образа (43GB!) docker pull vllm/vllm-openai:v0.11.2
Подробнее, как подобрать нужную версию vLLM под вашу GPU можно тут.
Шаг 3: Выбор моделей
OCR
Мне понравилось отзывы от новой deepseek-ocr, а еще она очень компактная, поэтому, я решил, что она идеально подойдет к моему кейсу. Вот ее требования:
DeepSeek-OCR:
Модель:
deepseek-ai/DeepSeek-OCRРазмер: 3B параметров
VRAM: ~8-10GB
Задача: Извлечение текста из изображений
Модель для структуризации
Моя задача развернуть все разом на одной видеокарте, поэтому я выбирал самые маленькие LLM, которые только есть. Более того, мне надо сырой текст преобразовывать в json, с чем микро модели будут справляться идеально и, самое главное, быстро.
Вот какие могут быть варианты для вас:
Модель | Параметры | VRAM | Качество |
|---|---|---|---|
Qwen2.5-1.5B-Instruct | 1.5B | ~5GB | Базовое |
Qwen2.5-3B-Instruct | 3B | ~8GB | Хорошее |
Qwen2.5-7B-Instruct | 7B | ~16GB | Отличное |
Мы выбрали 1.5B — влезает вместе с OCR на 24GB и достаточен для структуризации JSON.
Шаг 4: Конфигурация docker-compose.yml
version: '3.8' services: # DeepSeek-OCR vllm-ocr: image: vllm/vllm-openai:v0.11.2 container_name: vllm-ocr runtime: nvidia environment: - NVIDIA_VISIBLE_DEVICES=all ports: - "8001:8000" volumes: - ~/.cache/huggingface:/root/.cache/huggingface command: > --model deepseek-ai/DeepSeek-OCR --trust-remote-code --max-model-len 4096 --gpu-memory-utilization 0.30 deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] # Qwen для структуризации vllm-qwen: image: vllm/vllm-openai:v0.11.2 container_name: vllm-qwen runtime: nvidia environment: - NVIDIA_VISIBLE_DEVICES=all ports: - "8002:8000" volumes: - ~/.cache/huggingface:/root/.cache/huggingface command: > --model Qwen/Qwen2.5-1.5B-Instruct --trust-remote-code --max-model-len 16384 --gpu-memory-utilization 0.35 deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] # FastAPI Backend backend: build: . container_name: ocr-backend ports: - "8000:8000" volumes: - ./backend:/app/backend - ./frontend:/app/frontend - ./uploads:/app/uploads environment: - DATABASE_URL=mysql+pymysql://root:password@host.docker.internal:3306/ocr_documents - VLLM_OCR_URL=http://vllm-ocr:8000 - VLLM_QWEN_URL=http://vllm-qwen:8000 extra_hosts: - "host.docker.internal:host-gateway" command: uvicorn backend.main:app --host 0.0.0.0 --port 8000 --reload
Ключевые параметры:
Параметр | Значение | Зачем |
|---|---|---|
| 0.30 / 0.35 | Делим GPU между моделями |
| 4096 / 16384 | Максимальный контекст |
| - | Для кастомного кода моделей |
Шаг 5: Формат запросов к DeepSeek-OCR
Важнейший нюанс! DeepSeek-OCR использует особый формат промпта, не стандартный OpenAI.
Неправильно (400 Bad Request):
messages = [ {"role": "system", "content": "You are OCR assistant"}, {"role": "user", "content": "Extract text from image"} ]
Правильно:
messages = [ { "role": "user", "content": [ { "type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_base64}"} }, { "type": "text", "text": "<|grounding|>Convert the document to markdown." } ] } ]
Варианты промптов:
Промпт | Результат |
|---|---|
| Текст + координаты элементов |
| Только текст |
Шаг 6: Лимиты токенов
Потенциальная проблема, которая у вас может возникнуть — "max_tokens is too large"
DeepSeek-OCR имеет контекст 4096 токенов. Если запросить max_tokens=4096, а input занимает 100 токенов — ошибка.
Решение:
# OCR Service payload = { "model": "deepseek-ai/DeepSeek-OCR", "max_tokens": 3500, # Оставляем запас для input "temperature": 0.0, ... } # Qwen Structurizer payload = { "model": "Qwen/Qwen2.5-1.5B-Instruct", "max_tokens": 1024, # Достаточно для JSON "temperature": 0.1, ... }
Шаг 7: Контекст для структуризации
Проблема: OCR возвращает 8000+ токенов из многостраничного PDF, а Qwen имеет контекст 2048.
Решение: Увеличить --max-model-len в docker-compose:
vllm-qwen: command: > --model Qwen/Qwen2.5-1.5B-Instruct --max-model-len 16384 # Было 2048 --gpu-memory-utilization 0.35 # Увеличили с 0.20
Важно: Больший контекст = больше VRAM. На 24GB влезает 16K контекст для 1.5B модели.
Шаг 8: Промпт для структуризации
STRUCTURING_SYSTEM_PROMPT = """Ты - эксперт по структуризации данных из документов. Твоя задача - извлечь конкретные данные из текста документа и вернуть их в JSON формате. Правила: 1. Извлекай ТОЛЬКО те поля, которые указаны в схеме 2. Если поле не найдено в документе - укажи null 3. Сохраняй форматирование дат и чисел как в оригинале 4. Не выдумывай данные - только то, что есть в документе 5. Возвращай ТОЛЬКО валидный JSON, без пояснений""" def build_structuring_prompt(columns, ocr_text): fields = "\n".join([f'- "{col["name"]}": {col["description"]}' for col in columns]) return f"""Извлеки данные из документа по следующей схеме: ПОЛЯ ДЛЯ ИЗВЛЕЧЕНИЯ: {fields} ТЕКСТ ДОКУМЕНТА: \"\"\" {ocr_text} \"\"\" Верни JSON. Если поле не найдено - укажи null."""
Шаг 9: Запуск системы
# 1. Запуск моделей (первый раз скачивает веса ~10GB) docker-compose up -d vllm-ocr vllm-qwen # 2. Ожидание загрузки (5-10 минут) docker logs vllm-ocr -f # Ждём: "Uvicorn running on http://0.0.0.0:8000" # 3. Проверка моделей curl http://localhost:8001/v1/models # OCR curl http://localhost:8002/v1/models # Qwen # 4. Запуск backend docker-compose up -d backend # 5. Открыть UI # http://localhost:8000
Шаг 10: Использование
Пора зайти и посмотреть что получилось. Сразу скажу — весь фронт я навайбкодил, потому не ругайтесь на английский язык.

Уже неплохо. Можно создать свою схему-таблицу и настроить в ней колонки. Давайте это и сделаем.

Добавим поля-колонки

Вот так выглядит настроенная схема

Отлично, теперь давайте запустим первый PDF файл. В качестве примера, я зашел по первой ссылке в Яндекс Маркет и распечатал характеристики ведра

Примерно такой результат должна выдать программа (у меня не сохранился оригиналоцифровки ведра выше).


Заключение
По моему очень даже неплохой результат для такого серьезного кейса. А главное, работает все локально без запросов в облако.
Делитесь своим опытом настройки моего контейнера.
Буду рад за лайк и подписку на канал :) https://t.me/notes_from_cto
