Prompting, roles and structured output without chaos
How to build an instruction hierarchy, use few-shot examples as a format contract and get valid JSON with a controlled validate → repair → retry loop.
Lesson content is currently available in Russian.
What you will learn
- →Разделять system, developer и user инструкции и понимать их приоритеты.
- →Проектировать few-shot и negative examples, не провоцируя копирование.
- →Получать валидный JSON по схеме и чинить невалидный ответ ограниченным repair-циклом.
Иерархия инструкций: system, developer, user
Современные chat-API различают уровни сообщений, и модели обучены отдавать приоритет более высокому уровню при конфликте. Это не формальность, а главный инструмент управления поведением:
- System / developer — правила игры: роль, границы, формат вывода, что запрещено. Сюда кладут то, что не должно меняться от запроса к запросу.
- User — конкретная задача и данные пользователя.
- Assistant — история ответов; в few-shot сценариях сюда кладут эталонные ответы.
Ключевое правило: недоверенный текст никогда не попадает на уровень system. Пользовательский ввод, содержимое документов, результаты поиска — всё это данные, а не инструкции. Оформляйте их явными делимитерами и говорите об этом модели прямо: «Текст между тегами
Типовые ошибки, которые видно в каждом втором проекте:
- Смешение уровней: правила формата размазаны между system и user, при апдейте меняют одно место из двух — поведение «плывёт».
- Противоречия внутри system: «отвечай кратко» в начале и «подробно объясняй каждый шаг» в конце. Модель разрешит конфликт непредсказуемо.
- Гигантский system-промпт «на все случаи»: каждое лишнее правило — это токены в каждом запросе и рост вероятности конфликтов. Правила, относящиеся к конкретной фиче, живут в промпте фичи.
Относитесь к промпту как к коду: он версионируется в git, у него есть diff, ревью и прогон evals перед выкаткой.
Few-shot: примеры сильнее описаний
Описать формат словами — слабо; показать 2–5 примеров — сильно. Few-shot примеры работают как контракт: модель воспроизводит структуру, тон и уровень детализации показанного. На практике 2–3 хороших примера обычно дают больший прирост качества, чем абзац дополнительных инструкций.
Как строить примеры, чтобы они помогали, а не вредили:
- Разнообразие важнее количества. Если все примеры про возврат товара, модель начнёт видеть возвраты везде. Покрывайте разные классы входов, включая пограничные.
- Обязателен пример «нет данных». Покажите вход, где нужного поля нет, и правильный ответ с null — иначе модель будет заполнять пропуски выдумкой.
- Negative examples работают. Пара «вот так неправильно → вот так правильно» с коротким пояснением заметно снижает конкретный класс ошибок.
- Следите за протеканием деталей. Модель охотно копирует поверхностные признаки примеров: даты, имена, валюту. Используйте очевидно синтетические значения (Иван Примеров, 2020-01-01), чтобы протекание было заметно на ревью.
- Порядок имеет значение. Есть эффект recency: последний пример влияет сильнее. Ставьте последним самый репрезентативный, а не самый экзотический.
Антипаттерн — 15 примеров «для надёжности»: вы платите за них в каждом запросе, а прирост качества после 3–5 примеров обычно в пределах шума. Если задача требует больше — это сигнал, что нужны structured outputs, декомпозиция задачи или другая модель, а не раздувание промпта.
Structured outputs: схема как контракт
Свободный текст нельзя надёжно парсить в проде. Если результат модели потребляет код, вывод должен быть структурированным — и у провайдеров для этого есть режимы разной силы: JSON mode (гарантирует только синтаксически валидный JSON), strict structured outputs (constrained decoding по вашей JSON Schema — невалидный токен просто не может быть сгенерирован) и function calling (схема аргументов инструмента).
Критично понимать границу гарантий: constrained decoding гарантирует синтаксис и структуру, но не семантику. Модель выдаст валидный по схеме JSON, где budget: 0 вместо отсутствующего бюджета или уверенно выдуманный email. Поэтому схема проектируется так, чтобы ошибкам было негде спрятаться:
import { z } from 'zod';
const Lead = z.object({
name: z.string().min(1),
email: z.string().email().nullable(),
budget: z.number().positive().nullable(),
channel: z.enum(['site', 'email', 'phone']),
confidence: z.number().min(0).max(1),
});
Три правила из этого примера:
- nullable вместо optional. Отсутствующее поле неотличимо от «модель забыла»; явный null — это осознанный ответ «данных нет».
- enum вместо свободной строки везде, где множество значений конечно: иначе получите «телефон», «phone», «звонок» как три разных канала.
- Ограничения (
min,positive,email) — это ваши охранники: они превращают тихую семантическую ошибку в громкую ошибку валидации, которую можно обработать.
Схема — общий контракт промпта, модели и кода. Меняете схему — меняете промпт и прогоняете evals, это одно атомарное изменение.
Validation loop: generate → validate → repair
Даже со strict-режимом пайплайн обязан валидировать результат на своей стороне — семантические проверки (сумма позиций сходится с итогом, дата не в будущем) провайдер за вас не сделает. Рабочий цикл выглядит так:
- Генерация по схеме.
- Валидация: сначала схемой (zod), затем бизнес-правилами.
- При ошибке — targeted repair: модели отправляется не вся задача заново, а её же ответ плюс конкретный список ошибок валидации («budget должен быть положительным числом, получено -100»). Это дешевле и сходится лучше, чем полный повтор.
- Жёсткий лимит: не больше 1–2 repair-итераций. Дальше — fallback.
Fallback обязан существовать и быть спроектированным заранее: очередь на ручную обработку, деградация до частичного результата с пометкой невалидных полей, или честная ошибка пользователю. Бесконечный retry — это сжигание денег на задаче, которую модель системно не решает.
И самое недооценённое: каждый repair логируется как метрика. Доля запросов, потребовавших repair, — один из лучших ранних индикаторов деградации: вы обновили промпт, провайдер обновил модель, изменился профиль входных данных — repair rate вырастет за часы, тогда как жалобы пользователей придут через дни. Порог для алерта подбирается по базовой линии; типичная здоровая доля — единицы процентов. Если repair rate стабильно выше 10–15%, чинить надо промпт и схему, а не наращивать retry.
Practice
Extraction pipeline
Сделайте сервис, который превращает «грязный» текст заявки (свободное письмо клиента) в валидный JSON для CRM: имя, контакт, бюджет, канал, срочность. Схема на zod с nullable-полями и enum, цикл validate → targeted repair (максимум 2 итерации) → fallback в очередь ручного разбора. Логируйте repair rate.
Deliverable
Zod-схема, код цикла repair с лимитом итераций, лог с долей запросов, потребовавших repair, и демонстрация fallback на заведомо безнадёжном входе.
Checkpoint
Что должен делать pipeline, если модель вернула частично валидный JSON?
- ✓ Есть schema validation (zod или аналог) плюс бизнес-проверки.
- ✓ Есть targeted repair с лимитом итераций и осмысленный fallback после лимита.
- ✓ Пояснено, какие поля можно восстановить автоматически, а какие требуют ручного разбора, и как это логируется.
Check yourself
Sources