Я — Дмитрий Черняк, владелец компании, производящей цифровых консультантов «Нейроботник» и архитектор этого решения. Наиболее простой и ходовой вариант нашего продукта — консультант на сайте, с подключенной моделью и RAG‑базами. Простые задачи ограничиваются одной базой, более сложные - несколькими, совмещающими семантический поиск с целевым - по ключевым словам и фразам, с многошаговым ответом. Для большинства задач в данной нише этого достаточно.

Эта статья посвящена предновогоднему тесту эмбеддеров (табличка результатов прилагается).

Мы - в первую очередь, инженерная компания, нацеленная на конечную пользу, не математики. Поэтому любим использовать понятные метрики и простые подходы. Например, оценивая качество цифрового консультанта, вместо принятых по отрасли вопросов «достигает ли качество поиска XX%?» и «можем ли мы XX% превратить в XX.99%?», задаём практические: «Будет ли Нейроботник отвечать лучше кожаного?» и «Будет ли он отвечать быстрее?». За модой тоже не очень гонимся — решение должно, в первую очередь, работать и быть управляемым. По этой причине MemGraph и прочие чудеса изощрённого картографирования знаний нам до сих пор не пригодились, хотя и мониторим новинки, конечно.

Стоит ли говорить, что от качества эмбеддера наши решения зависят весьма существенно, тем более, что гоняем весь ИИ на собственном пуле (б...бережливость). И вот, в конце года пришла пора провести ревизию качества нашего RAG-поиска. Результаты получились довольно интересные, поэтому решил их опубликовать. Попутно - вернуть долг сообществу, тем, кто публиковал обзоры и бенчмарки, из которых мы брали модели в начале пути.

Для сравнения эмбеддеров традиционно принято использовать метрики открытого бенчмарка MTEB https://huggingface.co/spaces/mteb/leaderboard. Нас в нём интересует колонка Retrieval. Бенч хорошо смотреть для того, чтобы понять, не появилось ли на рынке что-то новое-прорывное. Однако, чрезвычайно полезная при обучении моделей метрика nDCG@10, по которой этот Retrieval сравнивается, совершенно не релевантна запросу простого крестьянина от ИТ-шной сохи: "оно мне найдёт, или нет?". Потому что если оно найдёт не в том порядке (на что обращает внимание nDCG@10), то отвечающий ИИ всё равно вычитает то, что ему нужно и пропустит то, что не релевантно. Второй момент - это то, что MTEB предлагает синтетический набор и насколько эмбеддер будет удачно работать в продакшине, на реальных данных реального клиента, сказать всегда сложно.

Инженерные вопросы ставятся относительно прикладной пользы и звучат они гораздо проще:

  1. В каком количестве случаев мы сможем найти точный ответ на вопрос клиента?

  2. В каком количестве случаев мы найдём хоть что-то по теме?

  3. В каком количестве случаев мы вообще ничего не найдём?

  4. Есть ли у метода общие для всех моделей слепые зоны?

  5. Чего нам это будет стоить?

Если ответы на эти вопросы удовлетворительные, продукт можно выкатывать, если нет - нужно дорабатывать.

А тесты на синтетике нам и вовсе не нужны - у нас есть нормальные, живые базы наших клиентов.

Какой фреймворк бенчмарков выбрать?
Это довольно интересный философский вопрос. Дело в том, что фреймворки появились для того, чтобы облегчить жизнь людей. Затем люди в них потонули. А затем появился ИИ.

В общем, кто ясно мыслит, тому достаточно Курсора. Задача незамысловатая и сажать за неё программиста - пустая трата времени (мои программисты до сих пор кодят ручками - пока ещё могут повышать свой скилл, потому что уже к концу этого года ручками станет, увы, неконкурентно, а косяки в коде ИИ оставлять не перестанет). К тому же был вечер пятницы и задачу было скинуть некому...

В Курсоре был создан EMBTEST.md такого содержания:

Нужно сделать workflow для сравнения эмбеддеров.
Принцип:
Берём базу данных, находящуюся в каталоге db/{name}/data
Она состоит из файлов md, в которых чанки размечены сигнатурой
"""

#!# название документа
#!## заголовок чанка
#!## TAGS тэги (опционально)
#!## SEARCH строка для поиска (опционально)

текст, включая заголовки формата md
"""

Берём список эмбеддинг-моделей и оценивающую LLM.

Нужно:
1. Составить список тестовых вопросов к чанкам базы. Записать его в файл db/{name}/tests/questions.txt
2. Создать индекс для базы средствами raglib.rag
3. При помощи raglib.rag задать все эти вопросы, получить чанки (скажем, по 5 штук). Сохранить в файл db/{name}/tests/{embname}.answ (embname санитизировать).
4. Вызвав LLM, которая создавала вопросы, провести оценку ответов, собрав следующие данные:
- количество чанков, исчерпывающе отвечающих на вопрос
- количесвто чанков, приблизительно отвечающих, или дополняющих вопрос
5. Провести пп.2-4 для всех заданных моделей.
6. Посчитать интегральную метрику
7. Показать вопросы, провальные для каждой модели
8. Показать вопросы, провальные для всех моделей

workflow должен настраиваться конфигом, содержащим определение оценивающей модели (код создания экземплляра llm) и определение эмбеддинг-моделей по формату из util.py, путь к каталогу db.

Индексы данных можно создавать в памяти, или перезаписывать, либо сохранять.

Разбей на скрипты с отдельными этапами.

Примерно через 5-7 минут у меня был готов бенч, состоящий из 4 фаз, выдающий отформатированные результаты, подсказки и снабжённый такой толпой документации, что пришлось сказать "горшочек не вари" и остановить генерацию.

В отборе участвовали:

  1. Разнообразные берты, запущенные в infinity.

  2. llm, запущенные в llama.cpp server, преимущественно серии Qwen3

  3. text-embedding-3-large от OpenAI, как state of the art.

760 чанков данных реального клиента (смысловая данных разбивка специальным алгоритмом).
Формирование вопросов и оценка проводились qwen2.5-coder-instruct-32b.

================================================================================
ИТОГОВЫЙ ОТЧЁТ
================================================================================

1. СРАВНЕНИЕ МОДЕЛЕЙ
--------------------------------------------------------------------------------
Модель                                   Score      Full %     Any %     
--------------------------------------------------------------------------------
text-embedding-3-large                   2.125      69.3       99.9      
Qwen3-Embedding-8B-Q4_K_M                2.117      67.1       99.7      
Qwen3-Embedding-4B-Q4_K_M                2.108      67.2       99.9      
Qwen3-Embedding-0.6B-Q8_0                2.051      68.1       99.2      
google/embeddinggemma-300m               1.640      54.3       97.0      
intfloat/multilingual-e5-large-instruct  1.632      53.6       97.2      
BAAI/bge-m3                              1.628      53.9       97.0      
deepvk/USER-bge-m3                       1.628      54.2       97.0      
cointegrated/LaBSE-en-ru                 1.625      54.0       97.4      
Llama-3.2-1B-Instruct-Q5_K_M             0.835      21.2       79.4      
--------------------------------------------------------------------------------
Score = (FULL + PARTIAL * 0.5) / total_questions
Full % = процент вопросов с хотя бы одним полным ответом
Any % = процент вопросов с хотя бы одним релевантным ответом

2. ПРОВАЛЬНЫЕ ВОПРОСЫ ПО МОДЕЛЯМ
--------------------------------------------------------------------------------

text-embedding-3-large: 1 провальных вопросов

Qwen3-Embedding-8B-Q4_K_M: 2 провальных вопросов

Qwen3-Embedding-4B-Q4_K_M: 1 провальных вопросов

Qwen3-Embedding-0.6B-Q8_0: 6 провальных вопросов

google/embeddinggemma-300m: 23 провальных вопросов

