
Проект PhotoMentor создавался как ИИ-ментор для фотографов. Механика простая: пользователь загружает снимок, а под капотом Gemini выступает в роли арт-директора: анализирует композицию, работу со светом, цветовую гармонию и выдает детальный фидбек с оценкой.
С главной проблемой Vision-моделей я столкнулся во время тестов на собственных фото. Скормил Gemini крупный портрет собаки, положившей морду на лапы.
Модель выдала в числе прочего:
"Снимок слишком сильно обрезан снизу; вы оставили слишком мало пространства между лапами собаки и краем кадра".
У фото снизу было около 15% пространства. Никакой «тесноты» там не было и в помине.
Прогнав еще несколько снимков, я убедился: это не редкий краевой баг. Gemini системно галлюцинировала в вопросах пространственной геометрии примерно на 15–20% фотографий. И я продолжаю бороться с этим до сих пор.

Модель могла уверенно заявить, что объект «обрезан краем кадра», когда он находился ровно по центру. Она видела «выбитые света» на идеально экспонированных снимках. Она описывала сцену с хирургической точностью, но промахивалась в базовой геометрии.
Я попробовал классический промпт-инжиниринг. Добавил в системный промпт "Будь объективен", "Описывай только то, что можешь подтвердить по пикселям", заставил модель отдавать поле confidence для каждого утверждения.
Частота галлюцинаций почти не изменилась.
Поэтому я сделал то, что сделал бы любой разработчик, у которого начинает дергаться глаз: начал городить многослойную систему костылей и проверок, чтобы нянчиться с большой языковой моделью.
Почему Vision LLM не понимают физику пространства
Если вы работали с мультимодальными LLM, для вас это не секрет: они мыслят токенами, а не геометрией. Когда LLM пишет "горизонт расположен строго по центру", она не высчитывает координаты пикселей. Она просто знает, что на фотографиях с похожим паттерном токенов кожаные критики часто используют фразу "centered horizon".
Хуже того, у LLM патологическая уверенность в себе. В моих логах был случай, когда одно и то же фото при двух разных запросах получило два противоположных вердикта: первый раз объект был "зажат слева", а второй раз "имел достаточно воздуха слева".
Попытка 1: Детерминированный Guard V1
Раз уговоры в промптах не работают, я написал скрипт постобработки (Guard V1). Архитектура была простой:
Gemini анализирует фото и отдает JSON с критикой.
Guard V1 перехватывает текст и ищет в нем рискованные утверждения (spatial claims) по ключевым паттернам (например,
CLAIM_HORIZON_CENTERED,CLAIM_BLOWN_HIGHLIGHTS).Если клейм найден, скрипт проверяет его честной математикой по пикселям исходного изображения.
Например, если модель заявляет про "выбитые света", скрипт на 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 в проде? Городите такие же многослойные костыли или научились решать это промптами?
