Сделайте качество AI-вывода release-критерием: фиксированный regression из прошлых инцидентов, distribution-diff к снапшоту прошлого релиза (20–50 реальных входов), human spot-check перед первым продом нового вида вывода — и один человек, который подписывает sign-off. Ниже — антипаттерн demo-grade, три слоя методики и минимальный harness, который падает в CI с понятным exit code.
Большинство AI-фич уезжает в прод в demo grade. Не потому что команда «не заботится» — потому что eval, который одобрил фичу, использовал те же входы, что и питч. Курированный датасет. Контролируемые условия. Длинный хвост реального трафика ни разу не прогоняли.
Потом прод показывает то, что демо не покрыло:
- RAG корректно достаёт документы на стейджинге с курированным корпусом и галлюцинирует на полном корпусе со stale-записями;
- классификатор тихо деградирует после смены входной схемы upstream;
- AI-код проходит автотесты, но вносит security-паттерн, который тесты не покрывали.
Три разных стека. Один failure mode: нет структурированного eval, нет named owner, одобрение «на демо выглядело нормально».
Agent drift — когда поведение модели или агента уезжает от того, что вы считали «проверенным», без красного алерта в момент релиза. Иногда это смена промпта или корпуса. Иногда — тихий сдвиг распределения входов. Симптом один: зелёный eval на демо-наборе, красный прод на живом хвосте.
Когда вообще нужен AI-output review
Не каждый деплой. Триггер простой: меняет ли релиз то, что модель видит, или то, что она выдаёт?
Проверяйте до релиза, если выполняется хотя бы одно:
- вывод идёт в customer-facing поток (ответ, рекомендация, классификация, поиск);
- вывод влияет на бизнес-критичное решение (approval, routing, pricing, alert);
- вывод строится на retrieval (RAG) — риск галлюцинации масштабируется с качеством retrieval;
- с прошлого релиза сменились модель, промпт или корпус retrieval.
Инфраструктурный деплой, чистый UI, конфиг без влияния на prompt/retrieval — полный eval не обязателен. Экономия здесь нормальна; самообман — когда «инфра» тихо подменяет индекс или system prompt.
Владелец: один человек, не «команда»
Один человек владеет eval. Не «все». Не «в стейджинге выглядело нормально».
Типичная схема:
| Роль | Что делает |
|---|---|
| Feature owner (инженер или PM) | формулирует «хороший вывод» в бизнес-терминах |
| ML/AI lead | валидирует методику и критерии sign-off |
| Release approver (EM / tech lead) | финальный gate; sign-off нельзя заменить фразой «в стейджинге вроде ок» |
Если перед релизом нельзя назвать владельца — это уже первый тревожный признак. Дальше спор «кто должен был поймать» почти гарантирован.
Три слоя минимального eval
Layer 1 — Regression (non-negotiable)
Фиксированный набор reference-кейсов с известным ожидаемым выводом. В наборе обязаны быть:
- edge-кейсы из прошлых инцидентов;
- кейсы из демо, которыми одобряли фичу (да — их тоже надо прогонять, но не только их);
- минимум один adversarial-вход на тип вывода: prompt injection, пустой ввод, malformed.
Критерий: все reference-кейсы в допустимом диапазоне. Любой новый фейл блокирует релиз.
Ключ: кейс рождается из инцидента. Поле origin в YAML — не бюрократия, а отличие regression set от demo-датасета. Входы взяты из того, что уже ломалось в проде, а не из слайда для совета директоров.
Пример из bundled-кейса rag-stale-corpus (репозиторий eval-harness):
id: rag-stale-corpus
origin: incident-2026-03
input:
query: "действующая ставка по тарифу X"
expected:
must_contain:
- "актуальный документ"
must_not_contain:
- "archived"
retrieved_chunks:
min_count: 1
must_contain:
- '"status": "active"'
must_not_contain:
- '"status": "archived"'
pass_criteria:
- no_archived_docs
- answer_grounded
must_contain / must_not_contain проверяют текст ответа. retrieved_chunks — сериализованные чанки (часто JSON). Needle надо подбирать под реальный формат вашего RAG: строка "status: archived" в лог-формате не найдётся в {"status": "archived"} и тест пройдёт «вхолостую».
В том же репозитории лежат adversarial-кейсы: пустой ввод, malformed, injection (ignore previous instructions…). Их смысл — зафиксировать отказ, а не «красивый ответ на атаку».
Layer 2 — Distribution check
Для RAG и классификаторов. Берёте 20–50 свежих реальных входов, прогоняете новую версию, сравниваете не с «идеалом», а с diff к снапшоту прошлого релиза:
- длина / формат ответа — выбросы часто значат сломанный промпт или смену схемы;
- retrieved-чанки — изменились ли, и почему;
- распределение confidence — сдвиг к краям = хрупкость.
В eval-harness флаги drift зашиты явно:
| Сигнал | Порог |
|---|---|
средняя длина output vs snapshot |
сдвиг > 30% |
std confidence |
рост > 20% |
среднее число chunks |
сдвиг > 30% |
| mean confidence к краям | < 0.35 или > 0.85 при заметном отличии от baseline |
Это не «научная истина порогов» — это рабочий стоп-кран: если распределение уехало без объяснения, релиз стопорится, пока кто-то не разберёт причину.
После принятого выката snapshot обновляют отдельно (--update-snapshot). В release pipeline этот флаг не передают — иначе baseline «подстроится» под деградацию.
Layer 3 — Human spot-check
Процесс, не код. Обязателен перед первым проддеплоем нового вида вывода: доменный человек читает 10–20 реальных выводов с конкретными вопросами:
- нет ли данных, к которым модель не должна иметь доступ;
- счёл бы эксперт это корректным;
- нет ли adversarial-эксплуатации.
На последующих релизах spot-check включается при смене модели, промпта, корпуса или после инцидента. Без этого слоя вы автоматизируете слепоту: regression зелёный, distribution «в допуске», а смысл ответа уже мусор.
Антипаттерн demo-grade
Demo grade — вывод, который выглядит корректным в контролируемых условиях и падает на реальном распределении входов.
Как это обычно уезжает в прод:
- фичу пилят и «тестируют» на примерах из брифа;
- бриф писали, чтобы показать capability, а не stress-test;
- стейджинг кормят курированным или синтетическим датасетом;
- ревьюер одобряет по «выглядит разумно»;
- прод открывает длинный хвост, которого в демо не было.
Симптомы, что вы релизите demo-grade:
- входы eval = примеры из питча или sprint review;
- до релиза фичу не гоняли на реальных production-входах;
- «eval» — это стейкхолдерское демо, а не математика;
- pass criteria формулируют после прогона («ну вроде ок»).
Фикс короткий и неприятный: demo dataset ≠ eval dataset. Демо показывает лучший случай. Eval меряет реальное распределение.
Артефакт: eval-harness в CI
Минимальный harness под Layer 1 + Layer 2: github.com/dobryakov/eval-harness. Python, YAML-кейсы, JSONL входов, snapshot, adapter interface.
Exit codes — чтобы gate был машиночитаемым:
| Exit code | Значение |
|---|---|
0 |
можно релизить |
1 |
упал Layer 1 (regression) |
2 |
упал Layer 2 (distribution drift) |
Быстрый старт:
pip install -r requirements.txt
python run_eval.py --layer regression
python run_eval.py --layer distribution --inputs inputs.jsonl
python run_eval.py --all --inputs inputs.jsonl
Свою модель / агента подключают через adapter.py:
def run(input: dict) -> dict:
return {
"output": str, # текст ответа
"chunks": list, # retrieved chunks
"confidence": float, # 0..1
}
В CI достаточно:
- run: pip install -r requirements.txt
- run: python run_eval.py --all --inputs inputs.jsonl
Каждый инцидент, который не должен повториться, становится permanent regression-кейсом в cases/. Иначе вы «починили» один раз и ждёте тот же баг под другим промптом.
Структура репозитория:
eval-harness/
run_eval.py
adapter.py
layers/
regression.py
distribution.py
cases/
*.yaml
snapshots/
latest.json
inputs.jsonl
Layer 3 в код не входит — и это правильно: human spot-check нельзя «закрыть зелёной галочкой скрипта».
Чеклист перед релизом, который меняет AI-вывод
- [ ] Owner назван: кто подписывает AI-output quality
- [ ] Regression set прогнан: все reference-кейсы зелёные
- [ ] Нет новых failure modes относительно прошлого релиза
- [ ] Distribution check сделан (для RAG / классификаторов): нет необъяснённого сдвига
- [ ] Human spot-check сделан, если: новая capability / смена модели / промпта / был инцидент
- [ ] Eval dataset ≠ demo dataset
- [ ] Pass criteria зафиксированы до прогона, не после
Ограничения метода
- Пороги distribution — эвристика. 30% / 20% ловят грубый drift; тонкую смысловую деградацию при стабильной длине ответа Layer 2 может пропустить — поэтому остаётся Layer 3.
- Пустой
originи кейсы «с потолка». Если regression собран из «красивых» примеров, вы снова получили demo-grade, только в YAML. - Vacuous pass на чанках. Неверный формат needle в
retrieved_chunksдаёт ложный зелёный. Сначала сериализуйте реальный chunk так же, как adapter, потом пишите кейс. - Нет owner — нет gate. Харнесс в CI без человека, который отвечает за интерпретацию красного, превращается в шум, который научатся обходить (
continue-on-error: true). - Snapshot после деградации.
--update-snapshotв момент «ну ладно, так и будет» закрепляет плохое как норму. - Не каждый деплой. Раздутый обязательный eval на любой hotfix убивает adoption; держите триггер «меняет ли это ввод/вывод модели».
Для кого
Для CTO, Head of AI и tech lead, у которых уже есть пилоты и вопрос совета директоров звучит не «есть ли у нас AI», а «что у нас с AI-качеством». Ответ «тестировали» здесь слабый. Ответ «вот regression и distribution diff к прошлому релизу» — рабочий.
Соседние слои той же дисциплины: периметр агента без опоры на system prompt (архитектурный least privilege) и онбординг скиллов (поведение ассистента до того, как оно разъехалось). Eval — gate на вывод модели; они — на доступ и на введение нового поведения.
Итог
Если eval совпадает с питчем — вы проверяете не продакшн, а презентацию. Разделите demo и eval, соберите regression из инцидентов, меряйте distribution diff, назначьте одного owner и не выпускайте фичу, пока три слоя не прошли по критерию — не потому что «выглядит хорошо».
Репозиторий: github.com/dobryakov/eval-harness