intfloat/multilingual-e5-large-instruct: 21 провальных вопросов

BAAI/bge-m3: 23 провальных вопросов

deepvk/USER-bge-m3: 23 провальных вопросов

cointegrated/LaBSE-en-ru: 20 провальных вопросов

Llama-3.2-1B-Instruct-Q5_K_M: 156 провальных вопросов

3. ВОПРОСЫ, ПРОВАЛЬНЫЕ ДЛЯ ВСЕХ МОДЕЛЕЙ
--------------------------------------------------------------------------------

Нет вопросов, провальных для всех моделей

================================================================================

Образцы провальных вопросов я тут поскипал.

Что можно сказать по результату.

  1. Модель, не предназначенная для вычисления эмбеддингов - Llama-3.2-1B-Instruct-Q5_K_M, показала откровенно слабый результат. Дело в том, что у всех больших языковых моделей, есть эмбеддер, который кодирует входные данные во внутреннее предстваление и к нему можно обращаться отдельно от модели. Но у эмбеддинг-моделей он натренирован на релевантность смысла расстоянию между векторами, а у генерирующих моделей он натренирован, чтобы внутренним слоям было удобно туда смотреть своим "внутренним взором", а взор этот не шибко-то линейный.

  2. Лучше OpenAI никто не обтаботал. Однако... отрыв от локальных остальных оказался неожиданно не велик.

  3. Модели серии Qwen3-Embedding показали на удивление мало различия между собой. Конечно, 8B выглядит лучше, чем 4B и 0.6B, однако, если в абсолютных цифрах, то речь идёт об 1-2 неотвеченных вопросах у 8-4B и о всего 6 неотвеченных у 0.6B. При том, что объём и скорость различаются в разы. Возможно, разница почувствовалась бы на данных из смешанных областей знания, но с практической точки зрения это не наш случай. Да и решается он алгоритмической фильтрацией промаркированных чанков гораздо надёжнее, чем онлайн-гаданием на ИИ.

  4. Следующим интересным открытием было то, что все берты, выделенные по результатам гуглинга, изучния таблички победителей MTEB и предварительного отбора, не смотря на разное происхождение и мерность, показали, можно считать, одинаковый результат. Причём, число метрик вектора, кажется, играет довольно небольшую роль. Если модель тащит, то она и на 700 мерностях тащит, как на 2000.

  5. Результат бертов оказался неплохой, но заметно хуже, чем у квенов. Из практики могу сказать, что им бывает присуща досадная «слепота» к некоторым чанкам. Зато они настолько быстрые, что их можно пробовать гонять даже без ускорителей. А с ускорителями — вообще «пулемёт».

  6. Некоторые из представленных моделей тренированы с префиксами. Типа, когда индексируем документ, вставляем в начало "document:", а когда считаем эмбеддинг для поиска, перед ним вставляем "query:" (для разных моделей свои префиксы). По этому тесту, результаты с префиксами оказались хуже, чем без и в отчёт не включены.

  7. В бенче ещё пыталась участвовать Сберовская FRIDA, но завести её так и не удалось: с префиксами она показывала один и тот же вектор, без префиксов - что-то похожее на Llama-3.2-Instruct.

Советы и напутствия:

  1. Тестируйте на своих данных.

  2. Тестируйте на метрике реальной пользы, сегодня собрать любой бенчмарк — минутное дело.

  3. Выбирая модель, не поленитесь зайти на https://huggingface.co/spaces/mteb/leaderboard, например, и посмотреть Embedding Dimensions — это повлияет на размер базы и скорость, а также на MaxTokens — это размер чанка, по которому можно вычислить вектор. Не гонитесь за большими чанками, но и не мельчите.

  4. Не доверяйте «чутью», тестируйте на статистически значимом количестве и смотрите цифру. В нашей практике были случаи, когда мы буквально «влюблялись» в новую модель, а потом оказывалось, что это старая, а на новую забыли переключить.

  5. Не ограничивайтесь одной моделью, сравнивайте.