Модель ответила — это полдела. Теперь учимся диктовать как именно она должна ответить: в каком формате, какой длины, где остановиться.
У тебя есть три рычага, которыми ты направляешь форму ответа: явная инструкция в промпте (что нужно получить и в каком виде), параметр max_tokens (потолок длины) и stop sequences (триггеры остановки). Без этих рычагов модель отвечает «как считает нужным» — и это всегда хуже, чем «как тебе нужно».
В Дне 1 ты послал запрос и получил какой-то ответ. Если играешь с моделью в плейграунде — норма. Если строишь систему — катастрофа.
Любая система с LLM состоит из двух частей: модель что-то генерирует и твой код это парсит. Если форма ответа не задана, модель свободна — а значит, парсер ломается на каждом втором ответе.
Запрос: «Дай 3 идеи названия для кофейни».
Тот же запрос + «Ответь только JSON-массивом строк, без преамбулы и эмодзи».
Разница не в качестве идей — она в предсказуемости. Второй ответ ты можешь распарсить в одну строку кода. Первый — нет. Конечно, ты можешь подмётками регулярок и LLM-чистильщиками привести любой ответ в порядок, но это лишние вызовы, лишние токены, лишние баги.
Управление формой ответа — это не косметика. Это разница между «прототипом, который иногда работает» и «системой, на которую можно опираться».
В твоём распоряжении три механизма. Они работают на разных уровнях и решают разные проблемы:
Самый мощный и универсальный рычаг. Ты прямо в тексте промпта говоришь модели, что и как ты ждёшь.
Что можно указывать:
Где работает: везде. Это базовый и самый управляемый рычаг.
Технический параметр, который ограничивает максимум токенов в ответе. Модель не может сгенерировать больше этого числа, даже если очень захочет.
Что это даёт:
Главное: это не способ задать длину. Если ты поставил max_tokens=100, а модели нужно 200 для нормального ответа — она просто оборвётся посередине. Ответ придёт с finish_reason: "length", и это будет битый ответ.
Привычка: ставить max_tokens с запасом, а реальную длину контролировать через инструкцию в промпте.
Массив строк. Как только модель в процессе генерации выдаёт любую из этих строк — она немедленно прекращает писать. До этой строки. Сама строка в ответ не попадает.
Зачем это нужно:
"\n\n", "###", "END""User:", чтобы модель не отвечала за пользователяКогда не использовать: большинство современных моделей сами останавливаются на нужном месте, если инструкция в промпте написана нормально. Stop sequences — это «жёсткий стоп-кран» на случай, когда модель упрямо лезет дальше.
Каждый из трёх — на своём уровне:
Инструкция в промпте говорит модели что сделать.
max_tokens задаёт сколько модель может потратить.
stop sequences задают когда именно остановиться.
Самая распространённая задача — заставить модель отвечать в строго определённом формате (чаще всего JSON), чтобы потом этот ответ парсить машиной. Тут есть несколько уровней «строгости».
Самый простой подход. Работает в 80-90% случаев, но иногда модель решит, что красивее будет добавить пояснение перед JSON или обернуть его в markdown ```code-блок```.
Уровень надёжности — низкий. Подходит для прототипов и нечастых вызовов, где ты можешь руками подчистить парсер.
У OpenAI и нескольких других провайдеров есть параметр response_format: "json_object". Когда он включён, провайдер гарантирует, что в ответе будет валидный JSON — иначе вернётся ошибка.
Что важно: валидный ≠ правильной структуры. Это будет JSON, но какие в нём ключи и типы — зависит от модели. Поля могут быть не те, что ты ждал.
Уровень надёжности — средний. JSON парсится всегда, но содержимое нужно проверять.
Самый надёжный механизм. Ты передаёшь провайдеру JSON Schema — описание ожидаемой структуры (какие поля, какие типы, что обязательно). На стороне провайдера декодер модели физически может выдавать только токены, валидные под эту схему.
Это есть у OpenAI (response_format: { type: "json_schema", ... }), у Anthropic есть аналогичное через tool calling. Многие маленькие провайдеры тоже поддерживают через библиотеку Outlines.
Уровень надёжности — максимальный. Если ответ пришёл — он 100% соответствует схеме. Но: чуть медленнее обычного режима и не все модели это умеют.
Старая, но очень мощная техника: показать модели 2-3 примера правильно оформленных ответов прямо в промпте. Модель будет имитировать формат.
Когда не нужно строгости (например, формат полусвободный — генерация текста с маркерами), few-shot работает лучше схем, потому что показывает стиль, а не только структуру.
Также полезно, когда структура сложная или непривычная для модели — двух примеров достаточно, чтобы она «поняла шаблон».
Распространённая ошибка — писать модели длинные списки запретов: «не пиши преамбулу, не используй emoji, не извиняйся, не пиши в конце "надеюсь это поможет"». Это работает плохо.
Модель лучше понимает позитивные инструкции, чем негативные. «Ответь одной строкой JSON» лучше, чем «не пиши длинно и не добавляй комментариев». Когда нужны запреты — оставляй максимум 1-2, действительно важные, и формулируй их в начале промпта.
Если формат постоянный для всех запросов системы — клади его в system. Это «правила игры», которые не меняются.
Если формат зависит от конкретного запроса — клади в user. Это «параметр текущей задачи».
Смешивание (формат и там и там) — путь к багам: разные части промпта будут противоречить.
«Хочу ответ в 50 слов — поставлю max_tokens=80». Не работает. Модель напишет нормальный ответ длиной 150 токенов, и в твоём случае он оборвётся на полуслове. max_tokens — это потолок, аварийная отсечка. Длину задают словами в промпте: «не более 50 слов», «в одно предложение».
Получил «JSON» от модели, кинул в JSON.parse — и в 5% случаев получишь исключение. Модель добавила пояснение, обернула в markdown, поставила лишнюю запятую. Любое использование free-form формата требует обработки ошибок: либо retry с уточнением промпта, либо Structured Outputs с гарантиями.
Если finish_reason === "length", ответ обрезан — независимо от того, как он выглядит. JSON может быть невалидным (пропущенная закрывающая скобка), текст оборван на полуслове. Это не «нормальный ответ покороче», это битый ответ. Лечится увеличением max_tokens или укорачиванием инструкции на «короче».
В плейграунде задай вопрос «Расскажи о трёх главных рисках работы с LLM в проде». Один раз — как есть, без инструкций по формату. Второй раз — добавь «Ответь JSON-массивом объектов с полями name, description, severity (high/medium/low). Без преамбулы и пояснений». Сравни — обрати внимание, что во втором случае ответ можно скормить парсеру, а в первом — нет.
Возьми длинный запрос («объясни подробно, как работает HTTPS»), поставь max_tokens в маленькое значение (50-80). Запусти. Посмотри в ответе на finish_reason — он будет "length". Сам текст оборван. Это даст тебе физическое ощущение того, как именно ломаются ответы из-за низкого лимита.
Возьми запрос «Дай совет, как улучшить мотивацию». Прогон 1: «не пиши вводных фраз, не используй списки, не добавляй emoji, не извиняйся». Прогон 2: «Ответь одним абзацем в 2-3 предложения, конкретно и по делу». Посмотри — какой из них модель выполняет точнее. Это даст интуицию про то, что позитивные инструкции работают лучше.