Большинство прототипов RAG под капотом выглядят одинаково. Кто-то подает PDF в конвейер, нарезает текст на аккуратные чанки по 512 токенов, сбрасывает их в векторную базу данных и считает задачу выполненной. Для демонстрации возможностей это может выглядеть впечатляюще. Но в продакшене такая система разваливается.
Фиксированный размер чанка не учитывает, что именно он разрезает. Он может разделить юридический контракт прямо посреди пункта о возмещении ущерба. Он может запихнуть пять несвязанных API-эндпоинтов в одно контекстное окно и завалить модель шумом. Это заставляет вас извлекать больше фрагментов, чем необходимо, что увеличивает задержку и сжигает токены. Результат — неполные ответы, галлюцинации и разочарованные пользователи.
Мы полностью пересмотрели наш уровень извлечения и перестроили его с нуля. Итогом стала система, которая достигла 95% полноты (recall), сократив при этом задержку на 40%. Вот как именно мы это сделали.
Почему фиксированные чанки не работают в продакшене
Значение по умолчанию в 512 токенов — это не осознанный дизайнерский выбор. Это побочный продукт контекстных окон ранних моделей эмбеддингов и стандартных настроек популярных библиотек. Это легко реализовать, но полагаться на это — катастрофично.
Документы неоднородны. Юридический пункт может занимать семьсот токенов без явных разрывов. Разрежьте его на пятьсот двенадцать — и вы получите два оторванных друг от друга фрагмента. Когда юрист или сотрудник службы комплаенса спросит об ограничении ответственности, система вернет лишь половину обязательства. Языковая модель либо галлюцинирует недостающую часть, либо, что еще хуже, заявит, что ограничения не существует.
Документация API страдает от обратной проблемы. Чанк в пятьсот токенов может поглотить целый модуль: заголовки аутентификации, коды ошибок, лимиты запросов и схемы вебхуков. Когда разработчик спросит, как обработать AUTH_4027, ретривер выдаст кашу из несвязанных функций. У модели не останется выбора, кроме как усреднить их в невнятную массу.
Плохое разбиение также раздувает задержку. Слабые фрагменты означают, что вам требуется больший top-k, чтобы охватить тему. Больше чанков — длиннее промпты. Длиннее промпты — медленнее генерация и выше счета. Пользовательский опыт умирает от тысячи мелких порезов.
Подбирайте размер чанка под тип документа
Мы перестали считать токены и начали вчитываться в материал. Правильная стратегия разбиения зависит от структуры источника.
Юридические документы требуют рекурсивного разбиения по символам с границами, учитывающими структуру пунктов. Сплиттер соблюдает иерархию: сначала он ищет заголовки разделов, затем нумерованные параграфы и только потом естественные разрывы предложений. Он никогда не разрывает подпункт и не разделяет смысловую фразу между чанками. Когда вы извлекаете отрывок о возмещении ущерба, вы получаете полный пункт, лимиты и исключения.
Документация API требует разбиения с учетом структуры. Мы парсим данные по определению функции, а не по бюджету токенов. Каждый чанк содержит полную сигнатуру функции, описания её параметров и непосредственно прилегающие заметки по обработке ошибок. Если разработчик ищет конкретный метод, он получает весь контракт, а не фрагмент, попавший в произвольный разрез.
Тикеты службы поддержки зашумлены и нелинейны. Тред может начаться с отчета об ошибке, содержать временное решение и закончиться внутренней заметкой об эскалации. Семантическое разбиение (semantic chunking) определяет смену тем, измеряя схожесть эмбеддингов между предложениями. Мы допускаем разрывы только на естественных тематических границах, чтобы разговор о сбоях при входе оставался отдельным от последующих вопросов о циклах оплаты.
Вики-системы были самыми сложными. Они разветвленные, перекрестно связанные и слабо организованные. Мы использовали агентское разбиение (agentic chunking), где легковесная LLM читает страницу и решает, где делать разрывы, основываясь на тематической связности. Это стоит немного дороже на этапе индексации, но полученные чанки самодостаточны и готовы к извлечению. Страница о лучших практиках развертывания разбивается на логические блоки: предварительные проверки, процедуры отката и настройка мониторинга, а не на произвольные текстовые блоки.
Гибридный поиск: ключевые слова и векторы вместе
Плотный векторный поиск понимает смысл. Но он ужасен в поиске точных строк. Если пользователь ищет конкретный код ошибки, такой как AUTH_4027, или имя клиента, например, "Stark Industries", векторные эмбеддинги могут промахнуться, так как они оптимизированы под концептуальную близость, а не под точность на уровне символов.
У чистого поиска по ключевым словам через BM25 есть обратный недостаток. Он идеально найдет AUTH_4027, но упустит концептуальную связь между "authorization failure" и "login denied".
Мы запускаем оба процесса параллельно. BM25 и векторный поиск работают независимо над одним и тем же корпусом. Их списки результатов объединяются с помощью Reciprocal Rank Fusion, который переупорядочивает кандидатов, балансируя их позиционные ранги. Вам не нужны откалиброванные веса. Вы просто получаете точность точного совпадения и интуитивность семантического поиска в едином ранжированном списке.
Затем мы добавляем cross-encoder реранкер. Это отдельная модель, которая оценивает каждый фрагмент текста относительно исходного запроса, выдавая сигнал релевантности гораздо более точный, чем у любого из ретриверов по отдельности. Это добавляет около 50 миллисекунд задержки. Это увеличивает полноту (recall) на 15 процентов. Если вам важно качество ответов, этот компромисс не обсуждается.
Расширение запроса: исправьте поиск до того, как он начнется
Плохие запросы — это грязный секрет любой системы поиска. Пользователи пишут не так, как устроено ваше эмбеддинг-пространство. Они пишут «it broke». Они вставляют усеченные стек-трейсы. Они используют внутренний жаргон, который ваш индекс никогда не видел.
Мы трансформируем запрос еще до того, как он попадет в индекс. Сначала мы расширяем один запрос до трех-пяти разнообразных поисковых терминов. Если исходный запрос — «payment failed», мы также ищем «transaction error», «billing declined» и «charge unsuccessful
