Проблема: промпт меняет всё сразу

В обычном коде функция изолирована: поправил calculate_tax() — затронул расчёт налога, остальное не тронуто. С промптом иначе: это глобальная инструкция для модели, и любая правка меняет поведение агента целиком и непредсказуемо. Добавил строчку «отвечай кратко» — и агент внезапно стал обрезать важные детали в совершенно других сценариях.

Отсюда специфика LLM-регрессий:

  • Нелокальность. Правка «для одного кейса» задевает все остальные — нет изоляции, как у функций.
  • Незаметность. Регрессия не роняет систему с ошибкой — агент продолжает бодро отвечать, просто хуже. Без проверки её не видно.
  • Накопление. Каждая правка чинит одно и чуть ломает другое; без защиты качество медленно «эродирует» от релиза к релизу.
ℹ️ Regression testing = «не сломал ли я то, что работало»

Термин из классической разработки: тесты, которые проверяют, что новое изменение не поломало старую функциональность. В отличие от A/B (стало ли в среднем лучше), regression фокусируется на конкретных известных кейсах, которые обязаны продолжать работать. A/B смотрит вперёд («лучше?»), regression — назад («не хуже на том, что уже умели?»). Оба нужны: A/B — про прогресс, regression — про сохранность.

Golden dataset

Сердце regression testing — golden dataset: курируемый набор эталонных примеров {вход → ожидаемое поведение}, представляющих то, что агент обязан уметь. Это «контрольный список функциональности», зафиксированный в данных. При каждом изменении промпта агент прогоняется на нём, и если что-то из закреплённого сломалось — тест падает.

100%
колёсико — масштаб  ·  зажать и тянуть — перемещение
Regression gate при изменении промпта Правка промпта новая версия Golden dataset закреплённые кейсы приветствие → вежливо ✓ возврат → политика возврата ✓ пустой ввод → переспросить ✗ жалоба → эскалация ✓ Сравнение judge / semantic, не == GATE 1 сломан → CI ПАДАЕТ 🔴 Раньше проходило, теперь нет = РЕГРЕССИЯ → блокируем релиз golden dataset = «контрольный список того, что агент ОБЯЗАН уметь» пополняется КАЖДЫМ найденным в проде багом (чтобы не повторился)

Что кладут в golden dataset:

  • Ключевые сценарии — то, что агент должен уметь всегда (основные типы запросов).
  • Edge cases — пустой ввод, очень длинный, на другом языке, провокации, попытки джейлбрейка.
  • Прошлые баги — каждый найденный в проде дефект превращается в golden-кейс, чтобы никогда не повториться (это и есть суть regression: «не наступить на те же грабли»).
  • Критичные для бизнеса кейсы — то, где ошибка дороже всего (юридические формулировки, безопасность).
✅ Каждый прод-баг → новый golden-кейс

Главная практика ведения набора: нашёл баг в проде (через трейсы/жалобы) — не просто почини, а добавь этот случай в golden dataset. Тогда любая будущая правка, которая воскресит этот баг, будет поймана автоматически. Так набор растёт вместе с продуктом и становится всё надёжнее — это дистиллированная история всех ошибок, которые вы обязались не повторять. Trace-to-dataset из уроков LangSmith/Langfuse — ровно этот механизм.

Почему «точное совпадение» не работает

Соблазн: сравнивать выход агента с эталоном через ==, как в обычных unit-тестах. Для LLM это ломается сразу: «Столица Франции — Париж» и «Париж является столицей Франции» — семантически идентичны, но == вернёт False. Строгое сравнение даст лавину ложных срабатываний, тест станет бесполезным шумом.

Поэтому в regression для LLM используют нестрогое (semantic) сравнение — от дешёвого к гибкому:

Способ сравненияКогда подходит
Точное / regexСтруктурированный выход: JSON-поле, число, статус, вызванный инструмент
Проверка свойств«Содержит слово X», «есть ссылка на политику», «нет запретных тем»
Semantic similarityЭмбеддинги: близок ли смысл к эталону (порог косинуса)
LLM-as-judge«Совпадает ли по смыслу с эталоном?» — самое гибкое (урок про судью)

