
Если вы строили RAG в 2023, ваш стек выглядел плюс-минус одинаково. BERT-семейство (BGE, e5) для семантики, BM25 для буквальных совпадений, cross-encoder для реранкинга, какой-нибудь Qdrant сверху. Этим жили два года, и многие до сих пор так живут.
Но если посмотреть, кто реально гоняется в продакшене у команд, которые ушли вперёд, ландшафт другой. Энкодеров там почти нет. Эмбеддит файнтюненная LLM. Реранкер — тоже LLM. Инференс на SGLang, а не на ONNX. И вся обвязка перестроилась под это.
Эта статья про то, что поменялось и как переиспользовать этот стек у себя. Особенно если вы работаете в узком домене, где готовых датасетов нет.
Что было: классический стек и его потолок
Дефолтный retrieval делал три вещи. Энкодер берёт текст, прогоняет через bidirectional трансформер, собирает информацию в один вектор — через [CLS] токен или усреднение. Этот вектор кладётся в векторную базу, по нему ищется ближайший сосед по косинусу. Параллельно работает BM25 для точных совпадений по словам — нужен, потому что эмбеддер плохо ищет коды, артикулы, номера законов. Сверху cross-encoder переранжирует топ-K и отдаёт пользователю.
Где этот стек упёрся:
Узкие домены он не вытягивает. Если BGE на претрейне не видел юридический корпус, вы не дотянете его до приличного качества простым bge-large вместо bge-base.
Длинный контекст — до свидания. Большинство энкодеров живёт в районе 512 токенов, чуть больше у Jina и Nomic. Поэтому появился весь зоопарк chunking-стратегий в LangChain.
Инструкции под задачу нельзя задать на лету. Хотите retrieval — учите модель под retrieval, под классификацию — учите отдельно.
Инференс-стек умер. ONNX Runtime поддерживается номинально, flash attention туда так и не завезли, prefix caching нет. Сообщество перешло на vLLM и SGLang под декодеры, а энкодерам ничего не досталось.
Логичный ход — не чинить энкодеры, а взять то, во что вкладываются все, и приспособить под эмбеддинг.
Главный сдвиг: декодер становится эмбеддером
Идея кажется странной. Декодер обучен предсказывать следующий токен, у него causal mask — каждый токен видит только себя и предшественников. Усреднять по токенам бессмысленно: первый токен ничего не знает о предложении.
Решение: пулят последний токен. Конкретно — <eos>. Он «видел» всю последовательность и собирает в себе её смысл. На этом трюке стоит большинство современных эмбеддеров.
Базовая статья тут — LLM2Vec (McGill, 2024). Авторы показали рецепт из трёх шагов: включить bidirectional attention, дообучить через masked next-token prediction, добить unsupervised contrastive learning по схеме SimCSE. Уже на этом этапе модель обгоняет энкодеры сопоставимого размера. Дальше — supervised contrastive с инструкциями и hard negatives.
Что даёт переезд на декодерную архитектуру:
Гигантский pretrain в наследство. В энкодеры заливали десятки миллиардов токенов. В современные декодеры — триллионы. Эта разница видна в качестве эмбеддингов.
Понимание инструкций. В промпт можно положить описание задачи: «Найди документ, отвечающий на вопрос». Та же модель переключается между retrieval, классификацией и кластеризацией без переобучения.
Длинный контекст из коробки. Qwen3-Embedding держит 32k токенов. Можно класть параграф целиком, не дробить.
Весь декодерный тулинг. Flash attention, KV-cache, prefix caching, vLLM, SGLang. Всё это работает.
NV-Embed добавил важный нюанс к рецепту: вместо тупого <eos> они используют latent attention pooling — отдельный обучаемый слой, который собирает информацию со всех токенов. На MTEB это даёт +1–2 пункта стабильно.

