Большинство инженерных команд сталкиваются с одной и той же проблемой при использовании генерации с дополненной выборкой (RAG). Они следуют стандартным руководствам: разбивают документы на фиксированные фрагменты по 512 или 1024 токена, пропускают их через одну модель эмбеддингов и обращаются к векторной базе данных с простым поиском top-k. На слайдах это выглядит убедительно. В продакшене всё разваливается.
Фиксированные фрагменты не учитывают содержание. Они могут запросто разрезать юридический контракт на полуслове, оставив пункты об ответственности разбросанными по двум несвязанным частям текста. Они могут свалить в один раздутый фрагмент описание целого API-эндпоинта, из-за чего конкретный параметр, о котором спросил пользователь, просто утонет в шуме. А когда поиск работает медленно, каждая миллисекунда задержки напрямую бьет по пользовательскому опыту. Мы усвоили этот урок на собственном горьком опыте. Затем мы полностью перестроили наш слой поиска. Наш показатель recall@10 вырос с 78% до 95%. При этом задержка не выросла — она резко сократилась.
Проблема «копипастного» RAG
Стандартный стек RAG стал своего рода настройкой по умолчанию. Маленькие фрагменты, одна модель эмбеддингов, векторный поиск — и готово. Такой подход работает на демо, потому что в демо используются чистые вопросы и аккуратные документы. Реальные данные никогда не бывают аккуратными.
Юридические документы имеют иерархическую структуру. Разделы содержат подразделы, а подразделы — пункты. Если разрезать их грубым счетчиком токенов, вы разрушите те самые связи, которые необходимы модели для рассуждений. Документация API тоже структурирована, но иначе. Сигнатура функции, её параметры, возвращаемое значение и пример использования образуют логическую единицу. Если попытаться втиснуть это в фиксированное окно токенов, вы либо обрежете пример, либо перегрузите фрагмент несвязанными функциями. Тикеты в техподдержку — это хаотичная разговорная речь с резкими сменами тем. Вики-страницы разветвлены и полны перекрестных ссылок. Одна стратегия разбиения не может подойти ко всему сразу, однако команды регулярно внедряют именно такой подход. Мы перестали делать вид, что это возможно.
Стратегическое разбиение: подбирайте метод под материал
Мы перешли к разбиению с учетом контента (content-aware chunking). Для юридических документов мы используем рекурсивное разбиение, которое учитывает иерархию документа. Это позволяет сохранять пункты целиком и поддерживать связи «родитель-потомок» между разделами. Для документации API мы разработали разбиение с учетом функций (function-aware chunking), где каждая функция или эндпоинт рассматриваются как граница. Если описание параметра слишком длинное, фрагмент расширяется вокруг этой функции, а не вокруг лимита токенов. Для тикетов техподдержки мы используем семантическое разбиение, которое определяет естественные границы тем. Когда клиент внезапно переходит от жалобы на оплату к техническому багу, разделение происходит именно на этом моменте. Для вики-систем и неструктурированных баз знаний мы используем агентское разбиение (agentic chunking), где легковесная LLM анализирует текст и решает, где должна проходить значимая граница. Это сложнее настроить, чем простое разделение по количеству символов, но именно это отличает работающий поиск от поиска, основанного на догадках.
Гибридный поиск: почему одного векторного поиска недостаточно
Векторный поиск понимает смысл, но может пропускать точные совпадения. Если пользователь вставит код ошибки, например ERR_CONNECTION_RESET_0x5F3, семантическое сходство может поставить его ниже абзацев, в которых просто обсуждаются ошибки сети в целом. BM25, с другой стороны, находит точные строки, но упускает концептуальную близость. Вам нужны оба метода.
Мы запускаем векторный поиск и BM25 параллельно. Затем мы объединяем результаты с помощью Reciprocal Rank Fusion (RRF), который нормализует оценки из двух разных пространств поиска, не пытаясь привести их к единой шкале. После объединения мы пропускаем лучшие кандидаты через реранкер на базе cross-encoder. Это добавляет небольшую задержку, но прирост точности того стоит. Реранкер читает запрос и каждого кандидата вместе и присваивает оценку релевантности, которая гораздо точнее, чем косинусное сходство исходных эмбеддингов. На практике такая комбинация находит точные коды ошибок, которые пропускает чистый векторный поиск, и при этом выдает концептуально связанные шаги по устранению неполадок, которые проигнорировал бы ключевой поиск.
Расширение запроса: исправление ввода пользователя перед поиском по индексу
Пользователи не пишут идеальные поисковые запросы. Они задают многоступенчатые вопросы, вроде «почему мой последний деплой провалился и как мне сделать откат?», что требует нахождения двух разных пластов знаний и их связывания. Или они задают расплывчатые вопросы, которые плохо соотносятся с индексом.
Мы трансформируем запросы перед поиском. Многоступенчатый вопрос разбивается на подвопросы. Нечеткое намерение разворачивается в несколько конкретных поисковых запросов. Мы обнаружили, что развертывание одного пользовательского запроса в пять различных поисковых запросов позволяет повысить полноту поиска (recall) с 78% до 96%. Речь не о том, чтобы давать LLM более сложные промпты. Речь о том, чтобы дать системе поиска больше попыток найти нужный контекст. Каждый сгенерированный запрос охватывает другой аспект или терминологию, а объединенные результаты создают полную картину.
Байесовская оптимизация: хватит гадать
Как только у вас появляются различные стратегии разбиения на фрагменты (chunking), гибридный поиск и расширение запросов, вы сталкиваетесь с новой проблемой. Слишком много параметров. Размер фрагмента, процент перекрытия, вес вектора по отношению к весу BM25, пороги переранжирования и значения top-k — все они взаимодействуют нелинейным образом. Ручная настройка превращается в игру в угадайку.
Мы перестали гадать. Мы рассматриваем...
