Перейти к содержимому

Ночной прогон 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 defaultdict
by_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.