RAG — это про замеры, а не про код. История одного бота, где почти всё «правильное» сделало хуже. rag.. rag. rag ai.. rag. rag ai. rag pipeline.. rag. rag ai. rag pipeline. RAG система.. rag. rag ai. rag pipeline. RAG система. RAG Техники.. rag. rag ai. rag pipeline. RAG система. RAG Техники. retrieval augmented generation.. rag. rag ai. rag pipeline. RAG система. RAG Техники. retrieval augmented generation. retrival augumented generation.

Есть жанр статей про 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

Источник