ESPN DeportesChepo de la Torre: No pagamos el precio por ser los mejoresESPNRice bests Boone's belief by slugging homers 40, 41The Jerusalem PostIsrael Election 2026: What does Itamar Ben-Gvir's Otzma Yehudit stand for?InquirerEx-DPWH exec in Zaldy Co case to cite lack of equipment as defenseBollywood HungamaKaran Johar’s Rs. 480 crore jump: How he went from 5th to 4th on Hurun’s Bollywood Rich ListRTP DesportoDinis Ferreira vice-campeão do mundo de triatlo de junioresPunchJUST IN: Anthony Joshua, Tyson Fury to fight in Cardiff December 11Global NewsKenneth Law’s sentencing hearing to hear from more victims’ familiesWirtualna PolskaMetropolita przemyski apeluje o modlitwę w intencji ofiar z Jarosławian-tv"Es ist beängstigend": Ex-England-Stürmer Carroll: "Wurde sexuell missbraucht"CBS NewsTrump to host China's Xi at White House with AI and tariffs on the agendaRai NewsSalerno, 20enne accoltellata alle spalle da uno sconosciuto mentre cammina per strada
The Daily Newsstand · Free, Always
Thursday, September 24, 2026

RAG умер, да здравствует RAG: история одного Reranker и как мы оживили нейропоиск среди 35 000 страниц

Translate

Привет, Хабр! На связи Александр Михеев, ML-инженер Центра гибридного интеллекта в Рунити. В прошлой статье мы рассказывали, как строим корпоративного RAG-ассистента. На небольшой выгрузке Confluence поиск выглядел вполне прилично. Но когда корпус вырос почти до 35 тысяч страниц и 80 тысяч чанков, наверх всё чаще стали попадать документы, похожие по теме, но не отвечающие на конкретный вопрос.

Мы пошли проверять привычные гипотезы: поменяли способ выгрузки Confluence, усложнили чанкирование, вынесли код в отдельные вектора, попробовали более крупную embedding-модель и добавили структурный граф. Почти ничего из этого не решило проблему. Главная подсказка пришла из метрик: нужный материал в большинстве случаев уже находился, но стоял слишком низко. В итоге лучший прирост дал не еще один retrieval-канал, а reranker.

Навигация по тексту:

Когда база знаний стала слишком большой для «просто dense»

Первая крупная выгрузка содержала 7 991 страницу. После расширения индекса до 20 пространств Confluence корпус вырос до 34 949 страниц и 79 671 чанка: страниц стало в 4,4 раза больше, чанков — примерно в 5,5 раза.

Вместе с полезной документацией в индекс попали старые версии инструкций, обзорные архитектурные страницы, таблицы, JSON, макросы, эндпоинты и внутренние идентификаторы. На небольшом корпусе dense-поиск легко находит ближайший по смыслу документ. На большом вокруг запроса появляется десяток почти правильных кандидатов: они похожи семантически, но только один содержит нужный факт. Поэтому точная инструкция может оказаться на 6–7-й позиции.

Ручная проверка десятка запросов здесь уже бесполезна: можно улучшить их и незаметно ухудшить сотни других. Поэтому первым результатом R&D стала не новая модель, а нормальный benchmark.

Мы сгенерировали 5 000 запросов по 1 700 уникальным страницам: 4 459 prose-запросов и 541 code/API-запрос. В набор вошли обычные вопросы на русском, exact-keyword, смешанные русско-английские формулировки, troubleshooting и API-термы. Expected page задавалась исходной страницей, по которой строился вопрос, а не выбиралась из результатов поиска. Это LLM-generated и затем LLM-corrected regression benchmark, а не human gold: исходная страница не обязательно является единственным релевантным документом или корпоративным source of truth.

Все эти запросы распределили между тремя экспертами: Minimax, Kimi и Qwen. По итогам первого прохода 3 859 запросов признали корректными, 1 097 отправили на переписывание, еще 44 пометили как некорректные. На втором проходе Minimax переписал 1 002 запроса, 139 оставил без изменений; итоговый набор сохранил все 5 000 строк.

