Как воспроизвести плохой ответ по трассе и eval
Узнайте, как воспроизвести плохой ответ по trace и eval, сохранив версии промптов, модель, RAG-контекст, инструменты и параметры генерации.

Плохой ответ нельзя исправить по одному скриншоту чата. Скриншот показывает симптом, но скрывает причину: другой системный промпт, иной маршрут модели, устаревший чанк из поиска, неудачный вызов инструмента или параметр генерации, который кто-то поменял в конфигурации.
Связка eval с точной трассой превращает жалобу «модель ответила неправильно» в проверяемый объект. У него есть вход, контекст, ход выполнения, оценка, версия критериев и воспроизводимый пакет. Без этой связки команда спорит о правдоподобных версиях. С ней она сравнивает факты.
Eval должен указывать на конкретный запуск
Eval не является свойством промпта, модели или датасета сам по себе. Он относится к конкретному выполнению приложения, в котором пользователь задал вопрос, система нашла документы, агент вызвал инструменты и модель сформировала ответ.
Это различие регулярно размывают. Команда заводит таблицу с колонками question, answer, score, а потом пытается понять, почему ответ получил ноль. В таблице нет того, что модель реально увидела. Туда редко попадают системные инструкции, история диалога, порядок найденных чанков, состояние инструментов и правила маршрутизации. В лучшем случае аналитик угадает причину. В худшем он исправит не тот компонент и создаст новую регрессию.
У каждой оценки должны быть две ссылки:
trace_idна корневую трассу пользовательского запроса;target_span_idна шаг, который оценивали: итоговый ответ, поиск, вызов инструмента или классификацию.
Корневая трасса отвечает на вопрос «что произошло в обработке запроса». Целевой span отвечает на другой вопрос: «что именно получило эту оценку». Одна пользовательская сессия может включать несколько генераций. Например, агент сначала планирует действия, затем запрашивает внутренний каталог, потом пишет клиенту ответ. Метка качества финального ответа не должна приклеиваться к планировочному вызову модели.
Хорошая запись оценки выглядит как событие, которое можно открыть отдельно от интерфейса наблюдаемости. Она содержит имя оценщика, его неизменяемую версию, тип оценки, результат, объяснение и ссылки на трассу. Если оценку дал человек, сохраните роль разметчика и причину решения. Если оценку дал LLM-судья, сохраните модель судьи, версию его рубрики и входные данные, поданные судье.
Документация Phoenix точно формулирует полезное разделение: трассы говорят, что происходило в запуске, а eval добавляет повторяемый сигнал о качестве. Это верно, но на практике нужен еще один шаг: оценка должна ссылаться на полный набор причинных артефактов запуска. Иначе она годится для графика качества, но плохо годится для инженерной работы.
Единицей расследования является run, а не диалог
Run, или выполнение, это неизменяемая запись одной попытки приложения обработать один запрос. Диалог может длиться часами и содержать десятки сообщений. Трасса может охватывать только один HTTP-запрос. Для воспроизведения нужен объект между ними: конкретная обработка конкретного пользовательского сообщения.
Присвойте run_id на границе приложения, до первого обращения к поиску или модели. Пробросьте его в span-ы, логи аудита, запись eval и очередь фоновых задач. trace_id остается техническим идентификатором телеметрии, а run_id становится идентификатором предметной операции. Так проще связать повторные попытки, асинхронные шаги и несколько трасс одного запроса.
Например, пользователь спрашивает: «Можно ли досрочно закрыть депозит без потери процентов?» Приложение нормализует запрос, ищет регламент, вызывает инструмент проверки продукта и формирует ответ. Если ответ неверен, расследованию нужны не «сообщения чата за день», а один run со всеми его дочерними операциями.
Не путайте повторную попытку с тем же run. Если сеть оборвалась после запроса к модели и клиент отправил запрос заново, создайте новый run_id, но сохраните parent_run_id или retry_of_run_id. Иначе статистика смешает технический сбой с независимым пользовательским запросом, а eval начнет считать две попытки одним наблюдением.
Полезный минимум идентификаторов:
run_idдля бизнес-операции;trace_idдля дерева телеметрии;span_idдля отдельного действия;request_idдля внешнего HTTP-вызова;conversation_idдля сессии, если история влияет на ответ.
Эти значения не заменяют друг друга. Когда команда кладет везде только conversation_id, она теряет границы конкретной попытки. Когда хранит только trace_id, ей трудно сопоставить запуск с записью продукта, обращением в поддержку или отложенной задачей.
Схема должна хранить фактические входы, а не намерения разработчика
Намерение разработчика звучит так: «мы используем промпт поддержки, модель X, поиск по базе знаний и температуру 0,2». Фактический запуск может выглядеть иначе. Маршрутизатор выбрал другую доступную модель, промпт подставился по актуальной метке, поиск применил фильтр по подразделению, а SDK передал параметр max_tokens из переменной окружения.
В схеме нужны фактические значения, которые ушли на каждый внешний или логический шаг. Не восстанавливайте их потом из репозитория, конфигурации и памяти дежурного инженера. Эти источники уже могли измениться.
Ниже минимальный пакет для финальной генерации. Поля можно разделить между хранилищем трасс, каталогом артефактов и базой eval, но связи между ними должны быть прямыми.
{
"run_id": "run_01JQ8K7C4V",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"started_at": "2026-07-23T14:18:09Z",
"application": {
"name": "deposit-assistant",
"release": "2026.07.23.4",
"environment": "production"
},
"prompt": {
"name": "deposit-answer",
"version_id": "prv_8d7f5a",
"template_sha256": "2d3a...91c4",
"rendered_messages_ref": "artifact://runs/run_01JQ8K7C4V/messages.json"
},
"generation": {
"requested_model": "reasoning-model",
"resolved_model": "provider/model-revision",
"provider": "provider-name",
"temperature": 0.2,
"top_p": 1.0,
"max_output_tokens": 700,
"seed": 18421,
"response_format": "text"
},
"retrieval_ref": "artifact://runs/run_01JQ8K7C4V/retrieval.json",
"tool_calls_ref": "artifact://runs/run_01JQ8K7C4V/tools.json",
"output_ref": "artifact://runs/run_01JQ8K7C4V/output.json"
}
Значение requested_model показывает выбор приложения. resolved_model показывает то, что реально обработало запрос. Это разные поля даже в системе без сложной маршрутизации: провайдер может возвращать ревизию, отличающуюся от логического имени из запроса.
Разделяйте ссылку на артефакт и сам текст. Небольшие безопасные метаданные удобно искать прямо в трассе. Полный prompt, найденные документы и ответы инструментов часто велики и чувствительны. Их разумнее хранить в защищенном хранилище с контролем доступа, контрольной суммой и сроком удаления. Трасса должна содержать достаточно, чтобы найти артефакт и проверить, что его не подменили.
OpenTelemetry в GenAI semantic conventions описывает именно такие категории: входные и выходные сообщения, имя провайдера, запрошенную и возвращенную модель, лимиты токенов, документы поиска, аргументы и результаты вызовов инструментов. Стандарт полезен как общий словарь. Не пытайтесь втиснуть в него все доменные детали. Добавляйте собственные атрибуты для версии индекса, политики доступа, идентификатора шаблона и причины маршрутизации.
Версия промпта должна быть неизменяемой
Название промпта не воспроизводит промпт. Метка production не воспроизводит промпт. Даже Git-коммит не всегда воспроизводит промпт, если шаблон подгружается из отдельного сервиса, а переменные собираются из нескольких конфигураций.
Храните как минимум три вещи: логическое имя, неизменяемый version_id и контрольную сумму от точного шаблона. Для расследования сохраняйте также отрендеренные сообщения после подстановки переменных. Версия объясняет, какой шаблон использовался. Отрендеренные сообщения объясняют, что именно увидела модель.
Есть важная граница. Не стоит писать в один идентификатор и шаблон, и пользовательские данные. Версия промпта должна меняться при изменении инструкций, формата, примеров, объявлений инструментов или логики рендеринга. Вопрос пользователя и найденные документы принадлежат запуску, а не версии шаблона.
Особенно опасны «тихие» изменения. Команда меняет текст системной инструкции через админ-панель, оставляет то же имя промпта и не обновляет версию. Через неделю eval показывает падение качества, но сравнить хорошие и плохие ответы уже нельзя. Все они формально относятся к одному промпту.
Промпт судьи требует такой же дисциплины. Если LLM-as-a-judge оценивает полноту ответа, его рубрика, модель и параметры являются частью методики измерения. Нельзя молча переписать рубрику, а затем рисовать один график до и после изменения как будто шкала не менялась. Это не улучшение качества продукта, это смена измерительного прибора.
Документы поиска нужно фиксировать после ранжирования
RAG ломается не только потому, что поиск вернул нерелевантный документ. Он ломается, когда документ был релевантным, но старым; когда нужный фрагмент оказался шестым, а в prompt попали первые четыре; когда фильтр доступа исключил актуальную политику; когда система обрезала чанк перед важным исключением.
Поэтому запись поиска должна отражать финальный набор контекста, а не только запрос к векторной базе. Сохраните исходный текст поискового запроса, название и ревизию индекса, фильтры, алгоритм ранжирования, список кандидатов в порядке после rerank и список чанков, реально вставленных в prompt.
Для каждого вставленного чанка нужны:
- стабильный
document_idиchunk_id; - версия или контрольная сумма текста;
- оценка первого поиска и оценка rerank, если он был;
- позиция в итоговом контексте;
- причина исключения, если кандидат не дошел до модели.
Не используйте только URL или путь к документу. Страница базы знаний по тому же адресу может поменяться десять раз. При повторном запуске вы получите правдоподобный, но уже другой контекст и ошибочно решите, что проблема исчезла.
Есть еще одно различие, которое часто портит eval. Релевантность документа и корректность итогового ответа измеряют разные вещи. Документ может быть тематически близким, но не содержать условия, нужного для ответа. И наоборот, набор хороших документов не гарантирует, что модель процитирует правильное правило. Документация Phoenix прямо разделяет retrieval eval по чанкам и системную Q&A-оценку. Держите эти оценки раздельно и связывайте обе с одним run.
Если финальный ответ неверен, начните с вопроса: «Могла ли модель дать верный ответ, опираясь только на переданный контекст?» Если нет, не тратьте день на переписывание системной инструкции. Исправляйте индекс, фильтры, разбиение, rerank или покрытие базы знаний.
Инструменты требуют журнала причин, а не только результата
У агента ответ часто зависит от внешнего мира сильнее, чем от модели. Проверка баланса, статуса заявки, тарифа, наличия товара или правила доступа меняется между двумя одинаковыми запросами. Если сохранить только финальный текст, повторный запуск в другой момент даст иной результат, и вы не узнаете, ошибся ли агент тогда или сейчас.
Для каждого вызова инструмента фиксируйте объявление инструмента, аргументы после валидации, время вызова, ответ, ошибку и решение агента после ответа. Особенно полезно хранить нормализованный результат, который реально передали модели. Сырой HTTP-ответ может включать поля, которые приложение затем удалило или преобразовало.
Рассмотрим типичный сбой. Агент должен вызвать get_deposit_terms с кодом продукта. Модель извлекает код из истории и передает DP-018. Инструмент возвращает условия для старого продукта, потому что сервис принял устаревший алиас. Ответ агента выглядит аккуратно, но содержит неверное правило. Если трасса хранит только «инструмент выполнен успешно», расследование упрется в стену. Если она хранит аргумент, нормализованный ответ и версию справочника, причина видна сразу.
Не сохраняйте секреты как «цену за отладку». Вызовы инструментов часто несут токены, номера счетов и персональные данные. Перед записью применяйте схему маскирования по полям. Поле account_number можно заменить стабильным хешем с солью, чтобы сопоставлять обращения, не раскрывая номер. Поле authorization вообще не должно попадать в трассу.
Вызов, который агент планировал, но не сделал, тоже является данными. Добавьте span или событие для выбора действия: доступные инструменты, выбранный инструмент, причина отказа, лимит итераций. Это помогает отличить «инструмент вернул неверный ответ» от «модель решила, что инструмент не нужен».
Пакет воспроизведения должен запускаться вне продакшена
Не пытайтесь воспроизводить плохой ответ прямым повтором боевого HTTP-запроса. Вы можете снова списать деньги, отправить письмо, изменить заявку или прочитать уже обновленные данные. Воспроизведение должно работать в изолированной среде и по умолчанию запрещать побочные действия.
Соберите из трассы пакет, который содержит манифест запуска и снимки зависимостей. В пакете нет необходимости дублировать весь observability storage. Нужны только артефакты, без которых меняется смысл ответа.
{
"replay_version": 1,
"source_run_id": "run_01JQ8K7C4V",
"mode": "offline",
"messages": "artifacts/messages.json",
"retrieval": "artifacts/retrieval-final.json",
"tool_transcript": "artifacts/tool-transcript.json",
"generation": {
"model": "provider/model-revision",
"temperature": 0.2,
"top_p": 1.0,
"max_output_tokens": 700,
"seed": 18421
},
"side_effect_policy": "deny",
"expected": {
"evals": ["groundedness=fail", "answer_correctness=fail"],
"output_sha256": "optional"
}
}
В режиме offline поиск не обращается к текущему индексу, а читает retrieval-final.json. Инструменты не вызывают боевые сервисы, а возвращают зафиксированные ответы из tool-transcript.json. Это не имитация продакшена ради красоты. Это способ изолировать уже случившееся решение от текущего состояния данных.
Нужны два режима replay. Первый, точный, повторно подает сохраненные сообщения, документы и ответы инструментов в модель. Он отвечает на вопрос, как модель ведет себя на историческом контексте. Второй, диагностический, запускает текущую систему на исходном пользовательском запросе. Он отвечает на вопрос, исправила ли новая версия проблему в реальных условиях. Не смешивайте их в одном отчете.
Поставьте ограничение на полный текст. В некоторых организациях пакет нельзя скачать на ноутбук разработчика. Тогда храните его в контролируемом окружении, запускайте replay рядом с защищенным хранилищем и предоставляйте доступ по роли. Воспроизводимость не оправдывает бесконтрольное копирование клиентских данных.
Совпадение текста не является единственным критерием
Многие команды объявляют replay успешным, только если новый ответ посимвольно равен старому. Для генеративной модели это слишком жесткое требование и часто бесполезный сигнал. Малое различие в формулировке не меняет причину сбоя, а обновление модели у провайдера может изменить текст даже при тех же параметрах.
Проверяйте воспроизведение по уровням. Сначала должно совпасть дерево действий: тот же prompt, тот же набор документов, тот же порядок инструментов, та же фактическая модель и те же параметры. Затем проверяйте смысл: та же неверная политика, тот же пропущенный факт, та же недопустимая операция или тот же провал eval.
Для структурированных ответов сохраните нормализованное представление. Если модель возвращает JSON, удалите поля времени, случайные идентификаторы и порядок ключей, затем сравните обязательные значения. Для текстовых ответов полезнее сравнивать утверждения: есть ли неправильная ставка, указан ли неверный срок, сослался ли ответ на документ, которого в контексте не было.
Seed помогает, но не обещает идентичность. Он не отменяет обновления модели, изменение системного слоя провайдера, иной порядок батчинга и различия реализации. Поле seed все равно нужно сохранять: оно уменьшает число переменных и делает расхождения честно видимыми.
Если точный replay расходится, не закрывайте расследование фразой «модель недетерминированна». Сначала сравните манифесты. Часто отличие скрывается в одном поле: новый max_output_tokens, другая схема ответа, измененный список инструментов, подмешанная история или провайдер, который принял совместимый запрос, но направил его к иной ревизии модели.
Eval должен давать маршрут к исправлению
Оценка полезна, когда по ней можно выбрать владельца проблемы. Метка bad ничего не говорит команде поиска, владельцу инструментов и разработчику prompt-а. Вместо одного общего балла заведите небольшую таксономию причин, которую можно проверять по трассе.
Например, для ответа RAG-системы достаточно начать с пяти категорий:
retrieval_missing: в контексте нет материала, необходимого для ответа;retrieval_wrong: контекст содержит неподходящий или устаревший материал;tool_failure: инструмент вернул ошибку, устаревшие данные или неверно интерпретировал аргументы;generation_ungrounded: нужные факты были в контексте, но модель их исказила или проигнорировала;policy_failure: ответ нарушил правило формата, безопасности или допустимых действий.
Один run может получить несколько меток. Это нормально. Неверный документ поиска может одновременно привести к необоснованной генерации. Не заставляйте разметчика выбрать одну «главную» причину, если по трассе видно цепочку причин.
Свяжите категории с действиями. retrieval_missing отправляет пример в очередь пополнения базы знаний или в набор запросов для улучшения поиска. tool_failure идет владельцу интеграции вместе с аргументами и снимком ответа. generation_ungrounded становится тестовым случаем для вариантов промпта, модели и декодирования. Так eval перестает быть витриной метрик и становится входом в инженерный цикл.
Набор провальных run-ов нужно превращать в регрессионный датасет, но не бездумно. Удалите дубликаты одной ошибки, сохраните распределение по типам запросов и прикрепите ожидаемую причину. Иначе команда научит систему отвечать на десяток похожих примеров, а редкие и дорогие сбои останутся без теста.
Срок хранения и маскирование определяют ценность трассы
Полная трасса LLM-приложения быстро становится архивом чувствительного контента. В ней есть вопрос пользователя, история сообщений, найденные документы, аргументы функций и ответ. Собирать все это без правил доступа и удаления опасно. Не собирать ничего означает лишить команду возможности расследовать инциденты.
Решение не в ложном выборе между «храним весь текст навсегда» и «оставляем только метрики». Разделите данные по чувствительности. Технические метаданные, хеши, версии, длительности, идентификаторы документов и результаты eval могут жить дольше. Полные сообщения и результаты инструментов должны иметь короткий срок хранения, маскирование, журнал доступа и отдельное защищенное хранилище.
OpenTelemetry предупреждает, что входные сообщения, поисковые запросы и выходы моделей могут содержать чувствительные данные. Отнеситесь к этому как к требованию проектирования, а не к примечанию в документации. Маскирование должно происходить до экспорта, потому что удаление после записи не гарантирует очистку реплик, резервных копий и сторонних систем.
Для команд, которым важно хранение данных в Казахстане, AI Router может быть частью архитектуры вызовов моделей и контроля телеметрии, но схема воспроизведения остается задачей самого приложения. Сохраните фактического провайдера, модель и маршрут рядом с run, а не в отдельном биллинговом отчете, который нельзя сопоставить с плохим ответом.
Начните с одного реального сбоя, который команда уже не смогла объяснить за час. Возьмите его trace, добавьте недостающие поля, соберите offline replay и привяжите к нему одну человеческую оценку и один автоматический eval. После этого станет видно, какие данные вы теряете сейчас и какие поля в следующем инциденте сэкономят дни работы.
Часто задаваемые вопросы
Чем eval отличается от трассировки LLM-приложения?
Трасса фиксирует, что произошло во время конкретного запуска: вызовы модели, поиск, инструменты, задержки и ошибки. Eval добавляет оценку результата по правилу или рубрике. Связка нужна для того, чтобы из метки «плохо» сразу перейти к точным входам и решениям системы.
Какие данные нужны, чтобы воспроизвести ответ LLM?
Для поиска причины обычно нужны исходный запрос, итоговый ответ, версия промпта, фактическая модель, параметры генерации, документы поиска и все вызовы инструментов. Если агент меняет маршрут или делает несколько вызовов модели, сохраните порядок дочерних шагов. Одного текста диалога для этого почти никогда не хватает.
Нужно ли версионировать промпты для eval?
Храните неизменяемый идентификатор версии, а не только имя вроде production или current. Метка окружения удобна для выкладки, но со временем она указывает на другой текст. При расследовании вам нужна именно та версия, которая попала в запрос.
Можно ли получить точно такой же ответ от недетерминированной модели?
Нет, если задача не требует фактической идентичности ответа. Для большинства расследований достаточно воспроизвести ход выполнения и понять, какой компонент дал неверный вход или принял неверное решение. Если нужен близкий к исходному текст, сохраняйте seed, параметры, снимок контекста и точную ревизию модели, но все равно учитывайте изменения у провайдера.
Что записывать о найденных RAG-документах?
Сохраняйте идентификаторы документов, версию индекса, текст или защищенный снимок чанков, их порядок, оценки поиска и примененные фильтры. Идентификатора документа недостаточно, если содержимое документа или алгоритм разбиения уже изменились. Для чувствительных данных текст можно вынести в защищенное хранилище, оставив в трассе ссылку и контрольную сумму.
Нужно ли трассировать вызовы инструментов агента?
Да. В противном случае вы увидите только финальный ответ и не поймете, модель ошиблась сама, получила неверный результат инструмента или агент вообще не вызвал нужный инструмент. Для каждого вызова храните имя инструмента, аргументы после маскирования, ответ, код ошибки и порядок относительно других шагов.
Можно ли хранить полный prompt в трассе?
К ним относятся персональные данные пользователя, секреты, токены доступа, полные тексты договоров, медицинские сведения и платежные реквизиты. Маскируйте поля до экспорта телеметрии и задавайте отдельные сроки хранения для исходного контента и технических метаданных. Не рассчитывайте на ручную очистку после инцидента.
Когда использовать LLM-as-a-judge, а когда кодовый eval?
Начните с детерминированных проверок там, где ответ можно проверить кодом: формат JSON, наличие обязательного поля, корректность ссылки, разрешенные действия. Судью на базе LLM используйте для полноты, соответствия документам и качества объяснения. Самого судью тоже нужно версионировать и трассировать, иначе оценка станет еще одним непрозрачным ответом модели.
Почему replay дает другой результат, хотя trace сохранена?
Сначала сопоставьте их по полям: промпт, модель, параметры, документы, инструменты и история сообщений. Затем проверьте, не изменились ли данные вне трассы, например документ в индексе или правило маршрутизации. Если все совпало, различие часто объясняется стохастичностью генерации или изменением поведения модели у провайдера.
Как маршрутизатор моделей влияет на воспроизводимость eval?
Роутер полезен, когда он возвращает в телеметрию фактическую модель, провайдера и параметры вызова, а не только желаемое имя модели в коде. AI Router можно использовать как OpenAI-совместимый шлюз, но воспроизводимость появится только если приложение само сохраняет снимок контекста, версии и результаты инструментов. Смена base_url не заменяет дисциплину данных.