Резюме
База знаний инженерной компании насчитывает около 5 300 документов, это около 48 тыс. фрагментов: инженерная документация, архитектурные решения, деловые документы, исследования. Язык русский, с английскими терминами и именами сервисов.
Задача: ИИ-агенты и инженеры должны находить нужный документ по смыслу вопроса, даже когда вопрос сформулирован не словами документа. Агенты обращаются к поиску через MCP-сервер. Поиск гибридный: плотный вектор и BM25, слияние RRF, затем кросс-энкодер переоценивает лучших кандидатов.
Поиск работал и раньше, но никто не знал, как часто агент получает нужный документ. За два дня, 13 и 14 октября 2026 года, мы построили замер полноты. Замер, подготовка эксперимента с контекстом фрагментов и сверка с практикой вскрыли четыре дефекта, которые ничем себя не выдавали; все четыре убраны в поиске по базе знаний. Устройство поиска сверено с международной практикой по 102 источникам.
Результаты. Наборы маленькие, размер указан при каждом числе.
- Нужный документ в первой пятёрке выдачи: 18 → 24 из 27, MRR 0,54 → 0,79. Это 27 контрольных вопросов к поисковому серверу: 21 к базе знаний и 6 к двум другим корпусам на том же сервере.
- Пустых ответов сервера на тех же 27 вопросах: 4 → 0. Все четыре давали вопросы к двум другим корпусам.
- Нужный документ среди кандидатов до реранка: 64 → 67 из 67 (27 + 40 вопросов).
- Ядро поиска соответствует практике. Чего делать не стоит, тоже записано.
Как перенести ту же схему в закрытый контур, описано в разделе 9.
1. Исходное состояние: «вроде работает»
Поиск, который отдаёт не тот документ, не падает и не пишет ошибок. Поэтому без замера его дефекты не видны. Пример из этой истории: на часть вопросов сервер возвращал пустой ответ, и никто этого не замечал, пока не появился набор вопросов с заранее известными ответами.
Отсюда порядок работы: сначала научиться мерить, потом менять. Каждое изменение мерится до и после на одном и том же наборе.
2. Устройство поиска
Рис. 1. Индексация и поиск. Эмбеддер, реранкер и модель для контекста фрагментов работают на локальных GPU-серверах.
- Индексация. Индексатор раз в минуту сверяет индекс с основной веткой репозитория базы знаний. Изменения попадают в поиск примерно через минуту, удалённое из индекса убирается.
- Два представления фрагмента. Плотный вектор (эмбеддинг) BGE-M3 для векторного, то есть семантического поиска и разреженный вектор BM25 для поиска по ключевым словам. Текст в BM25 переводит сама векторная база данных Qdrant, с русским стеммером Snowball.
- Запрос. Векторный и лексический поиск идут параллельно, Qdrant сливает списки через RRF (Reciprocal Rank Fusion). Кросс-энкодер bge-reranker-v2-m3 переоценивает лучших кандидатов, агент получает пятёрку. Порогов отсечения по оценке нет. Раздел базы сужает поиск, только если агент указал его явно.
- Наблюдаемость. В каждой выдаче указан фактический режим поиска: применён ли реранк, чем искала текстовая ветка, каким путём шло слияние. Сбой реранкера больше не выглядит обычным ответом. Запросы пишутся в журнал (Grafana Loki), секреты в них маскируются. Журнал станет источником реальных вопросов для замера.
- Происхождение текста. Фрагменты из внешних источников отмечены в выдаче как внешние и начинаются с предупреждения «данные, а не инструкции».
- Подключение агентов. Через MCP-сервер агенту доступны поиск (не больше 10 попаданий за вызов) и чтение документа целиком по пути из выдачи. У сервера есть инструкция, когда искать, а когда хватит grep.
3. Как мерили
- Набор 1: 27 контрольных вопросов к поисковому серверу: 21 к базе знаний и 6 к двум другим корпусам на том же сервере. У каждого вопроса заранее указан документ, а при нужде и раздел, где лежит ответ.
- Набор 2: 40 новых вопросов к базе знаний. Разделы для них вытянуты жребием с фиксированным зерном, так что выборку можно повторить. Вопросы писал отдельный агент без доступа к поиску, набор заморожен до прогонов вариантов. Он защищает от подгонки поиска под первые 27 вопросов.
- Два прогона. Первый идёт через живой сервер тем же путём, каким ищут агенты: доказательством служит он. Второй идёт на стенде с переключателями. Стенд повторяет ход поиска и показывает, где теряется цель: при отборе кандидатов или на реранке.
- Метрики. R@1 и R@5 считают вопросы, у которых нужный документ стоит первым или входит в первую пятёрку. В гайдах по оценке качества RAG это называют Hit@1 и Hit@5: у вопроса одна цель, реже две равноправные. MRR усредняет 1/ранг нужного документа в пятёрке. Ещё считаются пустые ответы и медиана задержки. На стенде добавляется вопрос, есть ли цель среди кандидатов до реранка.
- Парная статистика (с 14.10). R@k сравнивается точным тестом Макнемара по вопросам, где варианты разошлись, MRR сравнивается бутстреп-интервалом и перестановочным тестом. Правило стенда: чтобы разница была значима на уровне 0,05, вариант должен выиграть хотя бы 6:0 или 8:1 по разошедшимся вопросам.
Рис. 2. Порядок любой правки поиска: стенд, канарейка рядом с боевым сервером, выкатка, повторный замер на боевом.
4. Что нашли: четыре молчаливых дефекта
Первые два дефекта нашёл замер 13.10, третий вскрылся при подготовке эксперимента с контекстом фрагментов, четвёртый нашла сверка с практикой.
1. Автоподбор раздела. Если агент не указывал раздел, сервер угадывал его по словам запроса и ставил жёсткий фильтр. В базе знаний совпадение по подстроке, порой случайное, уводило поиск в чужой раздел, и нужный документ не попадал в пятёрку. В двух других корпусах угаданного раздела нет вовсе, и там фильтр давал пустой ответ: отсюда все 4 пустых ответа из 27. Из 9 промахов 7 терялись ещё при отборе кандидатов. Автоподбор убран: раздел теперь только явный.
2. Затухание по дате индексации. Сервер понижал давно проиндексированные документы. Но индексатор переиндексирует только изменённые файлы, и дата означала «когда правили», а не «насколько актуально»: неизменные справочные страницы проигрывали свежим журналам. Затухание выключено. Оговорка: на стенде разница небольшая (MRR 0,73 без затухания и 0,70 с ним), решение держится прежде всего на механизме и стоит в списке на перепроверку.
3. BM25, которого не было. Документация называла текстовую ветку BM25, а в коде это был фильтр без оценки: он пропускал только фрагменты, где есть все слова запроса. Длинный описательный вопрос обнулял ветку целиком. 14 октября в живую коллекцию добавлен настоящий BM25, без остановки поиска.
Текстовая ветка поиска
Настоящий BM25
Разреженный вектор в Qdrant с русским стеммером. Каждое слово запроса даёт свой вклад в оценку, слова работают по «или».
Фильтр по словам без оценки
Пропускает только фрагменты, где есть все слова запроса, и не ранжирует их.
4. Семантический кэш выдачи. Кэш отдавал сохранённую выдачу, если новый запрос похож на прежний: косинус не ниже 0,96. За 10 часов он не дал ни одного попадания. Зато запросы про порт 8080 и порт 8081, которые отличаются одним числом, дали косинус 0,965 и прошли бы порог: агент получил бы выдачу чужого запроса. Кэш выключен.
Одну идею замер отклонил:
Глубина реранка (предварительно, на 27 вопросах)
Оставить как есть: 30 кандидатов, 15 в реранк
Шире не значит лучше: добранные фрагменты приводят в реранк конкурентов нужного документа.
100 кандидатов, 60 в реранк
Все цели доходят до кандидатов, но в пятёрке их больше не становится.
5. Результаты
Через живой сервер, на одном и том же наборе из 27 вопросов к поисковому серверу:
| показатель | до правок | после правок 13.10 | после BM25, 14.10 |
|---|---|---|---|
| пустых ответов | 4 | 0 | 0 |
| нужный документ первым (R@1) | 12 | 17 | 19 |
| нужный документ в пятёрке (R@5) | 18 | 23 | 24 |
| MRR | 0,54 | 0,73 | 0,79 |
- Нужный документ среди кандидатов до реранка (стенд, 27 + 40 вопросов): 64 из 67 со старым фильтром, 67 из 67 с BM25.
- 40 новых вопросов (живой сервер): R@5 37 → 37, MRR 0,82 → 0,82. На этом наборе BM25 прироста не дал. Позже в тот же день R@5 стал 36 из 40: новые документы вытеснили цель одного вопроса. Это дрейф индекса; ловить его должен ежедневный прогон замера, он в бэклоге.
- Задержка не выросла. Медиана 13.10: 655 → 641 мс. Медиана 14.10: 568 → 483 мс на 27 вопросах и 712 → 471 мс на 40.
У этих чисел три оговорки.
- Прирост от BM25 на 27 вопросах (+1 по R@5, +2 по R@1) по правилу самого стенда остаётся в пределах шума. Настоящий эффект BM25 в другом: цель есть среди кандидатов у всех 67 вопросов вместо 64.
- Для правок 13.10 парный тест не считался: стенд получил его только 14.10. Доводом здесь служит механизм (фильтр раздела давал пустые ответы и чужие разделы), а не число.
- R@5 упирается в потолок около 91 % (61 из 67). Различать варианты дальше помогут трудные вопросы и метрики R@1 и MRR, а не рост набора.
6. Сверка с мировой практикой
14.10 мы сверили каждую подсистему со 102 источниками: документацией вендоров, независимыми бенчмарками, академическими работами. Выводы атаковал отдельный агент (состязательный проход). Двадцать чисел, на которых держатся выводы, и пятнадцать цитат перепроверены по живым страницам: подтвердились все.
| подсистема | как устроено | вердикт сверки |
|---|---|---|
| ядро | плотный вектор + BM25 → RRF → кросс-энкодер | соответствует практике |
| слияние | RRF без весов | соответствует: без размеченных данных RRF служит разумным умолчанием |
| нарезка | по заголовкам, путь заголовков в тексте фрагмента | соответствует; семантическая нарезка не окупается |
| эмбеддер и реранкер | BGE-M3 и bge-reranker-v2-m3 | соответствует: в задачах поиска на русскоязычных бенчмарках BGE-M3 в верхней группе |
| лексика | русский стеммер Snowball, токенизатор Qdrant | не проверено, как режутся IP-адреса, имена и порты |
| конвейеры запроса | два: через MCP и через командную строку | расхождение: в практике один путь, который мерится и пишется в журнал |
| кэш выдачи | был семантический, с порогом | исправлено 14.10: ложные попадания пороговых кэшей описаны в литературе |
| сбои | реранкер и BM25 деградировали молча | исправлено 14.10: режим поиска в каждой выдаче |
| журнал запросов | не было | исправлено 14.10 |
| замер | 27 + 40 вопросов | главная дыра: набор маленький и упирается в потолок |
| графы знаний | нет | правильно, что нет |
Сверка дала и список того, чего делать не стоит:
- GraphRAG и родственные схемы: на одношаговых вопросах графы не лучше обычного RAG, а индексация многократно дороже.
- Менять эмбеддер по лидерборду: лидерборд годится для короткого списка, решает A/B на своём стенде.
- Late chunking: метод рассчитан на усреднение векторов токенов, а BGE-M3 берёт вектор из токена [CLS].
- Семантическую нарезку: в независимых замерах она не окупается.
Источники для проверки. Выборка из 102; эти страницы открывались при сверке 14.10.2026:
- Qdrant: Hybrid and Multi-Stage Queries: RRF как безопасное умолчание.
- Microsoft: Azure AI Search, hybrid retrieval and reranking: гибрид и ранкер против чистого вектора, пост 2023 года.
- From BM25 to Corrective RAG: независимый бенчмарк стратегий поиска.
- ruMTEB и RusBEIR: эмбеддеры и реранкеры на русском.
- Anthropic: Contextual Retrieval: метод контекста фрагментов.
- RAG vs. GraphRAG: графы против обычного RAG.
- vCache: ошибки семантических кэшей.
- Urbano и др., 2019: проверка значимости в оценке поиска.
7. Эксперимент: контекст фрагментов (Contextual Retrieval)
Метод Anthropic: к каждому фрагменту модель дописывает одно-два предложения о том, из какого он документа и о чём он. Мы добавляем ещё до восьми слов, которыми фрагмент искали бы, но которых в нём нет. Этот текст идёт в плотный вектор, в BM25 и в реранк.
- Где. Контекст пишет локальная LLM (Qwen 3.8 27B через vLLM) на локальном GPU-сервере. Для этой работы текст базы внешним провайдерам не отправляется.
- Защита от выдумок. Слова с цифрами, которых нет в документе, выбрасываются при записи. Отдельный аудит сверяет каждое число из готовых контекстов с текущим текстом документа. После полного прогона сверено 45 255 контекстов, из них с цифрами 21 047; неподтверждённых нашлось 3, их удалили из кэша, модель напишет их заново.
- Режим наполнения. Первичный прогон закончен 14.10: 48 320 фрагментов. Контексты копятся, боевой индекс не меняется.
О включении решает правило, записанное заранее, а не ощущение. R@5 уже у потолка. А формулировка «нет значимых регрессий» на 67 вопросах ничего не отсекает: даже 5 проигрышей без единого выигрыша дают p = 0,0625. Независимые замеры поверх структурной нарезки, как здесь, дают единицы процентов или шум, и все они сделаны на английских корпусах. Поэтому мы ждём прироста меньше, чем в цифрах Anthropic.
Правило записано 14.10, до прогона. Контекст включается, только если выполнено всё:
- на 67 вопросах R@1 и MRR не ниже, чем у текущего поиска;
- по R@1 ухудшились не больше 2 вопросов из 67, и каждый разобран поимённо;
- аудит чисел в контекстах перед самым переключением даёт 0 неподтверждённых;
- на стресс-наборе из 15 вопросов улучшились хотя бы 3 и ухудшился не больше 1 (это фрагменты, где нужная сущность названа только в пути к файлу; вопросы к ним писала не та модель, что писала контексты);
- медиана задержки выросла не больше чем в 1,5 раза;
- перед переключением коллекция получает версию и алиас, чтобы откат занимал секунды.
Если первые два условия выполнены, а стресс-набор не улучшился, контексты продолжают копиться без включения. Если нарушено одно из первых двух, переключения нет, а перевернувшиеся вопросы разбираются.
Результат (15.10). Перед сравнением 1 408 файлов переиндексировали: их фрагменты лежали в старой нарезке, и контекст к ним не находился. Затем на стенде текущий поиск и вариант с контекстом сравнили на 103 вопросах: 67 основных, 15 из стресс-набора и 21 контрольный.
- Нужный документ первым: 70 → 77 вопросов, MRR 0,76 → 0,81, медиана задержки не выросла (715 и 623 мс). Разница незначима: 13 вопросов выиграли, 6 проиграли, p = 0,17.
- На стресс-наборе нужный документ стал первым у 9 вопросов вместо 6.
- На 67 основных вопросах 4 потеряли первое место, а правило допускает не больше 2. Контекст в боевой поиск не включён, фрагменты продолжают его копить.
Разбор четырёх вопросов. В одном выше встал соседний документ, который отвечает на вопрос не хуже цели. В одном цель выпала из выдачи из-за старой копии того же документа в репозитории. В двух соседний фрагмент обогнал цель из-за дописанных к нему слов для поиска: они совпали с формулировкой вопроса. Почти весь прирост дал контекст в тексте для реранкера, а не в векторах. Следующая проверка: контекст без дописанных слов в тексте для реранкера или только в реранкере, по новому правилу, записанному до прогона, и на новых вопросах.
8. Что не вышло и что осталось открытым
- Агенты в репозитории чаще ищут сами. По журналам за всю историю поиск вызывали 33–45 раз за 516 сессий Claude Code и ни разу за 513 сессий Antigravity, где за то же время grep вызывали 1 479 раз. Эксперимент 14.10 на 12 задачах (одна модель, задачи из наборов замера; в условии с поиском агенту велели начинать с него): точность одинаковая, 12 из 12 и с поиском, и без него. С поиском вызовов инструментов меньше на 30 % (189 → 132), времени на 20 % (695 → 559 с). Но разница незначима. По вызовам поиск выиграл в 8 задачах, проиграл в 3, одна вничью, знаковый тест даёт p ≈ 0,23. По времени с поиском быстрее в 8 задачах, медленнее в 4, p ≈ 0,39. Выигрыш почти целиком приходится на описательные вопросы, где слова вопроса не совпадают со словами документа. Это вывод, а не замер: поиск нужнее всего там, где у агента нет доступа к файлам, например в чат-приложениях и в сессиях других проектов.
- Замер маленький и у потолка. 27 + 40 вопросов, R@5 около 91 % (61 из 67). Нужны трудные срезы: идентификаторы, короткие запросы, смесь русского и английского, «старое против актуального». И нужны реальные запросы из журнала.
- Слепое пятно. Запросов из одних ключевых слов (имена, номера, версии) в замере нет. А на таких запросах чистый BM25 обгонял гибрид в замере Microsoft (пост 2023 года).
- Актуальность. Статуса «действует / устарело» у документов пока нет: старая и новая версии факта могут лежать рядом.
- Перепроверка. Решения, принятые по разнице в 1–3 вопроса из 27 (например, отказ от затухания по дате), внесены в список на перепроверку.
9. Та же схема в закрытом контуре
Все компоненты индексации и поиска работают на локальных серверах, и поиск не зависит от облачных API: Qdrant, модель эмбеддингов, реранкер и модель для контекста фрагментов. Поэтому схему можно перенести в закрытый контур, ответ там должна писать локальная модель. Вместе с архитектурой переносится порядок работы:
- замер с первого дня: контрольный набор из реальных вопросов с известными документами-ответами, заморозка до прогонов, парная статистика; каждая правка проходит через стенд и канарейку;
- фактический режим поиска в каждой выдаче и журнал запросов с маскированием секретов;
- если права доступа к документам разные, проверять их нужно в самом поиске на сервере: инструкция модели доступ не ограничивает.
Рекомендуемая конфигурация для промышленной эксплуатации. Локальный GPU-сервер с картами класса NVIDIA RTX A5000/A6000 под эмбеддер, реранкер и локальную модель ответа. Это рекомендация: на картах этого класса система не проверялась. Конфигурацию подбирают замером на корпусе и нагрузке конкретной площадки.
Вопросы и ответы
Почему RAG-система «работает», но находит не то?
Поиск, который отдаёт не тот документ, не падает и не пишет ошибок. Здесь такие дефекты нашлись только на наборе вопросов с заранее известными ответами: сервер сам угадывал раздел и сужал поиск, понижал давно проиндексированные документы, вместо BM25 работал фильтр без оценки, а кэш выдачи мог отдать результат чужого запроса (раздел 4).
Гибридный поиск или векторный: когда нужен BM25?
Векторный поиск находит документ по смыслу, но чаще промахивается на идентификаторах, портах, именах и версиях. Поиск по ключевым словам (BM25) их находит. Здесь настоящий BM25 вместо прежнего фильтра без оценки довёл до кандидатов все 67 целей вместо 64. Слияние идёт через RRF без весов: без размеченных данных подбирать веса не на чем.
Как измерить качество поиска в RAG-системе?
Нужен контрольный набор реальных вопросов с известными документами-ответами, замороженный до прогонов. Метрики: R@1 и R@5 (Hit@1 и Hit@5) и MRR. Варианты сравниваются парными тестами на одних и тех же вопросах, а правило решения записывается до прогона. Когда R@5 упирается в потолок, различать варианты помогают трудные срезы и R@1, а не больший набор.
Можно ли сделать такой поиск без облачных API?
Поиск здесь от облачных API не зависит: индекс, эмбеддинги, реранкер и модель для контекста фрагментов работают на локальном оборудовании. В закрытом контуре ответ должна писать локальная модель, а если права доступа к документам разные, проверять их нужно в самом поиске (раздел 9).
Как подключить ИИ-агента к базе знаний через MCP-сервер?
Агент подключается к MCP-серверу по HTTP. Через него доступны поиск и чтение документа целиком, а инструкция сервера подсказывает, когда искать, а когда хватит grep.