Практика · Продвинутый уровень

Как проверить качество поиска по базе знаний агента

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

Чтение: до 8 минут Уровень: продвинутый Результат: набор запросов, recall и ручная проверка

Что именно проверяем

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

Главный вопрос проверки: присутствует ли среди первых k результатов хотя бы один фрагмент, заранее признанный подходящим для данного запроса? Для более строгой оценки можно требовать нахождения всех необходимых фрагментов.

1. Зафиксируйте тестируемую систему

Результаты сравнимы только при неизменных условиях. Запишите снимок корпуса или его версию, модель эмбеддингов, алгоритм поиска, размер фрагмента, перекрытие, фильтры, значение k и параметры переранжирования. Не включайте в отчёт ключи API, строки подключения и содержимое закрытых документов.

{
  "corpus_version": "example-snapshot-001",
  "chunk_size": 700,
  "chunk_overlap": 100,
  "retrieval": "vector",
  "top_k": 5,
  "filters": {},
  "reranker": null
}

Это пример конфигурации, а не универсальные рекомендуемые значения. Конкретные размеры зависят от структуры документов и поискового движка.

2. Соберите контрольные запросы

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

Включите несколько типов запросов:

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

Последняя группа не участвует в обычном recall: у неё нет релевантных документов. Она нужна для проверки отказа от ответа и обнаружения ложной уверенности.

3. Назначьте ожидаемые фрагменты

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

{"id":"q01","query":"Пример вопроса о настройке лимита","relevant_ids":["doc-a#chunk-03"]}
{"id":"q02","query":"Пример перефразированного вопроса","relevant_ids":["doc-b#chunk-07","doc-b#chunk-08"]}
{"id":"q03","query":"Пример вопроса без ответа","relevant_ids":[]}

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

4. Сохраните выдачу без участия генератора

Запустите только поисковый слой и сохраните первые k результатов в отдельный JSONL-файл. Для каждого результата полезны идентификатор, позиция и оценка поиска. Текст можно хранить в защищённом контуре либо заменить ссылкой на локальный документ.

{"id":"q01","retrieved":[
  {"chunk_id":"doc-a#chunk-03","rank":1,"score":0.81},
  {"chunk_id":"doc-c#chunk-02","rank":2,"score":0.74}
]}
{"id":"q02","retrieved":[
  {"chunk_id":"doc-b#chunk-08","rank":1,"score":0.77},
  {"chunk_id":"doc-d#chunk-01","rank":2,"score":0.72}
]}

Не сравнивайте значения score между разными моделями или движками без калибровки: шкалы могут иметь разный смысл.

5. Рассчитайте recall

Recall@k для запроса — доля размеченных релевантных фрагментов, найденных в первых k позициях:

Recall@k = |релевантные ∩ найденные@k| / |релевантные|

Если для ответа достаточно любого одного фрагмента, дополнительно считайте Hit@k: 1, когда найден хотя бы один релевантный фрагмент, иначе 0. Эти метрики нельзя подменять друг другом. При двух обязательных фрагментах один найденный даст Hit@k = 1, но Recall@k = 0,5.

Следующий локальный скрипт читает только указанные файлы, ничего не изменяет и не выполняет сетевых запросов:

import json
from pathlib import Path

def load_jsonl(path):
    rows = {}
    with Path(path).open(encoding="utf-8") as source:
        for line_number, line in enumerate(source, 1):
            if not line.strip():
                continue
            row = json.loads(line)
            if row["id"] in rows:
                raise ValueError(f"Duplicate id at line {line_number}: {row['id']}")
            rows[row["id"]] = row
    return rows

gold = load_jsonl("gold.jsonl")
runs = load_jsonl("results.jsonl")
k = 5
recalls = []
hits = []

for query_id, expected in gold.items():
    relevant = set(expected["relevant_ids"])
    if not relevant:
        continue
    if query_id not in runs:
        recalls.append(0.0)
        hits.append(0)
        continue

    retrieved = {
        item["chunk_id"]
        for item in runs[query_id]["retrieved"][:k]
    }
    found = relevant & retrieved
    recalls.append(len(found) / len(relevant))
    hits.append(int(bool(found)))

print(f"Queries evaluated: {len(recalls)}")
print(f"Macro Recall@{k}: {sum(recalls) / len(recalls):.3f}")
print(f"Hit@{k}: {sum(hits) / len(hits):.3f}")

Сохраните код как evaluate_retrieval.py и запускайте из каталога с тестовыми файлами:

python3 evaluate_retrieval.py

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

6. Проведите ручную проверку

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

  • 2 — релевантен: непосредственно содержит данные для ответа;
  • 1 — частично релевантен: даёт необходимый контекст, но недостаточен сам по себе;
  • 0 — нерелевантен: совпадает формально или относится к другой теме;
  • отдельная отметка — вредный: выглядит убедительно, но содержит устаревшее либо противоречащее вопросу правило.

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

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

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

  1. Убедитесь, что число оценённых запросов совпадает с числом записей, где relevant_ids не пуст.
  2. Разберите каждый случай с Recall@k ниже 1, а не только среднее значение.
  3. Сгруппируйте ошибки по типу запроса, разделу корпуса, длине документа и причине ручной оценки.
  4. Сравните варианты на одном и том же наборе, с одинаковым k и неизменной разметкой.
  5. После изменения индекса повторите прогон и отдельно проверьте запросы, которые раньше проходили: локальное улучшение может вызвать регрессию.

Практический результат проверки — не одно число, а таблица: запрос, ожидаемые фрагменты, найденные позиции, Recall@k, Hit@k, ручные оценки и причина ошибки. Порог выпуска задаётся владельцем системы с учётом риска. Универсального «хорошего» значения recall не существует.

Что исправлять по симптомам

Симптом Вероятная область проверки
Нужный документ не появляется даже при большом k Индексация, фильтры, нормализация текста, модель эмбеддингов
Нужный фрагмент есть, но находится низко Гибридный поиск, формулировка запроса, переранжирование
Фрагмент обрывается перед ответом Правила разбиения, заголовки, перекрытие соседних фрагментов
Доминируют почти одинаковые результаты Дедупликация, разнообразие выдачи, группировка по документу
Смешиваются версии правил Метаданные версии, даты действия, фильтрация устаревших документов
Поиск хорош, но ответ неверен Промпт, сборка контекста, цитирование и генерация, а не retrieval

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

  • Проверять только знакомые точные формулировки. Такой набор завышает качество и не отражает пользовательские перефразы.
  • Размечать только один «идеальный» фрагмент. Другой фрагмент может быть равноценным; неполная разметка искусственно снижает метрику.
  • Менять одновременно индекс, k и переранжирование. После такого эксперимента невозможно понять причину изменения.
  • Оценивать итоговый ответ вместо выдачи. Модель иногда отвечает правильно из собственных параметров даже при плохом поиске.
  • Игнорировать запросы без ответа. Recall их не покрывает, но именно на них агент часто выдумывает подтверждение.
  • Оптимизировать только среднее. Высокий общий recall может скрывать провал в редком, но критичном разделе.
  • Использовать тестовые вопросы при настройке до бесконечности. Набор превращается в обучающий; для финальной проверки нужен отложенный срез.

Ограничения

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

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

Минимальный критерий готовности

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

Следующий шаг — встроить этот набор в регулярную проверку изменений поиска. Дополнительные рабочие схемы собраны в руководствах Agent Lab Journal, а определения метрик и компонентов — в глоссарии.