Основы промптинга: zero-shot, few-shot, роли, структура и формат ответа
Промпт — это не «заклинание» и не способ уговорить модель. Это единственный интерфейс, через который вы задаёте условие для распределения, из которого модель сэмплирует продолжение. Всё, что вы кладёте в контекст, сужает это распределение; всё, чего вы не положили, модель достроит из своих априорных представлений — и почти всегда не так, как вам нужно.
Эта статья — про фундамент: анатомию запроса, zero-shot и few-shot, роли, разделители, управление форматом ответа. Про chain-of-thought, self-consistency и верификацию — в следующей статье; здесь мы разбираем слой, без которого продвинутые техники дают шум вместо прироста.
Предполагается, что вы уже прочитали как устроена LLM с точки зрения инженера — токены, окно контекста, стоимость.
1. Что на самом деле делает промпт
Модель вычисляет $P(x_t \mid x_{<t})$ — распределение следующего токена при условии всего предыдущего. Промпт — это префикс $x_{<t}$. Отсюда три следствия, из которых выводится практически всё остальное:
Промпт — это условие, а не команда. Фраза «отвечай только JSON» не включает режим JSON. Она сдвигает вероятностную массу в сторону продолжений, которые в обучающих данных следовали за похожими инструкциями. Сдвигает сильно, но не до нуля вероятности отклонения — поэтому валидация на вашей стороне обязательна (см. структурированный вывод).
Позиция важна не меньше содержания. Внимание не распределено равномерно по контексту. Экспериментально показано, что точность извлечения факта из длинного контекста U-образно зависит от его позиции: начало и конец обрабатываются надёжнее середины (Liu et al., «Lost in the Middle», 2023).
Формат заразен. Модель продолжает то, что видит. Если в промпте разнобой в отступах, кавычках и стиле — в ответе будет разнобой. Если три примера подряд отвечают одной строкой без преамбулы — четвёртый ответ будет таким же. Это не «понимание инструкции», это статистика продолжений, и на неё можно опираться гораздо надёжнее, чем на вежливые просьбы.
2. Анатомия запроса
С точки зрения API современный чат-запрос состоит из нескольких слотов, которые провайдер
склеивает в один линейный текст по своему шаблону (chat template). Порядок склейки —
tools → system → messages — и он определяет, что можно кэшировать.
(JSON-схемы)"] --> B["System-промпт:
роль, правила, формат"] B --> C["Few-shot примеры
(как пары user/assistant)"] C --> D["Контекст задачи:
документы, RAG-выдача"] D --> E["Вопрос пользователя"] E --> F["Финальное напоминание
о формате"] F --> G{{"Единый текстовый префикс"}} G --> H["Декодирование:
сэмплирование токенов"] H --> I{"Ответ валиден
по схеме?"} I -- да --> J["Отдать вызывающему коду"] I -- нет --> K["Repair-проход
или повтор"] K --> H style G fill:#4f8fbf,stroke:#33536b,color:#fff style I fill:#c99a4a,stroke:#7a5b25,color:#fff
Роли (system, user, assistant) — не магия, а разметка, которую модель видела при
обучении. Она обучена относиться к system как к инструкции более высокого приоритета,
чем user, и именно поэтому туда кладут правила, которые не должен переопределять
пользовательский ввод. Это важнейший первичный барьер против prompt injection — но именно
барьер, а не гарантия; подробности в статье про безопасность.
границ ролей Tpl->>M: префикс из N токенов M-->>M: forward pass, кэш префикса M->>SDK: токены ответа (стрим) SDK->>App: content-блоки + usage Note over App: валидация, парсинг,
учёт стоимости
Что куда класть
| Слот | Что туда | Чего туда не класть |
|---|---|---|
tools |
Схемы функций, стабильные между запросами | Всё, что меняется от пользователя к пользователю |
system |
Роль, домен, правила, формат, границы поведения | Дату/время, ID сессии, имя пользователя |
messages[].user |
Задачу, данные, контекст | Правила, которые пользователь не должен уметь отменить |
messages[].assistant |
Few-shot ответы-образцы | Префилл финального ответа (на новых моделях запрещён — см. §7) |
Дата и ID в system — самая частая и самая дорогая ошибка. Системный промпт стоит в начале
префикса; изменившийся байт в нём инвалидирует кэш всего, что идёт дальше. Динамику
кладите в конец messages.
3. Zero-shot: инструкция без примеров
Zero-shot — запрос без демонстраций. Работает потому, что современные модели прошли instruction tuning: обучение на парах «инструкция → желаемый ответ» (Ouyang et al., InstructGPT, 2022). Модель научилась распознавать форму инструкции и отвечать на неё, а не продолжать её как текст.
Плохой zero-shot:
Проанализируй этот отзыв.
Модель не знает: что такое «проанализируй», для кого, в каком виде, какой длины. Она выберет самое вероятное — развёрнутое эссе с преамбулой «Конечно! Вот анализ отзыва:».
Рабочий zero-shot собирается из пяти компонентов:
SYSTEM = """\
Ты — аналитик службы поддержки SaaS-продукта.
Задача: классифицировать входящий отзыв клиента.
Категории (выбери ровно одну):
- bug — воспроизводимая техническая неисправность
- feature — запрос функциональности, которой нет
- billing — вопросы оплаты, тарифов, возвратов
- praise — положительный отзыв без запроса
- other — не подходит ни под одну категорию
Правила:
- Если отзыв содержит и жалобу, и запрос функции — выбирай bug.
- Если категория неочевидна, выбирай other, а не угадывай.
- Никаких преамбул и пояснений вне заданного формата.
Формат ответа — ровно три строки:
CATEGORY: <одна из категорий>
SEVERITY: <1-5, где 5 — блокирует работу>
SUMMARY: <до 15 слов>\
"""
Что здесь работает и почему:
- Роль сужает распределение до профессионального регистра.
- Закрытый список категорий превращает открытую генерацию в почти-классификацию: вероятностная масса концентрируется на пяти строках.
- Правила для граничных случаев — самая недооценённая часть. Модель всё равно примет решение в спорной ситуации; вопрос лишь, ваше это решение или её.
- Явный отказ (
other) снижает галлюцинации: без него модель обязана выбрать что-то из списка и выберет ближайшее по звучанию. - Схема ответа задаёт формат жёстче любой просьбы «будь краток».
Негативные инструкции работают хуже позитивных
«Не используй маркдаун» модель выполняет хуже, чем «выводи сплошным текстом». Причина механическая: чтобы обусловиться на «не X», модель должна сначала активировать представление X. Формулируйте, что делать, а не чего избегать. Если негатив всё же нужен — сопроводите его позитивной альтернативой в той же фразе.
Специфичность важнее длины
Простой тест: можно ли по вашему промпту однозначно оценить два разных ответа как «правильный» и «неправильный»? Если нет — промпт недоспецифицирован, и никакие «пожалуйста, будь внимателен» это не исправят.
4. Few-shot: обучение примерами в контексте
Few-shot — это добавление в промпт k демонстраций «вход → выход». Способность появилась как эмерджентное свойство масштаба и была подробно измерена в Brown et al., «Language Models are Few-Shot Learners», 2020. Порядок величин из этой работы (GPT-3 175B, TriviaQA): zero-shot 64.3 %, one-shot 68.0 %, few-shot 71.2 %. С тех пор zero-shot-качество моделей выросло драматически, и разрыв сократился — но не исчез там, где важна форма ответа.
Что примеры делают на самом деле
Ключевой и контринтуитивный результат: Min et al., «Rethinking the Role of Demonstrations», 2022 показали, что замена правильных меток в примерах на случайные ухудшает качество незначительно. Что действительно даёт прирост — это:
- распределение входов (какие бывают тексты),
- пространство меток (какие вообще бывают ответы),
- формат (как именно выглядит пара вход-выход).
Практический вывод, который экономит много времени: few-shot — это в первую очередь инструмент управления формой ответа и калибровки границ категорий, а не способ «научить» модель новому знанию. Если вам нужно новое знание — вам нужен RAG или дообучение.
Сколько примеров
| k | Когда достаточно | Риск |
|---|---|---|
| 0 | Простая задача, сильная модель, формат задан схемой | Разнобой в формате, преамбулы |
| 2–3 | Нужно задать тон и форму ответа | Почти нет |
| 5–8 | Нетривиальные границы категорий, редкие случаи | Растёт стоимость, копирование частностей |
| 16+ | Узкий домен со сложной разметкой | Overfitting на примеры; часто дешевле дообучить |
Практическое правило: начинайте с 3 примеров, покрывающих разные категории и хотя бы один граничный случай. Затем добавляйте по одному только под конкретную ошибку, которую видите в замерах. Пятнадцать однотипных примеров стоят дорого и почти не добавляют информации.
Три ловушки few-shot
Смещение по недавности. Модель непропорционально часто выбирает метку последнего примера. Классический разбор — Zhao et al., «Calibrate Before Use», 2021. Лечение: перемешивать метки так, чтобы последняя не совпадала с самой частой в вашем распределении.
Смещение по частоте. Если 4 из 5 примеров помечены bug, модель начнёт видеть bug
везде. Держите примеры сбалансированными по классам, даже если реальное распределение
перекошено.
Чувствительность к порядку. Lu et al., 2021 показали, что при одном и том же наборе примеров разные их перестановки дают разброс качества от уровня случайного угадывания до state-of-the-art. Если ваш few-shot-набор даёт большой разброс при перестановке — это сигнал, что примеры плохие, а не что нужно искать «счастливый» порядок.
Проверка занимает пять минут и стоит копейки:
import itertools, statistics
def order_sensitivity(examples, evaluate, max_perms=12):
"""Оценка разброса качества по перестановкам few-shot примеров.
evaluate(order) -> float (accuracy на валидационном срезе).
Сложность: O(max_perms * |валидация|) вызовов модели.
"""
perms = list(itertools.permutations(range(len(examples))))[:max_perms]
scores = [evaluate([examples[i] for i in p]) for p in perms]
return {
"mean": statistics.mean(scores),
"stdev": statistics.pstdev(scores),
"spread": max(scores) - min(scores),
}
Разброс больше 3–4 процентных пунктов — переделывайте набор примеров.
Few-shot через роли, а не через один текстовый блок
Частая ошибка — склеить примеры в один длинный user-месседж. Лучше передавать их так, как
модель видела при обучении: чередованием ролей. Это делает границу «вход/выход» однозначной.
import anthropic
client = anthropic.Anthropic()
SHOTS = [
("Приложение падает при экспорте в PDF на файлах больше 50 МБ.",
"CATEGORY: bug\nSEVERITY: 4\nSUMMARY: краш экспорта PDF на больших файлах"),
("Хотелось бы тёмную тему в мобильной версии.",
"CATEGORY: feature\nSEVERITY: 1\nSUMMARY: запрос тёмной темы в мобильном клиенте"),
("Списали дважды за март, верните деньги.",
"CATEGORY: billing\nSEVERITY: 3\nSUMMARY: двойное списание, требуется возврат"),
]
def build_messages(review: str) -> list[dict]:
msgs = []
for user_text, assistant_text in SHOTS:
msgs.append({"role": "user", "content": user_text})
msgs.append({"role": "assistant", "content": assistant_text})
msgs.append({"role": "user", "content": review})
return msgs
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=256,
system=[{
"type": "text",
"text": SYSTEM,
# префикс стабилен между запросами — кэшируем его
"cache_control": {"type": "ephemeral"},
}],
messages=build_messages("Кнопка «Сохранить» ничего не делает во втором шаге мастера."),
)
text = next(b.text for b in response.content if b.type == "text")
print(text)
print(response.usage.input_tokens, response.usage.cache_read_input_tokens)
Тот же приём на TypeScript, с явным разделением стабильной и волатильной частей:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
type Shot = { input: string; output: string };
// Стабильная часть: system + примеры. Всё, что меняется, — только последний user.
function buildMessages(shots: Shot[], question: string): Anthropic.MessageParam[] {
return [
...shots.flatMap((s): Anthropic.MessageParam[] => [
{ role: "user", content: s.input },
{ role: "assistant", content: s.output },
]),
{ role: "user", content: question },
];
}
const response = await client.messages.create({
model: "claude-opus-4-8",
max_tokens: 256,
system: [{ type: "text", text: SYSTEM, cache_control: { type: "ephemeral" } }],
messages: buildMessages(SHOTS, review),
});
5. Роли и персоны: что действительно помогает
«Ты — эксперт мирового уровня по X» — самый растиражированный приём и один из самых переоценённых. Zheng et al., 2023 систематически проверили сотни ролевых персон на наборе задач и не нашли устойчивого прироста качества по сравнению с промптом без персоны; выигрыш конкретной персоны на конкретном датасете плохо переносится на другие.
Что при этом персона делает надёжно — задаёт регистр, лексику и предположения о читателе. Это ценно, но это про форму, а не про способности.
Практическое разделение:
| Формулировка | Что реально даёт |
|---|---|
| «Ты — эксперт мирового уровня по безопасности» | Тон и лексику. Прироста в поиске уязвимостей ожидать не стоит |
| «Пиши для junior-разработчика, который знает Python, но не знает Kubernetes» | Много: уровень объяснений, что раскрывать, что можно опустить |
| «Ты — строгий рецензент. Отмечай всё, включая низкоприоритетное» | Много: калибрует порог срабатывания и полноту вывода |
Второй и третий варианты работают, потому что несут информацию о задаче, которой у модели не было. Первый — не несёт.
Отдельный, чисто инженерный аргумент за system-промпт: он повышает устойчивость к инъекциям и служит стабильным кэшируемым префиксом. Даже когда персона бесполезна, правила и формат должны жить именно там.
6. Структура: разделители, порядок, длинный контекст
Когда в промпте больше одного вида данных — инструкция, документ, вопрос, примеры — нужны однозначные границы. Модель не имеет метаданных о том, где кончается ваш текст и начинается пользовательский; она видит один поток токенов.
Разделители. XML-подобные теги — надёжный и хорошо переносимый вариант: они редко встречаются в естественном тексте, легко парсятся глазами и однозначно вложены.
<instructions>
Извлеки из договора все денежные обязательства.
</instructions>
<contract>
{{ document }}
</contract>
<output_format>
Одна строка на обязательство: <сторона> | <сумма> | <срок> | <цитата до 20 слов>
Если обязательств нет — выведи ровно: NONE
</output_format>
Тройные кавычки, ###-заголовки или --- тоже работают. Что не работает — отсутствие
разделителей или их непоследовательное применение внутри одного промпта.
Порядок. С длинным контекстом (документ на десятки тысяч токенов) порядок критичен:
- Инструкция — в начале, чтобы модель читала документ «уже зная», что искать.
- Документ — в середине.
- Вопрос и краткое напоминание формата — в конце, ближе всего к точке генерации.
Дублирование ключевого требования до и после длинного блока — не избыточность, а прямая компенсация U-образного профиля внимания. Стоит несколько десятков токенов, окупается почти всегда.
Экранирование пользовательского ввода. Если внутрь тега попадает недоверенный текст, он может содержать закрывающий тег и «выйти» из своего блока. Минимальная гигиена:
import re
def wrap(tag: str, content: str) -> str:
"""Оборачивает недоверенный текст, нейтрализуя попытки закрыть тег."""
safe = re.sub(rf"</?{re.escape(tag)}\b[^>]*>", "", content, flags=re.IGNORECASE)
return f"<{tag}>\n{safe}\n</{tag}>"
Это не защита от prompt injection — это защита от того, чтобы разметка не разъехалась. Настоящая защита разбирается в статье о безопасности.
7. Формат ответа
Здесь у промптинга заканчивается зона «уговоров» и начинается зона инженерных гарантий. Три уровня надёжности, от слабого к сильному:
Уровень 1 — попросить в промпте. Работает удивительно хорошо на сильных моделях и
абсолютно недостаточно для продакшена: рано или поздно придёт ответ, обёрнутый в
```json, или с фразой «Вот результат:» перед ним.
Уровень 2 — простой строчный формат. Недооценённый вариант. KEY: value по строкам
парсится тривиально, устойчив к обрезке (частичный ответ остаётся частично полезным),
дешевле JSON по токенам и почти не ломается. Для классификации и извлечения нескольких
полей это часто лучший выбор.
Уровень 3 — структурированный вывод на уровне API. Схема передаётся провайдеру и ограничивает сам декодер, так что невалидный по синтаксису JSON выдать физически нельзя:
from pydantic import BaseModel
from typing import Literal
class ReviewVerdict(BaseModel):
category: Literal["bug", "feature", "billing", "praise", "other"]
severity: int
summary: str
response = client.messages.parse(
model="claude-opus-4-8",
max_tokens=512,
system=SYSTEM,
messages=[{"role": "user", "content": review}],
output_format=ReviewVerdict,
)
verdict = response.parsed_output # валидный экземпляр ReviewVerdict
assert 1 <= verdict.severity <= 5 # семантику всё равно проверяем сами
Ключевой нюанс: схема гарантирует синтаксис, а не смысл. severity: 47 пройдёт схему
с типом int, если вы не задали ограничения — а числовые ограничения поддерживаются схемами
не всегда. Проверяйте бизнес-инварианты в своём коде.
Цена жёсткого формата
Ограничение формата не бесплатно. Tam et al., «Let Me Speak Freely?», 2024 показали, что жёсткие форматные ограничения могут заметно снижать качество на задачах, требующих рассуждения: модель тратит «пропускную способность» на соблюдение синтаксиса вместо решения.
Практическое лечение — разнести рассуждение и выдачу: сначала свободное поле для анализа, затем строгий результат.
{
"reasoning": "Пользователь описывает воспроизводимый сбой при экспорте; запроса новой функции нет.",
"category": "bug",
"severity": 4
}
Порядок полей в схеме имеет значение: reasoning должен идти первым, потому что
генерация авторегрессионна — то, что сгенерировано раньше, влияет на то, что позже.
reasoning после category — декорация, которая ничего не улучшает и только жжёт токены.
Про префилл
Классический приём «начать ответ ассистента за модель» (положить {" в последний
assistant-месседж) на актуальных моделях больше не поддерживается и возвращает ошибку —
его роль полностью заняли structured outputs. Если вы переносите старый код, где префилл
использовался для срезания преамбул, замените его инструкцией «выводи ответ напрямую, без
вводных фраз» плюс схемой вывода.
8. Как это измерять
Промптинг без замеров — это гадание с уверенным лицом. Минимальный рабочий цикл — 30–50 размеченных примеров и скрипт на полсотни строк.
import json, statistics
from concurrent.futures import ThreadPoolExecutor
def run_case(fn, case):
try:
pred = fn(case["input"])
return {"ok": pred == case["expected"], "pred": pred, "case": case}
except Exception as exc: # ошибки парсинга — тоже провал
return {"ok": False, "pred": None, "case": case, "error": repr(exc)}
def evaluate(fn, dataset, workers=8):
"""Прогон варианта промпта по датасету.
Время: O(N / workers) сетевых вызовов. Память: O(N).
"""
with ThreadPoolExecutor(max_workers=workers) as pool:
results = list(pool.map(lambda c: run_case(fn, c), dataset))
acc = statistics.mean(r["ok"] for r in results)
return acc, [r for r in results if not r["ok"]]
with open("reviews.jsonl", encoding="utf-8") as f:
dataset = [json.loads(line) for line in f]
for name, variant in {"zero_shot": v0, "few_shot_3": v3, "few_shot_8": v8}.items():
acc, failures = evaluate(variant, dataset)
print(f"{name:12} acc={acc:.3f} провалов={len(failures)}")
for r in failures[:3]:
print(" ", r["case"]["input"][:60], "→", r["pred"])
Три правила, которые отличают полезный замер от бесполезного:
- Меняйте одну вещь за прогон. Иначе вы не узнаете, что именно сработало.
- Смотрите на провалы, а не только на метрику. Три конкретных провала подскажут следующее правило в промпте; число 0.87 — не подскажет ничего.
- Держите отложенный набор. Промпт, доведённый до 100 % на 30 примерах, почти наверняка переобучен на них.
Что обычно даёт эффект (по убыванию, из практики над классификационными и извлекающими задачами):
| Изменение | Типичный эффект | Стоимость |
|---|---|---|
| Правила для граничных случаев | Большой | ~50 токенов |
| Явная категория «не знаю» | Большой на «мусорных» входах | ~10 токенов |
| Жёсткая схема ответа | Большой (устраняет ошибки парсинга) | ~30 токенов |
| 3 сбалансированных few-shot примера | Средний | 150–400 токенов |
Поле reasoning перед ответом |
Средний на задачах с рассуждением | 50–200 токенов вывода |
| Ещё 5 однотипных примеров | Малый | 300–700 токенов |
| Персона «ты эксперт» | Околонулевой на качество | ~15 токенов |
Значения в столбце «эффект» — качественные, а не измеренные константы: абсолютные цифры зависят от модели, версии и вашего датасета. Смысл таблицы в порядке приоритетов: сначала сделайте бесплатное и высокоэффективное, потом платное и среднеэффективное.
9. Стоимость
Few-shot оплачивается входными токенами на каждом запросе. Считаем на конкретных цифрах (тарифы на момент написания, Claude Opus 4.8 — 5 $ за 1M входных токенов, Sonnet 5 — $3, Haiku 4.5 — 1 $):
Пусть system + 8 примеров = 1 800 токенов, вопрос = 200 токенов, 1 млн запросов в месяц.
| Сценарий | Входных токенов/запрос | Стоимость входа/мес (Opus 4.8) |
|---|---|---|
| Zero-shot, без кэша | 400 | 2 000 $ |
| Few-shot ×8, без кэша | 2 000 | 10 000 $ |
| Few-shot ×8, кэш префикса (чтение ≈0.1×) | 1 800 из кэша + 200 | 1 900 $ |
Третья строка — не опечатка: кэшированный префикс читается примерно за десятую долю цены, поэтому восемь примеров с кэшем обходятся дешевле, чем zero-shot без него. Это меняет инженерное решение целиком: вопрос не «сколько примеров мы можем себе позволить», а «стабилен ли наш префикс до последнего байта».
Условия, при которых кэш действительно работает:
- Префикс байт в байт одинаков между запросами — никаких
datetime.now(), UUID и несортированногоjson.dumps. - Префикс длиннее минимального порога кэширования (у разных моделей — от ~1–4 тыс. токенов; короче порога кэш молча не создаётся).
- Запись в кэш дороже обычного входа (≈1.25×), так что окупается начиная с двух-трёх обращений в пределах TTL.
- Кэш привязан к модели и к набору инструментов: смена любого из них — полный пересчёт.
Проверять надо не по документации, а по факту — в ответе есть поля cache_read_input_tokens
и cache_creation_input_tokens. Если чтение стабильно нулевое при одинаковых префиксах —
где-то есть невидимый инвалидатор.
Отдельный рычаг — маршрутизация: простая классификация редко требует топовой модели. Тот же промпт на Haiku-классе стоит в пять раз дешевле входа и в пять раз дешевле выхода. Стратегии выбора модели и роутинга — в статье о проде и стоимости.
10. Где что применять
Общая таксономия того, из чего вообще собирается промпт:
11. Типичные ошибки
Вежливость вместо спецификации. «Пожалуйста, постарайся быть точным» не несёт
информации. «Если в тексте нет явной даты — выведи null, не выводи предполагаемую» —
несёт.
КАПС и «ЭТО ОЧЕНЬ ВАЖНО». На современных моделях агрессивные усилители чаще приводят к пересрабатыванию: инструмент, помеченный «CRITICAL: ты ОБЯЗАН использовать», начинает вызываться там, где не нужен. Пишите «используй X, когда …» — с условием применимости вместо восклицательного знака.
Динамика в системном промпте. Текущая дата, ID пользователя, счётчик — всё это инвалидирует кэш всего запроса. Выносите в конец сообщений.
Примеры вместо знаний. Few-shot не добавит модели фактов о вашем домене. Он покажет форму. Факты — это RAG или дообучение.
Один промпт на все случаи. Промпт, который умеет и классифицировать, и суммировать, и извлекать сущности, делает всё три хуже, чем три отдельных промпта. Декомпозиция почти всегда выигрывает — и по качеству, и по отлаживаемости.
Промпты, живущие в f-строках по всему коду. Их невозможно ни версионировать, ни протестировать, ни продиффить. Промпт — артефакт, а не строковый литерал.
Отсутствие обработки провала. Модель однажды вернёт не то, что вы ждёте. Код, написанный по принципу «этого не случится», падает в продакшене на четвёртые сутки.
12. Практика продакшена
Минимальная зрелая схема работы с промптами:
Промпты — версионируемые артефакты. Отдельные файлы (.md, .jinja, .yaml) в
репозитории, с полем версии. Изменение промпта проходит ревью так же, как изменение кода,
потому что это изменение поведения системы.
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class Prompt:
name: str
version: str
template: str
@classmethod
def load(cls, name: str, version: str) -> "Prompt":
path = Path("prompts") / name / f"{version}.md"
return cls(name=name, version=version, template=path.read_text(encoding="utf-8"))
def render(self, **kwargs) -> str:
return self.template.format(**kwargs)
CLASSIFIER = Prompt.load("review_classifier", "v4")
Логируйте версию промпта вместе с каждым вызовом. Когда через месяц метрика поедет, единственный способ понять причину — сопоставить деградацию с версией промпта, версией модели и распределением входов.
Тесты на промпты в CI. Не полноценный бенчмарк — 20–30 быстрых кейсов на дешёвой модели, которые ловят грубые регрессии: сломанный формат, потерянную категорию, пропавший отказ. Это дёшево и предотвращает большинство инцидентов.
Заморозьте версию модели. Алиас вроде «последняя модель» означает, что ваше поведение может измениться без вашего релиза. Пиньте конкретную версию и обновляйтесь осознанно, прогоняя датасет.
Отделяйте промпт от данных. Шаблон с плейсхолдерами + отдельная функция подстановки. Конкатенация недоверенного ввода прямо в инструкцию — источник и багов, и инъекций.
Более широкая картина наблюдаемости, кэширования и роутинга — в статьях о проде и стоимости и об оценке качества.
Мини-итог
- Промпт задаёт условие для распределения, а не отдаёт команду. Отсюда — необходимость валидации и обработки провала.
- Порядок блоков решает две задачи сразу: борется с провалом внимания в середине контекста и определяет, что можно кэшировать. Стабильное — вперёд, волатильное — в конец.
- Zero-shot держится на закрытых списках, правилах для граничных случаев и явном отказе. Это самые дешёвые и самые эффективные вложения.
- Few-shot задаёт формат и границы, а не знания; правильность меток в примерах влияет на результат меньше, чем их форма и сбалансированность.
- Персона влияет на регистр, а не на способности. Информативные роли («пиши для junior») работают, декоративные («ты гений») — нет.
- Формат ответа надёжен ровно настолько, насколько он ограничен на уровне API; жёсткий
формат может стоить качества рассуждений — разносите
reasoningи результат. - Кэширование стабильного префикса меняет экономику few-shot настолько, что восемь примеров могут стоить дешевле нуля примеров без кэша.
- Без датасета из 30–50 размеченных примеров любое утверждение про «этот промпт лучше» — это мнение, а не факт.
Что дальше
Продвинутые техники: chain-of-thought, self-consistency, tree-of-thoughts, chain-of-verification — как заставить модель рассуждать явно, когда это даёт прирост, а когда только жжёт токены, и как проверять её собственный вывод её же силами.
Источники
- Brown et al., «Language Models are Few-Shot Learners», 2020
- Ouyang et al., «Training language models to follow instructions», 2022
- Min et al., «Rethinking the Role of Demonstrations», 2022
- Zhao et al., «Calibrate Before Use», 2021
- Lu et al., «Fantastically Ordered Prompts», 2021
- Liu et al., «Lost in the Middle», 2023
- Zheng et al., «When “A Helpful Assistant” Is Not Really Helpful», 2023
- Tam et al., «Let Me Speak Freely?», 2024
- Anthropic — Prompt engineering overview
- Anthropic — Prompt caching
- OpenAI — Prompt engineering guide
- DAIR.AI — Prompt Engineering Guide