Ночной прогон eval завершён — скоркарта
Отличие от eval-регрессии: та молчит, пока всё хорошо, и кричит только при просадке. Здесь — штатный ежедневный отчёт: прогон закончился, вот скоркарта. Такой push вы хотите видеть каждое утро, даже когда всё зелёное, — это ваш «git status по качеству модели» перед началом дня.
Один push с агрегатом: pass rate, дельта к вчерашнему прогону и явный список того, что регрессировало.
Раннер: гоняем набор и собираем скоркарту
Заголовок раздела «Раннер: гоняем набор и собираем скоркарту»State-файл хранит результаты прошлого прогона — из них считаем дельты и ловим кейсы, которые вчера проходили, а сегодня нет (регрессии).
import os, json, time, requests
EVAL_FILE = "evals/suite.jsonl" # {"id":..., "input":..., "expected":...}STATE = "/tmp/eval-suite-last.json"
def notify(title, msg, prio): requests.post(f"{os.environ['NOTIFLY_URL']}/message", params={"token": os.environ["NOTIFLY_TOKEN"]}, json={"title": title, "message": msg, "priority": prio}, timeout=5)
def run_case(case) -> bool: out = my_app.answer(case["input"]) # ваш реальный пайплайн return grade(out, case["expected"]) # 0/1, судья-LLM или точное сравнение
def main(): cases = [json.loads(l) for l in open(EVAL_FILE)] results = {c["id"]: run_case(c) for c in cases}
passed = sum(results.values()) total = len(results) rate = passed / total
prev = json.load(open(STATE)) if os.path.exists(STATE) else {} prev_results = prev.get("results", {}) prev_rate = prev.get("rate", rate)
# кейсы, которые были ok, а стали fail = регрессии regressed = sorted(cid for cid, ok in results.items() if not ok and prev_results.get(cid) is True) fixed = sorted(cid for cid, ok in results.items() if ok and prev_results.get(cid) is False)
delta = rate - prev_rate arrow = "▲" if delta > 0 else "▼" if delta < 0 else "=" lines = [ f"Pass: {passed}/{total} ({rate:.0%})", f"Δ к прошлому: {arrow} {delta:+.1%}", ] if regressed: lines.append("Регрессии: " + ", ".join(regressed[:8])) if fixed: lines.append("Починилось: " + ", ".join(fixed[:8]))
# приоритет по-разному: есть регрессии — заметнее, всё зелено — тихо prio = 6 if regressed else 3 notify(f"🧾 Eval-скоркарта {rate:.0%} {arrow}", "\n".join(lines), prio)
json.dump({"rate": rate, "results": results, "ts": time.time()}, open(STATE, "w"))
if __name__ == "__main__": main()priority=3 (тихий) для штатного «всё зелено» — вы прочтёте за завтраком без
звонка среди ночи. Если появились регрессии — priority=6, заметнее, но
всё равно это отчёт, а не пожар.
Запуск по расписанию
Заголовок раздела «Запуск по расписанию»Ночью через cron/systemd-timer на своём сервере:
0 4 * * * /usr/bin/python3 /opt/evals/run_suite.pyЛибо тем же scheduled-паттерном на YC, если пайплайн доступен из облака. Утренний push станет частью дайджеста.
Разбивка по категориям
Заголовок раздела «Разбивка по категориям»Если набор размечен по темам (RAG / формат / безопасность / математика), полезно дать pass rate по каждой — сразу видно, где просело:
from collections import defaultdictby_cat = defaultdict(lambda: [0, 0])for c in cases: ok = results[c["id"]] by_cat[c.get("category", "other")][0] += ok by_cat[c.get("category", "other")][1] += 1
cat_lines = [f" {cat}: {p}/{t}" for cat, (p, t) in sorted(by_cat.items())]# добавьте cat_lines в message скоркартыЧто положить в текст алёрта
Заголовок раздела «Что положить в текст алёрта»- pass rate и дробь
passed/total; - дельта к прошлому прогону (со стрелкой);
- список id регрессировавших кейсов (топ-8) — самое важное;
- разбивка по категориям и ссылка на полный отчёт в S3/gist.