Деплой того агента, который вы прогнали через evals: промпты, тулы и модель как один артефакт
Evals показывают 94%, а в проде хаос. Дело почти никогда не в модели - дело в неотслеживаемом дрейфе между тем, что вы тестировали, и тем, что задеплоили. Разбираю workflow на основе манифеста и хеша.
Pavel Duglas
AI Automation & MVP Architect
В прошлом месяце мне написал клиент: их support-агент “за ночь отупел”. Деплоя не было. Код не меняли, промпт не трогали, инцидентов в логах нет. Eval-сьют по-прежнему выдаёт 94%. Количество жалоб в поддержку выросло втрое.
Причину нашли за сорок минут. Они дергали алиас модели вместо закреплённой версии, а провайдер прокрутил алиас вперёд. Новый снапшот лучше рассуждал и заметно хуже соблюдал формат вывода, на который завязан их парсер. А eval-сьют последний раз прогоняли три недели назад: другая модель, и промпт-файл, в котором стажёр с лучшими намерениями поправил два слова.
Они деплоили не того агента, которого прогоняли через evals. И так делают почти все.
Поверхность дрейфа, которую никто не инвентаризирует
Когда вы деплоите обычный сервис, вы деплоите контейнер с digest. Артефакт - это ровно то, что вы протестировали. Агенты ломают эту модель, потому что их поведение живёт в десятке мест, которых нет в вашей сборке.
Вот полный список того, что я проверяю на каждом аудите:
- Идентичность модели.
gpt-5,claude-sonnet-latest,openrouter/auto- это не версии. Это подписка на чужой релизный календарь. - Параметры сэмплинга. Temperature, top_p, лимит выходных токенов, reasoning effort, seed. Обычно захардкожены в одном месте для evals и в другом для прода.
- Системный промпт. Лежит в
.md, который никто не считает кодом, правится прямо на сервере или хранится в prompt-management SaaS, где любой коллега может нажать “Publish”. - Схемы тулов. Поменяли описание параметра с “ISO date” на “date” - и модель начинает присылать
12/03/2026. Описания тулов это промпт, а не документация. - Реализация тулов. Схема та же, но
search_ordersтеперь возвращает 50 строк вместо 10 и разносит контекстное окно. - Состояние retrieval. Индекс, на котором вы гоняли evals, содержал 4 000 документов. В проде их 40 000, включая три противоречащих друг другу PDF с регламентами, которые кто-то залил в пятницу.
- Сборка контекста. Порядок обрезки, окно истории, порог суммаризации. Почти всегда реализовано дважды: один раз в eval-харнессе, другой раз в реальном рантайме.
- Роутинг и фоллбэки. Ваш failover уводит 2% трафика на совсем другую модель, которую вообще ни разу не проверяли.
- Изменения на стороне провайдера. Safety-фильтры, поведение кэша, обновление токенизатора, тихие серверные вставки в промпт “для полезности”.
Каждый пункт из этого списка хотя бы раз ударил лично меня. И лечится это не количеством evals. Лечится тем, что объект тестирования становится физически идентичен объекту в проде.
Агент как один хешированный артефакт
Я складываю всё, что влияет на поведение, в один манифест и считаю от него хеш. Хеш изменился - это новый релиз, ему нужен новый прогон evals. Без исключений.
# agents/support/manifest.yaml
id: support-agent
version: 7
model:
provider: anthropic
# закреплённый снапшот, никогда не алиас
name: claude-sonnet-4-5-20250929
temperature: 0.2
max_output_tokens: 1200
fallback:
# проверяется отдельно, версионируется отдельно
manifest: agents/support/manifest.fallback.yaml
prompts:
system: prompts/support/system.v7.md
refusal: prompts/support/refusal.v3.md
tools:
- name: search_orders
schema: tools/search_orders.v2.json
impl: app.tools.orders:search
max_rows: 10
- name: issue_refund
schema: tools/issue_refund.v1.json
impl: app.tools.billing:refund
requires_approval: true
retrieval:
index: support-kb
snapshot: 2026-02-14T09:00:00Z
top_k: 6
context:
history_turns: 8
truncation: oldest_first
hard_token_cap: 24000
Хеш считается по манифесту плюс содержимому всех файлов, на которые он ссылается:
import hashlib, json, yaml
from pathlib import Path
def artifact_hash(manifest_path: Path) -> str:
manifest = yaml.safe_load(manifest_path.read_text())
h = hashlib.sha256()
h.update(json.dumps(manifest, sort_keys=True).encode())
for rel in sorted(_referenced_files(manifest)):
h.update(rel.encode())
h.update(Path(rel).read_bytes())
return h.hexdigest()[:16]
Этот короткий хеш становится настоящим номером версии агента. Он уходит в логи, в отчёт по evals, в аннотацию деплоя и в каждую запись об ответе. Когда кто-то говорит “агент стал хуже”, первый вопрос больше не “что изменилось?”, а “какой артефакт отвечал на этот запрос?”.
Закрепите модель, запретите алиасы в CI
Одна регулярка в CI закрывает целый класс аварий:
FORBIDDEN = ("latest", "auto", "preview")
def test_model_is_pinned(manifest):
name = manifest["model"]["name"]
assert not any(tag in name for tag in FORBIDDEN), \
f"model must be a pinned snapshot, got {name}"
Да, закрепление означает, что новые модели придётся подключать руками. В этом и смысл. Апгрейд модели - это релиз, а не погода за окном.
Версионируйте контракты тулов как публичный API
search_orders.v2.json - это файл, который меняется только рождением v3. Если вы правите описание на месте, меняется хеш, срабатывает eval-гейт, и вы узнаёте, что аккуратная правка формулировки стоила вам 6 пунктов на парсинге дат. Узнаёте до клиента, а не после.
Та же дисциплина на стороне реализации. max_rows живёт в манифесте, а не в теле функции, потому что размер ответа - это параметр поведения, а не деталь имплементации.
Снапшотьте слой retrieval
Пункт, который пропускают чаще всего, и при этом самый тихий источник дрейфа. Если ваша база знаний мутабельна, а evals гоняются по “тому, что сейчас в индексе”, то ваши цифры - это шум. Я использую append-only индексы с фильтром по времени, чтобы манифест мог сказать “документы на 14 февраля 09:00”, и eval с продом видели один и тот же корпус. Для небольших проектов версионированная папка с markdown-файлами в git абсолютно достаточна.
Гейт на деплой по хешу, а не по ощущениям
Правило в CI короткое: у каждого хеша артефакта должен быть проходной отчёт evals не ниже текущего baseline.
HASH=$(python -m agentkit hash agents/support/manifest.yaml)
if ! agentkit evals report --hash "$HASH" --exists; then
echo "Нет прогона evals для артефакта $HASH. Запустите: agentkit evals run"
exit 1
fi
agentkit evals compare --hash "$HASH" --baseline main --max-regression 2
Две детали, без которых это не выживает в реальной жизни:
- Eval-харнесс обязан грузить манифест. Если ваш харнесс сам собирает промпт и сам создаёт клиента, вы снова тестируете другого агента. Один загрузчик на оба пути, иначе весь ритуал бессмысленен.
- Оставьте бюджет на регрессию. Требование нулевой регрессии по шумной метрике приводит к тому, что тесты начинают отключать. Допуск в 2 пункта плюс жёсткий порог по критичным проверкам (валидность tool-call, соблюдение формата, поведение при отказе) - рабочая схема.
Проверяйте на старте, а не в три ночи
CI недостаточно, потому что у прода своё окружение. Я добавляю startup-check, который валит контейнер, если реальность не совпала с манифестом.
def assert_runtime_parity(manifest):
# провайдер реально отдаёт закреплённый снапшот
models = {m.id for m in client.models.list()}
assert manifest["model"]["name"] in models
# каждый заявленный тул существует и его схема совпадает с файлом
for tool in manifest["tools"]:
fn = import_impl(tool["impl"])
assert schema_of(fn) == load_json(tool["schema"])
# снапшот retrieval доступен
assert index_has_snapshot(manifest["retrieval"]["snapshot"])
Контейнер, который отказался стартовать, - это неприятный вторник. Агент, который тихо работает без одного тула и галлюцинирует его результаты, - это очередь на возвраты.
Канарейка и прогретая предыдущая версия
Поскольку артефакт - это единый хешированный блок, раскатка становится тривиальной. Я роутлю по хешу: 5% сессий на новый артефакт на пару часов, обе версии загружены в одном процессе. Сравниваю четыре вещи между когортами: долю ошибочных tool-call, средние токены на решённую сессию, долю эскалаций на человека и p95 по латентности. Оценки от LLM-судьи полезны, но медленные. Эти четыре операционных числа ловят большинство регрессий в течение часа.
Роллбэк - это флаг в конфиге, который переводит трафик на предыдущий хеш. Без пересборки и без археологии по промптам.
Версия для соло-основателя
Если вы один человек и пилите MVP, не надо строить платформу. Сделайте на этой неделе четыре вещи:
- Замените все алиасы моделей на закреплённые снапшоты. Десять минут.
- Вынесите промпты и схемы тулов в файлы в git, и пусть рантайм читает их с диска, а не из строковых литералов или хостед prompt-UI.
- Напишите функцию хеша выше и логируйте хеш с каждым прогоном агента.
- Держите 30-50 реальных кейсов в JSONL и скрипт, который прогоняет их через тот же загрузчик, что и прод. Это уже настоящий eval-сьют.
Это примерно полдня работы. И она превращает “агент как-то хуже стал” из вопроса без ответа в diff между двумя хешами. Всё остальное в этой статье - надстройка над этим фундаментом.
Неудобная правда в том, что большая часть работы над надёжностью агентов вообще не про модели. Это release engineering, применённый к системе, чьё поведение по случайности написано на естественном языке.
FAQ
Вопросы и ответы
Закрепление модели означает, что я буду сидеть на устаревшем снапшоте. Как не отстать?
Апгрейд модели становится обычной задачей в бэклоге, а не случайным событием. Я держу в CI отдельную nightly-джобу, которая гоняет тот же eval-сьют на новых доступных снапшотах и пишет отчёт в чат. Если новый снапшот выигрывает, я меняю одну строку в манифесте, получаю новый хеш, прогоняю канарейку и раскатываю. Разница в том, что переход происходит когда я к нему готов, а не в ночь с пятницы на субботу по решению провайдера.
Что делать, если база знаний обновляется каждый час и снапшотить её нереально?
Снапшот нужен не всей базе, а eval-прогону. Минимальный вариант: добавьте в документы поле ingested_at и научите retrieval принимать фильтр as_of. Тогда манифест фиксирует момент времени, evals воспроизводимы, а прод работает с актуальным индексом. Дополнительно я гоняю отдельный набор кейсов по свежему индексу раз в сутки: он ловит не регрессии агента, а мусор в контенте, вроде двух противоречащих регламентов. Это два разных вида проверок, не смешивайте их.
У меня Telegram-бот и пара BAS-сценариев, а не мультиагентная платформа. Это не оверинжиниринг?
Полный манифест с fallback и канарейкой - да, для бота это перебор. Но три вещи стоят своих тридцати минут даже на самом маленьком проекте: закреплённый снапшот модели, промпты и схемы тулов в git-файлах вместо литералов, и логирование хеша артефакта рядом с каждым ответом. Именно третий пункт спасает, когда через два месяца пользователь пришлёт скриншот странного ответа: вы сразу увидите, какая версия агента его породила, вместо того чтобы гадать по датам коммитов.
Похожие статьи
Сделаю под ключ
Соберу ИИ-агента под реальную задачу
С инструментами, памятью и логами, чтобы он работал в проде, а не только в демо.
от 1 500 $ · 1-2 недели