На page-level считали Page@1, Page@5, Page@10 и MRR@10, где Page@1 для нас особенно важен, так как емкость контекстного окна ограничена, а передавать в локальную LLM всё больше кандидатов — значит платить latency и пропускной способностью. Отдельно собрали chunk-benchmark на 3 000 запросов и считали Chunk@1/5/10/20 и EvidenceRecall. Chunk-gold там получен эвристически, а не полностью размечен людьми, поэтому это инструмент сравнения конфигураций, а не абсолютная production-истина. Page-level и chunk-level — разные выборки, поэтому мы не склеиваем их в одну «линию роста».

Как мы собирали benchmark: генерация запросов, независимый аудит и повторная обработка

Как мы собирали benchmark: генерация запросов, независимый аудит и повторная обработка

Dense + sparse: первый заметный прирост

Baseline был классическим: BGE-M3 dense embedding размерностью 1024 → cosine search в Qdrant → top-k chunks. На исправленном benchmark он показал Page@1 52,06%, Page@5 73,86%, Page@10 80,18% и MRR@10 0,6150.

То есть тему модель понимала хорошо, но правильная страница была первой лишь примерно в половине случаев. Особенно dense проигрывал на точных технических сигналах: именах параметров, эндпоинтах, идентификаторах и фрагментах команд.

Для этого добавили sparse-представление того же BGE-M3. Это не BM25 и не SPLADE: transformer вычисляет learned weight для входных token IDs, а итоговый score считается по пересекающимся токенам запроса и документа. Контекст влияет на вес терма, но модель не добавляет отсутствующие слова как SPLADE.

BM25, BGE-M3 sparse и SPLADE: три разных подхода к sparse-представлению

BM25, BGE-M3 sparse и SPLADE: три разных подхода к sparse-представлению

На vLLM sparse получили через task=token_classify, отдельно забрали token IDs через /tokenize с add_special_tokens=false, сопоставили IDs и веса, удалили нули и сохранили результат в Qdrant как named SparseVector. Один chunk в итоге имел два векторных представления: dense и sparse.

Оставалась проблема: cosine score и learned lexical score живут в разных шкалах. Складывать их напрямую бессмысленно, поэтому мы использовали weighted Reciprocal Rank Fusion. После grid search лучшей оказалась конфигурация k=60, по 100 кандидатов от dense и sparse и вес dense:sparse = 4:1. 

Dense против Dense + Sparse + RRF: прирост по Page@1, Page@5 и Page@10

Dense против Dense + Sparse + RRF: прирост по Page@1, Page@5 и Page@10

Page@1 вырос сразу на 12,1 п.п. Но дальше мы всё еще надеялись, что можно найти более сильный способ собирать кандидатов.

Пять гипотез, которые выглядели разумно

Чтобы не растягивать историю на пять одинаковых разделов, сведу эксперименты в одну таблицу. 

Сводка проверенных гипотез и решений по результатам экспериментов

Сводка проверенных гипотез и решений по результатам экспериментов

В нашем эксперименте проиграл не LlamaIndex как библиотека, а конкретный путь ConfluenceReader → Markdown/text → дополнительная очистка. В преобразованный текст попадали артефакты макросов, а URL и длинные технические строки местами сокращались. При этом SentenceSplitter из LlamaIndex остался в рабочем baseline. Поэтому вывод относится к использованной версии reader и настройкам подготовки текста, а не к LlamaIndex в целом. Мы также проверили Qwen/Qwen3-Embedding-4B в bfloat16, с полной размерностью 2 560 и batch size 64, sparse-канал оставили от BGE-M3. В этой конфигурации Qwen dense дал Page@1 29,8%, а Qwen dense + BGE sparse — 53,8% против 64,0% у BGE hybrid. Общий паттерн был один: мы всё время пытались добавить системе еще кандидатов. А затем посмотрели на метрику, которую до этого недооценивали.

Проблема была в самом порядке