Современные модели
Что брать сегодня, если хочется decoder-based эмбеддер с HuggingFace:
Модель | Размер | Контекст | MTEB | Особенности |
Qwen3-Embedding-0.6B | 0.6B | 32k | ~64 | MRL (матрёшка), 100+ языков |
Qwen3-Embedding-4B | 4B | 32k | ~68 | Тот же тренировочный пайплайн |
Qwen3-Embedding-8B | 8B | 32k | 70.58 | №1 в multilingual MTEB на момент релиза |
NV-Embed-v2 | 7B | 32k | 72.31 | Latent attention pooling, на Mistral |
E5-Mistral-7B-instruct | 7B | 32k | ~66 | Первая массовая instruction-модель |
Qwen3-Embedding из коробки умеет Matryoshka Representation Learning: один и тот же эмбеддинг можно усечь до 256 / 512 / 1024 размерности без переобучения, и качество просядет мягко, а не катастрофически. На практике это значит, что один индекс гоняется с разной точностью на разных стадиях пайплайна.
Под русский язык из коробки никто не идеален. У Qwen3 покрытие неплохое, но если домен специфичный, файнтюнить всё равно придётся.
Что делать, если в вашем домене нет датасетов
Тут самое интересное и больное. Раньше переход на новый эмбеддер в узкий домен — нефтянка, юриспруденция, бухгалтерия, медицина — упирался в один и тот же вопрос: где взять данные. Готовых датасетов нет, на HuggingFace их никто не выкладывает, собрать вручную — это месяцы работы команды разметчиков. На этом этапе многие проекты буксовали.
Декодерный эмбеддер этот блок снимает не до конца, но сильно ослабляет. Сразу с нескольких сторон.
Знания уже внутри модели. Декодерный претрейн на 15 триллионах токенов включает огромные куски Common Crawl, GitHub, arXiv, юридические корпуса, медицинские форумы, технические PDF. Модель уже видела термины своего домена, даже если он узкий. Файнтюнить нужно ранжирование пар «запрос → документ» в вашей конкретной задаче. Сам язык модель и так знает. Это на порядок дешевле.
Инструкции вместо обучения. В промпт можно положить:
Instruct: Найди в корпусе нормативных документов раздел, описывающий требования к промышленной безопасности на нефтеперерабатывающих установках. Query: {запрос пользователя}
И та же модель без файнтюна работает на этом домене лучше, потому что инструкция переводит её в нужный режим. Это бесплатный буст качества на 3–5 пунктов recall.
Синтетическая разметка той же LLM. Логика такая: у вас есть документы. Их обычно много. Запросов к ним нет, потому что либо продукт ещё не запущен, либо логи слишком скудные.
Берёте декодерную LLM (можно ту же, что эмбеддит, можно сильнее — Qwen3-32B, Llama, что угодно с API) и просите её сгенерировать запросы к каждому документу:
Документ: {текст документа} Сгенерируй 5 вопросов, которые мог бы задать инженер, ищущий именно этот документ. Вопросы должны быть разной формулировки: формальные, разговорные, с аббревиатурами, с опечатками.
На выходе — десятки тысяч пар (query, positive_doc) бесплатно. Качество не идеальное, но для дообучения эмбеддера хватает. Дальше синтетику фильтруют: прогоняют через эмбеддер до файнтюна, смотрят, какие пары он уже правильно ранжирует (выкидывают как слишком лёгкие), какие катастрофически путает (выкидывают как шум).

