Самое важное из мира AI — в канале MAX AgentLabОтдельные разборы, инструменты и практические схемы Читайте Agent Lab в Telegram Разборы, кейсы и новости о практических AI-агентах без лишней воды.

Как отличить регресс image-модели от смены конфигурации

Как отличить регресс image-модели от смены конфигурации
Temporary fallback cover; replace in editorial pass.

Уровень: продвинутый · Время чтения: до 12 минут · Обновлено:

Типичная ситуация: кто-то пишет «вчера лица получались похожими, а сегодня — нет» или «модель перестала слушаться промпта». По одной такой жалобе нельзя понять, что изменилось. Это может быть сама модель, интерфейс запуска (другие параметры по умолчанию, автоматическое переписывание промпта), аккаунт (тариф, уровень модерации, квоты) или исходный запрос — его могли «чуть-чуть поправить».

В статье разберём, как собрать regression harness — небольшой стенд, который прогоняет фиксированный набор входов, записывает полный отпечаток конфигурации и считает метрики по отдельности: лицо, референс, инструкция, safety-отказы и технические ошибки. Тогда вопрос «регресс или смена конфигурации?» превращается в сравнение двух журналов.

Почему разовые жалобы ничего не доказывают

Генерация изображений стохастична: даже с одними и теми же входами результат разбросан. Если сравнить одну картинку «до» с одной «после», вы сравниваете два случайных образца. Кроме того, сигналы смешиваются: если модерация отклоняет часть запросов, а интерфейс отдаёт заглушку или пустой ответ, пользователь видит это как «качество упало».

Значит, нужны три вещи:

  1. Фиксированные входы — чтобы исключить изменение запроса.
  2. Журнал конфигурации — чтобы увидеть изменение модели, параметров или аккаунта.
  3. Раздельные метрики с повторениями — чтобы отделить качество от отказов и сбоев и не принимать шум за тренд.

Четыре слоя, которые могут измениться

СлойЧто меняетсяКак это заметить
Модельверсия, снапшот, алиас вида «latest»идентификатор модели в ответе API, если он там есть; дата прогона
Интерфейс запускапараметры по умолчанию, размер, качество, переписывание промпта, версия SDK или UIполный набор отправленных параметров, версия клиента
Аккаунттариф, проект, ключ, регион, настройки модерациихеш идентификатора проекта или ключа (не сам ключ), заголовки ответа
Запростекст промпта, референсы, их сжатие и разрешениеSHA-256 промпта и каждого файла референса

Главное правило: метрики сравнивают только после того, как сравнили конфигурацию. Если отпечаток конфигурации отличается, падение метрик сначала объясняют конфигурацией.

Шаг 1. Зафиксируйте набор входов

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

harness/
  cases/
    v1/
      cases.jsonl          # один кейс на строку
      refs/
        face_01.png
        product_02.png
  runs/                    # результаты прогонов, только дозапись
  manifest.sha256          # хеши всех входов

Пример строки cases.jsonl:

{"id": "face-portrait-01", "prompt": "Студийный портрет человека с референса, нейтральный серый фон, мягкий свет слева", "refs": ["refs/face_01.png"], "checks": ["серый фон", "свет слева", "один человек в кадре"], "metrics": ["face", "instruction"]}
{"id": "product-02", "prompt": "Товар с референса на деревянном столе, вид сверху", "refs": ["refs/product_02.png"], "checks": ["деревянный стол", "вид сверху"], "metrics": ["reference", "instruction"]}

Зафиксируйте хеши входов — это безопасная операция, она только читает файлы:

cd harness/cases/v1
find . -type f | sort | xargs sha256sum > ../../manifest.sha256
# перед каждым прогоном:
sha256sum -c ../../manifest.sha256 --quiet && echo "входы не менялись"

Если проверка упала, дальше не идём: изменился запрос, и сравнение с прошлым прогоном некорректно.

Шаг 2. Ведите журнал конфигурации

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

{
  "run_id": "2026-10-03T09-00-00Z",
  "case_set": "v1",
  "inputs_manifest_sha256": "…",
  "model_requested": "имя-модели-как-в-запросе",
  "model_reported": "идентификатор из ответа, если API его возвращает",
  "params": {"size": "1024x1024", "quality": "…", "n": 1, "seed": null},
  "client": {"sdk": "имя@версия", "harness_git_sha": "…"},
  "account": {"project_hash": "a1b2c3d4", "key_hash": "e5f6a7b8"},
  "prompt_rewrite": "unknown"
}

Пара практических замечаний:

Шаг 3. Прогон с повторениями

Ниже — каркас прогона на Python. Функцию generate вы реализуете под своего провайдера; harness ничего не знает о конкретном API. Каждый вызов повторяется N раз, и каждый исход классифицируется в одну из корзин: ok, refusal, error.

import hashlib, json, time, pathlib

N_REPEATS = 5  # подберите под бюджет; один образец ничего не доказывает

def sha(s: bytes) -> str:
    return hashlib.sha256(s).hexdigest()

def fingerprint(cfg: dict) -> str:
    return sha(json.dumps(cfg, sort_keys=True, ensure_ascii=False).encode())[:16]

def generate(prompt, ref_paths, params):
    """Подключите своего провайдера. Верните dict:
    {"status": "ok"|"refusal"|"error", "image_bytes": ..., "meta": {...}, "error": "..."}"""
    raise NotImplementedError

