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

Просадка качества (eval regression)

В соло-разработке с ИИ нет QA-команды и нет dashboard-а с графиком метрик — зато есть eval-набор из 20–100 примеров, который может проверять «модель работает так, как мы хотим». Запускайте его автоматически и алёртите при просадке — это обнаружит:

  • релиз новой версии модели, который сломал ваш промпт;
  • провайдера, который тихо подменил endpoint на дешёвую модель;
  • ваш собственный коммит, который зацепил format-инструкцию;
  • деградацию качества RAG (см. vector-db).
import os, json, time, statistics, requests, anthropic
EVAL_FILE = "evals/qa.jsonl" # {"input":..., "expected":...}
THRESHOLD_DROP = 0.10 # 10% падение → алёрт
def grade(actual, expected):
# Самая дешёвая «оценка» — судья-LLM.
judge = anthropic.Anthropic().messages.create(
model="claude-haiku-4-5",
max_tokens=4,
messages=[{"role": "user", "content":
f"Ответ модели: {actual}\nЭталон: {expected}\n\n"
"Ответ корректен по смыслу? Ответь '1' или '0'."}],
)
return 1.0 if judge.content[0].text.strip().startswith("1") else 0.0
def run_eval():
cases = [json.loads(l) for l in open(EVAL_FILE)]
scores = []
for c in cases:
out = my_app.answer(c["input"]) # ваш реальный пайплайн
scores.append(grade(out, c["expected"]))
return statistics.mean(scores)
STATE = "/tmp/last_eval.json"
def main():
score = run_eval()
prev = (json.load(open(STATE)) if os.path.exists(STATE) else {}).get("score", score)
msg_lines = [f"Текущий score: {score:.3f}",
f"Прошлый score: {prev:.3f}"]
if prev - score >= THRESHOLD_DROP:
notify("📉 Eval-регрессия", "\n".join(msg_lines + [
"Возможные причины: смена модели, новый промпт, RAG-индекс.",
]), priority=9)
elif score - prev >= THRESHOLD_DROP:
notify("📈 Eval улучшение", "\n".join(msg_lines), priority=4)
json.dump({"score": score, "ts": time.time()}, open(STATE, "w"))
def notify(t, m, prio):
requests.post(f"{os.environ['NOTIFLY_URL']}/message",
params={"token": os.environ["NOTIFLY_TOKEN"]},
json={"title": t, "message": m, "priority": prio},
timeout=5)
if __name__ == "__main__":
main()

Запускать через cron / systemd-timer / GitHub Action раз в день, либо через scheduled cloud-функцию на YC.

Eval-runner полезен и как «дешёвый A/B перед раскаткой»: гоняем тот же набор на двух конфигурациях, шлём push с разницей.

score_a = run_eval(prompt=PROMPT_OLD, model="claude-sonnet")
score_b = run_eval(prompt=PROMPT_NEW, model="claude-sonnet")
notify(
"🧪 A/B prompts",
f"OLD: {score_a:.3f}\nNEW: {score_b:.3f}\nΔ: {score_b - score_a:+.3f}",
priority=5,
)

Это особенно ценно при работе с агентом — вы попросили Claude «упрости промпт», получили коммит, и хочется до мержа видеть, что качество не сел.

Anthropic / OpenAI делают релизы по средам — и это обычное время для тихого слома. Простая ловушка:

import requests
prev = open("/tmp/anthropic-models.txt").read() if os.path.exists("...") else ""
cur = requests.get("https://api.anthropic.com/v1/models",
headers={"x-api-key": KEY, "anthropic-version": "2023-06-01"}).text
if cur != prev:
notify("🆕 Anthropic models changed",
"Список моделей изменился. Если используете latest-alias, прогоните eval.",
priority=6)
open("/tmp/anthropic-models.txt", "w").write(cur)

То же самое для OpenAI (/v1/models) и любого провайдера с /models API.

  • старый/новый score;
  • сколько кейсов упало (топ-3 с input + expected + actual в gist-ссылке);
  • модель / версия промпта / коммит — чтобы потом откатить.