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

Автор: Обновлено

Что такое система управления промптами?

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

TL;DR

  • -Production-промпт — это релизуемый артефакт: шаблон, настройки модели, контракт ответа, tools, владелец и eval-датасет
  • -Используйте неизменяемые версии и перемещаемые labels production и canary; не перезаписывайте промпт, который уже обслуживает трафик
  • -Сначала запускайте детерминированные проверки контракта, затем сравнивайте candidate и production на одном датасете
  • -Распределяйте canary-трафик в приложении стабильным бакетированием; labels Langfuse обозначают варианты, но не делят трафик автоматически
  • -Привязывайте объект промпта к каждой generation, чтобы сравнивать latency, стоимость, ошибки и quality scores по версиям

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:

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: промпт задаёт инструкции, а остальной 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, которые указывают на версию. Модель данных промпта поддерживает text- и chat-промпты, переменные, конфигурацию, prompt references и message placeholders.

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 описаны 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, объясняющий его роль:

{"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, а затем сверяйте judge с решениями людей. Фразы «средний score вырос» недостаточно: смотрите критичные срезы и конкретные ошибки.

Langfuse Prompt Experiments умеет запускать варианты промптов и моделей на 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:

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-тестам использует разные labels и выбор на стороне приложения. Langfuse связывает варианты с результатами, но не распределяет трафик вашего сервиса автоматически.

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

Langfuse SDK кеширует промпты на стороне клиента. Актуальная документация по caching описывает default TTL в 60 секунд, background revalidation, prefetch и fallback prompt для cold start.

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. На этой связи строятся метрики по точной версии.

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 рекомендует передавать промпт в нужную generation, а не прикреплять его ко всему trace.

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

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

Metrics API Langfuse агрегирует стоимость, 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? Начните с шаблона библиотеки промптов: он стандартизирует владельцев, переменные и примеры.

Часто задаваемые вопросы

Где хранить промпты: в Git или в prompt registry?
Git подходит, если все изменения делают инженеры и обычный цикл деплоя приемлем. Registry нужен для независимого rollout, labels, редактирования через UI и аналитики по версиям. Частый гибрид: review в Git и синхронизация одобренных версий в runtime registry.
Как тестировать промпт перед production?
Сначала проверьте переменные, схемы, имена tools и парсинг ответа. Затем запустите candidate и текущую production-версию на одном versioned dataset. Сравнивайте task-specific scores в целом и на критичных срезах; универсального размера выборки и одной достаточной метрики нет.
Как устроен A/B-тест промптов в Langfuse?
Назначьте версиям разные labels, выбирайте label в приложении через стабильный user- или session-bucket и привязывайте выбранный промпт к generation. Langfuse собирает метрики по версиям, но распределением трафика управляет приложение или feature-flag сервис.