Руб Голдберг XXI
Руб Голдберг XXI

Проект PhotoMentor создавался как ИИ-ментор для фотографов. Механика простая: пользователь загружает снимок, а под капотом Gemini выступает в роли арт-директора: анализирует композицию, работу со светом, цветовую гармонию и выдает детальный фидбек с оценкой.

С главной проблемой Vision-моделей я столкнулся во время тестов на собственных фото. Скормил Gemini крупный портрет собаки, положившей морду на лапы.

Модель выдала в числе прочего:

"Снимок слишком сильно обрезан снизу; вы оставили слишком мало пространства между лапами собаки и краем кадра".

У фото снизу было около 15% пространства. Никакой «тесноты» там не было и в помине.

Прогнав еще несколько снимков, я убедился: это не редкий краевой баг. Gemini системно галлюцинировала в вопросах пространственной геометрии примерно на 15–20% фотографий. И я продолжаю бороться с этим до сих пор.

Нос кошки / Спина субъекта
Нос кошки / Спина субъекта

Модель могла уверенно заявить, что объект «обрезан краем кадра», когда он находился ровно по центру. Она видела «выбитые света» на идеально экспонированных снимках. Она описывала сцену с хирургической точностью, но промахивалась в базовой геометрии.

Я попробовал классический промпт-инжиниринг. Добавил в системный промпт "Будь объективен", "Описывай только то, что можешь подтвердить по пикселям", заставил модель отдавать поле confidence для каждого утверждения.

Частота галлюцинаций почти не изменилась.

Поэтому я сделал то, что сделал бы любой разработчик, у которого начинает дергаться глаз: начал городить многослойную систему костылей и проверок, чтобы нянчиться с большой языковой моделью.

Почему Vision LLM не понимают физику пространства

Если вы работали с мультимодальными LLM, для вас это не секрет: они мыслят токенами, а не геометрией. Когда LLM пишет "горизонт расположен строго по центру", она не высчитывает координаты пикселей. Она просто знает, что на фотографиях с похожим паттерном токенов кожаные критики часто используют фразу "centered horizon".

Хуже того, у LLM патологическая уверенность в себе. В моих логах был случай, когда одно и то же фото при двух разных запросах получило два противоположных вердикта: первый раз объект был "зажат слева", а второй раз "имел достаточно воздуха слева".

Попытка 1: Детерминированный Guard V1

Раз уговоры в промптах не работают, я написал скрипт постобработки (Guard V1). Архитектура была простой:

  1. Gemini анализирует фото и отдает JSON с критикой.

  2. Guard V1 перехватывает текст и ищет в нем рискованные утверждения (spatial claims) по ключевым паттернам (например, CLAIM_HORIZON_CENTERED, CLAIM_BLOWN_HIGHLIGHTS).

  3. Если клейм найден, скрипт проверяет его честной математикой по пикселям исходного изображения.

Например, если модель заявляет про "выбитые света", скрипт на Python быстро строит гистограмму. Если в пересвете находится меньше 2% пикселей, утверждение помечается как галлюцинация и хирургически вырезается из текста до того, как его увидит пользователь.

Если модель пишет про "заваленный" или "центральный" горизонт, я прогоняю Canny edge detection + преобразование Хафа (Hough transform). Если реальная линия горизонта находится на 19% высоты кадра, а модель заявляет, что он "по центру" (что подразумевает 45–55%), строка удаляется, а итоговый балл корректируется.

Результат: Guard V1 отловил около 70% галлюцинаций с горизонтом и 60% вранья про экспозицию.

Но он абсолютно провалился на самом бесячем моменте - кадрировании. Я не мог написать детерминированную проверку для фразы "главный объект слишком близко к краю", потому что я не знал, что именно и где находится на фото.

YOLO-инсайт: Нам нужна база

Я понял, что мне нужен пространственный Ground Truth. А лучший способ его получить - старый добрый Object Detection.

