Structured outputs, которые не работают: пять ситуаций, где схема есть, а гарантий нет

Введение: обещание структуры — и реальность багов

Представьте: вы внедряете AI-агента, который должен парсить счета-фактуры и выдавать JSON с полями «дата», «сумма», «ИНН». Вы чётко описали схему, использовали structured outputs (или function calling), протестировали на пяти примерах — всё идеально. А через неделю продакшн-агент начинает присылать null в поле «дата» и ломать интеграцию с бухгалтерией.

Это не гипотетический сценарий. Разработчики и инженеры всё чаще сталкиваются с тем, что structured outputs не гарантируют того, что обещают — строгого соответствия JSON-схеме. Как пишут авторы статьи на Habr Источник, проблема кроется не в самом инструменте, а в неправильных ожиданиях и граничных случаях, которые не покрываются типовыми тестами.

В этой статье мы разберём пять реальных ситуаций, когда схема есть, а гарантий нет. Вы узнаете, какие подводные камни ожидают AI-инженеров, и как их избежать.

1. Модель «забывает» поля при сложных запросах

Первая и самая распространённая проблема — модель может опустить обязательное поле, если запрос слишком длинный или контекст перегружен. Это происходит, когда модель жертвует структурой ради «успешного» ответа по смыслу.

Пример из практики:

Команда, разрабатывающая AI-помощника для техподдержки, настроила structured output с полями:
- issue_type (обязательно)
- priority (обязательно)
- customer_name (опционально)
- description (обязательно)

При обработке длинного диалога клиента (более 4000 токенов) модель в 12% случаев опускала поле priority. Система валидации не пропускала ответ, и агент «зависал» в бесконечном цикле повторных попыток.

Почему так происходит:

Structured outputs работают через встраивание схемы в промпт или через вызов функции. Модель не выполняет код — она предсказывает токены. Если длина входного контекста близка к лимиту, модель может «забыть» часть требований схемы.

Что делать:

  • Установите лимит на длину входного текста (например, не более 3000 токенов для GPT-4).
  • Используйте чекеры на стороне приложения: если ответ не проходит JSON Schema validation — отправляйте запрос повторно с уменьшенным контекстом.
  • Разбивайте задачу на подшаги: сначала извлеките сущности, потом сформируйте JSON.

2. JSON валиден, но данные — мусор

Вторая ситуация ещё коварнее. Ответ приходит в 100% правильном JSON — все поля на месте, типы данных корректны. Но содержимое не имеет смысла.

Кейс из статьи:

Разработчики AI-сервиса для анализа договоров настроили схему с полем contract_date формата date. Модель стабильно возвращала строку в формате "2024-13-01" — несуществующая дата, 13-й месяц. JSON-схема не выявила ошибку, потому что строка прошла валидацию как string. Только бизнес-юристы заметили, что договоров с такой датой не существует.

Почему так происходит:

Стандартная JSON Schema не умеет проверять семантическую корректность — только синтаксическую. Она не знает, что в году 12 месяцев, а в феврале 28 или 29 дней.

Что делать:

  • Добавьте дополнительный слой валидации: после получения JSON запускайте проверку логики (например, month <= 12).
  • Используйте библиотеки типа pydantic с кастомными валидаторами.
  • Для критичных полей (даты, суммы, коды) внедрите регулярные выражения или lookup-таблицы.

3. Иллюзия воспроизводимости: тест прошёл — продакшн упал

Одна из главных ловушек — считать, что если structured outputs работают на тестовых данных, то будут работать и в продакшене. Модели LLM недетерминированы: один и тот же промпт может дать разные результаты при разных температурах.

Пример:

Разработчики из статьи настроили температуру 0.0 для structured outputs, надеясь на полную детерминированность. Но даже при нулевой температуре модель может выдавать разные ответы из-за floating-point rounding в GPU. В 2% запросов структура нарушалась.

Почему так происходит:

  • Распределение вероятностей при температуре 0.0 не гарантирует выбора одного и того же токена — зависит от аппаратного обеспечения.
  • Разные версии модели (gpt-4-0613 vs gpt-4-0125-preview) могут по-разному обрабатывать одну и ту же схему.

Что делать:

  • Зафиксируйте версию модели в продакшене.
  • Добавьте возможность повторной попытки (retry) с тем же промптом до 3 раз.
  • Пишите юнит-тесты на конкретные граничные случаи (например, пустой ввод, экстремально длинные строки).

4. Схема есть, но модель игнорирует enum

Если в схеме указано поле с перечислением (enum), модель иногда возвращает значение, не входящее в список. Особенно часто это случается с названиями стран, валют или типов ошибок.

Реальный случай:

Для системы маршрутизации тикетов настроили enum ["billing", "technical", "general"]. Модель в 5% случаев возвращала "account_issue" — слово, похожее по смыслу, но отсутствующее в enum. Валидатор отбрасывал ответ, агент тратил время на повторные запросы.

Почему так происходит:

LLM не исполняет код — она генерирует текст, похожий на обучающие данные. Если в обучающей выборке часто встречается термин "account_issue", модель может его выдать, проигнорировав ограничения схемы.

Что делать:

  • Используйте постпроцессинг: маппинг нестандартных значений на ближайшее из enum (например, с помощью cosine similarity).
  • Включите описание enum в промпт: "Варианты: billing, technical, general. Если не уверен — выбери general".
  • Увеличьте количество примеров для few-shot, включая граничные случаи.

5. Проблема с вложенными структурами и рекурсией

Пятая ситуация — когда схема содержит вложенные объекты или массивы объектов. Модель может «срезать» уровни вложенности, возвращая неполную структуру.

Иллюстрация:

Схема для извлечения списка контактов из письма:

{
  "contacts": [
    {
      "name": "string",
      "email": "string",
      "phone": {
        "country_code": "string",
        "number": "string"
      }
    }
  ]
}

Модель часто возвращает phone как простую строку "+1234567890", игнорируя вложенную структуру.

Почему так происходит:

Генерация вложенных JSON — задача с высокой когнитивной нагрузкой для модели. Она может «забыть» один уровень, особенно при большом количестве полей.

Что делать:

  • Упростите схему: избегайте вложенности глубже 2 уровней.
  • Используйте плоскую структуру с префиксами (phone_country_code, phone_number).
  • Если вложенность обязательна — разбейте задачу на два последовательных запроса: сначала извлеките контакты, потом для каждого контакта запросите телефон.

Заключение: доверяй, но проверяй

Structured outputs — мощный инструмент, но не панацея. Как показывают примеры из статьи на Habr Источник, схемы не гарантируют ни полноты данных, ни их семантической корректности, ни воспроизводимости.

Ключевые принципы, которые стоит внедрить:

  1. Многослойная валидация: проверяйте не только синтаксис JSON, но и логику полей.
  2. Ретраи с адаптацией: уменьшайте контекст при повторных попытках.
  3. Тестирование на граничных случаях: пустой ввод, максимальная длина, редкие enum.
  4. Мониторинг в реальном времени: логируйте каждый вызов structured output и отслеживайте процент ошибок.

Только сочетание корректно настроенной схемы и надёжного прикладного кода позволяет получить настоящие гарантии. Иначе схема есть — а гарантий нет.

Если вы работаете с AI-интеграциями и хотите автоматизировать извлечение данных из писем, документов или чатов — ASI Biont поддерживает подключение к Telegram, Google Sheets и CRM через API — подробнее на asibiont.com/courses.

← Все статьи

Комментарии

Читайте также