Продакшн-разработка ИИ-агентов · Модуль 2 · Урок 2.0

Эмбеддинги: как получаются и где хранятся (фундамент модуля)

Цели урока

После урока вы сможете:

  • объяснить, как из текста получается вектор (эмбеддинг) и что такое «близость» в этом пространстве
  • осознанно выбирать модель эмбеддингов по размерности, длине контекста, языку и типу (dense/sparse)
  • выбирать хранилище под задачу: от pgvector в существующем Postgres до специализированной векторной БД
  • понимать, зачем нужен ANN-индекс (HNSW/IVF) и какой компромисс recall/латентность/память он даёт
  • видеть, что эмбеддинги — общий слой под RAG (гл.3) и памятью (гл.4), а не деталь одного из них

Что нового (дельта к базовому курсу)

В базовом курсе эмбеддинги звучали как «посчитали вектор, нашли ближайшие по косинусу». На проде этого мало: от выбора модели зависит качество поиска, от выбора хранилища и индекса — латентность, стоимость и масштаб. Дельта в одной фразе: от «эмбеддинг — это чёрный ящик» к «понимаю, что внутри вектора, какой моделью он посчитан и в каком индексе живёт».

Как из текста получается эмбеддинг

Эмбеддинг — это вектор фиксированной размерности (например, 768 или 1536 чисел), который кодирует смысл текста. Конвейер такой:

  1. Токенизация. Текст режется на токены (под-слова) — так же, как для генеративной модели.
  2. Проход через encoder-модель. Это обученная нейросеть (часто на базе трансформера-энкодера). Она выдаёт по вектору на каждый токен.
  3. Pooling. Векторы токенов сворачиваются в один вектор всего текста: усреднением (mean pooling) или берётся вектор служебного токена (CLS). Получается один эмбеддинг на фрагмент.
  4. Нормализация. Вектор обычно приводят к единичной длине (L2-норма). Тогда косинусная близость сводится к скалярному произведению, и сравнивать векторы быстро и удобно.

Ключевая идея: модель обучена так, что близкие по смыслу тексты дают близкие векторы. «Машина» и «автомобиль» окажутся рядом, хотя пишутся по-разному. Именно это превращает поиск по смыслу в геометрию: «найти похожее» = «найти ближайшие точки».

«Близость»: косинус и почему он, а не расстояние

Для текстовых эмбеддингов чаще всего берут косинусную близость — косинус угла между векторами. Она измеряет направление вектора, игнорируя его длину; а у нормализованных векторов длина и так единичная. Это устойчивее, чем евклидово расстояние, к разнице в длине текстов.

Важное практическое следствие: значения близости сравнимы только внутри одной модели. Косинус 0.82 у модели A и 0.82 у модели B — это разные вещи; порог отсечения калибруется под конкретную модель. Нельзя смешивать в одном индексе векторы, посчитанные разными моделями (или даже разными версиями одной модели).

Модели эмбеддингов: чем отличаются

Моделей много, и выбор влияет на качество не меньше, чем ретривер. На что смотреть:

  • Размерность (например, 384 / 768 / 1536). Больше — потенциально точнее, но дороже по памяти и быстродействию индекса.
  • Длина контекста. Сколько токенов модель принимает за раз — это потолок размера чанка (см. гл.3).
  • Язык. Мультиязычные модели нужны, если корпус не на одном языке; «английская» модель на русском просядет.
  • Dense vs sparse. Dense — плотный вектор (смысл). Sparse (например, SPLADE) — разреженный вектор по словарю, ближе к лексике; хорош для точных терминов и естественно сочетается с BM25 (гибрид, гл.3).
  • Matryoshka-эмбеддинги. Обучены так, что вектор можно усечь до меньшей размерности с небольшой потерей качества — удобно для компромисса «точность ↔ стоимость» без переэмбеддинга.

Слаги конкретных моделей и их лимиты версионно-зависимы — сверяйтесь с актуальными доками провайдера. Практическое правило: фиксируйте модель и версию рядом с индексом; смена модели = переэмбеддинг всего корпуса.

Где хранить: векторные БД и их различия