На практике — смесь: детерминированные проверки для формальных требований (дёшево, стабильно) + judge/semantic для смысла. Это ровно палитра scorers из урока про offline-метрики, применённая к фиксированному эталонному набору.

⚠️ Судья в regression тоже недетерминирован

Раз сравнение опирается на LLM-judge, он наследует его свойства: небольшой разброс и стоимость (уроки про judge). Следствия: держи судью на temperature=0, а «падение» одного кейса при пограничном score перепроверяй, прежде чем блокировать релиз, — чтобы regression-гейт не флапал из-за шума самого судьи. И проверяй формальное кодом там, где можешь, оставляя judge только для смысла.

Порог прохождения

Ключевой вопрос: когда считать, что regression-набор «упал»? Два режима, и выбор зависит от критичности кейсов.

  • Строгий (per-case gate). Каждый golden-кейс обязан пройти; хоть один сломан → релиз блокируется. Для критичных наборов (безопасность, юридические, «баги, которые не должны вернуться»).
  • Пороговый (aggregate). Набор проходит, если доля успешных ≥ порога (например, ≥ 95%). Допускает мелкие колебания на больших, менее критичных наборах.
  • Без снижения (no-regression). Новая версия не должна набрать меньше предыдущей: сравниваешь score билда с baseline и падаешь при просадке.
✅ Разделяй наборы по критичности

Практичный приём: держи критический набор (мало кейсов, строгий per-case gate — безопасность, ключевая функциональность, воскресшие баги) и расширенный набор (много кейсов, пороговый режим — общее качество). Тогда мелкий шум на большом наборе не блокирует релиз, но провал в критичном кейсе останавливает всё. Не суй всё в один режим — либо задолбают ложные падения, либо пропустишь важное.

Интеграция в CI/CD

Regression testing работает, только если запускается автоматически при каждом изменении — иначе про него забудут ровно тогда, когда он нужен. Место — CI/CD: тот же гейт, что для обычных тестов, но для качества LLM. Промпт меняется через PR → CI гоняет golden dataset → красный тест блокирует мёрж.

Regression-гейт в CI (концептуально, pytest-стиль)
python
import json, pytest

GOLDEN = json.load(open("golden_dataset.json"))   # закреплённые кейсы

@pytest.mark.parametrize("case", GOLDEN)
def test_no_regression(case):
    output = run_agent(case["input"])              # прогоняем НОВУЮ версию промпта

    # нестрогое сравнение (НЕ ==): свойства + judge
    if case["type"] == "exact":
        assert case["expected"] in output          # формальное — кодом
    else:
        score = llm_judge_similarity(output, case["expected"])  # смысл — судьёй (temp=0)
        assert score >= case.get("threshold", 0.8), \
            f"Регрессия в кейсе '{case['id']}': {output!r}"

# в CI: промпт меняется в PR → эти тесты гоняются → красный блокирует мёрж
# критический набор — per-case gate; расширенный — порог ≥95% успешных

Это замыкает весь раздел оценки в единый защитный контур: offline-метрики и scorers дают способ измерить кейс, judge — сравнить нестрого, golden dataset фиксирует эталон, A/B проверяет прогресс на трафике, а regression в CI не даёт откатиться назад. Вместе — это машина, которая позволяет менять промпты смело, но безопасно.

⚠️ Учитывай стоимость и flakiness прогона

Regression-набор гоняет реальные LLM-вызовы — это токены (деньги) и время на каждый PR, плюс возможная нестабильность (модель/судья недетерминированы). Держи набор компактным и репрезентативным, кэшируй где можно, запускай тяжёлый расширенный набор реже (ночью/перед релизом), а быстрый критический — на каждый PR. Иначе CI станет медленным и дорогим, и его начнут отключать — что убивает всю защиту.

Типичные ошибки

❌ Ошибка 1: сравнение через ==

Точное совпадение для свободного текста даёт лавину ложных падений — семантически верные ответы «не проходят». Используй проверку свойств / semantic / judge, а == только для структурированного выхода.

❌ Ошибка 2: не пополнять набор из прод-багов

