Глава 11: Оптимизация промптов
Принцип
Промпты можно итеративно улучшать. Метрики качества позволяют объективно сравнивать варианты.
Итеративное улучшение
Цикл оптимизации
1. Baseline: начальный промпт
2. Test: запуск на тестовых примерах
3. Analyze: анализ ошибок
4. Modify: корректировка промпта
5. Repeat: повтор до достижения целевого качества
Пример итерации
Итерация 1 (Baseline):
Промпт: "Извлеки email из текста"
Результат: извлекает, но с лишним текстом
Проблема: формат не указан
Итерация 2:
Промпт: "Извлеки email из текста. Верни только email, без другого текста."
Результат: лучше, но иногда добавляет пояснения
Проблема: модель "хочет помочь"
Итерация 3:
Промпт: "Извлеки email из текста.
Формат ответа: только email, одна строка, без пояснений.
Если email нет — верни 'NOT\_FOUND'"
Результат: стабильный формат
Проблема: не обрабатывает несколько email
Итерация 4 (Final):
Промпт: "Извлеки все email адреса из текста.
Формат: JSON массив строк.
Если email нет — верни пустой массив $$.
Пример: $'a@b.com', 'c@d.com'$"
Результат: корректный
Метрики качества
Для генерации кода
| Метрика | Описание | Как измерить |
|---|---|---|
| Корректность | Код работает | Unit тесты |
| Полнота | Все требования | Чеклист |
| Стиль | Соответствие стандартам | Линтер |
| Эффективность | Сложность | Профилирование |
def evaluate\_code\_prompt(prompt: str, test\_cases: list) -\> dict:
"""Оценка качества промпта для генерации кода."""
results = $$
for case in test\_cases:
response = llm.generate(prompt.format(\*\*case$'input'$))
code = extract\_code(response)
results.append(\{
'compiles': check\_syntax(code),
'passes\_tests': run\_tests(code, case$'tests'$),
'lint\_score': run\_linter(code),
'complexity': analyze\_complexity(code)
\})
return \{
'compile\_rate': mean($r\['compiles'$ for r in results\]),
'test\_pass\_rate': mean($r\['passes\_tests'$ for r in results\]),
'avg\_lint\_score': mean($r\['lint\_score'$ for r in results\]),
'avg\_complexity': mean($r\['complexity'$ for r in results\])
\}
Для классификации
| Метрика | Формула |
|---|---|
| Accuracy | |
| Precision | |
| Recall | |
| F1 |
Для извлечения информации
| Метрика | Описание |
|---|---|
| Exact Match | Полное совпадение |
| Partial Match | Частичное совпадение |
| F1 Token | F1 по токенам |
A/B тестирование промптов
Структура эксперимента
import random
def ab\_test\_prompts(
prompt\_a: str,
prompt\_b: str,
test\_cases: list,
metric\_fn: callable
) -\> dict:
"""A/B тест двух вариантов промпта."""
results\_a = $$
results\_b = $$
for case in test\_cases:
\# Случайный порядок для устранения bias
prompts = $(prompt\_a, results\_a), (prompt\_b, results\_b)$
random.shuffle(prompts)
for prompt, results in prompts:
response = llm.generate(prompt.format(\*\*case))
score = metric\_fn(response, case$'expected'$)
results.append(score)
return \{
'prompt\_a\_mean': mean(results\_a),
'prompt\_b\_mean': mean(results\_b),
'p\_value': ttest\_ind(results\_a, results\_b).pvalue,
'winner': 'A' if mean(results\_a) \> mean(results\_b) else 'B'
\}
Пример эксперимента
Prompt A: "Классифицируй sentiment: positive/negative"
Prompt B: "Классифицируй sentiment.
Варианты: positive, negative.
Ответ — одно слово."
Тест-кейсы: 100 отзывов с метками
Результаты:
- Prompt A: accuracy 0.78
- Prompt B: accuracy 0.91
- p-value: 0.002
Вывод: Prompt B статистически значимо лучше
Техники улучшения
1. Добавление примеров
До:
"Преобразуй дату в ISO формат"
После:
"Преобразуй дату в ISO формат.
Примеры:
'January 5, 2024' → '2024-01-05'
'5.01.24' → '2024-01-05'
'05/01/2024' → '2024-01-05'"
Улучшение: +25% точности на нестандартных форматах
2. Уточнение формата
До:
"Найди ошибки в коде"
После:
"Найди ошибки в коде.
Формат ответа:
\`\`\`json
$\{'line': int, 'error': str, 'fix': str\}$
Если ошибок нет — пустой массив."
Улучшение: парсинг 100% vs 60%
\#\#\# 3. Добавление контекста
До: "Объясни этот код"
После: "Объясни этот код.
Контекст:
-
Язык: Python 3.11
-
Проект: веб-сервис на FastAPI
-
Аудитория: junior разработчик
Объяснение должно быть понятно новичку."
Улучшение: релевантность +30%
\#\#\# 4. Chain-of-Thought
До: "Реши задачу: "
После: "Реши задачу:
Рассуждай пошагово:
-
Что дано
-
Что требуется найти
-
Какой метод применить
-
Пошаговое решение
-
Ответ"
Улучшение: +40% на математических задачах
\#\#\# 5. Негативные примеры
До: "Напиши безопасный код"
После: "Напиши безопасный код.
НЕ ДЕЛАТЬ:
-
eval() с пользовательским вводом
-
SQL без параметров
-
pickle.loads() с недоверенными данными
Пример плохого кода: query = f'SELECT * FROM users WHERE id = {user_id}' # SQL injection!
Пример хорошего кода: query = 'SELECT * FROM users WHERE id = %s' cursor.execute(query, (user_id,))"
Улучшение: security issues -60%
\#\# Оптимизация по токенам
\#\#\# Сжатие без потери качества
До (150 токенов): "Я хочу, чтобы ты написал для меня функцию на языке Python, которая будет принимать на вход список чисел и возвращать новый список, содержащий только те числа, которые больше нуля. Пожалуйста, убедись, что код читаемый и следует стандартам PEP8."
После (40 токенов): "Python функция: фильтр положительных чисел из списка. PEP8, type hints."
Экономия: 73% токенов, качество то же
\#\#\# Оптимальная длина
Зависимость качества от длины промпта (эмпирически):
$Q(L) = Q\_\{max\} \\cdot (1 - e^\{-L/L\_0\}) - \\alpha \\cdot L$
где:
- $L$ — длина промпта
- $L\_0$ — характерная длина (~50-100 токенов)
- $\\alpha$ — штраф за длину
Оптимум обычно в диапазоне 50-200 токенов для типовых задач.
\#\# Систематический подход
\#\#\# Grid search по параметрам
\`\`\`python
\# Параметры для оптимизации
params = \{
'include\_examples': $True, False$,
'num\_examples': $1, 2, 3$,
'format\_explicit': $True, False$,
'add\_constraints': $True, False$
\}
\# Генерация вариантов
def generate\_prompt(task: str, params: dict) -\> str:
prompt = f"Задача: \{task\}\\n"
if params$'format\_explicit'$:
prompt += "Формат: JSON\\n"
if params$'add\_constraints'$:
prompt += "Ограничения: максимум 100 строк\\n"
if params$'include\_examples'$:
for i in range(params$'num\_examples'$):
prompt += f"Пример \{i+1\}: ...\\n"
return prompt
\# Оценка всех комбинаций
best\_score = 0
best\_params = None
for combination in itertools.product(\*params.values()):
current\_params = dict(zip(params.keys(), combination))
prompt = generate\_prompt(task, current\_params)
score = evaluate(prompt, test\_cases)
if score \> best\_score:
best\_score = score
best\_params = current\_params
Логирование экспериментов
\# Структура лога
experiment = \{
'id': 'exp\_001',
'timestamp': '2024-01-15T10:30:00',
'prompt\_version': 'v3',
'prompt\_text': '...',
'parameters': \{
'temperature': 0.0,
'max\_tokens': 500
\},
'test\_cases': 100,
'metrics': \{
'accuracy': 0.92,
'latency\_p50': 1.2,
'latency\_p99': 3.4,
'cost\_per\_request': 0.002
\},
'notes': 'Добавлены примеры, улучшился edge case handling'
\}