Эфир 18.11 · Поток 318 ноября в 19:00 МСК — бесплатный практический эфир по ИИ-агентам · Живой Поток 3 стартует 23.11Эфир 18 ноября →Поток 3 (23.11)
РелизКод / SDK · В работуMachine Learning Mastery→ оригинал

Схема пройдена, а сумма в счёте занижена в 10 раз: как проверять structured output LLM

Модель вернула идеально валидный JSON: все ключи на месте, типы верные, цитата из счёта настоящая. Только сумма — 12 500 центов вместо 125 000, то есть 125 евро вместо 1 250. Ни парсер, ни JSON Schema такое не ловят. Автор разбирает три слоя защиты: строгий контракт Pydantic со сверкой с исходным текстом, ограниченные ретраи и constrained decoding.

AI-обработка оригинала Machine Learning Mastery; редакция Hamidun News
Схема пройдена, а сумма в счёте занижена в 10 раз: как проверять structured output LLM
Источник: Machine Learning Mastery. Коллаж: Hamidun News.
Слушать статью
Паспорт материала и цифры
ПараметрДанные
ПервоисточникMachine Learning Mastery ↗
Главный редактор
Ключевой факт #1strict-режим Pydantic не даёт строке "125000" тихо стать числом
Ключевой факт #2Ретраи — только для shape_error, максимум три попытки
Ключевой факт #3Число из ответа сверяйте с цитатой из исходного документа

Вот счёт из четырёх строк: номер 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 включайте как экономию на ошибках формы, но не вместо своих проверок. Каждый путь отказа стоит покрыть тестами с мок-клиентом, как это делает автор.

Сюжет

Вопросы и ответы по материалу

Что известно по теме: контракт и сверка с источником?

Первый слой — контракт, которым владеет приложение, а не промпт. В примере это модель Invoice с четырьмя полями. Два параметра конфигурации делают основную работу. Режим strict=True отключает неявные приведения Pydantic: строка "125000" в целочисленном поле даёт ошибку int_type, а не молча превращается в число. То же с булевым True, которое иначе стало бы единицей. Параметр extra="forbid" превращает лишний ключ в ошибку, а не в игнорируемый мусор.

Что известно по теме: когда повторять запрос, а когда нет?

Второй слой — ретраи с жёсткой политикой. Повторять имеет смысл только shape_error: модель можно попросить исправить форму, передав ей список ошибок. Если же ответ rejected, повтор бесполезен, потому что содержимое счёта не изменится. Отказ провайдера или ошибка конфигурации тоже останавливают цикл сразу. Максимум три попытки: первая и не больше двух регенераций. Обратная связь ограничена пятью причинами, чтобы промпт не превращался в стенограмму всех неудач. Схема и исходный текст между попытками не меняются.

Что известно по теме: Constrained decoding: полезен, но не панацея?

Третий слой — constrained decoding. Генератор физически может выдавать только токены, которые не выводят ответ за пределы схемы, поэтому целый класс ошибок формы до валидации просто не доходит. Но схема не выражает правило «центы должны совпадать с цитатой». Декодер, идеально соблюдающий схему, всё равно выдаст те самые 12 500. Валидный объект — это пропуск на следующую проверку, а не результат. Ограничения тоже есть. Пример учебный: две валюты, простая проверка цифр.

ЖХ
Жемал Хамидун — CPO Alpina Digital, продакт-менеджер AlpinaGPT. Делает AI-продукты с 2019 года. Hamidun News — pet project, который вырос в production-агрегатор с 6000+ читателей.
✦ Единое AI-пространство · Nomad

Протестируйте новые LLM и мультимодальные модели в одном окне

Платформа Nomad AI (nomadnet.ai) даёт прямой доступ к Claude Opus 5.5, GPT-6, Gemini 3.1, DeepSeek и автономным агентам без VPN, зарубежных карт и зоопарка подписок.

Открыть Nomad AI →Автономный Nomad Agent
Что вы думаете?
Загружаем комментарии…