def run(case_file, cfg, out_dir):
    fp = fingerprint(cfg)
    out = pathlib.Path(out_dir) / f"{cfg['run_id']}.jsonl"
    out.parent.mkdir(parents=True, exist_ok=True)
    with open(case_file, encoding="utf-8") as f, open(out, "a", encoding="utf-8") as log:
        for line in f:
            case = json.loads(line)
            for i in range(N_REPEATS):
                t0 = time.time()
                try:
                    r = generate(case["prompt"], case["refs"], cfg["params"])
                except Exception as e:  # сбой клиента или сети — тоже данные
                    r = {"status": "error", "error": type(e).__name__}
                rec = {"case": case["id"], "rep": i, "fp": fp, "cfg": cfg,
                       "status": r["status"], "error": r.get("error"),
                       "meta": r.get("meta", {}), "latency_s": round(time.time() - t0, 2)}
                if r.get("image_bytes"):
                    img = out.parent / f"{cfg['run_id']}_{case['id']}_{i}.png"
                    img.write_bytes(r["image_bytes"])
                    rec["image"] = img.name
                    rec["image_sha256"] = sha(r["image_bytes"])
                log.write(json.dumps(rec, ensure_ascii=False) + "\n")

Различайте отказ и ошибку на уровне кода. Отказ — это явный ответ модерации (специальный код, флаг или текст отказа от провайдера). Ошибка — всё остальное: тайм-ауты, 5xx, лимиты, пустой или повреждённый файл. Если смешать их, рост лимитов из-за смены тарифа будет выглядеть как «модель стала строже».

Шаг 4. Раздельные метрики

Пять метрик считаются независимо, и каждая — только на тех кейсах, где она осмысленна (поле metrics в кейсе).

МетрикаЧто измеряетКак считатьЗнаменатель
Лицосохранение идентичности человека с референсакосинусное сходство эмбеддингов лица (модель распознавания лиц, которую вы уже используете) между референсом и результатом; отдельно — доля кадров, где лицо не найденоуспешные генерации
Референссохранение объекта, стиля, композицииперцептивное сходство или эмбеддинги изображений; при спорных случаях — ручная разметкауспешные генерации
Инструкциявыполнение явных требований промптадоля выполненных пунктов checks; проверяет человек или мультимодальная модель-оценщик с фиксированной версией и промптомуспешные генерации
Safety-отказыдоля запросов, отклонённых модерациейcount(refusal) / все попыткивсе попытки
Технические ошибкисбои, не связанные с содержаниемcount(error) / все попытки, с разбивкой по типувсе попытки

Обратите внимание на знаменатели. Метрики качества считаются только по успешным генерациям, иначе отказы и ошибки «утягивают» качество вниз и маскируют свою истинную причину.

Оценщик сам является частью конфигурации. Если инструкцию проверяет модель, её версия и промпт оценщика тоже входят в журнал. Иначе вы получите регресс оценщика, а не генератора.

Каркас агрегации по корзинам:

import json, sys
from collections import Counter, defaultdict

def summarize(path):
    by_fp = defaultdict(Counter)
    for line in open(path, encoding="utf-8"):
        r = json.loads(line)
        by_fp[r["fp"]][r["status"]] += 1
        if r["status"] == "error":
            by_fp[r["fp"]]["error:" + str(r.get("error"))] += 1
    for fp, c in by_fp.items():
        total = c["ok"] + c["refusal"] + c["error"]
        print(fp, f"n={total}",
              f"refusal={c['refusal']/total:.2%}",
              f"error={c['error']/total:.2%}",
              {k: v for k, v in c.items() if k.startswith("error:")})

summarize(sys.argv[1])

Метрики лица, референса и инструкции добавляются отдельным скриптом, который читает сохранённые изображения и дописывает оценки в отдельный файл. Сырые результаты при этом не перезаписываются.

Шаг 5. Сначала дифф конфигурации, потом метрики

Чтобы сравнить два прогона, начните с отпечатков:

jq -c '.cfg | del(.run_id)' runs/A.jsonl | sort -u > /tmp/cfg_a.json
jq -c '.cfg | del(.run_id)' runs/B.jsonl | sort -u > /tmp/cfg_b.json
diff <(jq -S . /tmp/cfg_a.json) <(jq -S . /tmp/cfg_b.json) || echo "конфигурация различается"

Дальше разбор идёт по таблице решений:

НаблюдениеВероятная причинаСледующее действие
Хеши входов не совпадаютизменился запросвернуть исходные входы и прогнать заново
Отпечаток различается в params или clientсмена интерфейса запускапрогнать новую модель со старыми параметрами явно
Отпечаток различается в accountсмена аккаунта или проектапрогнать на обоих аккаунтах одновременно
Конфигурация та же, растут только ошибкиинфраструктура, лимитысмотреть разбивку по типам ошибок и время прогона
Конфигурация та же, растут только отказыизменение модерациисравнить, на каких кейсах появились отказы
Конфигурация та же, падают метрики качества на нескольких прогонахкандидат в регресс моделиповторить прогон в другое время, проверить стабильность оценщика

Если model_reported отличается при одинаковом model_requested, это сильный сигнал смены модели за алиасом. Но учтите, что не все API такой идентификатор возвращают.

Проверка результата

Перед тем как доверять стенду, проверьте сам стенд:

Типовые ошибки

Ограничения

Что дальше

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