В хорошем тестовом наборе для RAG обязательно есть вопрос, на который база знаний ответить не может. Скажем, сотрудник спрашивает: «Сколько дней отпуска положено при переводе в другой филиал?», а в загруженных документах есть только общее положение об отпусках и ни слова о переводах. Если система выдаёт уверенный, грамотный, оформленный абзац с цифрой, то перед вами не удача, а провал. Красивый ответ здесь хуже пустого: пустой хотя бы честен, а красивый будет скопирован в письмо, в инструкцию, в решение.
Большинство первых наборов проверок состоят из вопросов, на которые ответ точно есть. Это понятно: их легко придумать, легко сверить и приятно смотреть на зелёные галочки. Но такой набор измеряет только одно умение системы: отвечать, когда ответ лежит в базе. Умение молчать, когда ответа нет, и умение заметить, что два документа спорят, остаются без присмотра. А именно они определяют, можно ли системе доверять.
Три типа вопросов, которые нельзя смешивать
Прежде чем писать хоть одну проверку, разложите вопросы по тому, что на самом деле лежит в базе.
- Ответ найден. Нужный факт есть в одном или нескольких фрагментах, и из них можно собрать верный ответ.
- Сведений нет. Тема соседняя, слова знакомые, но ответа в документах нет. Ожидаемое поведение: прямо сказать, что данных недостаточно, и по возможности указать, что именно найдено рядом.
- Версии конфликтуют. Старый регламент говорит одно, новый другое, или два отдела описывают процесс по-разному. Ожидаемое поведение: показать обе версии со ссылками и их даты, а не молча выбрать одну.
Эти типы нужно размечать отдельно, потому что у них разные критерии успеха и разная цена ошибки. Если свалить всё в одну кучу и посчитать «долю правильных ответов», хороший результат на лёгких вопросах скроет провал на трудных. Тридцать вопросов первого типа и два второго дадут красивые девяносто с лишним процентов, и никто не заметит, что оба отрицательных случая провалены.
Откуда брать вопросы? Лучше всего из реальных обращений: журналов поддержки, вопросов коллег в чате, запросов к поиску по внутренней вики. Вопросы «из головы» получаются слишком гладкими: люди в жизни пишут обрывками, с опечатками и с неверными предпосылками. Отдельная полезная категория: вопрос с ложной предпосылкой («Как отключить двухфакторную проверку для администраторов?», когда в документах она обязательна и отключения не предусмотрено). Такой вопрос проверяет, не поддакивает ли система.
Четыре карточки, на которых всё становится видно
Теперь конкретика. Допустим, у нас есть небольшая база внутренних документов компании: регламент отпусков, инструкция по командировкам и два варианта политики возмещения расходов, старый и новый. Ниже четыре тестовые карточки. Это условный пример для иллюстрации; числа в нём придуманы.
| № | Вопрос | Тип | Ожидаемое поведение |
|---|---|---|---|
| 1 | Сколько календарных дней длится основной ежегодный отпуск? | Ответ найден | Назвать срок из регламента отпусков и дать ссылку на раздел, где он указан. |
| 2 | Какой лимит суточных в командировке по России? | Ответ найден (два фрагмента) | Собрать ответ из таблицы лимитов и примечания об исключениях; сослаться на оба места. |
| 3 | Положен ли дополнительный отпуск при переводе в другой филиал? | Сведений нет | Сказать, что в документах такого правила нет; не выдумывать срок; можно упомянуть, что общий регламент о переводах молчит. |
| 4 | В какой срок нужно сдать авансовый отчёт? | Конфликт версий | Показать срок из старой и из новой политики с датами; отметить, какая действует по дате документа, либо попросить уточнить. |
Обратите внимание: у каждой карточки есть не только «правильный ответ», но и описание допустимого поведения. Для карточки 3 эталонного текста нет вообще, и это правильно. Эталон здесь — свойство ответа: он не содержит конкретной цифры про дополнительный отпуск и содержит признание отсутствия данных.
Сначала поиск, потом ответ: две разные поломки
RAG состоит из двух частей, и ломаются они по-разному. Поиск достаёт фрагменты, модель по ним пишет ответ. Если смотреть только на итоговый текст, непонятно, кого чинить. Поэтому проверки стоит разделить на два слоя.
Первый слой: извлечение. Для каждой карточки мы заранее знаем, какие фрагменты должны попасть в контекст. Для карточки 1 это один раздел регламента отпусков. Для карточки 2 два: таблица лимитов и примечание. Для карточки 4 оба документа политики. Для карточки 3 «нужных» фрагментов нет, и вот тут слой извлечения проверяется иначе: смотрим, не подсунул ли поиск что-то, что вводит в заблуждение, например абзац про отпуск по уходу за ребёнком, который по словам похож, а по смыслу не относится.
Второй слой: ответ. Дан набор фрагментов, получился ли из них верный, полный и честный текст. Здесь можно проверять и независимо: подать модели заведомо правильный контекст и посмотреть, справляется ли она. Если с правильным контекстом ответ плох, виновата генерация или формулировка запроса. Если с правильным контекстом ответ хорош, а в рабочей цепочке плох, виноват поиск.
Precision и recall на наших карточках
Понятия precision и recall (точность и полнота) подробно описаны в документации scikit-learn по метрикам, там же различаются F-меры и способы усреднения по классам. Но чтобы они были полезны для RAG, их лучше почувствовать на примерах, а не на формулах.
Возьмём карточку 2 про суточные. Нужны два фрагмента: таблица и примечание. Пусть поиск вернул пять фрагментов: таблицу, примечание и три постороннего содержания (например, про выплаты при заграничных поездках). Из пяти возвращённых полезных два, значит точность извлечения здесь 2 из 5. Из двух нужных найдены оба, значит полнота 2 из 2. Поиск «ничего не потерял», но нагрузил модель шумом. Шум опасен тем, что модель может ухватиться за заграничный лимит и подставить его в ответ про Россию.
Теперь представим другой поиск: он вернул только таблицу. Точность 1 из 1, абсолютная, но полнота 1 из 2. Ответ получится аккуратным и ссылка будет корректной, но про исключения из примечания система не узнает. Это тихая неполнота, самая неприятная: ответ выглядит безупречно.
На карточке 4 про авансовый отчёт полнота важнее всего. Если поиск вернул только новую политику, система уверенно назовёт один срок и не заметит, что у старого другой. Формально фрагмент релевантный, точность стопроцентная, а задача «показать конфликт» провалена. Это хорошая иллюстрация того, почему одной метрики мало: для разных типов вопросов важны разные стороны.
Наконец, карточка 3. Нужных фрагментов нет, значит «полноту» считать не из чего. Зато появляется другая мера: как часто на вопросы без ответа система всё же отвечает по существу. Если в наборе десять таких вопросов, а система отказалась корректно в шести, то на этом классе у неё шесть из десяти. Документация scikit-learn как раз допускает оценку отдельных классов и разное усреднение, и для нашей задачи это важно: считайте показатели по типам вопросов раздельно. Средняя по всему набору цифра будет зависеть от того, сколько вопросов каждого типа вы случайно написали.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Что должно быть в цитате
Ссылка на источник — это не украшение, а проверяемое обещание. Чтобы её можно было проверять автоматически, разложите на свойства:
- Существование. Указанный документ и страница или раздел действительно есть в базе. Модели иногда выдумывают правдоподобные номера пунктов.
- Поддержка. Процитированный фрагмент действительно содержит утверждение, которое им подкреплено. Ссылка на верный документ, но не на то место, формально «рабочая», по сути пустая.
- Достаточность. Если в ответе три утверждения, ссылки должны покрывать все три, а не одно из них.
- Соответствие вопросу. Ответ говорит о том, о чём спросили. Бывает, что фрагмент верный и ссылка точная, но отвечено на соседний вопрос, например про суточные за границей вместо суточных по России.
- Актуальность. Если в базе несколько версий, ссылка ведёт на действующую или явно помечает версию.
Первые два свойства проверяются механически: идентификатор документа есть в списке, цитируемый кусок встречается в тексте источника. Остальные требуют суждения. Тут обычно подключают другую модель в роли судьи, и это допустимо, но с оговоркой: судья тоже может ошибаться, и его оценки нельзя считать эталоном. Разумная практика: вручную проверить выборку решений судьи на тех же карточках, посмотреть, где он расходится с человеком, и поправить формулировку критерия. Пока расхождения заметны, автоматической оценке по этим свойствам верить в полной мере нельзя.
Инструменты для такой работы существуют. Например, в документации Promptfoo описаны assertions: проверки результата, которые можно объединять в группы условий, а также учёт стоимости и задержки; критерии задаются конфигурацией теста. Для нашей задачи это значит, что условие «ответ содержит ссылку на раздел X и не содержит конкретной цифры» можно записать как набор условий и прогонять после каждого изменения. Но любой инструмент лишь исполняет то, что вы описали; если карточки плохие, он аккуратно подтвердит плохое.
Цена ошибки неодинакова
Теперь главный вопрос: что хуже, ложный ответ или лишний отказ? Универсального ответа нет, и именно поэтому нужно решить это явно, до измерений.
Ложный ответ вредит тем, что скрыт. Человек не знает, что ошибся, и действует. Цена зависит от области: в справочнике по меню корпоративной столовой ошибка обойдётся в неудобный обед, в базе по охране труда или по юридическим шаблонам — в реальные последствия. Лишний отказ вредит иначе: человек видит «не нашёл», понимает, что помощи нет, и идёт искать сам. Это потеря времени и постепенная утрата доверия к системе: после десятка пустых отказов ею перестают пользоваться.
Разумно закрепить это таблицей весов, пусть даже грубой. Условный пример для внутренней базы регламентов:
| Ситуация | Тяжесть | Почему |
|---|---|---|
| Ответ с выдуманной цифрой на вопрос без данных | Критическая | Сотрудник поступит по неверному правилу и не заметит. |
| Ответ из устаревшей версии без пометки | Высокая | Звучит убедительно, ссылка формально верна. |
| Верный ответ с неполной ссылкой | Средняя | Результат правильный, но проверить его труднее. |
| Отказ при наличии ответа в базе | Средняя | Теряется время, но вреда решению нет. |
Числа веса вы подберёте сами под свою область; важнее, чтобы разница существовала и была записана. Тогда при сравнении двух версий системы вы не станете радоваться росту «процента верных ответов», если он куплен ценой двух выдуманных цифр в вопросах без данных.
Есть и поведенческий компромисс. Система, настроенная отказывать при малейшем сомнении, будет безопасной и бесполезной. Система, всегда готовая ответить, будет полезной и опасной. Обычно хорошее поведение лежит между: отвечать, когда фрагменты явно поддерживают ответ, и частично отвечать с оговоркой, когда поддержка неполная («в регламенте указан общий срок, но для вашего случая правило не найдено»). Такие частичные ответы стоит включить в тестовые карточки отдельно, иначе их качество вообще никто не оценивает.
Повторяемое сравнение после каждого изменения
Тестовый набор ценен не разовым замером, а тем, что его можно повторить. Изменили размер фрагментов при разбиении документов, добавили реранкер, сменили модель, поправили системный запрос — прогнали набор и сравнили. Про то, когда реранкер действительно даёт прирост, есть отдельный разбор: реранкер в RAG и скачок качества. Но и в этом случае вывод «стало лучше» нужно подтверждать на своих карточках, а не на чужих примерах.
Чтобы сравнение было честным, соблюдайте несколько правил.
- Фиксируйте всё, кроме изменяемого. Если вы одновременно меняете модель и размер фрагментов, вы не узнаете, что сработало. Меняйте по одному параметру за прогон.
- Храните результаты каждого прогона. Нужны не только итоговые цифры, но и сами ответы, список возвращённых фрагментов и версия конфигурации. Без этого нельзя разобрать, почему карточка 4 вдруг перестала проходить.
- Смотрите на разницу по карточкам, а не только на среднее. Две версии могут иметь одинаковый общий результат, но у одной ломаются вопросы без ответа, а у другой конфликтные. Для вас это совершенно разные системы.
- Повторяйте прогоны. Ответы языковой модели могут различаться от запуска к запуску, поэтому единичный прогон нестабилен. Если на карточке результат то зелёный, то красный, это сам по себе сигнал: система ненадёжна на этом типе вопросов.
- Учитывайте стоимость и задержку. Версия с реранкером и большим контекстом может отвечать лучше, но медленнее и дороже. Promptfoo, например, предусматривает такие метрики наряду с проверками содержимого, так что компромисс можно записать в тот же прогон.
Набор нужно и поддерживать. Документы в базе меняются, регламенты обновляются, и карточка с «верным ответом» через полгода может устареть. Закрепите владельца набора, дату последней проверки каждой карточки и причину её появления. Когда из-за реального сбоя в работе появляется новый случай, добавляйте его в набор в тот же день: так набор растёт из настоящих ошибок, а не из воображения. И не переносите результат на будущее механически: процент прохождения на сегодняшних документах ничего не обещает для документов, которые вы загрузите через месяц.
Отдельно стоит помнить, что прохождение набора не равно нагрузочному испытанию. Зелёные карточки говорят о качестве ответов на одиночных вопросах. Как система поведёт себя при множестве одновременных запросов, покажет только отдельный замер на вашем окружении.
Где запускать прогоны
Сам по себе набор из нескольких десятков карточек — небольшая нагрузка: это серия запросов, сохранение результатов и сравнение. Для такой работы удобна отдельная среда, не связанная с рабочей системой: туда можно разворачивать кандидатную версию поиска, не трогая пользователей. На VPS MAATRIX есть NVMe-диски, процессоры AMD EPYC и root-доступ, а значит, можно поставить свой тестовый стенд, базу эмбеддингов и инструмент прогонов. В публичном каталоге также предложены n8n и Nextcloud с установкой, а точное наличие и операционная система проверяются в форме заказа; n8n может пригодиться, если вы хотите запускать прогон по расписанию или после обновления документов. Тарифы Ba, Uab, Heka, Maat и Djed отличаются числом vCPU, объёмом памяти и диска (от 1 vCPU и 2 ГБ RAM до 16 vCPU и 32 ГБ), но эти параметры описывают ресурсы, а не гарантированную ёмкость, поэтому подходящий размер стоит определять замером на вашей модели и вашем наборе. GPU в предложении не заявлен, так что если вы собираетесь запускать модель локально, этот момент надо уточнять заранее; для прогонов через внешний API локальные вычисления не нужны.
Один вопрос, который всё показал
Представьте, что вы улучшили поиск: уменьшили размер фрагментов, чтобы находить более точные места. На большинстве карточек стало лучше: ответы короче, ссылки точнее, полнота выросла. Общий результат вырос. Можно было бы запускать. Но карточка 4, про срок авансового отчёта, стала красной. Мелкие фрагменты разорвали таблицу сроков, и в контекст попала только строка из новой политики, а строка старой осталась в соседнем куске, который в выдачу не вошёл. Система без колебаний назвала один срок и сослалась на новый документ. Ссылка верная, текст гладкий, предупреждения о конфликте нет.
В пользовательском потоке эту ошибку никто бы не заметил: ответ выглядел безупречно, а сотрудник, у которого ещё действовал старый срок, получил бы неверный ориентир. Один вопрос в наборе показал то, что общий процент скрыл. Благодаря ему изменение не ушло в работу в том виде, а превратилось в задачу: сохранять таблицы целиком или поднимать соседние фрагменты вместе. И после правки та же карточка стала первой, которую вы проверяете при любом следующем изменении поиска.