Схема пройдена, а сумма в счёте занижена в 10 раз: как проверять structured output LLM
Модель вернула идеально валидный JSON: все ключи на месте, типы верные, цитата из счёта настоящая. Только сумма — 12 500 центов вместо 125 000, то есть 125 евро вместо 1 250. Ни парсер, ни JSON Schema такое не ловят. Автор разбирает три слоя защиты: строгий контракт Pydantic со сверкой с исходным текстом, ограниченные ретраи и constrained decoding.
AI-обработка оригинала Machine Learning Mastery; редакция Hamidun News
Вот счёт из четырёх строк: номер 2026-0417, плательщик Northwind Traders, «Total due: EUR 1,250.00», оплатить в течение 30 дней. Вы просите модель достать номер, валюту, сумму в центах и строку, из которой она взяла сумму. Ответ приходит такой: сумма 12 500, валюта EUR, цитата «Total due: EUR 1,250.00».
JSON валиден, все ключи на месте, типы правильные, EUR входит в разрешённый список, цитата действительно есть в счёте. Но 12 500 центов — это 125 евро, а в счёте 1 250. Ошибка в десять раз, и ни один парсер её не заметит: 12500 — отличное целое число. Поймать её можно только сравнением с исходным текстом, и это сравнение придётся писать самому.
У Machine Learning Mastery вышел разбор, который показывает, как строить защиту в три слоя. Structured output давно стал стандартной функцией: Responses API у OpenAI принимает JSON-схему или модель Pydantic, vLLM делает то же на локальном сервере через xgrammar. Большинство туториалов заканчивается на моменте «JSON распарсился». Автор считает, что именно здесь всё и начинается.
Контракт и сверка с источником
Первый слой — контракт, которым владеет приложение, а не промпт. В примере это модель Invoice с четырьмя полями. Два параметра конфигурации делают основную работу. Режим strict=True отключает неявные приведения Pydantic: строка "125000" в целочисленном поле даёт ошибку int_type, а не молча превращается в число. То же с булевым True, которое иначе стало бы единицей. Параметр extra="forbid" превращает лишний ключ в ошибку, а не в игнорируемый мусор.
Ещё одна деталь: три поля допускают null, но значения по умолчанию у них нет. Модель может честно сказать «в счёте этого нет», но не может промолчать и не должна выдумывать число, лишь бы не писать null. Деньги хранятся в центах как int, чтобы 1 250,00 не превратилось в 1249,9999.
Дальше функция accept() делает четыре шага. Сначала парсинг и проверка формы, это единственный шаг, который может вернуть shape_error. Потом проверка, что цитата есть в источнике дословно. Затем сверка номера счёта и валюты с текстом. Наконец сумма сравнивается с цифрами из цитаты: «1,250.00» без разделителей даёт 125000, модель прислала 12500, объект отклоняется. Исходов четыре: accepted, needs_review, rejected и shape_error. Если сумма в счёте не указана, это needs_review, то есть решает человек, а не выдуманный ноль.
Когда повторять запрос, а когда нет
Второй слой — ретраи с жёсткой политикой. Повторять имеет смысл только shape_error: модель можно попросить исправить форму, передав ей список ошибок. Если же ответ rejected, повтор бесполезен, потому что содержимое счёта не изменится. Отказ провайдера или ошибка конфигурации тоже останавливают цикл сразу. Максимум три попытки: первая и не больше двух регенераций.
Обратная связь ограничена пятью причинами, чтобы промпт не превращался в стенограмму всех неудач. Схема и исходный текст между попытками не меняются. По тезисам статьи, отдельно нужно держать под контролем скрытые транспортные ретраи самого SDK, иначе реальное число запросов окажется больше, чем вы заложили в бюджет.
Constrained decoding: полезен, но не панацея
Третий слой — constrained decoding. Генератор физически может выдавать только токены, которые не выводят ответ за пределы схемы, поэтому целый класс ошибок формы до валидации просто не доходит. Но схема не выражает правило «центы должны совпадать с цитатой». Декодер, идеально соблюдающий схему, всё равно выдаст те самые 12 500. Валидный объект — это пропуск на следующую проверку, а не результат.
Ограничения тоже есть. Пример учебный: две валюты, простая проверка цифр. Для реальных форматов нужен код, который понимает ваши десятичные разделители. Если счета приходят сканами, сначала проверяйте распознанный текст: неверная цифра на входе пройдёт все проверки, пока объект внутренне согласован.
Практический вывод простой. Не доверяйте «зелёной» валидации схемы: храните в ответе цитату-доказательство и проверяйте число против неё. Ретраи используйте только там, где повтор может помочь, и ставьте жёсткий лимит. Constrained decoding включайте как экономию на ошибках формы, но не вместо своих проверок. Каждый путь отказа стоит покрыть тестами с мок-клиентом, как это делает автор.
- 12 нояб. 2026 г.Дженсен Хуанг ушёл с поста CEO Nvidia: компания потеряла $1,4 трлн за две недели
- 12 нояб. 2026 г.Профсоюз Окленда добился переобучения вместо увольнений из-за ИИ: за забастовку 94,1%
- 12 нояб. 2026 г.Восемь стран и OpenAI с Anthropic обещают «рубильник» для ИИ-моделей, но без сроков и штрафов
- 12 нояб. 2026 г.OpenAI и ModRetro выпустили консоль за $200 для игр, написанных в Codex
Вопросы и ответы по материалу
Что известно по теме: контракт и сверка с источником?
Первый слой — контракт, которым владеет приложение, а не промпт. В примере это модель Invoice с четырьмя полями. Два параметра конфигурации делают основную работу. Режим strict=True отключает неявные приведения Pydantic: строка "125000" в целочисленном поле даёт ошибку int_type, а не молча превращается в число. То же с булевым True, которое иначе стало бы единицей. Параметр extra="forbid" превращает лишний ключ в ошибку, а не в игнорируемый мусор.
Что известно по теме: когда повторять запрос, а когда нет?
Второй слой — ретраи с жёсткой политикой. Повторять имеет смысл только shape_error: модель можно попросить исправить форму, передав ей список ошибок. Если же ответ rejected, повтор бесполезен, потому что содержимое счёта не изменится. Отказ провайдера или ошибка конфигурации тоже останавливают цикл сразу. Максимум три попытки: первая и не больше двух регенераций. Обратная связь ограничена пятью причинами, чтобы промпт не превращался в стенограмму всех неудач. Схема и исходный текст между попытками не меняются.
Что известно по теме: Constrained decoding: полезен, но не панацея?
Третий слой — constrained decoding. Генератор физически может выдавать только токены, которые не выводят ответ за пределы схемы, поэтому целый класс ошибок формы до валидации просто не доходит. Но схема не выражает правило «центы должны совпадать с цитатой». Декодер, идеально соблюдающий схему, всё равно выдаст те самые 12 500. Валидный объект — это пропуск на следующую проверку, а не результат. Ограничения тоже есть. Пример учебный: две валюты, простая проверка цифр.
Протестируйте новые LLM и мультимодальные модели в одном окне
Платформа Nomad AI (nomadnet.ai) даёт прямой доступ к Claude Opus 5.5, GPT-6, Gemini 3.1, DeepSeek и автономным агентам без VPN, зарубежных карт и зоопарка подписок.
Главное из мира ИИ — раз в неделю
7 ключевых событий недели, отобранных вручную. Без шума, репостов и пресс-релизов.
Готово! Проверьте почту — мы отправили подтверждение.