Продвинутый уровень · до 10 минут · практическое руководство

Регрессионные тесты RAG при обновлении корпуса документов

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

Что именно считать регрессией

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

  • Регрессия поиска: нужный источник перестал попадать в первые k результатов.
  • Регрессия цитирования: ответ содержит утверждение, но указанная цитата его не подтверждает.
  • Регрессия ответа: исчез обязательный факт, появился запрещённый вывод или изменилось решение пользователя.

Средняя оценка ответа скрывает эти причины. Тестировать нужно каждый слой отдельно, сохраняя связь: запрос → найденный фрагмент → цитата → утверждение ответа.

Сначала зафиксируйте неизменяемые параметры

Сравнение корпусов корректно только при одинаковом окружении. Для обеих версий зафиксируйте:

  • идентификатор модели и параметры генерации, включая temperature и seed, если они поддерживаются;
  • системный и пользовательский промпты;
  • модель эмбеддингов, нормализацию текста и размерность векторов;
  • алгоритм нарезки, overlap, фильтры метаданных, top_k и reranker;
  • порядок документов и стабильные идентификаторы источников;
  • версии кода и конфигурации.

Если одновременно поменялись корпус и retriever, тест покажет различие, но не позволит приписать его одной причине. Такие изменения проверяйте отдельными экспериментами.

Минимальная структура стенда

Ниже приведён пример структуры. Имена файлов условны и не описывают реальный проект:

rag-regression/
├── corpus/
│   ├── baseline/
│   └── candidate/
├── fixtures/
│   └── cases.jsonl
├── runs/
├── config.json
└── evaluate.py

baseline — выпущенная версия, candidate — новая. Не перезаписывайте baseline после начала проверки: иначе сравнение перестанет быть воспроизводимым.

Воспроизводимые шаги

1. Снимите манифесты корпусов

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

cd rag-regression

find corpus/baseline -type f -print0 \
  | sort -z \
  | xargs -0 sha256sum > runs/baseline.sha256

find corpus/candidate -type f -print0 \
  | sort -z \
  | xargs -0 sha256sum > runs/candidate.sha256

diff -u runs/baseline.sha256 runs/candidate.sha256 \
  > runs/corpus.diff || test $? -eq 1

Код возврата 1 для diff означает найденные различия, а не сбой. Не публикуйте манифест без проверки: пути к файлам могут содержать внутренние названия.

2. Подготовьте набор проверок

Один JSONL-объект описывает один пользовательский запрос. Проверяйте не точную фразу ответа, а наблюдаемые требования. Следующий фрагмент — синтетический пример формата, а не фактическое правило какой-либо организации:

{"id":"policy-001","query":"Когда требуется согласование операции?","expected_source_ids":["instruction-07"],"required_facts":["Согласование требуется до выполнения операции"],"forbidden_facts":["Согласование можно получить после выполнения"],"tags":["critical","policy"]}
{"id":"lookup-002","query":"Где описан порядок отмены?","expected_source_ids":["manual-03"],"required_facts":[],"forbidden_facts":[],"tags":["navigation"]}

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

Полезно включить четыре группы случаев:

  • ключевые операции и запреты;
  • факты из изменённых документов;
  • факты из неизменённых соседних документов — для поиска побочных эффектов;
  • вопросы без ответа в корпусе — для контроля обоснованного отказа.

3. Индексируйте версии независимо

Не обновляйте один индекс на месте: оставшиеся в нём чанки могут исказить результат. Создайте два новых пространства имён. Пример конфигурации:

{
  "embedding_model": "PINNED_MODEL_ID",
  "chunking": {
    "strategy": "paragraph",
    "max_tokens": 450,
    "overlap_tokens": 60
  },
  "retrieval": {
    "top_k": 8,
    "rerank_top_n": 4
  },
  "generation": {
    "model": "PINNED_MODEL_ID",
    "temperature": 0
  },
  "indexes": {
    "baseline": "rag-corpus-baseline-immutable",
    "candidate": "rag-corpus-candidate-build"
  }
}

Замените заглушки PINNED_MODEL_ID на идентификаторы из собственного окружения. Ключи доступа храните в менеджере секретов или переменных окружения; не записывайте их в конфигурацию и результаты.

4. Сохраните сырой результат каждого прогона

Для каждого кейса обе версии должны сформировать запись одного формата:

{
  "case_id": "policy-001",
  "corpus_version": "candidate",
  "retrieved": [
    {
      "rank": 1,
      "source_id": "instruction-07",
      "chunk_id": "instruction-07:section-4:p2",
      "score": 0.81,
      "text_hash": "sha256:..."
    }
  ],
  "citations": [
    {
      "source_id": "instruction-07",
      "chunk_id": "instruction-07:section-4:p2",
      "claim": "Согласование требуется до выполнения операции"
    }
  ],
  "answer": "...",
  "runtime": {
    "config_hash": "sha256:...",
    "index_id": "rag-corpus-candidate-build"
  }
}

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

5. Сравните полноту поиска

Для каждого запроса вычислите hit@k: найден ли хотя бы один допустимый источник среди первых k. Для нескольких обязательных источников используйте полноту:

retrieval_recall@k =
  число ожидаемых source_id в top-k
  / общее число ожидаемых source_id

Сравнивайте метрику попарно по каждому кейсу, а не только в среднем. Важнейший сигнал — переход 1 → 0 для критического запроса. Изменение similarity score само по себе не доказывает регрессию: шкала зависит от поисковой реализации и состава индекса.

6. Проверьте цитаты

Для каждого значимого утверждения ответьте на три вопроса:

  1. Есть ли у утверждения ссылка на конкретный найденный фрагмент?
  2. Содержит ли этот фрагмент достаточное основание для утверждения?
  3. Не противоречит ли утверждение более актуальному фрагменту из того же корпуса?

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

7. Сравните итоговые ответы по требованиям

Разделите проверки на детерминированные и оценочные:

  • Детерминированные: обязательный источник найден, цитируемый чанк существует, запрещённое значение отсутствует, структурированный ответ проходит схему.
  • Оценочные: обязательный смысл сохранён, ответ не расширяет правило, отказ уместен, цитата действительно подтверждает вывод.

Не используйте полное строковое совпадение: допустимая переформулировка создаст ложное падение. Для чисел, дат, кодов и перечислений, напротив, полезно извлекать нормализованное значение и сравнивать его точно.

8. Введите порог выпуска

Порог должен быть принят до просмотра результатов candidate. Пример политики, которую нужно адаптировать под риск проекта:

release:
  block_if:
    - any_critical_case_changes_from_pass_to_fail
    - any_citation_points_to_missing_chunk
    - any_forbidden_fact_appears
    - unanswered_case_becomes_unsupported_answer
  review_if:
    - retrieval_recall_at_8_decreases
    - answer_requirements_change
    - citation_support_is_uncertain

Это пример конфигурации, а не универсальный стандарт. Для низкорисковых справочных ответов допустимы агрегированные пороги; для безопасности, финансовых решений или прав доступа разумнее блокировать выпуск по единичному критическому ухудшению.

Как проверить результат

Итоговый отчёт должен позволять перейти от сводки к конкретному случаю:

Кейс Поиск baseline → candidate Цитаты Ответ Решение
policy-001 1.00 → 0.00 Источник пропал Обязательный факт отсутствует Блокировать
lookup-002 1.00 → 1.00 Поддержана Смысл сохранён Принять

Строки таблицы — иллюстрация ожидаемого отчёта, а не результаты реального запуска.

Перед выпуском убедитесь, что:

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

Как локализовать найденное ухудшение

  1. Нужного source_id нет в top-k. Сравните манифест, структуру документа, чанки и метаданные фильтрации.
  2. Источник найден, но опустился ниже порога. Проверьте новые конкурирующие фрагменты, заголовки и распределение содержимого между чанками.
  3. Чанк найден, цитата слабая. Убедитесь, что правило не разделилось между соседними чанками и контекст не потерял условие или исключение.
  4. Поиск одинаков, ответ изменился. Проверьте порядок фрагментов, нестабильность генерации и фактическое равенство промптов.
  5. Ответ улучшился, но ссылка неверна. Не засчитывайте кейс: правильный вывод без подтверждаемой опоры остаётся дефектом трассируемости.

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

  • Сравнивать только средние показатели. Улучшение двадцати простых вопросов может скрыть провал одного критического.
  • Переиндексировать baseline. Изменение библиотек или нарезки превратит исторический эталон в новую систему.
  • Считать высокий score доказательством релевантности. Проверять нужно источник и поддерживаемый им факт.
  • Привязывать ожидание к номеру чанка. После редактирования номера сдвигаются; используйте логические source_id и устойчивые секции.
  • Проверять наличие ключевых слов вместо смысла. Отрицание может содержать те же слова, что и правильное утверждение.
  • Оценивать только новые документы. Они могут вытеснить из top-k релевантные фрагменты старого корпуса.
  • Менять тесты после просмотра candidate. Исправление ошибочного ожидания допустимо, но его нужно документировать и отдельно повторить оба прогона.
  • Отправлять закрытые тексты внешнему судье. Сначала проверьте режим обработки данных и применимую политику доступа.

Ограничения метода

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

Детерминированность также ограничена: удалённые модели, approximate nearest neighbor и reranker могут давать небольшие колебания. Если инфраструктура не гарантирует повторяемость, выполняйте несколько прогонов и отделяйте систематический сдвиг от шума, не подменяя этим проверку критических случаев.

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

Короткий рабочий цикл

  1. Заморозить baseline и конфигурацию.
  2. Снять манифест обеих версий.
  3. Создать независимые индексы.
  4. Прогнать один набор запросов.
  5. Сохранить выдачу, цитаты, ответы и версии компонентов.
  6. Сравнить результаты попарно по каждому кейсу.
  7. Вручную разобрать критические и неоднозначные изменения.
  8. Применить заранее определённый порог выпуска.

Дополнительные схемы построения проверяемых агентных систем собраны в руководствах Agent Lab Journal. Определения метрик и компонентов ищите в глоссарии.