Для каждой анализируемой страницы мы эвристически выбирали от одного до пяти допустимых evidence chunks по evidence_quote, source excerpt и обязательным термам. Evidence@k показывает долю запросов, для которых хотя бы один такой фрагмент оказался в первых k результатах. На историческом наборе из 3 000 запросов baseline дал Chunk@5 54,8%, Chunk@10 64,5%, Chunk@20 72,8%, но Evidence@10 уже достигал 89,5%, а Evidence@20 — 94,1%. 

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

Для этого подошел cross-encoder reranker. Dense- и sparse-энкодеры кодируют query и document независимо — поэтому ими можно быстро искать по всему корпусу. Cross-encoder получает пару [query, candidate chunk] целиком и оценивает релевантность после совместного чтения. Это дороже, зато top-100 после RRF — уже управляемый объем.

Мы взяли проверенный для русского и английского языка BAAI/bge-reranker-v2-m3, multilingual cross-encoder на 568 млн параметров, и прогнали его по ста кандидатам. 

Page MRR@10 вырос с 0,597 до 0,769, Chunk MRR@10 — с 0,412 до 0,629. Обратите внимание на разницу: EvidenceRecall вырос совсем немного, а top-1/top-5 — резко. Reranker не столько нашел новые документы, сколько поднял уже найденные правильные кандидаты наверх.

Эффект cross-encoder reranker: основной выигрыш пришелся на порядок результатов

Эффект cross-encoder reranker: основной выигрыш пришелся на порядок результатов

На локальном DEV-стенде с RTX 4090 48GB исторический прогон с reranking top-100 показывал около 2,04 запроса в секунду. Это только ориентир той конфигурации: batch size, максимальная длина пары, precision и concurrency в сохраненных артефактах не зафиксированы. В финальном frozen-5000 эксперименте чистый end-to-end latency-тест не проводился, потому что использовались vector и candidate caches. 

Что подготовили к A/B

Финальный retrieval-пайплайн получился двухэтапным: 

В production ACL-фильтр должен применяться как metadata filter уже при dense/sparse retrieval, до fusion иreranker. Тогда закрытые документы не участвуют в ranking и не вытесняют разрешенные результаты. Перед формированием ответа доступ проверяется повторно. На упрощенной схеме ACL показан в финальном контуре, но фактическая проверка начинается до выдачи кандидатов.

Финальная архитектура retrieval-пайплайна: два retrieval-канала, weighted RRF и cross-encoder reranker

Финальная архитектура retrieval-пайплайна: два retrieval-канала, weighted RRF и cross-encoder reranker

Главные выводы

Рост базы знаний усложняет саму задачу поиска. На маленьком корпусе достаточно найти что-то похожее. На большом нужно отличить правильный документ от десятков почти правильных.

Data pipeline важнее удобства loader. Для технической документации потерянный URL, code block или heading может стоить дороже, чем несколько сотен строк собственного ingestion-кода.

Сложнее — не всегда лучше. Трехкратный рост числа chunks, отдельные code-вектора и graph expansion выглядели логично, но добавляли конкуренцию между кандидатами быстрее, чем полезный сигнал.

Размер embedding-модели сам по себе ничего не гарантирует. Qwen3-Embedding-4B в нашей конфигурации проиграл BGE-M3. Но в отдельных экспериментах GigaEmbeddings 10B на frozen-5000, наоборот, подняла dense Page@1 с 51,78% у BGE до 62,12%, а связка Giga dense + BGE sparse + reranker достигла 78,08%. Значит, важны не только число параметров, но и обучение модели, query instruction, pooling, truncation и совместимость с конкретным корпусом.

Главное — сначала понять, чего не хватает: recall или ordering. Evidence@20 = 94,1% показал, что искать еще больше нам уже почти не нужно. Нужный материал был в выдаче — требовалось лучше его ранжировать.

В итоге апгрейд RAG оказался не заменой одной embedding-модели на другую, а нормальным конвейером: данные → benchmark → dense + sparse candidates → rank fusion → cross-encoder reranking → ACL → ответ со ссылками.

Во второй части отдельно разберу использование RAG в агентной платформе (Harness), безопасность и ограничение выдачи по доступам (ACL-проверки). Следите за обновлениями!

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.