# Промпт-инженерия в production: версии, evals и rollback

> Практическая система управления промптами: версии, eval-датасеты, canary rollout, метрики по версиям, кеширование и безопасный rollback в Langfuse.
> Author: Roman Belov · Published: 2026-03-26 · Source: https://futurecraft.pro/ru/blog/prompt-engineering-system/

Production-промпт — не строка. Это релизуемый артефакт: шаблон, переменные,
настройки модели, контракт ответа, определения tools, владелец и доказательство, что
новая версия не хуже той, которая уже обслуживает пользователей.

Такой контроль может понадобиться намного раньше условных «50 промптов». Один промпт
для оценки платёжного риска опаснее пятидесяти суммаризаторов. Выбирайте строгость
процесса по последствиям ошибки, а не по магическому порогу.

Минимально полезная система должна без раскопок в коммитах и логах отвечать на четыре
вопроса:

1. Какая версия промпта обработала этот запрос?
2. Что изменилось относительно предыдущей версии?
3. Прошла ли новая версия ту же проверку, что и production?
4. Можно ли вернуть трафик назад без деплоя приложения?

## Четыре части production-системы промптов

```
┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│  Registry    │───▶│  Evaluation  │───▶│   Rollout    │
│ версии       │    │ контракты +  │    │ labels +     │
│ labels       │    │ task scores  │    │ traffic split│
└──────┬───────┘    └──────────────┘    └──────┬───────┘
       │                                       │
       └──────────────▶ Observability ◀────────┘
                         prompt → trace
```

**Registry.** Хранит неизменяемые версии и позволяет приложению получать версию по
перемещаемому label: `production`, `staging` или `canary`.

**Evaluation.** Проверяет runtime-контракт и качество на versioned dataset до
переключения трафика.

**Rollout.** Решает, какие пользователи или сессии получат каждую одобренную версию.

**Observability.** Связывает выбранный промпт с generation, а затем группирует quality,
latency, стоимость и ошибки по версиям.

Начать можно с Git и тестового скрипта. Runtime registry нужен, когда релизы промптов
должны идти независимо от релизов приложения.

## Сначала определите артефакт, потом выбирайте инструмент

Во многих репозиториях хранится только текст промпта. Параметры, которые реально
определяют поведение, разбросаны по коду. Положите рядом manifest:

```yaml
name: ticket-classifier
owner: support-platform
type: chat
model: ${CLASSIFIER_MODEL}
variables:
  - ticket_text
output_schema: schemas/ticket-classification.json
tools: []
dataset: datasets/ticket-classifier-v3.jsonl
fallback: rules-based-routing
```

Модель задаётся через environment value, а не копируется по десяткам файлов. Схема
ответа версионируется. Fallback указан явно. Ревьюер видит operational contract, не
читая всё приложение.

По возможности разделяйте изменения:

- текст промпта;
- модель и sampling configuration;
- retrieval logic и передаваемый контекст;
- tool schemas;
- output parser.

Если всё меняется одним релизом, у деградации сразу пять возможных причин. Здесь
промпт-инженерия встречается с [context engineering](/ru/blog/context-engineering-guide/):
промпт задаёт инструкции, а остальной request pipeline определяет, что модель видит и
какие действия может вызвать.

## Registry: Git, Langfuse или гибрид

### Только Git

Git — нормальный registry, когда промпты меняют инженеры, а обычный релиз приложения не
мешает скорости работы. Он даёт review, blame, tags и воспроизводимые сборки. Но Git не
даёт независимого production label, prompt-aware кеширования и метрик по runtime-версии.

Некорректно говорить, что у промптов в Git «нет версионирования». Ограничение другое:
история Git не является системой управления runtime-деплоем.

### Langfuse как runtime registry