Hard negatives без разметки. Подход из NV-Retriever (2024). Берёте базовый эмбеддер, для каждого запроса достаёте топ-100 документов. Те, что близки к позитиву, но не он, — кандидаты в hard negatives. Дальше фильтруете positive-aware: если скор кандидата подозрительно близок к скору позитива, это, скорее всего, false negative (документ на самом деле релевантен, просто не размечен), такие выкидываете. Остальное идёт в обучение.
Связка этих приёмов даёт работающий пайплайн без размеченного датасета на старте:
Берёте Qwen3-Embedding-0.6B как baseline.
Прикручиваете instruction-промпт под свой домен.
Генерируете синтетику запросов через сильную LLM.
Майните hard negatives positive-aware фильтром.
Дообучаете через LoRA на 1–2 GPU за пару дней.
На выходе — доменный эмбеддер, который на вашем бенчмарке выдаёт recall@10 в районе 0.8–0.9. На практике этого хватает.
Бенчмарк, кстати, придётся собрать руками. Минимум 200–500 пар. Это единственное место, где нельзя срезать: без него вы не отличите, ваш файнтюн улучшил качество или сломал.
Инференс: SGLang против vLLM
Когда вы переезжаете с энкодеров на декодерные эмбеддеры, дефолтный sentence-transformers перестаёт тянуть. Под капотом он гоняет через transformers/PyTorch, без flash attention, без префикс-кэша. На decoder-based эмбеддере вы на этом теряете половину скорости.
vLLM и SGLang — основные движки под продакшен. Оба умеют эмбеддинги, оба гоняют reranker через batched scoring. Разница в архитектуре кэша:
vLLM использует обычный prefix caching: один общий префикс для серии запросов.
SGLang использует RadixAttention — строит radix-дерево по всем кэшированным KV-страницам и переиспользует любой общий префикс между любыми запросами.
Для retrieval это важно в одном конкретном месте — реранкере. Он работает так: один запрос пользователя + 50 кандидатов из топа. В каждой паре префикс — это инструкция + запрос. Этот префикс одинаковый для всех 50 пар. SGLang за счёт RadixAttention считает его один раз и переиспользует во всех 50 forward-pass. На H100 это даёт до 6x ускорение на типичной RAG-нагрузке.
vLLM проще ставится, у него больше сообщество и шире поддержка железа. SGLang быстрее в RAG-сценариях, кодовая база чище, новый ресёрч обычно сначала появляется там. Если выбирать с нуля под retrieval — SGLang.
Хранилище: что менять в Qdrant
Когда источников много — внутренние вики, юридический корпус, бухгалтерия, тикеты поддержки — есть искушение свалить всё в одну коллекцию. Так делать не надо. Эмбеддер, даже хороший, путается между доменами, и юристу прилетит бухгалтерская справка.
Официальная рекомендация Qdrant — две схемы:
Collection-per-tenant. Подходит, если источников меньше 500, у них разный объём, нужна строгая изоляция (регуляторика, GDPR), разные размерности вектора.
Payload-based. Одна коллекция, в payload каждой точки лежит tenant_id (или source_id). Подходит, если источников тысячи и они однородные. Обязательно: keyword-индекс по tenant_id с флагом is_tenant=true, иначе Qdrant делает полное сканирование при каждом фильтре.
Метаданные — отдельная история, которую часто забывают. Дата создания, тип документа, автор, версия — всё это надо класть в payload. Тогда работают пре-фильтры (сначала отрезаем устаревшие документы, потом ищем) и пост-фильтры (нашли по семантике, отсортировали по дате). Без метаданных эмбеддер делает за вас то, что должна делать база.
И последнее: индекс не пересобирается с нуля. Никогда. Qdrant умеет инкрементальные апдейты, добавляйте точки по мере появления, удаляйте устаревшие. Если совсем надо переиндексировать — по расписанию раз в месяц.
Минимальный продакшен-стек на сегодня
Если бы я собирал retrieval сегодня с нуля, выглядело бы так:
Эмбеддер: Qwen3-Embedding-0.6B на старт. Если запросов много и качество не дотягивает — Qwen3-Embedding-4B.
Реранкер: Qwen3-Reranker-0.6B поверх топ-50 от эмбеддера.
Инференс: SGLang. Один pod на эмбеддер, один на реранкер.
Хранилище: Qdrant. Payload-based multi-tenancy с source_id в индексе.
BM25: оставляем, гибридный поиск. Эмбеддер хорошо ищет смысл, BM25 — артикулы, номера, имена собственные. Их результаты сливаем через reciprocal rank fusion.
Под домен: генерация синтетики + hard negatives + LoRA, как описано выше.
Этот стек по качеству бьёт классический BGE+BM25+cross-encoder на 15–20% recall и при этом инфраструктурно проще: одна архитектура (декодер) для эмбеддера и реранкера, один движок инференса, одна база.
Что осталось за кадром
Мультимодальные эмбеддеры. Qwen3-VL-Embedding мапит текст, картинки, PDF, видео в одно векторное пространство. Если ищете по корпусу со схемами и фотографиями — стоит посмотреть.
ModernBERT. Энкодер ещё не совсем мёртв. ModernBERT (декабрь 2024) — современный энкодер с flash attention, sliding window, длинным контекстом. На задачах semantic discrimination держится наравне с декодерами и в разы быстрее на инференсе. Если у вас классификация или кластеризация, а не retrieval — посмотрите в его сторону.
Сжатие декодерных эмбеддеров. Дистилляция в маленькие модели, квантизация, MRL-усечение. Тема активная, но это уже на следующую статью.
Переход с энкодеров на декодерные эмбеддеры — это не «возьмём модель побольше». Это смена архитектурного семейства. С ней приходят инструкции в промпте, длинный контекст и весь инференс-стек, который раньше был только у генераторов. И главное — порог входа в узкие домены упал. Там, где раньше уходили месяцы на сбор датасета, сейчас можно собрать рабочий пайплайн за пару недель.
Если у вас есть опыт перехода с энкодеров на decoder-эмбеддеры, особенно в узком домене — расскажите в комментариях. Интересно сравнить, какие модели и приёмы зашли, а какие нет.
Источники
LLM2Vec: Large Language Models Are Secretly Powerful Text Encoders — arxiv.org/abs/2404.05961
NV-Embed: Improved Techniques for Training LLMs as Generalist Embedding Models — arxiv.org/abs/2405.17428
NV-Retriever: Improving text embedding models with effective hard-negative mining — arxiv.org/abs/2407.15831
Qwen3 Embedding (blog) — qwenlm.github.io/blog/qwen3-embedding
Qwen3-Embedding-8B на HuggingFace — huggingface.co/Qwen/Qwen3-Embedding-8B
SGLang vs vLLM, бенчмарки 2026 — particula.tech/blog/sglang-vs-vllm-inference-engine-comparison
Qdrant Multi-tenancy guide — qdrant.tech/articles/multitenancy
ModernBERT — huggingface.co/docs/transformers/model_doc/modernbert-decoder
