Регрессии в промптах: сравнивать надо свойства, а не текст
Промпт — это код, у которого по умолчанию нет ни тестов, ни диффа. Разбираю, почему обычное сравнение ответов не работает и что класть в журнал прогона вместо него.
Промпт живёт в репозитории как строка. У строки есть история изменений, но нет ничего из того, что делает код кодом: ни тестов, ни способа увидеть, что именно сломала правка.
Первая мысль — сделать дифф ответов: прогнать до и после, сравнить. Мысль правильная ровно до момента, когда вы это попробуете.
Почему дифф ответов не работает
Прогоните один и тот же промпт дважды, ничего не меняя. Ответы разойдутся: переставлены абзацы, другой синоним, лишняя запятая. Побитового повтора не даёт даже нулевая температура, и причин тому несколько — от порядка суммирования чисел с плавающей точкой до состава пакета, в который попал ваш запрос на стороне провайдера.
Значит, обычный текстовый дифф покажет расхождения всегда. Прогон до правки и прогон после будут отличаться, даже если правки не было. Такой инструмент бесполезен: он не различает шум и сигнал, а значит не различает ничего.
Вывод неприятный, но он же и открывает выход: сравнивать надо не текст ответа, а его проверяемые свойства.
Свойство вместо текста
Свойство — это утверждение об ответе, которое проверяется программой и не зависит от формулировки.
| Плохой критерий | Свойство, которое его заменяет |
|---|---|
| ответ совпадает с эталонным | ответ разбирается как JSON по схеме |
| ответ выглядит правильным | поле amount совпадает с ожидаемым числом |
| модель не выдумала дату | значение date присутствует в исходном тексте |
| ответ по делу | упомянут хотя бы один из ключевых терминов |
| ответ не слишком длинный | длина в пределах заданного диапазона |
| формат не сломался | нет текста до первой открывающей скобки |
Свойства скучны, и в этом их достоинство: они дают бинарный исход, устойчивый к перефразированию. А бинарный исход на фиксированном наборе примеров — это ровно то, что можно сравнить между прогонами и посчитать статистически.
Здесь стоит остановиться на частой ошибке. Соблазн — попросить оценку у другой модели и сравнивать её баллы. Это не свойство: у модели нет устойчивой шкалы, её оценка сама плавает от прогона к прогону, и вы меняете один источник шума на другой, только менее прозрачный.
Что класть в журнал прогона
Свойства дают сигнал, но без контекста сигнал бесполезен: через неделю вы увидите, что стало хуже, и не сможете сказать, от чего.
Минимальный набор полей, который окупается почти сразу:
- хеш промпта — не текст, а именно хеш: он влезает в таблицу и мгновенно отвечает на вопрос «промпт вообще менялся?»;
- идентификатор модели с версией — не «claude», а полная строка; провайдеры обновляют модели под тем же именем, и это самая коварная из причин, потому что в вашем репозитории не изменилось ничего;
- параметры сэмплирования — температура, top-p, лимит токенов;
- идентификатор примера — чтобы сравнение было парным;
- результат по каждому свойству — булево, а не текст;
- сырой ответ — целиком, для разбора руками;
- число токенов на входе и выходе — стоимость это тоже регрессия;
- время до первого токена и до последнего.
Половина этих полей нужна не для метрики, а для ответа на вопрос «что изменилось». Самое частое объяснение внезапной просадки — не ваша правка, а обновление модели на стороне провайдера, и без записанной версии доказать это невозможно.
Порог, за которым это окупается
Признак простой и проверяется без раздумий: если вы боитесь трогать промпт — обвязка уже нужна.
Страх правки означает, что стоимость проверки выше стоимости изменения. В коде это состояние называют отсутствием тестов, и лечат одинаково.
До этого порога — если промпт правится раз в месяц и результат читает живой человек — вся описанная машинерия избыточна.
Условие несогласия
Всё написанное относится к промптам, результат которых уходит в код. Для черновиков, разовых задач и текстов, которые человек всё равно перепишет, свойства бессмысленны: единственное осмысленное свойство там — «мне нравится», а оно не проверяется программой.
Граница та же, что и везде в этой теме: кто следующий читатель ответа.
Чего я делать не стал бы
Не стал бы хранить только последний прогон. Ценность журнала появляется на третьем-четвёртом, когда становится видно тренд, а не точка.
Не стал бы прогонять весь набор на каждое сохранение файла. Это дорого и приучает игнорировать результат. Прогон перед слиянием ветки — достаточно.
Не стал бы делать свойства слишком строгими. Проверка «ответ содержит ровно эту фразу» сломается от синонима и приучит вас чинить тест, а не систему; проверка «упомянут срок возврата» переживёт перефразирование.
Открытый вопрос
Свойства хорошо ловят то, что вы уже придумали проверять. Регрессия, которая не попала ни в одно свойство, невидима полностью — и вероятность такого тем выше, чем дольше набор не обновлялся. Формального способа оценить эту слепоту у меня нет: чтобы измерить, чего не хватает в проверках, нужно знать, чего именно, а это и есть исходный вопрос.