Структурированный вывод: JSON вместо прозы
Ваш бот-ревьюер вечером честно обещал: «Ответ дай строго в формате JSON». И первые двадцать PR всё было хорошо. На двадцать первом модель вошла во вкус: «Конечно! Вот мои замечания в запрошенном формате:» — и дальше JSON. На двадцать втором она обернула ответ в
```json-забор. На двадцать третьем поставила запятую после последнего элемента массива. json.loads падает в 03:40 ночи, CI красный, а модель ни в чём не виновата: она выполнила просьбу «насколько поняла».
Урок здесь один, и он фундаментальный: договорённость «отвечай JSON» — это не контракт, а просьба. Контракт — это когда некорректный ответ невозможно не заметить: он либо парсится и проходит валидацию, либо программа падает с внятной ошибкой и просит у модели исправление. Такой контракт строится в три уровня защиты.
Уровень 1: правильно попросить
Первый уровень — промпт из прошлого занятия, доведённый до конца. Работают три вещи:
Схема. Не «в формате JSON», а точная структура: список полей, типы, допустимые значения. Модель отлично следует схеме — когда она видна.
Пример. Few-shot образец ответа (тот самый из шаблона) стабилизирует формат сильнее любых описаний.
Пустой случай. Отдельно опишите, что делать, когда замечаний нет: {"comments": []}. Без этой строчки модели любят отвечать прозой «Замечаний нет!» — и ломают парсер на ровном месте.
Уровень 1 резко снижает долю кривых ответов, но не отменяет их. На большом потоке PR двадцать первый ответ всегда наступает. Поэтому дальше — жёсткая часть.
Уровень 2: режимы провайдера
Многие провайдеры умеют включать JSON-режим: параметр вроде response_format в запросе, после которого модель технически не может выдать невалидный JSON — синтаксис гарантирован на уровне декодера. Ставится обычно одной строкой рядом с выбором модели.
Важная оговорка, на которой спотыкаются: JSON-режим гарантирует синтаксис, но не схему. Ответ {"result": "ok"} — прекрасный валидный JSON, который ваш парсер comments не найдёт. Поля могут отсутствовать, типы — гулять ("line": "14" строкой вместо числа). Провайдер следит за грамматикой, за смыслом отвечаете вы.
Ближайший родственник — tool calling: вы описываете модели «функцию» с параметрами по схеме, и вместо текста она возвращает структурированный вызов. Это самый сильный механизм получения схемы (а в модуле 02 на нём будут стоять инструменты агента). Для простых задач хватает JSON-режима плюс валидации — к tool calling вернёмся, когда появится петля.
Уровень 3: парсинг-гигиена
Какой бы уровень ни работал выше, ваш код обязан пережить всё. Три правила.
Правило 1: прощайте обёртки. Присказки («Конечно! Вот...»),
```json-заборы, пустые строки — обычный мусор вокруг массива. Не падайте на него: найдите первый [ и последний ] в ответе и разбирайте срез между ними.
Правило 2: валидируйте каждое поле. json.loads гарантирует только синтаксис. Дальше проверяйте сами: file — непустая строка, line — целое ≥ 1, severity — одно из трёх разрешённых значений, message — непустая строка. Неверное значение — ValueError с текстом, называющим поле и проблему. Проверка на «целое» в питоне с подвохом: isinstance(True, int) — истина, а True как номер строки вам не нужен; проверяйте type(value) is int.
Правило 3: ошиблись — повторите с traceback'ом. Модель исправляет свои ошибки лучше, чем делает с нуля, когда ей показывают, что именно сломалось: «поле line: ожидалось целое ≥ 1, получено 0». Отправляете текст ошибки обратно — и получаете исправленный ответ. Обязательно с лимитом повторов (два-три): ретрай без счётчика — это та же бесконечная петля, только вежливая.
Всё вместе это называется «контракт»: некорректный ответ не может проскочить незамеченным — он либо чинится автоматически, либо громко падает с внятной ошибкой.
pydantic в бою, dataclass в учёбе
В боевом коде правило 2 обычно закрывает библиотека pydantic: описываете класс с типами полей — она валидирует и конвертирует:
from pydantic import BaseModel
class ReviewComment(BaseModel):
file: str
line: int
severity: str # в бою — Literal["info", "warning", "critical"]
message: str
comment = ReviewComment.model_validate({"file": "app.py", "line": 14,
"severity": "warning", "message": "..."})model_validate бросает ошибку с именем поля и причиной — ровно то, что нужно для ретрая. В домашнихках этого модуля обойдёмся dataclass и ручной проверкой: зависимостей меньше, а механика валидации — та же, и однажды заменив свой if на pydantic, вы будете понимать, что именно он делает.
Что запомнить
«Отвечай JSON» — просьба, не контракт. На потоке кривые ответы неизбежны: присказки, заборы, гуляющие поля.
Три уровня защиты: схема и пример в промпте → JSON-режим провайдера (синтаксис, не схему!) → парсинг-гигиена: снять мусор, валидировать поля, повторить с текстом ошибки.
Ретрай — с лимитом. Бесконечные «попробуй ещё раз» — та же зацикленная петля.
pydantic закрывает валидацию в бою; dataclass и ручные проверки — наш учебный путь, механика та же.
Дополнительно
pydantic — документация: модели,
model_validate, типы полей.
Как устроены LLM: токены, контекст, цена
10 мин
Домашка: токен-калькулятор
4 мин
Квиз: Как устроены LLM: токены, контекст, цена
8 мин
Промпты как код: шаблоны и переменные
8 мин
Домашка: промпт ревьюера
3 мин
Квиз: Промпты как код: шаблоны и переменные
7 мин
Структурированный вывод: JSON вместо прозы
6 мин
Домашка: парсер ревью
3 мин
Квиз: Структурированный вывод: JSON вместо прозы
7 мин
Выбор модели: GPT, Claude, открытые
7 мин
Домашка: бенчмарк вслепую
4 мин
Квиз: Выбор модели: GPT, Claude, открытые
7 мин
HARD-задача: смета агента
3 мин
Локальная модель: Ollama по-настоящему
8 мин
Квиз: Локальная модель: Ollama по-настоящему
6 мин