Просадка качества (eval regression)
В соло-разработке с ИИ нет QA-команды и нет dashboard-а с графиком метрик — зато есть eval-набор из 20–100 примеров, который может проверять «модель работает так, как мы хотим». Запускайте его автоматически и алёртите при просадке — это обнаружит:
- релиз новой версии модели, который сломал ваш промпт;
- провайдера, который тихо подменил endpoint на дешёвую модель;
- ваш собственный коммит, который зацепил format-инструкцию;
- деградацию качества RAG (см. vector-db).
Минимальный eval-runner
Заголовок раздела «Минимальный eval-runner»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 requestsprev = 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"}).textif 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-ссылке);
- модель / версия промпта / коммит — чтобы потом откатить.