Если найденные баги не превращаются в golden-кейсы, они возвращаются снова и снова. Каждый прод-дефект → новый кейс, иначе regression не выполняет свою главную функцию.

❌ Ошибка 3: гонять вручную «когда вспомню»

Ручной regression не запускается ровно перед тем релизом, где нужен. Автоматизируй в CI на каждое изменение промпта — только тогда он ловит регрессии.

❌ Ошибка 4: один жёсткий порог на всё

Строгий per-case gate на огромном наборе → флап из-за шума судьи; мягкий порог на критичном → пропустишь опасный баг. Разделяй наборы по критичности с разными режимами.

Шпаргалка

Regression testing — всё в одном месте
text
REGRESSION = «не сломал ли новый промпт то, что РАБОТАЛО» (назад) ≠ A/B (вперёд)
  промпт = ГЛОБАЛЬНАЯ настройка: правка меняет всё поведение непредсказуемо
  регрессия НЕзаметна (агент отвечает, просто хуже) → без теста не видно

GOLDEN DATASET = курируемый набор {вход → ожидаемое поведение}, «что агент ОБЯЗАН уметь»
  кладём: ключевые сценарии · edge cases (пустой/длинный/джейлбрейк) ·
          ПРОШЛЫЕ БАГИ (чтобы не вернулись) · критичные для бизнеса
  ★ каждый прод-баг → новый golden-кейс (trace-to-dataset)

СРАВНЕНИЕ (НЕ ==! семантика):
  точное/regex — структурир. выход (JSON/число/tool) · проверка свойств («содержит X»)
  semantic similarity (эмбеддинги) · LLM-judge («совпадает по смыслу?») — гибче всего
  ⚠️ судья недетерминирован → temp=0, перепроверять пограничные, формальное — кодом

ПОРОГ:
  строгий per-case — критичные (хоть 1 сломан → блок) · пороговый ≥95% — большие наборы
  no-regression — не ниже baseline
  ★ РАЗДЕЛЯЙ: критический набор (строго) + расширенный (порог)

CI/CD: автоматом на КАЖДОЕ изменение промпта (PR → golden → красный блокирует мёрж)
  ⚠️ стоит токенов/времени + flaky → набор компактный, тяжёлый реже, критический на каждый PR

СВЯЗЬ: offline (измерить) + judge (сравнить) + golden (эталон) + A/B (прогресс) +
       regression в CI (не откатиться) = менять промпты СМЕЛО, но БЕЗОПАСНО

ГРАБЛИ: == сравнение · не пополнять из багов · ручной прогон · один порог на всё

Практическое задание

Задание: собери regression-защиту

  1. Для агента из модулей 03–05 собери мини golden dataset из 8–10 кейсов: ключевые сценарии, 2–3 edge case, 1 «прошлый баг». Для каждого — вход и ожидаемое поведение.
  2. Для каждого кейса выбери способ сравнения (точное / свойства / semantic / judge) и обоснуй, почему == тут не годится.
  3. Испорти промпт так, чтобы сломать один кейс, но не другие. Ловит ли твой набор именно эту регрессию? Какие кейсы прошли?
  4. Раздели набор на критический (per-case gate) и расширенный (порог ≥ 90%). Какие кейсы куда и почему?
  5. Напиши псевдокод pytest-гейта, который блокирует «мёрж» при регрессии. Как отличишь настоящую регрессию от шума судьи?
  6. Прикинь стоимость: набор из 200 кейсов на каждый PR — это сколько LLM-вызовов? Как удешевить (что гонять на каждый PR, что реже)?
  7. Со звёздочкой: возьми «баг» из прод-трейса (LangSmith/Langfuse), преврати его в golden-кейс и убедись, что регрессия по нему теперь ловится автоматически.

Что дальше

Раздел «Оценка качества LLM-систем» закрыт: ты умеешь мерить качество offline и человеком, масштабировать оценку судьёй, сравнивать версии через A/B и защищать от деградации regression-набором. Дальше — раздел «Тестирование агентов»: как тестировать агента как программу (unit, integration, оценка траектории), а не только как «генератор ответов». Golden dataset из этого урока станет там evaluation dataset.