Продакшн-разработка ИИ-агентов · Модуль 2 · Урок 2.0
Эмбеддинги: как получаются и где хранятся (фундамент модуля)
Цели урока
После урока вы сможете:
- объяснить, как из текста получается вектор (эмбеддинг) и что такое «близость» в этом пространстве
- осознанно выбирать модель эмбеддингов по размерности, длине контекста, языку и типу (dense/sparse)
- выбирать хранилище под задачу: от pgvector в существующем Postgres до специализированной векторной БД
- понимать, зачем нужен ANN-индекс (HNSW/IVF) и какой компромисс recall/латентность/память он даёт
- видеть, что эмбеддинги — общий слой под RAG (гл.3) и памятью (гл.4), а не деталь одного из них
Что нового (дельта к базовому курсу)
В базовом курсе эмбеддинги звучали как «посчитали вектор, нашли ближайшие по косинусу». На проде этого мало: от выбора модели зависит качество поиска, от выбора хранилища и индекса — латентность, стоимость и масштаб. Дельта в одной фразе: от «эмбеддинг — это чёрный ящик» к «понимаю, что внутри вектора, какой моделью он посчитан и в каком индексе живёт».
Как из текста получается эмбеддинг
Эмбеддинг — это вектор фиксированной размерности (например, 768 или 1536 чисел), который кодирует смысл текста. Конвейер такой:
- Токенизация. Текст режется на токены (под-слова) — так же, как для генеративной модели.
- Проход через encoder-модель. Это обученная нейросеть (часто на базе трансформера-энкодера). Она выдаёт по вектору на каждый токен.
- Pooling. Векторы токенов сворачиваются в один вектор всего текста: усреднением (mean pooling) или берётся вектор служебного токена (CLS). Получается один эмбеддинг на фрагмент.
- Нормализация. Вектор обычно приводят к единичной длине (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 ближайших по косинусу"]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 и по объёму индекса — задокументируйте компромисс.
- Заведите в индексе поле метаданных (язык/домен) и проверьте, что фильтрация убирает шум из чужого домена.
Проверка знаний
Почему нельзя складывать в один индекс векторы, посчитанные разными моделями эмбеддингов?
Верный ответ: B
B верно. Каждая модель формирует своё пространство; геометрия (косинус) сопоставима только внутри одной модели и версии. Смешав их, вы получите бессмысленные расстояния. A — про производительность, не про корректность; C — эмбеддинг это вектор чисел; D — дело не в лицензии.
В индексе 50 млн векторов, p99 латентности поиска вышла за SLA. Сейчас используется точный перебор (exact kNN).
Что решает проблему по существу?
Верный ответ: A
A верно. Exact kNN — это O(N) на запрос и не масштабируется; ANN (HNSW/IVF) даёт логарифмический поиск ценой небольшой потери recall. B только увеличит стоимость; C ухудшит скорость; D — выбор метрики не лечит O(N)-перебор.
Команде нужно семантическое хранилище на ~300 тыс. документов; данные уже в Postgres, отдельной инфраструктуры заводить не хотят.
Какой выбор хранилища разумнее как стартовый?
Верный ответ: B
B верно. На таком объёме pgvector закрывает задачу без новой инфраструктуры: транзакции, метаданные, бэкапы из коробки. A — преждевременная сложность; C теряет данные при рестарте; D не даёт быстрого поиска.