Свободный текст приятен человеку и неприятен программе.
Когда LLM отвечает абзацем, backend получает загадку вместо данных. Её пытаются разобрать регулярками, split-ами, поиском ключевых слов и надеждой. Последняя особенно плохо работает как dependency.
Production-интеграция начинается с вопроса: какой объект должна вернуть модель?
Не «что она должна сказать», а именно «какой объект».
Плохой контракт
Определи, можно ли одобрить заявку. Ответь кратко.Возможные ответы:
- «Да, можно»
- «Скорее да»
- «Одобрить»
- «Заявка выглядит допустимой»
- «Нет, потому что...»
- «Недостаточно данных»
Для человека это терпимо. Для backend — грязь.
Хороший контракт
{
"decision": "approve",
"confidence": 0.91,
"missing_fields": [],
"risk_flags": ["low_document_quality"],
"human_review_required": false,
"explanation": "Все обязательные поля заполнены, явных противоречий нет"
}Теперь система может:
- маршрутизировать заявку;
- показывать объяснение оператору;
- собирать статистику;
- искать частые risk_flags;
- ставить regression tests;
- менять модель без переписывания всего продукта.
Structured output — это не косметика
Structured output нужен не потому, что JSON красивый. Он нужен потому, что backend-система живёт контрактами.
Контракт ответа должен быть:
- минимальным;
- типизированным;
- расширяемым;
- валидируемым;
- независимым от текста prompt-а;
- понятным бизнесу и разработчикам.
Если поле не используется downstream-системой, не добавляйте его «на всякий случай». Всякий случай потом станет вашим техническим долгом.
Чеклист главы
- Ни один backend-компонент не парсит свободный текст модели регулярками или split-ами.
- Для каждого LLM-вызова записан контракт ответа: поля, типы, ограничения.
- Каждое поле контракта используется downstream — «на всякий случай» полей нет.
- Enum-поля закрыты списком значений, а не «строка, в которой обычно категория».
- Человекочитаемое объяснение (
explanation) отделено от машиночитаемого решения (decision).
Практикум
Задача 7.1 — вскрытие свободного текста. ⭑
Попросите модель без схемы ответить «кратко, можно ли одобрить заявку» на 15 разных входах (сгенерируйте заявки сами: полные, неполные, противоречивые). Соберите все варианты формулировок ответа. Затем напишите парсер этих ответов на регулярках — честно, как сделала бы команда без контракта.
Критерии приёмки:
- Собрано минимум 6 различных формулировок одного и того же решения.
- Парсер написан и на каких-то ответах ошибается или падает — зафиксируйте, на каких.
- Написан вывод: сколько строк костылей заменяет одна схема из 6 полей.
Подсказки:
- Это «задача-прививка»: цель — один раз прочувствовать боль, чтобы больше никогда так не делать.
Задача 7.2 — спроектировать контракт от потребителя. ⭑⭑
Возьмите типовой кейс: модель оценивает заявку на возврат средств. Потребители результата: (а) автоматика, которая одобряет возвраты до 1000 ₽; (б) оператор, которому нужно объяснение; (в) аналитик, который строит отчёт по причинам возвратов. Спроектируйте Pydantic-контракт, отталкиваясь от нужд всех трёх потребителей — и ни полем больше.
Критерии приёмки:
- Для каждого поля указано, какой потребитель его использует.
- Поля, которые не нужны ни одному потребителю, отсутствуют (проверьте себя:
confidenceкому-то реально нужен? как он будет использоваться?). - Контракт валидируется: enum для причин, ограничение длины для объяснения,
ge/leдля чисел. - Написан один негативный тест: модель вернула причину вне enum — что происходит?
Подсказки:
- Начните с SQL-запроса, который аналитик напишет по вашим данным. Если запрос невозможен — контракт плохой.