Files
BlackboxBook/book/06_prompt_is_a_protocol.md
2026-05-20 20:55:03 +03:00

656 lines
46 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ГЛАВА 6. ПРОМПТ — ЭТО ПРОТОКОЛ, А НЕ ПРОСЬБА
---
## 6.1. Почему «сделай хорошо» не работает
Представьте, что вы приходите в ресторан и говорите официанту: «Принесите что-нибудь вкусное». Может быть, вам повезёт. А может — вам подадут суши, хотя у вас аллергия на рыбу. Теперь представьте другой вариант: «Стейк medium rare, без гарнира, с соусом отдельно». Здесь официант не гадает — он выполняет спецификацию.
Промпт работает точно так же. Он может быть расплывчатой просьбой — или точным протоколом. Разница между ними — это разница между «авось повезёт» и «стабильный результат».
### Самый простой пример
Для начала — минимальная иллюстрация:
**✗ Плохой промпт:**
```
Напиши текст про собак.
```
**✓ Хороший промпт:**
```
Напиши информационный абзац (5080 слов) о породе золотистый ретривер
для детской энциклопедии. Стиль: простой, дружелюбный.
Упомяни: происхождение, характер, для кого подходит.
```
Почему второй промпт лучше? Потому что в нём закрыто **пять пробелов**, которые в первом промпте модель заполняла бы случайно:
- **Жанр** — информационный абзац (не эссе, не стихотворение)
- **Объём** — 5080 слов (не страница и не два предложения)
- **Тема** — конкретная порода (не «собаки вообще»)
- **Аудитория** — дети (определяет стиль)
- **Содержание** — три конкретных аспекта
Каждый незакрытый пробел — это развилка, где модель бросает монетку. Чем больше развилок, тем менее предсказуем результат.
### Пример посложнее
Теперь рассмотрим два промпта для реальной разработки:
**Промпт A:**
```
Напиши код для обработки данных.
```
**Промпт B:**
```xml
<role>Senior Python engineer, data pipeline specialist</role>
<task>
Write a function that:
- Reads CSV from S3 (boto3)
- Validates schema against Pydantic model
- Transforms: rename columns per mapping, convert dates to ISO 8601
- Returns list[ProcessedRecord]
</task>
<constraints>
- Python 3.12+, type hints required
- No pandas (use csv module + dataclasses)
- Handle: FileNotFoundError, ValidationError, malformed rows (skip + log)
- Max 80 lines
</constraints>
<output_format>
Single Python file, no comments except docstring
</output_format>
```
Промпт A генерирует неопределённый ответ, потому что модель должна заполнить пробелы вероятностно: какой язык? какие данные? какой формат? какие ограничения? Каждый пробел — развилка, и модель выбирает **наиболее вероятное** продолжение, а не наиболее полезное для вас.
Промпт B минимизирует неопределённость. Каждый пробел закрыт явно. Модель не угадывает — она выполняет спецификацию.
Три аналогии помогут запомнить эту идею:
- **Промпт как рецепт.** В рецепте указаны ингредиенты, пропорции, температура, время. Уберите любой элемент — и результат становится непредсказуемым. «Сделай торт» — это не рецепт. «Бисквит из 4 яиц, 200 г муки, 180°C, 25 минут» — рецепт.
- **Промпт как API-спецификация.** Разработчик не пишет эндпоинт с описанием «делает что-то полезное». Он определяет входные параметры, типы, формат ответа, коды ошибок. Промпт — это ваш API-контракт с моделью.
- **Промпт как юридический договор.** В договоре двусмысленность ведёт к спорам. Чем точнее формулировки, тем меньше пространства для разночтений. «Разумные сроки» — повод для суда. «30 календарных дней» — нет.
### Формализация: информационная энтропия промпта
Промпт с высокой энтропией (много неопределённости) → широкое распределение возможных ответов → непредсказуемый результат.
Промпт с низкой энтропией (всё специфицировано) → узкое распределение → предсказуемый, стабильный результат.
**Цель инженера**: минимизировать энтропию промпта, оставляя модели свободу только там, где это необходимо (например, в формулировках, не в структуре).
---
## 6.2. Прайминг: сначала рамка мышления, потом контекст, потом задача
Из каузальной механики ([Глава 4](04_causal_reading_and_the_power_of_the_first_frame.md)) следует оптимальный порядок элементов промпта. Это не «рекомендация» — это следствие того, как attention accumulation формирует скрытые состояния.
### Каузальный порядок промпта
```
[1. Роль/Режим] → Задаёт «кто модель» → активирует нужные MLP-ассоциации
[2. Цель/Критерии] → Задаёт «что делать» → фокусирует attention на релевантных паттернах
[3. Ограничения] → Задаёт «чего нельзя» → подавляет нежелательные траектории
[4. Контекст/Данные] → Предоставляет материал → входные данные для обработки
[5. Формат вывода] → Задаёт структуру → constrained decoding через ожидания
[6. Примеры (опц.)] → Few-shot learning → калибровка формата и стиля
[7. Триггер] → Запускает генерацию → «Начни.» / «Output:»
```
### Почему этот порядок работает
**Роль первой**: `Ты — старший инженер PostgreSQL с 15 годами опыта` активирует MLP-ассоциации, связанные с экспертизой PostgreSQL. Все последующие токены интерпретируются через эту призму.
**Цель до данных**: модель «знает», зачем она получает данные, ещё до их получения. Это позволяет attention фокусироваться на релевантных частях данных.
**Ограничения до данных**: запреты формируют «забор», внутри которого модель генерирует. Если ограничения идут после данных, модель может уже «выбрать» траекторию, которая нарушает ограничение.
**Триггер последним**: явный сигнал к началу генерации. Без триггера модель может начать генерировать мета-текст (`Конечно, я помогу вам с...`) вместо полезного вывода.
---
## 6.3. Промпт как контракт: спецификация, а не просьба
Вернёмся к аналогиям из начала главы и разберём каждую подробнее.
### API-аналогия
Если вы разработчик, подумайте о промпте как о **спецификации API-эндпоинта**. Никто не пишет в документации: «POST /api/data — делает что-то с данными». Вместо этого — чёткая схема запроса, схема ответа, перечень ошибок. Промпт заслуживает такого же подхода:
| Элемент API | Элемент промпта | Пример |
|-------------|-----------------|--------|
| Endpoint | Задача | `Сгенерировать SQL-запрос` |
| Request schema | Входные данные + формат | `{table: string, filters: Filter[], limit: int}` |
| Response schema | Формат вывода | `{query: string, estimated_rows: int}` |
| Validation rules | Ограничения | `Только SELECT, без подзапросов, PostgreSQL 16` |
| Error handling | Fallback | `Если невозможно — верни {error: string}` |
| Auth/Permissions | Роль | `DBA с read-only доступом` |
### Аналогия с договором
Юристы знают: в договоре каждое слово имеет значение. Фраза «в разумные сроки» — повод для годового судебного спора. Фраза «в течение 30 календарных дней с момента подписания» — нет.
В промпте работает тот же принцип. «Ответь кратко» — неоднозначно (кратко — это одно предложение? один абзац? страница?). «Ответь одним абзацем из 35 предложений» — однозначно.
### Три свойства хорошего промпта-контракта
**1. Детерминированность**: одинаковый вход → одинаковый формат выхода.
Не обязательно одинаковый текст (при `temp > 0`), но обязательно одинаковая **структура**. Если вы просите JSON — всегда получаете валидный JSON. Если просите 3 варианта — всегда 3, не 2 и не 5.
**2. Верифицируемость**: выход можно программно проверить.
Опишите Pydantic-модель с полями `query: str` и `estimated_rows: int`, получите ответ модели через structured outputs и валидируйте его через `model_validate_json`. Если запрос не начинается с `SELECT` — это сигнал ошибки. Для генерации такого валидатора используйте промпт:
> **Промпт для ИИ:** «Напиши на Python (Pydantic v2) валидатор ответа LLM: модель `SQLResponse` с полями `query` и `estimated_rows`, функцию `validate_llm_response(raw_json: str) -> SQLResponse`, которая валидирует JSON и проверяет, что query начинается с SELECT. Покажи пример использования.»
**3. Модульность**: части промпта можно заменять без переписывания целого.
Роль, задача, ограничения и формат вывода хранятся как отдельные переменные или конфигурационные блоки. Функция `build_prompt()` собирает их в финальный промпт. Изменение одного блока (например, формата вывода) не требует переписывания остальных.
---
## 6.4. Structured Outputs и Function Calling
### Что такое Structured Outputs и зачем они нужны
Если вы новичок, начнём с основ. **Structured output** — это ответ модели не свободным текстом, а в заранее определённом машиночитаемом формате (обычно JSON). Зачем? Потому что ваш код не может надёжно работать со свободным текстом — ему нужны предсказуемые поля с предсказуемыми типами.
**JSON Schema** — это способ описать «форму» JSON-ответа: какие поля должны быть, каких типов, обязательные или нет. Думайте об этом как о формочке для печенья — тесто (ответ модели) всегда примет нужную форму.
Пример JSON Schema:
```json
{
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"city": {"type": "string"}
},
"required": ["name", "age"]
}
```
Эта схема говорит: «ответ — объект, в нём обязательно `name` (строка) и `age` (число), плюс необязательный `city`».
### Проблема свободного текста
Когда модель генерирует свободный текст, парсинг ненадёжен:
```
Модель: "Вот запрос: SELECT * FROM users WHERE age > 25 ORDER BY name.
Примерное количество строк: около 150."
```
Как извлечь запрос? Регулярные выражения? А если модель решит добавить пояснение перед запросом? Или после? Нестабильно, хрупко, не масштабируется.
### JSON Schema Validation: стандарт индустрии
К апрелю 2026 года **все фронтир-провайдеры** поддерживают native structured outputs на основе JSON Schema. Это больше не экспериментальная функция — это стандарт:
| Провайдер | API-параметр | Статус (2026) |
|-----------|-------------|---------------|
| OpenAI | `response_format: {type: "json_schema", ...}` | GA, во всех моделях |
| Anthropic | `tool_use` с JSON Schema (forced tool choice для structured output) | GA, все модели Claude 4.x |
| Google | `response_schema` в GenerationConfig | GA, Gemini 2.0+ |
| Mistral | `response_format: {type: "json_schema", ...}` | GA, все модели |
> **Промпт для ИИ:** «Покажи вызов OpenAI Chat Completions API (Python SDK) с параметром `response_format` типа `json_schema` для модели GPT-5.4. Схема: объект с полями `query` (string) и `estimated_rows` (integer), оба обязательные. Используй актуальный формат SDK.»
Модель **гарантированно** вернёт валидный JSON, соответствующий схеме. Это не «пожелание», а constraint на уровне декодирования.
### Реальный пайплайн: от текста к базе данных
Рассмотрим полный пайплайн, где structured outputs встраиваются в продакшн-пайплайн. Задача: извлечь из отзывов клиентов структурированные данные и сохранить в БД.
Подход: определяете Pydantic-модель (`ReviewExtraction` с полями `product_name`, `sentiment`, `issues`, `rating_mentioned`, `summary`), передаёте её JSON-схему в `response_format`, вызываете модель для каждого отзыва и записываете результат в БД. Между моделью и базой данных нет хрупкого парсинга регулярками — Pydantic-схема одновременно описывает контракт для модели и валидирует результат.
> **Промпт для ИИ:** «Напиши на Python (OpenAI SDK + Pydantic v2) пайплайн извлечения данных из отзывов клиентов. Pydantic-модель `ReviewExtraction` с полями: product_name (str), sentiment (enum: positive/negative/neutral), issues (list[str]), rating_mentioned (int | None, 15), summary (str). Используй `response_format` с JSON Schema из `model_json_schema()`. Покажи обработку потока отзывов с записью в БД.»
### Grammar-Guided Decoding
Для open-source моделей существуют библиотеки, которые ограничивают декодирование на уровне грамматики:
- **Outlines** (Python) — принимает JSON Schema и гарантирует, что модель сгенерирует валидный JSON, соответствующий схеме.
- **Guidance** (Microsoft) — позволяет задать регулярные выражения и выбор из вариантов прямо в шаблоне генерации.
> **Промпт для ИИ:** «Покажи пример использования библиотеки Outlines для генерации структурированного JSON из Llama 3 (Hugging Face Transformers). Схема: объект с полями query (string) и estimated_rows (integer, minimum 0). Используй `outlines.generate.json`.»
### Function Calling (вызов функций)
Если structured outputs — это «модель отвечает в нужном формате», то **function calling** (tool use) — это следующий шаг: «модель сама решает, какое действие выполнить».
Представьте, что вы дали модели список кнопок: «проверить погоду», «найти в базе данных», «отправить email». Модель читает запрос пользователя и «нажимает» нужную кнопку с нужными параметрами. Но нажатие — виртуальное: модель генерирует JSON с именем функции и аргументами, а ваш код выполняет реальное действие.
К 2026 году function calling (tool use) — **стандартная возможность** всех фронтир-моделей: GPT-5.4, Claude Opus 4.6 / Sonnet 4.6, Gemini 3.1 Pro, Mistral Large, Command R+. Вы описываете инструменты как JSON-объекты с именем, описанием и параметрами — модель решает, когда и с какими аргументами вызвать функцию, а вы контролируете, что она делает.
Подробное руководство по проектированию инструментов, бест-практисы описания параметров и интеграция с agent loop — в [Главе 11](11_tools.md) (§11.2, §11.5).
### MCP: стандартизация описания инструментов
**Model Context Protocol (MCP)** — открытый стандарт, который унифицирует способ описания инструментов, контекстов и действий для LLM. Если function calling — это «дать модели кнопки», то MCP — это «единый стандарт для производства кнопок»: описываете инструмент один раз в формате MCP — и он работает с любой моделью. Подробно о протоколе, его архитектуре и практическом применении — в [Главе 11, §11.4](11_tools.md).
---
## 6.5. Паттерны промптов для типовых задач
### Паттерн 1: Extraction (извлечение данных)
```xml
<role>Data extraction specialist</role>
<task>
Extract structured information from the following text.
Return ONLY the JSON, no explanation.
</task>
<schema>
{
"company_name": "string",
"founded_year": "integer | null",
"headquarters": "string | null",
"revenue_usd": "number | null",
"employees": "integer | null"
}
</schema>
<rules>
- If information is not found, use null
- Do not infer or guess missing values
- Dates: extract year only
- Revenue: convert to USD if possible, else null
</rules>
<text>
{input_text}
</text>
```
### Паттерн 2: Generation (генерация кода)
```xml
<role>Senior {language} engineer</role>
<task>
Implement function with the following contract:
</task>
<contract>
Name: {function_name}
Input: {input_types}
Output: {output_type}
Behavior: {description}
Edge cases: {edge_cases}
</contract>
<constraints>
- {language} {version}+
- Type hints required
- No external dependencies beyond {allowed_libs}
- Max {N} lines
- Handle errors: {error_types}
</constraints>
<examples>
Input: {example_input} → Output: {example_output}
</examples>
<output>
Single code block, no explanation.
</output>
```
### Паттерн 3: Analysis (аналитика)
```xml
<role>Senior data analyst</role>
<task>
Analyze the following dataset and answer the question.
</task>
<dataset>
{data in CSV/JSON format}
</dataset>
<question>
{specific_question}
</question>
<constraints>
- Base conclusions only on provided data
- If data is insufficient, state explicitly
- Include confidence level: high/medium/low
- Cite specific data points
</constraints>
<format>
{
"answer": "string",
"confidence": "high|medium|low",
"supporting_data": ["string"],
"caveats": ["string"]
}
</format>
```
---
## 6.6. Prompt Caching и оптимизация стоимости
Системные промпты в продакшн-приложениях часто содержат тысячи токенов: роль, инструкции, примеры, правила. Если вы отправляете 2000 токенов системного промпта с каждым из 10 000 запросов в день — вы платите за 20 миллионов входных токенов, из которых 99.99% повторяются.
**Prompt caching** решает эту проблему. И Anthropic, и OpenAI предлагают кэширование на уровне API:
- **Anthropic** — явное кэширование через параметр `cache_control: {type: "ephemeral"}` в системном промпте. До 90% скидки на кэшированные токены.
- **OpenAI** — автоматическое кэширование повторяющихся префиксов промптов (1024+ токенов). Скидка 50% на cached input tokens.
### Практические правила кэширования
1. **Размещайте статичный контент в начале** — кэш работает по принципу совпадения префикса. Динамические данные (запрос пользователя) — в конце.
2. **Минимальный порог зависит от провайдера и модели**у OpenAI это 1024+ токенов для автоматического prompt caching, у Anthropic пороги и TTL зависят от модели и типа кэша.
3. **Переиспользуйте системные промпты** — один и тот же system prompt для всех запросов в рамках задачи.
4. **Группируйте few-shot примеры** — вынесите их в кэшируемую часть, а не повторяйте в каждом запросе.
Prompt caching может заметно снижать стоимость и latency на повторяющихся префиксах, но конкретная экономия зависит от hit rate, TTL, длины общего префикса и модели. В книге полезно мыслить так: кэш — не «магическая скидка», а инженерный приём для стабильных повторяющихся блоков.
---
## 6.7. Промпт как код: компиляция промптов
Новое направление (20242026) — фреймворки, которые превращают промпты из текстовых шаблонов в **компилируемые программы**:
**DSPy** (Stanford NLP): описываете задачу декларативно через сигнатуры (typed вход/выход), фреймворк автоматически оптимизирует промпт, few-shot примеры и даже выбор модели на основе ваших метрик качества. Например, сигнатура `"text: str -> facts: list[str]"` автоматически превращается в оптимизированный промпт.
**LMQL**: язык запросов к языковым моделям с контролем типов и ограничениями — например, `where SENTIMENT in ["positive", "negative", "neutral"]` ограничивает генерацию до трёх вариантов.
Оба подхода объединяет идея: промпт — это не строка, а **программа** с типами, ограничениями и автоматической оптимизацией.
> **Примечание о reasoning-моделях.** Модели с цепочкой рассуждений (o1, o3, Claude с extended thinking) могут обрабатывать менее структурированные промпты — они сами «достраивают» недостающую структуру в процессе рассуждения. Но даже для них протокольный подход даёт более стабильные результаты, особенно при программной обработке вывода.
---
## 6.8. Антипаттерны промптов
### Антипаттерн 1: «Вежливая просьба»
```
✗ "Пожалуйста, не мог бы ты помочь мне написать функцию для сортировки?
Было бы замечательно, если бы она работала быстро. Спасибо!"
```
Проблемы: нет спецификации (какая сортировка? какие данные? какой язык?), вежливые обороты — это шум, который расходует токены и размывает attention.
### Антипаттерн 2: «Многозадачный запрос без структуры»
```
✗ "Напиши код сортировки, объясни алгоритм, сравни с другими алгоритмами,
добавь тесты и документацию."
```
Проблемы: 5 задач в одном запросе. Модель попытается сделать всё и сделает всё посредственно. Attention распределяется по 5 целям.
### Антипаттерн 3: «Инструкция после данных»
```
✗ [50K токенов данных]
"Теперь из этих данных извлеки имена компаний в JSON."
```
Проблемы: модель обработала 50K токенов данных, не зная, что с ними делать. Attention распределился равномерно. Инструкция пришла последней → ей досталось минимум attention.
### Антипаттерн 4: «Негативные инструкции без позитивных»
```
✗ "Не используй рекурсию. Не пиши комментарии. Не создавай классы.
Не используй глобальные переменные."
```
Проблемы: модель «знает» чего не делать, но не знает, что делать. Каждое «не» парадоксально активирует ассоциации с запрещённым — модель «думает» о рекурсии, пытаясь её избежать.
**Исправление**: дополняйте запреты позитивными альтернативами: `Используй итеративный подход (не рекурсию)`.
### Антипаттерн 5: «Копипаста из ChatGPT-гайдов»
```
✗ "Ты — полезный, безопасный и честный ассистент. Твоя цель — помочь
пользователю наилучшим образом. Думай шаг за шагом."
```
Проблемы: это generic-преамбула, которую модель уже «видела» в миллионах примеров. Она не добавляет информации, а тратит токены. «Думай шаг за шагом» — мощная техника, но только когда за ней следует конкретная задача с конкретным форматом вывода.
**Исправление**: убрать преамбулу, перейти сразу к роли и задаче.
### Антипаттерн 6: «Примеры противоречат инструкциям»
```
✗ Инструкция: "Отвечай на русском языке."
Пример: "Input: Hello → Output: This is a greeting."
```
Проблемы: модель обучена на примерах больше, чем на инструкциях ([Глава 4](04_causal_reading_and_the_power_of_the_first_frame.md), эффект прайминга). Если пример на английском — модель с высокой вероятностью продолжит на английском, проигнорировав инструкцию.
**Исправление**: примеры должны **демонстрировать** все инструкции, включая язык, формат, стиль.
### Антипаттерн 7: «Температура 0 вместо структуры»
```
✗ "Я поставлю temperature=0, значит ответ будет стабильным."
```
Проблемы: `temperature=0` делает генерацию детерминированной при прочих равных, но **не гарантирует нужный формат**. Модель стабильно вернёт один и тот же ответ — который может быть стабильно неправильным. Температура влияет на случайность, а не на качество.
**Исправление**: temperature=0 + structured outputs + чёткий промпт = стабильный **и** правильный результат.
---
## 6.9. Библиотека шаблонов
Готовые шаблоны для четырёх распространённых задач. Копируйте, адаптируйте под свои нужды.
### Шаблон 1: Суммаризация документа
```xml
<role>Technical writer, expert in concise summarization</role>
<task>
Summarize the following document.
</task>
<rules>
- Output language: same as input
- Length: 35 bullet points, each 12 sentences
- Focus on: key decisions, action items, deadlines
- Omit: pleasantries, background context, repetition
- If the document contains no actionable content, state: "No action items found."
</rules>
<format>
{
"title": "Brief document title",
"bullets": ["string"],
"action_items": ["string"],
"next_deadline": "ISO date or null"
}
</format>
<document>
{input_text}
</document>
```
### Шаблон 2: Код-ревью
```xml
<role>Senior software engineer, code reviewer</role>
<task>
Review the following code change (diff). Focus on correctness,
security, and maintainability. Do NOT suggest style changes.
</task>
<severity_levels>
- CRITICAL: bugs, security vulnerabilities, data loss risks
- WARNING: performance issues, error handling gaps
- SUGGESTION: optional improvements
</severity_levels>
<format>
{
"issues": [
{
"severity": "CRITICAL | WARNING | SUGGESTION",
"line": "number or range",
"description": "What's wrong",
"fix": "Suggested fix (code snippet)"
}
],
"summary": "Overall assessment in 12 sentences",
"approve": true/false
}
</format>
<diff>
{code_diff}
</diff>
```
### Шаблон 3: Классификация текста
```xml
<role>Text classification specialist</role>
<task>
Classify the following message into one of the categories.
If confidence is below 0.7, set category to "unknown".
</task>
<categories>
- billing: payment, invoice, refund, charge
- technical: bug, error, crash, not working
- feature_request: wish, would be nice, please add
- account: login, password, access, permissions
</categories>
<format>
{
"category": "string",
"confidence": 0.0-1.0,
"reasoning": "One sentence explaining the classification"
}
</format>
<message>
{input_message}
</message>
```
### Шаблон 4: Генерация тест-кейсов
```xml
<role>QA engineer, test design specialist</role>
<task>
Generate test cases for the following function signature.
Cover: happy path, edge cases, error cases.
</task>
<function>
Name: {function_name}
Input: {input_types}
Output: {output_type}
Description: {description}
</function>
<constraints>
- 58 test cases total
- Use pytest format
- Include parametrize where appropriate
- Each test: clear name, arrange/act/assert structure
</constraints>
<output>
Single Python file with test functions. No explanation.
</output>
```
---
## 6.10. Prompt A/B testing в production
Промпт, который работает на 10 ручных примерах, может провалиться на реальном трафике. А/B-тест промптов — это способ сравнить два варианта промпта на живых пользователях и принять решение на данных, а не на интуиции.
### Когда A/B-тест оправдан
- Вы меняете структуру промпта (новый формат, перестановка секций, другой режим reasoning).
- Вы переходите на новую модель и хотите сравнить её со старой в связке с промптом.
- Вы оптимизируете стоимость — более дешёвая модель с улучшенным промптом vs дорогая с базовым.
Не оправдан: косметические правки формулировок (меняете «пожалуйста» на «будьте добры» — идите в eval-набор, а не в A/B).
### Метрики A/B-теста
Первичные метрики — те, на которые вы оптимизируете:
| Метрика | Что измеряет | Как считать |
|---------|-------------|-------------|
| Accuracy / Correctness | Доля ответов без ошибок | LLM-judge или ручная разметка |
| Task completion rate | Доля запросов, где задача выполнена | Автоматическая проверка результата |
| User satisfaction | Thumbs up/down, CSAT | Сбор через UI |
Вторичные метрики — контролируете, чтобы не просели:
| Метрика | Что измеряет | Алерт при |
|---------|-------------|----------|
| Parse rate | Доля ответов с валидной структурой | < 95% |
| Latency P95 | Время ответа | Рост > 20% vs baseline |
| Cost per request | Средняя стоимость | Рост > 30% vs baseline |
| Hallucination rate | Доля фактических ошибок | Рост > 2 п.п. |
### Дизайн эксперимента
1. **Рандомизация.** Каждый запрос случайно направляется в вариант A (текущий промпт) или B (новый). Не чередуйте A/B/A/B — истинная рандомизация. Используйте hash(user_id) для sticky-экспериментов, если нужна консистентность для одного пользователя.
2. **Размер выборки.** Минимум 200 запросов на вариант для обнаружения разницы в 10 п.п. с 80% мощностью. Для разницы в 5 п.п. — около 800 запросов на вариант. Используйте online-калькулятор размера выборки (ABBA, Evan Miller) для точного расчёта.
3. **Длительность.** Минимум один полный бизнес-цикл (обычно неделя), чтобы захватить все паттерны использования. Не останавливайте тест, увидев разницу через 2 часа — это может быть шум.
4. **Статистическая значимость.** Не доверяйте «на глаз». Используйте тест пропорций (z-test) для бинарных метрик (accuracy, parse rate) или Mann-Whitney U для непрерывных (latency, cost). Порог: p < 0.05. Если p > 0.05 после целевого размера выборки — разницы нет, выбирайте тот вариант, который дешевле или проще.
### Чек-лист A/B-теста промпта
| # | Шаг | Проверка |
|---|-----|----------|
| 1 | Определена primary metric | Одна метрика для принятия решения |
| 2 | Определены guardrail metrics | Минимум latency, cost, parse rate |
| 3 | Настроена рандомизация | Hash-based или true random |
| 4 | Рассчитан размер выборки | 200+ на вариант (10 п.п. разницы) |
| 5 | Задана длительность | ≥ 1 бизнес-цикл |
| 6 | Инструментация собирает метрики | Все метрики логируются |
| 7 | Настроен дашборд теста | Видно разницу A vs B в реальном времени |
| 8 | Критерий остановки | p < 0.05 ИЛИ достигнут размер выборки без значимой разницы |
### Антипаттерны
- **Peeking.** Смотреть результаты каждый час и останавливать, как только p < 0.05 это inflates false positive rate. Зафиксируйте размер выборки и длительность заранее.
- **Слишком много метрик.** Если вы измеряете 10 метрик, одна из них покажет «значимую» разницу чисто случайно. Выберите одну primary metric.
- **A/B без eval-набора.** Если оба варианта проваливаются на одних и тех же кейсах проблема не в промпте, а в архитектуре. Сначала прогоните оба варианта через offline eval.
---
## Практический вывод
### Чек-лист проектирования промпта
| # | Правило | Проверка |
|---|---------|----------|
| 1 | **Проектируйте как API-контракт** | Есть ли schema для входа и выхода? |
| 2 | **Используйте каузальный порядок** | Роль Цель Ограничения Данные Формат? |
| 3 | **Специфицируйте формат явно** | JSON Schema, Pydantic model, пример вывода? |
| 4 | **Одна задача = один промпт** | Нет ли нескольких несвязанных целей? |
| 5 | **Тестируйте на edge-cases** | Что произойдёт, если данных нет? Если формат неверный? |
| 6 | **Избегайте антипаттернов** | Нет вежливого шума? Позитивные инструкции? Примеры не противоречат? |
| 7 | **Используйте structured outputs** | JSON Schema / function calling вместо свободного текста? |
| 8 | **Кэшируйте повторяющиеся части** | Статичный system prompt в начале? Prompt caching включён? |
| 9 | **Примеры соответствуют инструкциям** | Язык, формат и стиль примеров = инструкциям? |
| 10 | **A/B-тест перед деплоем** | Прогнан ли тест на 200+ запросах? |
### Задания
**Задание 1. Промпт-контракт для вашего проекта.** Возьмите реальную задачу из текущего проекта (extraction, classification, генерация кода). Напишите промпт по каузальному порядку из §6.2: роль цель ограничения данные формат. Запустите 5 раз с `temp=0.3`. Измерьте: (a) parse rate (все ли ответы соответствуют формату?), (b) семантическую стабильность (насколько похожи ответы между собой). Ожидаемый результат: parse rate 95%, если промпт-контракт составлен корректно.
**Задание 2. Structured outputs в продакшн-пайплайне.** Реализуйте пайплайн, который принимает свободный текст (отзыв, баг-репорт, тикет), извлекает структурированные данные через `response_format` с JSON Schema и записывает в БД. Используйте Pydantic-модель как единый контракт для LLM и валидации. Проверьте: что происходит, если в тексте нет нужных данных? Если текст на другом языке?
**Задание 3. Антипаттерн-аудит.** Соберите 510 промптов из вашей кодовой базы. Для каждого проверьте по списку антипаттернов из §6.8: есть ли вежливый шум, негативные инструкции без позитивных, инструкции после данных, противоречащие примеры? Исправьте найденные проблемы и сравните результаты до/после.
**Задание 4. Проведите A/B-тест промпта.** Возьмите работающий промпт (вариант A) и создайте вариант B (изменённый формат, другая модель, другие few-shot примеры). Настройте рандомизацию, соберите 200 ответов на вариант, проанализируйте primary metric и guardrail metrics с z-test. **Ожидаемый результат:** отчёт с p-value, рекомендацией и анализом trade-off (качество vs стоимость vs latency).
---
## Источники
- OpenAI. "Structured Outputs." API Documentation (20242026).
- Anthropic. "Tool Use" and "Prompt Caching." Claude Documentation (20242026).
- Google. "Structured Output with Gemini." Vertex AI Documentation (20252026).
- Outlines. "Structured Generation." https://github.com/outlines-dev/outlines
- Microsoft. "Guidance." https://github.com/guidance-ai/guidance
- Khattab, O. et al. "DSPy: Compiling Declarative Language Model Calls." Stanford NLP (20242026).
- Beurer-Kellner, L. et al. "LMQL: Programming Large Language Models." (20232025).
- Anthropic. "Model Context Protocol (MCP)." https://modelcontextprotocol.io (20242026).
---
**Навигация:**
- Назад: [Глава 5. Длинный контекст — иллюзия, что модель «видит всё»](05_long_context.md)
- Далее: [Глава 7. Разметка, теги и архитектура сложного промпта](07_markup_tags_and_prompt_architecture.md)