Эмбеддинги нужно хранить и быстро искать по ним ближайших. Вариантов — спектр от «таблица в вашем Postgres» до managed-сервиса. Сравнение по практичным осям — в таблице ниже. Главное правило выбора:

  • Начните с pgvector, если данные уже в Postgres, объём — до миллионов векторов, и не хочется новой инфраструктуры: один источник истины, транзакции, привычные бэкапы, фильтры по метаданным обычным SQL.
  • Берите специализированный движок (Qdrant/Weaviate/Milvus), когда нужны десятки–сотни миллионов векторов, горизонтальное масштабирование, встроенный гибридный поиск и продвинутая фильтрация по метаданным на больших объёмах.
  • FAISS — это библиотека (не сервис): максимальная скорость ANN в одном процессе, но персистентность, фильтры и шардирование делаете сами. Хороша для офлайн-поиска и исследований.
  • Managed (Pinecone и аналоги) снимает эксплуатацию, но это внешняя зависимость и стоимость; взвесьте data residency.

ANN-индексы: почему не точный перебор

Точный поиск ближайших (exact kNN) сравнивает запрос со всеми векторами — это O(N) на каждый запрос и не масштабируется на миллионы. Поэтому используют приближённый поиск (ANN, approximate nearest neighbors): чуть жертвуем полнотой (recall) ради огромного выигрыша в скорости.

Два главных семейства индексов:

  • HNSW (граф из «миров» на слоях). Строит многослойный граф, по которому поиск «спускается» к ближайшим за логарифмическое число шагов. Очень быстрый и точный поиск, но много памяти и дороже вставка. Дефолт в большинстве векторных БД.
  • IVF (инвертированные списки + кластеры). Делит пространство на ячейки, ищет только в ближайших. Экономнее по памяти, но точность зависит от числа просматриваемых ячеек (параметр nprobe).

Поверх индекса часто применяют квантизацию (PQ/scalar): сжатие векторов, чтобы уместить больше в память ценой небольшой потери точности. Это всё — настройка компромисса recall ↔ латентность ↔ память, которую вы выбираете под объём и SLA.

Связь с RAG и памятью

Эмбеддинги — это общий слой, на котором стоят обе следующие главы. В RAG (гл.3) по эмбеддингам идёт семантическая стадия гибридного поиска, а модель эмбеддингов и чанкинг определяют качество recall. В памяти (гл.4) теми же векторами ищут релевантные факты, а dense-вектор сочетают с графом связей. Поэтому решения этого урока — какая модель, какое хранилище, какой индекс — задают потолок качества для всего модуля.

Конвейер: из текста — в вектор и в индекс
flowchart LR
  TXT["Текст / чанк"] --> TOK["Токенизация"]
  TOK --> ENC["Encoder-модель"]
  ENC --> POOL["Pooling (mean/CLS) + нормализация"]
  POOL --> VEC["Вектор (напр. 768d)"]
  VEC --> IDX[("ANN-индекс HNSW / IVF")]
  Q["Запрос"] -. тот же путь .-> VEC
  IDX --> NN["top-k ближайших по косинусу"]
HNSW: многослойный граф, поиск спускается сверху вниз
flowchart TB
  subgraph L2["Слой 2 (разреженный)"]
    A2((узел)) --- B2((узел))
  end
  subgraph L1["Слой 1"]
    A1((узел)) --- B1((узел)) --- C1((узел))
  end
  subgraph L0["Слой 0 (все векторы)"]
    A0((узел)) --- B0((узел)) --- C0((узел)) --- D0((узел))
  end
  Q["запрос"] --> A2
  A2 --> B1
  B1 --> C0
  C0 --> R["ближайший сосед"]
Косинусная близость на нормализованных векторах сводится к скалярному произведению
package embed

import "math"