В Langfuse промпт состоит из неизменяемых нумерованных версий и labels, которые
указывают на версию. [Модель данных промпта](https://langfuse.com/docs/prompt-management/data-model)
поддерживает text- и chat-промпты, переменные, конфигурацию, prompt references и
message placeholders.

```python
from langfuse import get_client

langfuse = get_client()

prompt = langfuse.get_prompt(
    "ticket-classifier",
    label="production",
)
messages = prompt.compile(ticket_text=ticket_text)
```

Без явного label SDK вернёт версию с `production`, но в коде лучше показать намерение.
Новая версия получает `latest`, а custom labels подходят для staging, tenants и
экспериментов. В официальном
[гайде по version control](https://langfuse.com/docs/prompt-management/features/prompt-version-control)
описаны promotion и rollback через labels.

### Гибрид: review в Git, выдача из Langfuse

Если нужны и pull-request review, и независимый rollout:

1. Храните в Git промпт, manifest, схему и ссылку на dataset.
2. Запускайте contract checks и evals в CI.
3. После merge создавайте новую версию промпта в Langfuse.
4. Сначала назначайте `staging`, а не `production`.
5. Продвигайте проверенную версию перемещением label.

Sync job должен быть идемпотентным и записывать Git commit в metadata промпта. Не
редактируйте production-версию на месте: создавайте candidate, который можно сравнить и
отклонить.

## Evaluation: сначала контракт, потом качество

У eval-пайплайна две разные задачи. Если смешать их, на дашборде будет «quality score»,
а пользователи всё равно увидят поломанный JSON.

### 1. Детерминированные проверки контракта

Где возможно, запускайте их без LLM:

- все обязательные переменные существуют и компилируются;
- после рендера не осталось неизвестных переменных;
- ответ соответствует JSON Schema;
- указанные tools существуют, а их схемы валидны;
- fixtures укладываются в выбранный token budget;
- в тестовых артефактах и логах нет секретов и исходных персональных данных.

Если продукту нужен structured output, проверяйте структуру напрямую. Не просите
judge-модель решить, «похож» ли сломанный JSON на правильный.

### 2. Оценка качества задачи

Полезный dataset начинается с реальных failure modes, а не с круглого числа примеров.
У каждого item должны быть ID, ожидаемый результат и slice, объясняющий его роль:

```jsonl
{"id":"billing-001","input":"Карта отклонена дважды","expected":"billing","slice":"short-ru"}
{"id":"security-004","input":"Сбросьте MFA бывшему админу","expected":"security","slice":"high-risk"}
{"id":"mixed-012","input":"Списали дважды, и я не могу войти","expected":"billing","slice":"multi-intent"}
```

Начните с production-инцидентов, проверенных вручную примеров и пограничных случаев.
Синтетические вариации полезны для проверки покрытия, но помечайте их отдельно и не
подменяйте ими реальный трафик.

Для классификации и extraction предпочитайте детерминированное сравнение. Для
open-ended generation используйте rubric, pairwise review или
[LLM-as-a-Judge quality gate](/ru/blog/llm-as-judge-automated-quality-gate/), а затем
сверяйте judge с решениями людей. Фразы «средний score вырос» недостаточно: смотрите
критичные срезы и конкретные ошибки.

Langfuse [Prompt Experiments](https://langfuse.com/docs/evaluation/experiments/experiments-via-ui)
умеет запускать варианты промптов и моделей на dataset и подключать code- или
model-based evaluators. Для собственного runner действует то же правило: candidate и
production должны получить одну версию dataset.

### Практический release gate

Отклоняйте candidate, если:

- не прошёл хотя бы один contract check;
- ухудшился критичный slice;
- общее качество вышло за допуск конкретной задачи;
- latency или стоимость превысили budget;
- ревьюеры не могут объяснить оставшиеся ошибки.

Допуски зависят от продуктового риска и наблюдаемой дисперсии. Универсального правила
«каждому промпту нужно 30, 100 или 200 примеров» не существует.

## Rollout: labels обозначают варианты, приложение делит трафик

Для низкорискового промпта можно сразу переместить `production` на проверенную версию.
Для canary держите два label и выбирайте один стабильным bucket:

```python
import hashlib


def prompt_label(subject_id: str, canary_percent: int = 5) -> str:
    digest = hashlib.sha256(subject_id.encode("utf-8")).digest()
    bucket = int.from_bytes(digest[:4], "big") % 100
    return "canary" if bucket < canary_percent else "production"


label = prompt_label(user_id)
prompt = langfuse.get_prompt("ticket-classifier", label=label)
```

Стабильное бакетирование оставляет пользователя или сессию на одном варианте. Случайный
выбор при каждом запросе создаёт crossover noise и может поменять поведение посреди
многошагового диалога.

Официальный [гайд Langfuse по A/B-тестам](https://langfuse.com/docs/prompt-management/features/a-b-testing)
использует разные labels и выбор на стороне приложения. Langfuse связывает варианты с
результатами, но не распределяет трафик вашего сервиса автоматически.

### Учитывайте кеш промптов

Langfuse SDK кеширует промпты на стороне клиента. Актуальная
[документация по caching](https://langfuse.com/docs/prompt-management/features/caching)
описывает default TTL в 60 секунд, background revalidation, prefetch и fallback prompt
для cold start.

```python
prompt = langfuse.get_prompt(
    "ticket-classifier",
    label="production",
    cache_ttl_seconds=300,
)
```

Кеш повышает устойчивость, но перемещение label не переключает все инстансы мгновенно.
Заложите TTL в ожидания для rollout и rollback. Отключение кеша убирает stale version,
но возвращает runtime-зависимость от registry — это осознанный trade-off.

## Observability: привязывайте объект промпта, а не только имя

Metadata помогает при отладке, но Langfuse умеет напрямую связать объект промпта с
конкретной generation. На этой связи строятся метрики по точной версии.

```python
from langfuse import get_client

langfuse = get_client()
prompt = langfuse.get_prompt("ticket-classifier", label=label)
messages = prompt.compile(ticket_text=ticket_text)

with langfuse.start_as_current_observation(
    as_type="generation",
    name="ticket-classification",
    model=model_id,
    input=messages,
    prompt=prompt,
) as generation:
    result = call_model(model_id=model_id, messages=messages)
    generation.update(output=result)
```

[Гайд по trace linking](https://langfuse.com/docs/prompt-management/features/link-to-traces)
рекомендует передавать промпт в нужную generation, а не прикреплять его ко всему trace.

Следите за четырьмя группами сигналов:

| Сигнал | Примеры | Зачем |
| --- | --- | --- |
| Contract | parse errors, missing fields, tool-call errors | Находит сломанные интеграции |
| Quality | task score, ручная правка, acceptance | Показывает, помог ли ответ |
| Operations | p50/p95 latency, retries, provider errors | Отделяет промпт от инфраструктурных проблем |
| Economics | input/output tokens, cost per successful task | Находит слишком дорогие «улучшения» |

[Metrics API Langfuse](https://langfuse.com/docs/metrics/features/metrics-api) агрегирует
стоимость, usage, latency, volume и scores по поддерживаемым dimensions. Для нового кода
используйте актуальный SDK-вызов `langfuse.api.metrics.get(...)`; legacy API устарел.

Не логируйте исходные пользовательские промпты только потому, что observability-сервис
это позволяет. Редактируйте персональные данные, ограничивайте доступ к проекту,
задавайте retention и решайте, какие поля могут покидать вашу инфраструктуру.

## Rollback — обычный release path, а не аварийная импровизация

Хороший rollback скучен:

1. Остановите расширение canary.
2. Верните serving label на последнюю known-good версию.
3. Учитывайте cache TTL и следите, как уходит трафик со старой версии.
4. Сохраните неудачную версию, traces и результаты dataset.
5. Добавьте production-ошибку в regression dataset.
6. Исправляйте проблему новой неизменяемой версией.

Не считайте `current_version - 1` автоматически безопасной. Labels могли перескочить
через несколько версий, а старый промпт — зависеть от уже изменившейся модели или tool
schema. Записывайте весь known-good tuple: версия промпта, model configuration, версия
tool schema и retrieval release.

## Последовательность, которая работает и в маленькой системе

**Inventory.** Выпишите все промпты, владельцев, callers, output contracts и последствия
ошибок.

**Version.** Положите промпт и runtime-контракт в Git или registry. Не смешивайте serving
label с `latest`.

**Evaluate.** Превратите известные инциденты и boundary cases в versioned dataset.
Сравнивайте candidate с production, а не с памятью команды.

**Observe.** Привязывайте точную версию промпта к каждой generation и добавьте
продуктовые quality signals.

**Roll out.** Начните с внутреннего трафика или маленькой стабильной когорты. Расширяйте
её только после проверки quality, latency, errors и cost budgets.

**Learn.** Каждая пропущенная ошибка становится тест-кейсом. Dataset — это операционная
память системы.

Результат — не «prompt ops» ради процесса. Это release workflow, где у изменения есть
владелец, доказательства, контролируемый blast radius и проверенный путь назад.

---

*Нужен простой формат до внедрения registry? Начните с
[шаблона библиотеки промптов](/ru/blog/prompt-library-template/): он стандартизирует
владельцев, переменные и примеры.*
