Зачем нужен легкий эмбеддер: проблема «тяжелых» retrievers в RAG
Retrieval-Augmented Generation стал стандартом для фактологически точных ответов LLM. Схема простая: retriever находит релевантные документы, генеративная модель пишет ответ на их основе. Но за простотой скрывается узкое место - сам retriever. Стандартные dense-эмбеддеры на базе BERT-подобных архитектур содержат 12 и более слоёв, что напрямую бьёт по скорости индексации и инференса.
Типичный сценарий: вы разворачиваете RAG-систему на одном GPU. Тяжелый эмбеддер отъедает вычислительные ресурсы, снижая throughput генеративной LLM. Индексация 100 тысяч документов занимает часы. При совместном размещении latency ответа растёт, а количество обрабатываемых запросов в секунду падает. Решение - облегчить retriever без катастрофической потери качества поиска.
Модель STRIZH (24,4 млн параметров) закрывает эту проблему. Она получена обрезкой 12-слойного RuModernBERT-small до 4 слоёв с последующим контрастивным обучением. Результат: на русском retrieval качество почти не отличается от тяжелых аналогов, индексация ускоряется в 9-10 раз, а throughput генеративной LLM при совместном размещении практически не снижается. Разберём процесс создания, ошибки, которые стоили 14 процентных пунктов recall, и практическое внедрение.
Архитектура STRIZH: как мы обрезали RuModernBERT-small до 4 слоёв
Исходная модель - RuModernBERT-small, 12 трансформерных слоёв, обученная на русскоязычных текстах. Задача: получить максимально легкий эмбеддер, сохранив качество retrieval. Выбор пал на обрезку слоёв, а не на дистилляцию. Почему - разберём отдельно.
Процесс механически прост. Из 12 слоёв оставляем первые 4, остальные 8 удаляем. Веса оставшихся слоёв инициализируются из предобученной модели. Итоговое количество параметров - 24,4 млн. Это примерно втрое меньше исходных ~70 млн у RuModernBERT-small. Но простая обрезка разрушает качество эмбеддингов: модель теряет способность строить семантически осмысленные векторные представления. Нужно дообучение.
Почему именно обрезка, а не дистилляция?
Дистилляция требует teacher-модель, которая генерирует целевые эмбеддинги для student-модели. Это удваивает вычислительный бюджет на этапе обучения: teacher должен прогнать весь датасет, часто с большим batch size. Обрезка лишена этого недостатка - веса уже есть, нужно только восстановить качество после удаления слоёв.
Минус обрезки: потеря знаний, накопленных в верхних слоях. В BERT-подобных архитектурах верхние слои кодируют высокоуровневые семантические признаки, критически важные для retrieval. Без дообучения обрезанная модель даёт эмбеддинги, близкие к случайным. Поэтому следующий шаг - контрастивное обучение.
Контрастивное обучение: ключ к сохранению качества retrieval
Контрастивное обучение решает задачу напрямую: модель учится сближать векторы семантически близких текстов и разносить векторы нерелевантных. Функция потерь - InfoNCE (Noise Contrastive Estimation). Для каждого запроса positive-пара - релевантный документ, negative-пары - нерелевантные документы из батча.
Датасет для дообучения собран из русскоязычных пар «запрос-документ»: новостные статьи, техническая документация, FAQ. Размер батча - 64, learning rate - 2e-5, количество эпох - 3. После контрастивного обучения модель STRIZH восстанавливает качество retrieval до уровня, сопоставимого с полным RuModernBERT-small. Разница в recall@5 - менее 2 процентных пунктов.
Для понимания контекста: аналогичные подходы к облегчению моделей применяются и в других задачах. Например, методология A.L.F.R.E.D. показывает, как дистилляция шаблонов позволяет моделям на 2B параметров превзойти 35B-гигантов в эффективности. В нашем случае обрезка слоёв даёт схожий выигрыш по скорости без сложной инфраструктуры дистилляции.
Типичные ошибки при оптимизации эмбеддеров и как мы их обошли
Создание STRIZH не было гладким. Три ошибки едва не похоронили проект. Каждая из них - самостоятельный урок для ML-инженера, работающего с retrieval-системами.
Почему MSE не работает для retrieval: история одного провала
Первая идея выглядела логично: обучим обрезанную модель предсказывать эмбеддинги teacher-модели (полного RuModernBERT-small) через регрессию на MSE. Результат - катастрофа. Качество retrieval упало ниже приемлемого уровня.
Причина в природе функции потерь. MSE минимизирует попиксельное (в нашем случае - поэлементное) расстояние между векторами. Но retrieval опирается на относительные расстояния: важно, чтобы вектор запроса был ближе к правильному документу, чем к неправильным. MSE игнорирует эту структуру. Контрастивный loss, напротив, оптимизирует относительный порядок - и это работает. Оценка качества embedding-моделей - отдельная сложная тема; новый бенчмарк RTEB 2026 как раз решает проблему разрыва между лабораторными метриками и реальной производительностью.
Leaky evaluation: когда тестовые данные подглядывают в обучение
После перехода на контрастивное обучение метрики взлетели. Recall@5 показал +8 процентных пунктов к baseline. Эйфория длилась недолго - при ручной проверке качество поиска оказалось посредственным. Диагноз: leaky evaluation.
Проблема в пересечении документов между train и test. Один документ может содержать несколько фактов, релевантных разным запросам. Если документ попал в train, модель запоминает его содержание, а не учится семантическому поиску. На тесте она находит «знакомый» документ, а не релевантный по смыслу. Решение: разбиение датасета по документам, а не по парам «запрос-документ». Все запросы, связанные с одним документом, попадают либо в train, либо в test. После исправления recall@5 упал на 6 пунктов - это была реальная оценка, а не завышенная утечкой данных.
Как model_max_length в 256 токенов убил 14 п.п. recall
Самая неочевидная ошибка. После обрезки модели захотелось дополнительно ускорить инференс. Решение: уменьшить model_max_length в tokenizer_config.json с 512 до 256 токенов. Логика простая - меньше токенов, быстрее вычисления. Результат: recall рухнул на 14 процентных пунктов.
Анализ показал, что многие русскоязычные документы в датасете длиннее 256 токенов. Обрезание отбрасывало вторую половину текста, где часто находилась ключевая информация для retrieval. Модель просто не видела релевантные фрагменты. Решение: вернуть model_max_length к исходному значению 512. Ускорение от обрезки токенов оказалось мнимым - потеря качества перечеркнула весь выигрыш. Вывод: параметры токенизатора критичны, их изменение требует обязательной валидации на целевом датасете.
Бенчмарки: STRIZH против тяжелых аналогов на русском retrieval
Сравнение проводилось на трёх русскоязычных датасетах: новостные статьи, техническая документация и пользовательские вопросы из FAQ. Baseline - полный RuModernBERT-small (12 слоёв) и multilingual-e5-base.
Качество retrieval: почти без потерь
| Модель | Recall@1 | Recall@5 | Recall@10 | MRR |
|---|---|---|---|---|
| RuModernBERT-small (12 слоёв) | 0.72 | 0.89 | 0.93 | 0.80 |
| multilingual-e5-base | 0.70 | 0.87 | 0.91 | 0.78 |
| STRIZH (4 слоя) | 0.70 | 0.87 | 0.91 | 0.78 |
Разница между STRIZH и полным RuModernBERT-small - 2 процентных пункта по recall@1 и recall@5. Для большинства RAG-сценариев это приемлемая цена за десятикратное ускорение.
Скорость индексации и инференса: ускорение в 9-10 раз
| Модель | Документов/сек (индексация) | Запросов/сек (инференс) |
|---|---|---|
| RuModernBERT-small (12 слоёв) | 120 | 85 |
| STRIZH (4 слоя) | 1150 | 810 |
Индексация ускоряется в 9.6 раз, инференс - в 9.5 раз. Замеры на NVIDIA A10, batch size 32, документы средней длиной 350 токенов.
Совместное размещение с генеративной LLM: влияние на throughput
Эксперимент: на одном GPU (NVIDIA A10) одновременно работают генеративная LLM (Qwen2.5-7B) и retriever. Замеряем throughput генеративной модели в токенах/сек при фоновой нагрузке от эмбеддера.
| Конфигурация | Throughput LLM (токенов/сек) |
|---|---|
| LLM без эмбеддера | 45 |
| LLM + RuModernBERT-small (12 слоёв) | 28 (-38%) |
| LLM + STRIZH (4 слоя) | 42 (-7%) |
STRIZH снижает throughput генеративной LLM всего на 7%, тогда как полный RuModernBERT-small - на 38%. Для production-среды с ограниченными GPU-ресурсами это решающий фактор.
Практическое руководство: как внедрить STRIZH в ваш RAG пайплайн
Модель доступна для загрузки. Ниже - минимальный рабочий пример интеграции с предостережениями, вынесенными из описанных выше ошибок.
Загрузка модели и tokenizer: правильная конфигурация
from transformers import AutoTokenizer, AutoModel
import torch
model_name = "ai-manual/strizh-embedder-v1"
tokenizer = AutoTokenizer.from_pretrained(model_name)
# КРИТИЧНО: не переопределяйте model_max_length без валидации
tokenizer.model_max_length = 512
model = AutoModel.from_pretrained(model_name)
model.eval()
device = "cuda" if torch.cuda.is_available() else "cpu"
model.to(device)
Проверьте tokenizer_config.json после загрузки. Если model_max_length переопределён на 256 или иное значение - верните 512. Эта ошибка стоила 14 п.п. recall, не повторяйте её.
Индексация документов и поиск: минимальный рабочий пример
import faiss
import numpy as np
def get_embedding(text):
inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=512, padding=True)
inputs = {k: v.to(device) for k, v in inputs.items()}
with torch.no_grad():
outputs = model(**inputs)
# mean pooling
attention_mask = inputs["attention_mask"]
embeddings = (outputs.last_hidden_state * attention_mask.unsqueeze(-1)).sum(1)
embeddings = embeddings / attention_mask.sum(1, keepdim=True)
return embeddings.cpu().numpy()
# Индексация
documents = ["Документ 1 текст...", "Документ 2 текст...", "..."]
doc_embeddings = np.vstack([get_embedding(doc) for doc in documents])
index = faiss.IndexFlatIP(doc_embeddings.shape[1])
index.add(doc_embeddings.astype('float32'))
# Поиск
query = "Как оптимизировать эмбеддер?"
query_embedding = get_embedding(query)
scores, indices = index.search(query_embedding.astype('float32'), k=5)
print(f"Top-5 документов: {indices[0]}")
Для production-среды используйте батчинг. Оптимальный batch size для STRIZH на GPU с 16 ГБ VRAM - 64. Это даёт максимальную утилизацию без out-of-memory.
Ограничения STRIZH и когда стоит выбрать более тяжелую модель
STRIZH не универсальное решение. Сценарии, где она уступает:
- Документы длиннее 512 токенов. Модель обучена с model_max_length=512. Для длинных документов используйте чанкинг или более тяжелый эмбеддер с поддержкой extended context.
- Узкоспециализированные домены без дообучения. Медицина, юриспруденция, технические стандарты - если данные сильно отличаются от общего русского языка, дообучите STRIZH на доменных парах «запрос-документ». Процесс контрастивного обучения описан выше, он применим и к вашим данным.
- Требования к максимальному recall. Если потеря 2 п.п. recall критична (например, legal-tech с обязательностью нахождения всех релевантных документов), используйте полный RuModernBERT-small или ансамбль моделей.
Для оценки качества retrieval на ваших данных используйте метрики recall@k и MRR. Избегайте leaky evaluation: разбивайте датасет по документам, а не по запросам. Подробнее о метриках и подходах к оценке - в разборе бенчмарка RUMBA, где аналогичные принципы применяются к диалоговым системам.
STRIZH - это компромисс, выверенный практикой. 24,4 млн параметров, 9-10-кратное ускорение индексации и минимальное влияние на throughput генеративной LLM делают её рабочим выбором для продакшена. Три ошибки, которые мы совершили в процессе - MSE-регрессия, leaky evaluation и обрезание model_max_length - обошлись в недели отладки. Не наступайте на те же грабли.