Есть жанр статей про RAG, который выглядит так: подключаем векторную базу, берём эмбеддер, сверху реранкер, для верности — гибридный поиск с BM25, и вот у нас продакшн-ready система. Двадцать строк кода, стрелочки на схеме, всё летает.
Я прошёл этот путь до конца на реальных данных. И почти каждое «правильное» улучшение из этого списка на моих данных сделало хуже. BM25 уронил метрику. Реранкер уронил метрику. Дважды, в двух разных конфигурациях. Единственное, что реально помогло, — не код, а несколько дней возни с данными и замерами.
Это не статья «RAG не работает». RAG работает. Это статья о том, что настоящая работа в RAG — не в коде, который пишется за день, а в eval-харнессе, чистке корпуса и честных замерах, которые показывают, что из индустриальных дефолтов вам подходит, а что вредит. И проверить это можно только на своих данных — «так принято» тут не аргумент.
Откуда взялась задача
Мой знакомый сделал бота для модерации групп. В отличии от большинства современных ботов построенных на текстовых фильтрах и списках, его бот решает большинство задач на основе локальной ИИ-модели. Если интересно, то можно почитать вот тут. Бот уже висит в нескольких группах и с задачей своей справляется. Я, после 20 лет работы в веб-разработке осознал, что с развитием ИИ мой труд уже скоро станет невостребован и решил заняться темой ИИ и агентов как будущей профессией. Сначала написал сервис для генерации графики на основе разложения слабого интента на оси неопределенности и закрытие их вопросами для клиента. Потом, просматривая вакансии на hh наткнулся на такую нишу, как RAG-системы. Ну и загорелся. Изучать новое лучше на живых задачах, поэтому вместо сборок туториалов я решил сделать что то, что реально решает проблему. Тема скама мне достаточно интересна, за несколько лет работы с криптой и проектами в этой нише я сталкивался с ним ни раз. Увы, несмотря на развитие технологий, большинство бирж и проектов решают эту задачу по старинке: набирают штат дешевой рабочей силы из разных часовых поясов, которая отвечает пользователям, ставят кучу ботов для закрытия простых дыр и пытаются бороться информированием. Полное исследование — на отдельной странице, здесь коротко. Главная проблема в том, что скам пользователей уже давно эволюционировал и если раньше постили ссылки на конкурсы прямо в группах, то сейчас весь скам идет в личных сообщениях и атакующий может даже не состоять в группе. Вторая проблема в том, что большинство людей по ряду причин не читают закрепы, регулярные напоминания тонут в потоке новых сообщений и основные усилия должны быть направлены на адресное предупреждение и скорость, а не количество сообщений.
Я собрал публичные экспорты 11 крупных чатов криптобирж — около 3.9 млн сообщений за полгода — и прогнал по ним классификатор, который ищет одну конкретную вещь: сообщения от людей, у которых что-то случилось. Заморозили средства, завис вывод, потерян доступ к аккаунту, не проходит верификация.
Таких — тысячи. И вот ключевая цифра: в большинстве чатов публичный ответ от админа получают от 19% до 42% обратившихся, медиана около 30%. Две трети людей с проблемой не получают публичного ответа вообще. Не потому что модерация плохая — а потому что поток превышает пропускную способность тех, кто на смене.
Отсюда идея модуля, о котором эта статья: раз админы не успевают, пусть человеку, написавшему «не могу вывести», бот сам подкинет ссылку на прошлый ответ админа на такую же проблему. Не заменить поддержку — подстраховать. Нашлось похожее — показали, не нашлось — молчим, админ подхватит. Единственное “но” — бот должен писать только новым пользователям или тем, кто не писал давно. В противном случае это будет авто-ответ на все вопросы, что очень быстро убьет любой чат, потому как если человек получил ответ на свой вопрос, желание общаться дальше у него пропадает.
Технически это классический RAG: база вопросов-ответов, векторный поиск по вопросам, отдаём привязанные ответы. Казалось бы, тот самый «день кода». На деле день ушёл на код, а две недели — на то, чтобы понять, работает ли он.
Чтобы что-то улучшать, нужно это измерять. А чтобы измерять — нужен честный эталон
Первое, что я сделал, — eval-линейка. Набор вопросов, к каждому размечены правильные ответы из корпуса. Метрика — hit@3: попал ли правильный ответ в топ-3, которые увидит пользователь.
И тут первый урок, который стоил мне нескольких дней: эталон занижал сам себя.
Я размечал по одному правильному ответу на вопрос — брал самый подходящий. Логично для обучающего датасета, где нужен один эталон. Но для замера ретрива это ошибка: в корпусе на один вопрос часто есть несколько взаимозаменяемых ответов. Бот выдаёт годный ответ, а метрика засчитывает промах — потому что id другой.
Когда я стал разбирать «промахи» глазами, оказалось: бот находит правильную тему, ставит верный по смыслу ответ в топ, но с id, которого нет в моей разметке. Метрика показывала 38%, а реальная цифра была заметно выше — просто эталон был кривой.
Мораль, которую стоит повесить на стену: прежде чем чинить систему, убедись, что твоя метрика не врёт. Я чуть не пошёл улучшать поиск, который на самом деле работал, — спотыкалась разметка, а не модель.
Ещё одна ловушка того же класса. Классические precision@k и recall@k не умеют учитывать право системы промолчать. А у меня молчание — штатный и желательный режим: нет ответа в базе — не выдумывай. Пришлось завести свои метрики: silence (зря промолчал, хотя ответ был), noise (выдал мимо) и none_ok (правильно промолчал на вопрос без ответа). Без них картина была бы слепой ровно в том месте, которое для этого бота важнее всего.
Диагностика: где именно теряется качество
Когда эталон стал честным, я добавил в замер диагностику, которая, по-хорошему, должна быть в любом RAG-проекте, а её почти нигде нет. Для каждого вопроса — на какой позиции стоит правильный ответ в сыром топ-50, до всякого порога.
Это разделяет две совершенно разные болезни, которые обычно сваливают в одну:
-
Правильный ответ есть в кандидатах, но стоит не в топ-3 (позиции 4-20) → проблема ранжирования → теоретически чинит реранкер.
-
Правильного ответа нет в топ-50 вообще → проблема полноты поиска → реранкер бессилен, нужен другой эмбеддер или лексический поиск.
Без этого разделения улучшать RAG — гадание. Ты не знаешь, тебе нужен реранкер, BM25 или новый эмбеддер, и берёшь то, про что чаще пишут в блогах.
Цифры на моих данных:
|
метрика |
значение |
|---|---|
|
hit@3 (что видит пользователь) |
74% |
|
hit@10 |
81% |
|
hit@20 |
84% |
|
hit@50 (потолок recall) |
86% |
|
правильного нет в топ-50 |
14% |
Читается так: ранжирование у меня работает прилично — почти всё, что найдено, лежит близко к верху. А потолок в 86% означает, что в 14% случаев правильного ответа нет даже в топ-50, и это упор в эмбеддер, а не в порядок выдачи. Больше 74% я подниму, только если поменяю сам эмбеддер, — но об этом в конце.
Поворот первый: порог решает больше, чем любая модель — и он бесплатный
Прежде чем тянуться к тяжёлой артиллерии, я посмотрел на самое дешёвое — порог отсечения по близости. Ниже порога — система молчит.
Оказалось, что 26 процентных пунктов hit@3 терялись на пороге. Правильный ответ был в топ-3, но его близость — чуть ниже отсечки, и его глушило. Сырой hit@3 — 75%, а в проде при пороге 0.55 — 49%. Разница уходила не в поиск, а в одну неправильно выставленную константу.
И тут важный вывод, который сломал мне интуицию: порог RAG нельзя выбрать «объективно». Он зависит от роли бота в продукте.
-
Если бот — финальный ответчик (заменяет поддержку), то ошибиться хуже, чем промолчать: высокий порог, режем сомнительное.
-
Если бот — подстраховка (как у меня: админ всё равно ответит), то промах = молчание = нулевой вред, человек просто ждёт админа как раньше. А вот выдать уверенно неправильное — плохо: собьёт с толку, подорвёт доверие.
Одна и та же система, один и тот же ретрив, но правильный порог отличается в зависимости от того, что вы строите. Метрика без определения роли продукта бессмысленна: сначала «что хуже — промолчать или ошибиться», и только потом число.
Это было единственное изменение за весь проект, которое улучшило результат. Всё остальное — ниже.
Поворот второй: BM25 сделал хуже, потому что сигнал был не с той стороны
Гибридный поиск — вектор плюс лексический BM25 — это индустриальный дефолт. «Вставил и забыл», как мне справедливо заметили. BM25 хорошо ловит точные токены: тикеры, коды сетей, номера — то, что векторный поиск смазывает. У меня в промахах как раз мелькали LTC, USDC, base chain — казалось бы, идеальный случай для BM25.
Я построил FTS5-индекс, прикрутил слияние через RRF, прогнал замер. Результат:
|
режим |
hit@3 |
|---|---|
|
чистый вектор |
74% |
|
вектор + BM25 (гибрид) |
55% |
BM25 не помог — он уронил метрику на 19 пунктов.
Почему — я понял за пять минут одним запросом к базе ещё до полного замера. BM25 ловит точные токены. Но эти токены (USDC, LTC) есть в вопросах пользователей, а мой корпус — это вопросы-образцы, на которые ответили админы, и они сформулированы обобщённо: «не могу вывести», «аккаунт заблокирован». Точных тикеров в корпусе почти нет. Проверка SELECT ... WHERE question LIKE '%USDC%' вернула пусто.
То есть лексический сигнал, который ищет BM25, был не на той стороне: он в запросе, а не в индексе. BM25 не находил точные совпадения (их негде найти) — он подмешивал лексически похожие, но семантически неверные записи и вытеснял правильные ответы вниз. На позиции 1 у чистого вектора было 35 правильных ответов, у гибрида — 25. BM25 разбавил хорошее шумом.
Вывод не «BM25 плохой». BM25 отличный — когда лексический сигнал есть в корпусе. Вывод в том, что «стандарт индустрии» проверяется smoke-тестом на своих данных за пять минут, а не переносится на веру. Я потратил бы день на прод-интеграцию того, что структурно не могло сработать.
Поворот третий: реранкер тоже сделал хуже — и это было предсказуемо
Ладно, BM25 не зашёл. Но реранкер-то — это святое. Cross-encoder берёт пары «запрос + кандидат» вместе и переранжирует точнее, чем bi-encoder. Он должен вытащить те правильные ответы из позиций 4-20 в топ-3.
Проверил две модели, два способа сравнения (запрос с вопросом кандидата и с ответом), поверх гибрида и поверх чистого вектора. Лучший результат:
|
режим |
hit@3 |
|---|---|
|
чистый вектор |
74% |
|
вектор + реранкер |
64% |
|
гибрид + реранкер |
57% |
Реранкер во всех конфигурациях сделал хуже. На чистом векторе — уронил с 74% до 64%.
Механику видно на позициях: у чистого вектора 35 правильных ответов на позиции 1, после реранкера — 30. Реранкер утащил 5 правильных ответов с первого места вниз, а поднял снизу меньше, чем испортил сверху.
И это, задним числом, было предсказуемо. Есть общее правило, которое стоит запомнить: реранкер опасен, когда базовый поиск уже хорош. У меня 35 из 51 правильного ответа уже на позиции 1 — верхушка почти идеальна. Чтобы реранкер помог, он должен поднимать низ, не трогая верх. А универсальный реранкер на 118 млн параметров понимает мой узкий домен (короткий крипто-жаргон, смесь языков) хуже, чем эмбеддер, дообученный на моих же данных. Поэтому его перестановки в среднем вредят: downside больше upside. Когда базовый поиск слабый — реранкер спасает. Когда сильный — он рискует сломать то, что уже работает.
Что в итоге сработало
Свожу весь путь улучшений в одну таблицу — она и есть главный результат:
|
режим |
hit@3 |
|---|---|
|
чистый вектор + калибровка порога |
74% |
|
вектор + реранкер |
64% |
|
гибрид (вектор + BM25) |
55% |
|
гибрид + реранкер |
57% |
Чистый векторный поиск с правильно выставленным порогом побеждает все «улучшения». Единственное, что реально помогло за весь проект, — калибровка порога под роль продукта. Ноль строк модного кода. BM25, реранкер, две модели, два способа сравнения — всё проиграло.
74% при потолке recall в 86% — для короткого многоязычного крипто-жаргона это честный, добротный результат. Не выдающийся, но и упор здесь не в ранжирование (оно работает), а в recall эмбеддера. Поднять выше можно только сменой или дообучением эмбеддера — и вот это был бы следующий шаг, если бы я строил систему на продажу. Не реранкер, не гибрид, а базовая модель, которая находит. Плюс — курирование корпуса: половина «нет в топ-50» оттого, что конкретного ответа в базе просто нет, только роутер «обратитесь в поддержку». Никакая модель не найдёт то, чего в корпусе нет.
Что из этого забрать
Если убрать конкретику про крипточаты, остаётся несколько вещей, которые, кажется, переносятся на любой RAG-проект:
Сначала эталон, потом всё остальное. Кривая разметка занижала мою метрику и чуть не отправила чинить работающий поиск. Час проверки промахов глазами сэкономил бы дни.
Разделяйте recall и ранжирование. hit@3 против hit@20 одним взглядом показывает, что чинить: полноту поиска или порядок выдачи. Без этого выбор инструмента — гадание.
Порог зависит от роли продукта, а не от данных. Сначала ответьте «что хуже — промолчать или ошибиться», потом выбирайте число.
«Best practice» — это гипотеза, а не факт. BM25 и реранкер — прекрасные инструменты, которые на моих данных навредили. Проверять их надо на своих данных, и часто это дело пяти минут. Отрицательный результат, пойманный замером, ценнее, чем слепое следование стандарту, — он экономит дни интеграции того, что не работает.
Данные важнее модели. Самый большой рычаг оказался не в коде, а в том, что лежит в корпусе и как размечен эталон. Отсюда и заголовок: RAG — это про замеры, а не про код. Код здесь — двадцать строк. Всё остальное — данные и честность к цифрам.
Это первая из цикла статей. Здесь — про то, почему RAG оказался про данные и замеры. Во второй разберу, как вся система собрана технически: детектор просьб о помощи, типизатор, архитектура. Ссылка появится здесь.
Автор: noroots


