MAATRIX / Блог / Мониторинг качества ответов ИИ в продакшене

Мониторинг качества ответов ИИ в продакшене

MAATRIX

Вы протестировали систему на 100 заранее подготовленных вопросах, метрики вышли приемлемыми, и вы выкатили её в прод. Через два месяца пользователи начали жаловаться, а метрики на том же тестовом наборе всё ещё зелёные — потому что тестовый набор не менялся, а реальные вопросы, база знаний и внешние сервисы вокруг системы поменялись незаметно. Разовое тестирование отвечает на вопрос «работает ли система сейчас, на этих примерах», но не отвечает на вопрос «работает ли она через месяц, на реальном трафике». Нужен отдельный контур — непрерывный мониторинг качества в проде, и вот как его построить.

Почему тестирования перед запуском недостаточно

Разовая оценка — обязательный этап, но она проверяет систему в статичных, заранее известных условиях. У продакшена условия другие, и они меняются во времени по причинам, которые на этапе тестирования просто не с чем сравнить.

Во-первых, реальные пользователи формулируют вопросы иначе, чем авторы тестового набора. Тестовые вопросы обычно пишет тот же человек, кто строил систему — он неосознанно формулирует их так, чтобы система на них хорошо отвечала. Живые пользователи не знают внутреннего устройства базы знаний, путают термины, задают вопросы не по теме, пишут с ошибками, объединяют два вопроса в один. Покрытие тестового набора и покрытие реального трафика — разные множества, и чем дольше система работает, тем сильнее они расходятся.

Во-вторых, характер входящих запросов дрейфует со временем. Если система отвечает на вопросы о продукте, а продукт обновился — часть вопросов сместится в сторону новых функций, о которых в момент тестирования никто не спрашивал. Сезонность, новые категории пользователей, изменения в интерфейсе, откуда люди попадают к ассистенту — всё это постепенно меняет распределение запросов, и модель, которая хорошо справлялась со старым распределением, не обязана справляться с новым.

В-третьих, база знаний, на которую опирается система (документы для RAG, промпт с инструкциями, справочные таблицы), меняется без отдельного цикла регрессионного тестирования. Кто-то обновил документ, удалил страницу, добавил противоречивую формулировку — и это тихо портит часть ответов, пока никто специально не прогонит тесты заново после каждой правки контента, что на практике происходит редко.

В-четвёртых, внешние зависимости деградируют независимо от вашего кода. Провайдер модели меняет версию за тем же именем API, эмбеддинг-сервис начинает отвечать медленнее и вы срезаете таймаутом часть запросов, реранкер начинает молча возвращать пустой список при ошибке — деталь методики оценки качества RAG-ответов на подготовленном наборе разбирается в статье как оценить качество RAG-ответов: там речь о том, как раскладывать ответ на компоненты и мерить их автоматически перед выкаткой. Мониторинг в проде — это тот же принцип, но применённый не к статичному набору из 50-100 вопросов, а к живому, постоянно обновляющемуся потоку реальных диалогов.

Вывод простой: разовое тестирование и непрерывный мониторинг закрывают разные риски, и один не заменяет другой. Тестирование ловит регрессии до выкатки. Мониторинг ловит деградацию, которая появляется после выкатки и накапливается медленно — то есть именно то, что тестовый набор в принципе не может увидеть, потому что он не меняется вместе с реальностью.

Явная обратная связь: лайки, дизлайки, жалобы

Самый прямой сигнал — когда пользователь сам говорит, доволен он ответом или нет. Если в интерфейсе уже есть чат с ИИ, добавить пару кнопок дёшево и почти всегда оправдано.

Минимальная схема хранения на Postgres:

CREATE TABLE ai_feedback (
    id BIGSERIAL PRIMARY KEY,
    conversation_id UUID NOT NULL,
    message_id UUID NOT NULL,
    user_id BIGINT,
    rating SMALLINT NOT NULL,  -- 1 = лайк, -1 = дизлайк
    comment TEXT,              -- опциональный текст жалобы
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ON ai_feedback (created_at);

Дневная агрегация для дашборда — доля дизлайков от всех оценённых ответов:

SELECT
    date_trunc('day', created_at) AS day,
    count(*) FILTER (WHERE rating = -1)::float
        / NULLIF(count(*), 0) AS dislike_rate,
    count(*) AS total_rated
FROM ai_feedback
GROUP BY 1
ORDER BY 1 DESC
LIMIT 30;

Важное ограничение: явную обратную связь оставляет меньшинство. По опыту работы с чат-интерфейсами оценку в среднем ставят лишь несколько процентов диалогов — это ориентир, у вас может быть иначе, но факт в том, что молчание большинства не означает их удовлетворённость. Недовольный пользователь чаще молча уходит, чем кликает на дизлайк. Поэтому явную обратную связь стоит читать не как полную картину, а как срез — если доля дизлайков резко выросла, проблема точно есть и она масштабнее, чем видно по цифре; если дизлайков мало, это не доказывает, что всё хорошо, а лишь то, что явных жалоб мало.

Отдельно стоит читать текстовые жалобы (comment), если поле для них есть — хотя бы новые за неделю. Числовая метрика скажет «что-то не так», текст жалобы часто прямо скажет что.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть ИИ на сервере

Косвенные поведенческие сигналы

Часть пользователей никогда не поставит оценку, но своим поведением покажет, что ответ не подошёл. Эти сигналы косвенные — ни один из них по отдельности не доказывает проблему, но в совокупности и в динамике они дают полезную картину.

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

Аномальная длина диалога. Слишком короткий диалог (пользователь задал вопрос и сразу ушёл, не дождавшись уточнений) и слишком длинный диалог (десять сообщений подряд по одной теме) — оба могут быть признаком проблемы, просто разной. Полезно следить за распределением длины диалогов по дням и смотреть на сдвиг медианы или появление второго пика, а не только на среднее — среднее легко смазывает картину при выбросах.

Брошенные диалоги без финального ответа. Если пользователь закрыл вкладку или ушёл из чата, не получив ответа (например запрос упал по таймауту или ошибке пайплайна), это тоже поведенческий сигнал неудовлетворённости — только уже не про качество текста ответа, а про надёжность самого сервиса. Такие случаи стоит считать отдельно от «получил ответ, но не понравился».

Ни один из перечисленных сигналов не заменяет прямую оценку, но вместе они закрывают ту часть пользователей, которая никогда не поставит лайк или дизлайк.

Автоматические метрики качества на живых запросах

Третий контур — тот же подход, что при разовой оценке LLM-как-судья, описанной выше, но применённый не к фиксированному тестовому набору, а к выборке из реального трафика, регулярно и по расписанию.

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

Пример скрипта — упрощённо, без обработки ошибок и ретраев:

SAMPLE_SIZE = 200  # ориентир для трафика в несколько тысяч диалогов/сутки

def sample_conversations(conn, since):
    with conn.cursor() as cur:
        cur.execute("""
            SELECT id, question, answer, retrieved_context
            FROM conversations
            WHERE created_at >= %s
            ORDER BY random() LIMIT %s
        """, (since, SAMPLE_SIZE))
        return cur.fetchall()

def judge(question, answer, context):
    # тот же промпт-критерий, что и при разовой оценке перед запуском:
    # просим оценить релевантность и grounding по шкале 1-5,
    # ответ модели-судьи — JSON {"relevance": N, "grounding": N}
    ...

def run_daily_check(conn, since):
    rows = sample_conversations(conn, since)
    scores = [judge(q, a, c) for _, q, a, c in rows]
    avg_relevance = sum(s["relevance"] for s in scores) / len(scores)
    avg_grounding = sum(s["grounding"] for s in scores) / len(scores)
    return avg_relevance, avg_grounding

Результат каждого прогона стоит писать в ту же систему метрик, что и остальной мониторинг сервера (например через pushgateway в Prometheus, если стек уже на нём) — тогда качество ответов окажется на одном дашборде рядом с задержкой и нагрузкой инференса, о которых подробно в статье как мониторить нагрузку локальной LLM, и будет видно, коррелирует ли просадка качества с ростом задержки пайплайна.

Важные практические оговорки. Выборка должна быть случайной, а не «первые N диалогов дня» — иначе вы систематически смотрите на один и тот же часовой пояс пользователей. Модель-судья тоже может ошибаться и иметь свой дрейф (например после обновления версии за тем же именем) — поэтому абсолютное значение оценки судьи менее важно, чем его изменение относительно собственной истории. И конечно, если диалоги содержат персональные данные, выборку для судьи нужно анонимизировать или прогонять через модель, развёрнутую в вашем периметре, а не отправлять во внешний API как есть.

Как настроить алерты на отклонение от базового уровня

Метрики бесполезны, если их никто не смотрит до жалобы клиента — это отдельная и частая проблема мониторинга вообще, разобранная в статье про антипаттерн мониторинга, который никто не смотрит: для метрик качества ответов ИИ он актуален вдвойне, потому что просадка качества не роняет сервер и не видна в стандартных алертах на аптайм.

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

Пример правила для Prometheus/Alertmanager — доля дизлайков сегодня выросла более чем в полтора раза относительно скользящего среднего за 14 дней:

groups:
  - name: ai_quality
    rules:
      - alert: AIDislikeRateSpike
        expr: >
          rate(ai_feedback_dislikes_total[1d])
          / rate(ai_feedback_dislikes_total[14d] offset 1d)
          > 1.5
        for: 2h
        labels:
          severity: warning
        annotations:
          summary: "Доля дизлайков ответов ИИ выросла более чем в 1.5 раза к базовому уровню"
      - alert: AIJudgeGroundingDrop
        expr: ai_judge_avg_grounding < (avg_over_time(ai_judge_avg_grounding[14d]) * 0.85)
        for: 6h
        labels:
          severity: warning
        annotations:
          summary: "Средняя оценка grounding от судьи упала более чем на 15% от нормы"

Пороги (1.5, 0.85, for: 2h/6h) — ориентир для старта, не готовое значение: на малом трафике доля дизлайков шумит сама по себе, и слишком чувствительный порог даст ложные срабатывания, а слишком мягкий — пропустит проблему. Начните с широких порогов и сужайте по мере накопления истории алертов.

Отдельно стоит алертить на технические причины плохих ответов, которые проще поймать напрямую: рост доли пустых ответов реранкера, рост доли таймаутов до модели, рост доли ответов без найденного контекста в RAG-пайплайне. Эти сигналы приходят раньше, чем деградация доходит до пользователя и отражается в дизлайках.

Регулярный ручной разбор диалогов

Числовые метрики — необходимое, но не достаточное условие понимания того, что происходит с качеством. Метрика скажет, что дизлайков стало больше на 40%, но не скажет, из-за чего именно: пользователи массово спрашивают о новой функции, о которой в базе знаний ничего нет, или реранкер стал систематически терять один конкретный тип документов, или просто выросла доля спам-запросов, не относящихся к теме вообще.

Практика, которая закрывает этот разрыв — регулярный, а не только по факту жалобы, просмотр человеком выборки реальных диалогов. Формат, который работает без больших затрат времени:

  • раз в неделю один человек из команды читает 20-30 случайных диалогов за неделю (не только с дизлайками — иначе выборка смещена в сторону уже известных проблем);
  • отдельно читает все диалоги с явным дизлайком или жалобой за неделю, если их не десятки;
  • по каждому диалогу фиксирует короткую заметку: тип вопроса, что пошло не так (если пошло), гипотеза причины;
  • раз в месяц эти заметки просматриваются вместе — так видны повторяющиеся паттерны, которые по отдельности выглядят как разовые случаи.

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

Разделение ответственности простое: метрики и алерты держат постоянный фоновый контроль и ловят резкие изменения быстро, а ручной просмотр раз в неделю-две даёт качественное понимание причин и ловит медленные, плавные проблемы, которые ни один порог не заметит. Одно без другого работает хуже: только метрики — вы знаете, что что-то не так, но не знаете что; только ручной просмотр — вы пропустите резкое ухудшение между сессиями обзора.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть ИИ на сервере

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

С чего начать, если сейчас в проде вообще ничего не измеряется?

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

Нужен ли отдельный сервер под LLM-судью или можно использовать ту же модель, что отвечает пользователям?

Технически можно использовать ту же модель, но лучше отдельный вызов с отдельным, более строгим промптом-критерием — модель хуже замечает собственные систематические ошибки, если её просят оценить саму себя тем же контекстом мышления, которым она отвечала.

Какой процент трафика нужно прогонять через LLM-судью?

Однозначного числа нет — это зависит от объёма трафика и бюджета на вызовы судьи. Ориентир для старта — фиксированное число диалогов в день (например 100-300), а не процент, так проще контролировать стоимость независимо от роста трафика.

Что делать, если дизлайков мало, но по ощущениям пользователи недовольны?

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

Как отличить реальную деградацию качества от естественного шума метрики?

Смотреть не на разовый скачок, а на устойчивое отклонение от базового уровня за несколько дней (for: Nh в правиле алерта) и сверять с ручным просмотром диалогов за этот период — если находка в диалогах подтверждает гипотезу метрики, это не шум.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →