Минимальный eval перед релизом: два автоматизируемых слоя в CI. Exit code 0 — можно релизить; non-zero — блок.
| Exit code | Значение |
|---|---|
0 |
Все проверки пройдены |
1 |
Layer 1 (regression): хотя бы один кейс упал |
2 |
Layer 2 (distribution): значимый drift относительно снапшота |
Layer 1 — Regression (non-negotiable). Фиксированный набор reference-кейсов с известным ожидаемым выводом: edge-кейсы прошлых инцидентов, одобренные демо-кейсы, adversarial-входы (prompt injection, пустой, malformed). Любой новый фейл блокирует релиз. Реализован в layers/regression.py.
Layer 2 — Distribution check. 20–50 свежих реальных входов через новую версию. Сравнение не с «идеалом», а с diff против снапшота прошлого релиза: длина ответа, число retrieved-чанков, сдвиг confidence (рост std >20% — сигнал хрупкости; сдвиг длины >30% — drift). Реализован в layers/distribution.py.
Layer 3 — Human spot-check. Обязателен перед первым проддеплоем нового вида вывода: доменный эксперт читает 10–20 реальных выводов. Это процесс, не код; в харнесс не входит.
Owner. Один человек подписывает sign-off на релиз. Если владельца нельзя назвать до выката — это уже красный флаг.
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После успешного релиза обновите baseline распределения:
python run_eval.py --layer distribution --inputs inputs.jsonl --update-snapshotРеализуйте контракт в adapter.py:
def run(input: dict) -> dict:
# input — поле input из YAML-кейса или строка JSONL
# return:
return {
"output": str, # текст ответа
"chunks": list, # retrieved chunks (любая структура)
"confidence": float, # 0..1
}Демо-стаб в репозитории проходит bundled regression-кейсы; замените его на вызов API, локальной модели или агента.
- Создайте
cases/<short-id>.yaml:
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_groundedmust_contain/must_not_contain— подстроки в тексте ответа (output).retrieved_chunks— подстроки в сериализованных чанках (dict/list→ JSON, как вadapter.py). Подбирайте needle под реальный формат вашего RAG, иначе тест пройдёт vacuously. Пример для{"doc_id": "…", "status": "active"}: в кейсе указано'"status": "active"', а не"status: archived"(лог-формат) — последнее в JSON-чанках не встретится и ничего не проверит.min_count/max_count— границы числа чанков;must_contain/must_not_contain— хотя бы один / ни один чанк;each_must_contain— в каждом чанке.
pass_criteria— документация для ревью; в CI не исполняется.- Прогоните
python run_eval.py --layer regressionлокально, затем в CI.
Каждый инцидент, который не должен повториться, должен стать permanent regression-кейсом.
- inputs.jsonl — по одному JSON-объекту на строку (те же поля, что передаёте в
adapter.run). - snapshots/latest.json — метрики прошлого релиза; создаётся/обновляется флагом
--update-snapshot.
Флаги drift (Layer 2):
- средняя длина
outputизменилась >30% относительно снапшота; confidencestd вырос >20% (ответы стали «рванее» по уверенности);- среднее число
chunksизменилось >30%; - mean confidence сместился к краям (<0.35 или >0.85) при заметном отличии от baseline.
- run: pip install -r requirements.txt
- run: python run_eval.py --all --inputs inputs.jsonlНе передавайте --update-snapshot в release pipeline — только после выката, когда метрики приняты.
eval-harness/
run_eval.py
adapter.py
layers/
regression.py
distribution.py
cases/
*.yaml
snapshots/
latest.json
inputs.jsonl # пример; подставьте свои 20–50 входов
requirements.txt