Одинаковый промпт даёт разные eval-результаты: фиксируем выдачу поиска
Команда сравнивает два промпта на одной сотне вопросов. Второй показывает больше ошибок, хотя изменение было небольшим. При разборе выясняется: между запусками обновили документы и перестроили индекс. Один вопрос получил новое исключение из регламента, другой — другой порядок источников. В такой проверке нельзя уверенно сказать, что именно ухудшил новый промпт: поменялись и инструкция, и материал для ответа.
Когда нужно оценить генерацию отдельно, сохраните результат retrieval и подавайте его обеим версиям. Это подготовленный набор контекстов для повторного прогона. Полная оценка приложения с живым поиском по-прежнему нужна, но отвечает на другой вопрос: насколько хорошо работает вся текущая связка. Разделение позволяет быстрее найти причину изменения.
Что именно сохранить
Для каждого вопроса нужны идентификатор случая, исходный текст вопроса, список найденных фрагментов в порядке передачи модели и полный текст каждого фрагмента. Ссылки только на текущие документы недостаточны: после обновления они укажут на другое содержание. Если приложение добавляет заголовки, цитатные метки или сокращает длинные фрагменты, сохраните результат после этих преобразований.
Возьмём запрос: «До какого дня принимают заявления на компенсацию?» Первая выдача содержит положение со сроком 15 ноября и пояснение про перенос выходного дня. Вторая после загрузки новой редакции содержит срок 20 ноября. Оба ответа могут соответствовать своим источникам. Сравнивать их по одному старому эталону и называть разницу регрессией промпта было бы неверно.
Пример ниже сохраняет небольшой объект выдачи в стабильное представление JSON и вычисляет отпечаток. Сортировка ключей делает порядок полей словаря несущественным, но порядок элементов списка остаётся значимым. Здесь он отражает порядок фрагментов в контексте. Технический идентификатор fixture не включает случайное время запуска, поэтому повторный запуск на тех же данных даёт тот же отпечаток.
import copy
import hashlib
import json
fixture = {
"case_id": "deadline-17",
"question": "До какого дня принимают заявления на компенсацию?",
"chunks": [
{"id": "policy-v2:p3", "text": "Заявления принимаются до 15 ноября."},
{"id": "policy-v2:p4", "text": "Если дата выходная, срок переносится на следующий рабочий день."},
],
}
def digest(value):
serialized = json.dumps(value, ensure_ascii=False, sort_keys=True,
separators=(",", ":"), allow_nan=False)
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
same = copy.deepcopy(fixture)
changed = copy.deepcopy(fixture)
changed["chunks"][0]["text"] = "Заявления принимаются до 20 ноября."
reordered = copy.deepcopy(fixture)
reordered["chunks"].reverse()
assert digest(same) == digest(fixture)
assert digest(changed) != digest(fixture)
assert digest(reordered) != digest(fixture)
print("same_input:", digest(same) == digest(fixture))
print("text_change:", digest(changed) != digest(fixture))
print("order_change:", digest(reordered) != digest(fixture))
Вывод показывает, что копия совпадает с исходником, а изменение текста и перестановка источников меняют отпечаток. Это разные причины, но обе меняют вход генератора. Отпечаток помогает обнаружить разницу; для объяснения откройте сами сохранённые объекты и сравните текст и порядок. Секретность или подлинность содержимого одним хешем не обеспечивается.
Как прогнать две версии честно
Сформируйте один набор сохранённых выдач, затем для каждого случая запустите старый и новый промпт с одинаковым контекстом. Рабочая инструкция модели: «Ответь на вопрос о крайнем сроке по приложенным положениям. Сохрани календарную дату и условие переноса; процитируй идентификаторы источников». Для проверки потребуются ожидаемые факты, а также правило обработки противоречий, если в контексте несколько редакций.
Рядом с ответом сохраняйте отпечаток fixture, версию промпта, модель и параметры вызова. Если два результата ссылаются на разные отпечатки, сравнение генерации нужно остановить и исправить вход. После этого можно разбирать, сохранил ли новый промпт срок и условие переноса. Пересоздавать ответ старого промпта на изменённой выдаче следует как отдельный эксперимент.
Полный поисковый прогон полезно вести параллельно в собственном отчёте. Там фиксируются текущие версия индекса, настройки поиска и найденные идентификаторы. Если качество упало только с живым retrieval, а на сохранённых контекстах обе версии работают одинаково, исследуйте поиск и подготовку источников. Если падает и фиксированный прогон, смотрите инструкцию, сборку контекста и генерацию.
Как не превратить набор в устаревший архив
Сохранённая выдача должна иметь дату создания и смысловую привязку к редакции правил, но эти служебные поля можно хранить снаружи хешируемого входа генератора. Когда бизнес-правило меняется, создавайте новую версию случая и эталона. Старую оставляйте для воспроизведения прежнего эксперимента. Не смешивайте вопросы по старым и новым правилам под одним идентификатором.
Контексты могут содержать закрытые документы. Храните их с теми же ограничениями доступа, что и исходники, и задайте срок хранения. Для разработки удобно готовить синтетические аналоги, сохраняя структуру проблемы: дата, исключение, порядок источников. Стабильная сериализация JSON помогает сравнивать входы, а право хранить конкретный текст определяется вашей системой доступа.
В платной программе «LLMOps & Evaluation: из песочницы в продакшн» вы будете собирать тестовые наборы, версионировать промпты и добавлять проверки качества в CI/CD. Следующая задача после этого примера — связать сохранённый вход с ответом и отчётом, чтобы решение о релизе можно было воспроизвести. Откройте программу курса и соберите собственный контур оценки.
Продолжить на практике
- LLMOps & Evaluation: из песочницы в продакшн ↗
Практический курс по LLMOps. Соберёте продакшн-контур для LLM: eval-фреймворк (LangSmith/Ragas/OpenAI Evals), quality-гейты в CI/CD, алёрты на дрейф, монитринг скорости и стоимости, управление промпт-версиями и безопасность. Плюс профессиональные блоки: synthetic data для тестов, LLM-based judges, отчётность для релиз-комитета и runbook’и инцидентов.
