RAG для бизнеса: как оценить качество поиска и ответа
Практический гайд по оценке RAG-системы: отдельно проверяем retrieval и генерацию, собираем тестовый набор, считаем groundedness и не путаем красивый ответ с полезным.
Содержание статьи
RAG-система может звучать убедительно и всё равно отвечать не по тем документам. У неё как минимум две разные задачи — найти релевантный контекст и сформулировать ответ на его основе, — и по финальному тексту не видно, сломался поиск, промпт, модель или сами документы. Поэтому разбор идёт по слоям: сначала решаем, что вообще стоит отдавать поиску, а что остаётся инструкцией или обычным запросом к базе, затем собираем набор вопросов, на котором версии можно сравнивать, и только после этого считаем Recall@k, groundedness и пользу для человека.
Коротко
Оцените RAG по слоям: retrieval, grounded generation и полезность для пользователя. Сначала соберите небольшой эталонный набор вопросов с ожидаемыми документами и ответами. Затем отдельно измерьте, попал ли нужный фрагмент в top-k, не добавила ли модель факты вне контекста и помог ли ответ выполнить задачу.
- Один вопрос может иметь несколько допустимых документов и формулировок ответа.
- Recall@k отвечает за найденный контекст, а не за правдивость финального ответа.
- Groundedness проверяет связь ответа с контекстом, но не заменяет проверку самого контекста.
- Оценивайте abstain: ассистент должен уметь сказать «не нашёл».
- Логи вопросов, retrieval и feedback нужны для постоянного улучшения.
RAG — это цепочка, а не одна модель
В типичном потоке пользовательский вопрос превращается в embedding или поисковый запрос, затем система выбирает несколько чанков, собирает prompt и просит LLM сформировать ответ. Ошибка на любом этапе меняет результат. Плохое разбиение документов может скрыть нужную фразу, слабый retriever не найдёт правильную страницу, а модель может проигнорировать контекст даже после хорошего поиска.
| Слой | Вопрос проверки | Пример сигнала |
|---|---|---|
| Источник | документ актуален и доступен этой роли? | устаревшая политика |
| Chunking | нужный смысл не разрезан между чанками? | фрагмент без заголовка |
| Retrieval | релевантный chunk попал в top-k? | низкий Recall@5 |
| Generation | ответ следует найденному контексту? | неподтверждённое утверждение |
| UX | ответ помогает сделать следующий шаг? | пользователь переспрашивает |
Каталог в системном промпте: 36 000 токенов на каждый вопрос и цены позавчерашние
Почти каждый ассистент начинается не с поиска, а с длинной инструкции. В нашем случае в системный промпт положили выгрузку каталога: 1 200 позиций по строке на каждую, примерно по 30 токенов на строку — 36 000 токенов, которые модель читает перед каждым ответом. При 3 000 вопросов в день это 108 миллионов входных токенов, и к конкретному вопросу относится одна строка из тысячи двухсот. Выгрузку при этом обновляют руками, поэтому на вопрос про цену ассистент отвечает состоянием каталога на момент последней правки промпта.
Разделить слои помогает один признак: меняется ли знание и нужна ли на него ссылка. Промпт держит то, что меняется редко и описывает поведение — тон, формат, порядок шагов, правило отказываться вместо догадки. Поиск держит то, что меняется и должно быть подтверждено документом: регламенты, политики, база знаний. А точное состояние конкретной сущности — статус заказа 10482, остаток по SKU — не ищут текстом вообще. RAG отвечает на «похоже на», и для вопроса «где мой заказ» похожесть не нужна: нужна строка по ключу, то есть вызов API или запрос к базе.
Проверка занимает минуту: опишите, как выглядит эталонный ответ. Если это один и тот же текст для всех, кто спросит, — перед вами документ, и дальше вопрос только в качестве поиска. Если ответ зависит от того, кто спросил, и может измениться за час, — перед вами поле в системе, и единственный честный слой здесь tool call. Промпт не закрывает ни первый случай, ни второй: он задаёт, как говорить, а не что известно.
Неправильный выбор слоя стоит дороже, чем кажется, потому что плохой поиск маскируется под работу. Recall@5 первой версии индекса — 0,62 (ту самую панель мы смотрим дальше в статье): в 38 вопросах из ста модель получает контекст, в котором ответа нет, и отвечает по нему так же уверенно, как по правильному. Формально ответ «опирается на документы», фактически опирается на соседний раздел регламента. Длинный промпт в этом смысле предсказуемее — он ошибается одинаково и виден на глаз.
Слои складываются, а не выбираются раз и навсегда: один ответ может взять политику отказа из инструкции, формулировку условия из найденного регламента и срок доставки из API. Порядок внедрения обратный сложности — сначала проверьте, не решается ли вопрос запросом по ключу, потом добавляйте retrieval, а промпт оставьте для поведения. И если окажется, что нужного факта нет ни в документах, ни в системе — он живёт в переписке менеджеров, — ни один слой не поможет. Сначала появляется поле и его владелец, потом ассистент.
| Вопрос | Слой | Почему именно он | Чем проверяется |
|---|---|---|---|
| Как вы оформляете ответ клиенту? | промпт | правило поведения, меняется раз в квартал | ручной просмотр 20 ответов |
| Когда возвращают деньги за отменённый заказ? | retrieval | ответ лежит в регламенте, нужна ссылка | Recall@k по нужному документу |
| Где мой заказ 10482? | tool call | значение привязано к сущности и меняется за час | сверка с полем в системе |
| Сколько я заказал за год? | запрос к базе | это агрегат, а не текст | сверка с отчётом |
| Сделайте скидку 30% | промпт | границы полномочий, а не знание | доля корректных отказов |
| Когда привезут поставку от партнёра? | нет источника | факт живёт в переписке, а не в системе | сначала поле и владелец |
Назовите документ или поле, откуда берётся эталонный ответ. Если ни того, ни другого нет, задача не в модели и не в поиске: ассистент просто начнёт красиво пересказывать устаревший файл.
- Возьмите десять последних вопросов из лога и помести каждый в один из четырёх слоёв: промпт, retrieval, tool call, запрос к базе.
- Вопросы, для которых слой не нашёлся, — это не задача для AI, а пропущенное поле в системе.
- Стабильное и процедурное знание держите в инструкции: оно не требует ссылки и не стареет между релизами.
- Всё, что меняется и должно быть подтверждено, выносите туда, где есть версия и дата обновления.
Соберите evaluation set до улучшения промпта
Тестовый набор лучше составить из реальных вопросов пользователей, а не только из вопросов, которые удобно придумать разработчику. Для каждого примера сохраните вопрос, роль пользователя, ожидаемый intent, допустимые источники, обязательные факты и критерий полезного ответа. Добавьте негативные вопросы: на них система должна корректно отказаться или объяснить, что данных недостаточно.
Не смешивайте в один набор все сценарии. FAQ, поиск по регламентам и подготовка сводки требуют разных критериев. Пометьте каждый пример типом: lookup, comparison, multi-hop, summarization, refusal. Тогда падение одного класса задач будет видно, а средняя оценка не замаскирует проблему.
case = {
'id': 'refund_policy_01',
'question': 'Когда клиенту возвращают деньги за отменённый заказ?',
'intent': 'lookup',
'expected_sources': ['returns-policy-v3'],
'must_include': ['срок', 'условие отмены'],
'should_abstain': False,
}Средние 71% по набору прячут 40% на отказах: кейсы отбирают из логов и считают по категориям
Состав кейса понятен, остаётся вопрос, откуда их брать и сколько. За месяц в логе 4 200 вопросов. Если разметить руками двести случайных, видно, что восемнадцать формулировок покрывают примерно половину потока — дальше идёт длинный хвост. Набор собирается с двух концов: частые вопросы попадают в него потому, что ошибка в них повторится сто раз за день, редкие — потому, что ошибка дорогая: возврат денег, персональные данные, обещанный срок. В нашем наборе 45 кейсов: 30 из крупных кластеров и 15 редких, но дорогих.
Считать результат нужно по категориям, а не одним числом. Из 45 кейсов прошли 32 — это 71%, и цифра выглядит рабочей. Внутри она распадается: lookup проходит в 15 случаях из 16, multi-hop в 4 из 8, а отказы — в 4 из 10. Дороже всего именно отказы: уверенный ответ там, где данных нет, уходит клиенту и становится обещанием. Поэтому порог релиза формулируется не через среднее, а через категории: ни одна не падает больше чем на один кейс, а категория с отказами не падает вовсе.
Набор делится на две части ещё до первой правки промпта. Development — то, на чём крутят инструкцию, chunking и top-k. Holdout открывается раз в несколько релизов. Причина арифметическая: каждая итерация правок чуть-чуть подгоняет систему под конкретные формулировки, и после двадцати итераций «прошло 40 из 45» означает, что вы выучили свой набор, а не улучшили ассистента. Держите 30 кейсов в development и 15 в holdout, и смотрите на holdout только когда development перестал расти.
Отдельно следите за утечкой самих тестов. Few-shot примеры удобно копировать из набора — и тогда кейс проверяет, запомнила ли модель строку. Правило простое: примеры в инструкции берутся только из development, никогда из holdout, а сборка промпта автоматически проверяет, что ни один holdout-вопрос в её тексте не встречается. Та же утечка приходит обходным путём: кто-то пишет раздел документации по жалобе, которая уже лежит в наборе, и retrieval начинает находить ответ дословно.
Шкалу оценки стоит делать грубой. Десятибалльная у двух людей разъезжается на третьем кейсе; три уровня — прошёл, прошёл с оговоркой, не прошёл — с явным критерием на каждый держатся. Возьмите 20 кейсов, размечайте двумя людьми независимо и посчитайте долю совпадений: пока она ниже 80%, спорит не система, а формулировка критерия. Автоматическую оценку подключают после этого, и про её сверку с людьми речь идёт дальше.
Набор стареет быстрее, чем индекс. Продукт добавляет раздел, поддержка меняет процесс, приходит новый тип вопросов — и кейсы начинают проверять прошлое. Раз в квартал пересчитывайте покрытие: какую долю нынешнего потока вопросов закрывают кластеры, из которых собран набор. Падение с 80% до 50% означает, что регрессии проходят, а половина реальных запросов никем не проверяется.
| Категория | Кейсов | Прошло | Откуда взяли |
|---|---|---|---|
| lookup | 16 | 15 | топ кластеров лога |
| comparison | 6 | 5 | кластеры среднего размера |
| multi-hop | 8 | 4 | вопросы, где ответ в двух документах |
| summarization | 5 | 4 | запросы сводки от руководителей |
| refusal | 10 | 4 | редкие дорогие случаи и границы области |
| всего | 45 | 32 | взвешенный результат — 71% |
Те же 32 пройденных кейса из 45 дают среднее 71%. Добавьте в набор десяток простых lookup-вопросов — среднее вырастет, а отказы останутся на 40%.
FLOOR = {'lookup': 0.90, 'comparison': 0.75, 'multi_hop': 0.45, 'summarization': 0.70, 'refusal': 1.00}
def release_report(results, floor=FLOOR):
by_kind = {}
for case in results:
bucket = by_kind.setdefault(case['kind'], [0, 0])
bucket[0] += 1
bucket[1] += int(case['passed'])
blocked = []
for kind, (total, passed) in by_kind.items():
rate = passed / total
if rate < floor[kind]:
blocked.append(f"{kind}: {passed}/{total} = {rate:.0%} < {floor[kind]:.0%}")
return blocked # пусто — релиз можно публиковать
# holdout не должен попасть в инструкцию
assert not any(case.question in system_prompt for case in holdout)Retrieval: Recall@k и MRR
Recall@k показывает, попал ли хотя бы один релевантный документ в первые k результатов. Если нужный регламент найден на позиции 8, Recall@5 будет нулевым, хотя человек мог бы найти ответ при большем списке. MRR учитывает позицию первого релевантного результата и помогает отличить «нашли где-то» от «показали сразу».
Для multi-hop вопроса одного документа может быть мало. Тогда помечай все необходимые источники и считайте, сколько из них попало в контекст. Отдельно следите за долей нерелевантных чанков: слишком широкий top-k увеличивает стоимость и даёт модели больше возможностей выбрать не тот факт.
Recall@k = cases with relevant source in top-k / all casesДля нескольких обязательных источников заранее договоритесь, считается ли кейс найденным при частичном совпадении.
Ответ может быть красивым и неподтверждённым
Groundedness — это проверка, что утверждения ответа поддерживаются переданным контекстом. Она не отвечает на вопрос, правильный ли сам контекст. Если retriever вернул устаревший документ, модель может идеально пересказать его и всё равно дать бизнесу неверное решение. Поэтому нужна связка: качество источника, качество retrieval и опора ответа на контекст.
Разбейте ответ на атомарные claims. Для каждого отметьте: подтверждён, противоречит контексту, не проверяется или добавлен без основания. Для критичных процессов автоматическую оценку дополняй ручной разметкой. LLM-as-a-judge полезен как быстрый фильтр, но его оценки нужно сверять с людьми на калибровочной выборке.
| Наблюдение | Вероятная причина | Следующая проверка |
|---|---|---|
| нужного документа нет в top-k | retriever или chunking | Recall@k и примеры чанков |
| контекст правильный, ответ добавляет факт | generation / prompt | claim-level groundedness |
| ответ верный, но устарел | источник или freshness | версия документа и дата обновления |
| ответ осторожно отказывается всегда | порог или политика abstain | coverage по типам вопросов |
| пользователь не делает следующий шаг | UX и формат ответа | task success и feedback |
Полезность — не только правильность
В рабочем процессе нужно измерять, помог ли ответ выполнить задачу. Пользователь может получить фактически корректное объяснение, но не понять, какой документ открыть, кому написать или какие поля заполнить. Для разных сценариев зафиксируйте task success: найден ли нужный регламент, сократилось ли время ответа, не пришлось ли делать повторный поиск.
Онлайн-метрики собирайте осторожно. CTR по ссылкам может вырасти из-за любопытства, а не из-за полезности. Доля повторных вопросов может означать как плохой ответ, так и сложность исходной задачи. Сочетайте события интерфейса, явный feedback и выборочную ручную проверку диалогов.
Метрики нужно смотреть по типу сценария, роли и версии индекса.
Что мониторить после запуска
После запуска качество не застывает. Документы обновляются, меняются названия разделов, появляются новые типы вопросов и растёт база пользователей. Храните версию индекса, retriever, top-k, latency, стоимость и результат проверки. Иначе команда не сможет понять, почему вчерашний ответ стал хуже.
Нужен маршрут для обратной связи. Плохой ответ должен превращаться в evaluation case с исходным вопросом, контекстом и ожидаемым поведением. Через несколько недель это станет живым набором регрессий, который полезнее абстрактного «потестировали на десяти примерах».
- Доля вопросов с корректным abstain.
- Recall@k по ключевым сценариям.
- Groundedness и citation correctness.
- Latency и стоимость одного ответа.
- Доля повторных вопросов и ручных эскалаций.
- Качество по ролям и чувствительным источникам.
Минимальный план внедрения
Для первого релиза не нужно строить огромную систему оценок. Выберите 30–50 реальных вопросов, разметьте источники и обязательные факты, зафиксируйте baseline и прогоняйте набор после каждого изменения retriever, chunking или prompt. Публикуйте не только среднюю оценку, но и пять самых дорогих ошибок с владельцем исправления.
Материалы по теме
AI-ассистент — это не чат. Это рабочий процесс
Как бизнесу думать об AI-внедрении: не промпты ради промптов, а повторяемый процесс с входами, проверками и метриками качества.
Почему ML-модель ломается после запуска: утечка признаков и дрейф
Модель с ROC-AUC 0,94 на валидации даёт 0,68 в проде. Как найти утечку признаков по времени их доступности и как поймать дрейф данных и концепта по PSI, качеству и сегментам.

Юнит-экономика: что это и как посчитать на примере магазина
Пошаговый расчёт юнит-экономики: от заказа после скидок и возвратов до маржи, CAC и точки безубыточности. Таблица и сценарии на учебном магазине.