Если я натравлю YOLO на картинку, я получу bounding box: person at [0.05, 0.10, 0.85, 0.95]. Теперь я точно знаю, какой зазор в процентах (edge gap) остается между объектом и краем кадра. Если LLM рассказывает про "отлично отцентрированный объект с кучей воздуха", а gap меньше пороговых 12% - клейм летит в корзину.

Это был тот самый момент, когда я спросил себя: "А не занимаюсь ли я оверинжинирингом?" Разворачивать целую модель детекции объектов (пусть даже YOLO Nano) просто чтобы факт-чекать языковую модель? Это значит грузить в память веса, добавлять около 80–100 мс задержки и поддерживать еще один микросервис. И ради чего? Чтобы отловить оставшиеся 10–15% галлюцинаций?

Но именно эти "мелкие" галлюцинации могут разрушить доверие к продукту. Когда пользователь загружает хорошо скомпонованный кадр, а ИИ на серьезных щах отчитывает его за "обрезанные краем руки", хотя до границы еще куча места. Юзер понимает, что ИИ на самом деле слеп.

Guard V2: YOLO как оракул геометрии

В общем, в таком виде я и выкатил это в прод. Текущи�� пайплайн выглядит так:

1. YOLO Prepass: Картинка улетает в YOLO Nano. Модель выплевывает bounding boxes, считает отступы от краев, пересечения и пакует это в JSON geometry_facts.

2. Gemini Analysis: Мы отдаем Gemini саму картинку + текстовый JSON с фактами от YOLO в системный промпт.

3. Guard V1: Отрабатывает детерминированные проверки (гистограммы, Хаф).

4. Guard V2 : Ищет в сгенерированном тексте заявления о краях и кадрировании. Пытается сматчить сущность из текста (например, "левая фигура") с конкретным BBox от YOLO. Если зазоры BBox противоречат тексту, текст удаляется.

Вот реальный лог из продакшена:

[GUARD_V2_SHADOW] claims=1 supported=0 contradicted=1 unknown=0
                  final_supported=0 final_contradicted=1 final_unknown=0

[GUARD_V2_TRACE] idx=0 type=edge_close target="cat's nose (right edge)"
                 base=contradicted final=contradicted
                 reason=contradicted_by_box_gap (right_gap=0.22 > threshold)

Здесь Gemini утверждал, что нос кошки вызывал клаустрофобию, «почти касаясь» правого края. YOLO измерил фактическое расстояние от ограничительной рамки до правой стороны кадра — оно составляло комфортные 22 % от ширины. LLM галлюцинировал напряжение там, где его не было. Претензия снята.

Третий слой: Targeted Vision

Иногда YOLO находит просто person, а Gemini делает глубокомысленное замечание про "перья на шляпе". Guard V2 не может их сматчить.

Для таких случаев я добавил фоллбэк - Targeted Vision. Если система не уверена в клейме, скрипт кропает именно ту часть изображения, о которой идет речь, отправляет этот микро-кроп обратно в Gemini и за��ает жесткий вопрос с бинарным ответом:

"Посмотри на ЛЕВЫЙ КРАЙ этого обрезанного участка. Есть ли там объект, который обрезан границей кадра? Ответь только CONFIRMED или NOT_CONFIRMED."

Это стоит копейки (микро-картинка и промпт на 20 токенов), работает очень быстро и закрывает последние слепые зоны.

Цена безумия

Вся эта машина Руба Голдберга (YOLO + детерминированные алгоритмы OpenCV + фоллбэки в Targeted Vision) добавляет к обработке одной фотографии около 100 мс latency и стоит мне дополнительных $6–10 в месяц за ресурсы Cloud Run.

Много ли это? По сравнению со стоимостью привлечения пользователя и ценой его разочарования (churn rate из-за того, что "эта железяка не понимает мои фото") – это ничто.

Технически, я развернул дешевую, тупую и быструю нейросеть (YOLO), чтобы она стояла с палкой над дорогой, умной и медленной нейросетью. Довольно иронично, да? :)


А как вы боретесь с галлюцинациями LLM в проде? Городите такие же многослойные костыли или научились решать это промптами?