// cosine считает косинусную близость двух векторов одинаковой длины.
// На L2-нормализованных векторах это просто скалярное произведение.
func cosine(a, b []float32) float64 {
    if len(a) != len(b) || len(a) == 0 {
        return 0 // несовместимые размерности — скорее всего, разные модели
    }
    var dot, na, nb float64
    for i := range a {
        dot += float64(a[i]) * float64(b[i])
        na += float64(a[i]) * float64(a[i])
        nb += float64(b[i]) * float64(b[i])
    }
    if na == 0 || nb == 0 {
        return 0
    }
    return dot / (math.Sqrt(na) * math.Sqrt(nb))
}

// normalize приводит вектор к единичной длине (L2). После этого cosine(a,b)
// можно считать как обычный dot-product — это и делают векторные БД внутри.
func normalize(v []float32) []float32 {
    var n float64
    for _, x := range v {
        n += float64(x) * float64(x)
    }
    if n == 0 {
        return v
    }
    inv := float32(1 / math.Sqrt(n))
    out := make([]float32, len(v))
    for i, x := range v {
        out[i] = x * inv
    }
    return out
}

Anti-patterns

Грабли при работе с эмбеддингами и их хранением
Анти-паттернПочему плохоКак правильно
Смешивать в индексе векторы разных моделей/версийИх пространства несравнимы; близость теряет смыслОдин индекс — одна модель и версия; смена модели = переэмбеддинг
Сравнивать косинус-пороги между моделями0.8 у разных моделей — разное; ложные срабатыванияКалибровать порог под конкретную модель на размеченных парах
Не нормализовать векторы, потом считать dot-productДлина искажает близость, ранжирование «плывёт»Нормализовать при индексации и запросе (или честно считать косинус)
Точный kNN на миллионах векторовO(N) на запрос — не масштабируется по латентностиANN-индекс (HNSW/IVF), настроить recall/латентность под SLA
Сразу тащить специализированную БД «на вырост»Лишняя инфраструктура и эксплуатация на малом объёмеНачать с pgvector; мигрировать, когда упрётесь в масштаб/функции
Один индекс на смешанные домены/языкиРазмытое пространство, шумная выдачаРазделять индексы по домену/языку или фильтровать по метаданным

Практическое задание (PRO-M2-EMB)

  • Посчитайте эмбеддинги для 20–30 коротких фраз одной моделью; нормализуйте и убедитесь, что синонимичные пары ближе, чем случайные.
  • Реализуйте exact kNN перебором и измерьте латентность; прикиньте, как она вырастет на 1M векторов — обоснуйте необходимость ANN.
  • Поднимите pgvector (или Qdrant) и загрузите те же векторы с HNSW-индексом; сравните выдачу и скорость с перебором.
  • Возьмите вторую модель эмбеддингов и покажите, что её косинус-пороги другие: один и тот же порог даёт разную точность.
  • Сравните размерности (например, полная vs усечённая Matryoshka) по качеству топ-5 и по объёму индекса — задокументируйте компромисс.
  • Заведите в индексе поле метаданных (язык/домен) и проверьте, что фильтрация убирает шум из чужого домена.

Проверка знаний

Почему нельзя складывать в один индекс векторы, посчитанные разными моделями эмбеддингов?

  • A Потому что у разных моделей разная скорость работы
  • B Их векторные пространства несравнимы: «близость» между векторами разных моделей не имеет смысла
  • C Потому что разные модели возвращают текст, а не числа
  • D Потому что это запрещено лицензией

В индексе 50 млн векторов, p99 латентности поиска вышла за SLA. Сейчас используется точный перебор (exact kNN).

Что решает проблему по существу?

  • A Перейти на ANN-индекс (например, HNSW) и настроить recall/латентность
  • B Увеличить размерность эмбеддингов
  • C Хранить векторы в JSON-файле
  • D Считать косинус вместо евклида

Команде нужно семантическое хранилище на ~300 тыс. документов; данные уже в Postgres, отдельной инфраструктуры заводить не хотят.

Какой выбор хранилища разумнее как стартовый?

  • A Сразу поднять кластер специализированной векторной БД на вырост
  • B pgvector в существующем Postgres: один источник истины, SQL-фильтры, привычные бэкапы; мигрировать при упоре в масштаб
  • C Хранить эмбеддинги в памяти процесса без персистентности
  • D Не использовать индекс вообще