> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apiyi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Практическая настройка embedding

> Как действительно получать пользу от embeddings: измеренное сравнение bge-m3 с тремя моделями OpenAI, насколько крупно разбивать на фрагменты, как задавать пороги сходства, сколько использовать пакетирования и параллельных запросов, как на самом деле работает тарификация и настройка LangChain по умолчанию, которая незаметно снижает полноту с 80% до 15%.

[Текстовые эмбеддинги](/ru/api-capabilities/text-embedding) описывает, как вызывать API. На этой странице объясняется **как правильно его использовать**.

Почти ни одна проблема с embedding не проявляется как неудачный вызов. endpoint возвращает 200, размеры верны,
а качество retrieval незаметно падает. Каждая рекомендация ниже основана на измерениях, выполненных на
шлюзе APIYI 2026-08-25 (UTC+8), — это не общие советы.

<Note>
  **Метод**: 20 китайских документов об интеграции LLM gateway и 20 соответствующих английских документов,
  20 китайских и 20 английских запросов с ответами, размеченными вручную, при этом все три модели запускались
  на одном и том же корпусе в один и тот же временной интервал. Корпус небольшой, поэтому **различия менее чем
  в 5 процентных пунктов не являются убедительными** — воспроизведите результаты на своих данных, прежде чем
  считать что-либо окончательным.
</Note>

## 1. Сначала выберите подходящую модель

|                                        | `bge-m3`             | `text-embedding-3-small` | `text-embedding-3-large` |
| -------------------------------------- | -------------------- | ------------------------ | ------------------------ |
| Цена                                   | **\$0.01/1M tokens** | \$0.02/1M tokens         | \$0.13/1M tokens         |
| Размерность                            | **1024**             | 1536                     | 3072                     |
| Максимальная длина                     | 8192 tokens          | 8191 tokens              | 8191 tokens              |
| Предварительно нормализовано           | Да                   | Да                       | Да                       |
| Параметр `dimensions`                  | ❌ явный 400          | ✅                        | ✅                        |
| Recall\@1 для китайского               | 80%                  | 80%                      | 85%                      |
| Recall\@1 для английского              | 70%                  | 80%                      | 85%                      |
| Recall\@1 для смешанного корпуса zh+en | 65%                  | 80%                      | 85%                      |
| Плотность китайского текста            | **2.1 chars/token**  | 0.9 chars/token          | 0.9 chars/token          |

<CardGroup cols={2}>
  <Card title="Выбирайте bge-m3, когда" icon="check">
    * Ваш корпус **в основном китайский** (или японский / русский): качество retrieval соответствует 3-small,\
      цена по прайсу вдвое ниже, а тот же текст использует лишь 42% tokens — **примерно 1/5 фактических расходов**
    * Вам нужен меньший объём хранилища: 1024 dims — это на 33% меньше, чем 1536, и на 66% меньше, чем 3072
    * Вам нужен охват редких языков (поддерживается 100+)
    * Вы хотите запускать ту же open-source модель локально, чтобы офлайн- и онлайн-векторы совпадали
  </Card>

  <Card title="Выбирайте OpenAI, когда" icon="check">
    * Ваш корпус **в основном на английском или код**: качество на ступень выше. bge-m3 действительно расходует 15%–50%\
      больше tokens на таком контенте, но вдвое меньшая цена за единицу всё равно делает его дешевле в целом — поэтому **в этом случае\
      ориентируйтесь на качество, а не на цену**
    * Ваша база знаний **содержит материалы на разных языках**, и вам нужен только один ответ
    * Вам нужен `dimensions`, чтобы сократить объём хранилища
    * У вас уже есть пороговые значения, настроенные по диапазонам оценок OpenAI, и вы не хотите перенастраивать их
  </Card>
</CardGroup>

<Warning>
  **Смешанный по языкам корпус — слабое место bge-m3** (Recall\@1 65%). Не потому, что кросс-языковой retrieval плох —\
  как раз наоборот. Он почти одинаково оценивает китайскую и английскую версии одного и того же факта\
  (показатель 0.75–0.87, против 0.56–0.69 у OpenAI), поэтому китайский запрос часто ставит английскую копию выше\
  китайской.

  **Если ваш RAG возвращает только один ответ**, разделите индекс по языкам или добавьте языковой фильтр во время запроса.\
  **Если вам нужно собирать материал на разных языках**, это скорее преимущество, чем недостаток.
</Warning>

## 2. Всегда разбивайте длинные документы на фрагменты

`bge-m3` имеет контекстное окно на 8192 token, поэтому длинное руководство помещается в один вызов. **Это не делает такой подход хорошей идеей.**

Измерено: 20 разделов, объединённых в одно длинное руководство, при этом 20 коротких документов (каждый соответствовал одному
разделу) были добавлены как сильные отвлекающие варианты, а запросы задавались 20 вопросами —

| Подход                        | Результат                                                                                            |
| ----------------------------- | ---------------------------------------------------------------------------------------------------- |
| Весь документ как один вектор | Руководство занимает первое место в **10%** случаев                                                  |
| Разбивка по разделам          | Top1 попадает внутрь руководства в **75%** случаев; попадает в *правильный раздел* в **65%** случаев |

Для одного и того же вопроса правильный раздел в среднем получает оценку на **+0.10** выше, чем весь документ:

| Вопрос                                  | Весь документ | Правильный раздел | Разница    |
| --------------------------------------- | ------------- | ----------------- | ---------- |
| Как RMB конвертируется в USD            | 0.487         | 0.684             | **+0.198** |
| Как гарантировать корректный вывод JSON | 0.487         | 0.650             | **+0.163** |
| Если вы получили 401, что делать        | 0.485         | 0.612             | +0.127     |

Единый вектор длинного документа — это **среднее** по всему, что в нём есть, поэтому любой короткий
фрагмент с точной формулировкой его превосходит.

<Tip>
  **Рекомендации по разбиению на фрагменты**

  * Разбивайте по смысловым границам на **200–500 token**, с перекрытием 10%–15%
  * В китайском языке в среднем около 2.1 символа на token, поэтому 200–500 token — это примерно **420–1050 китайских символов**
  * **Не дробите на слишком мелкие фрагменты**: каждый ввод содержит 2 фиксированных специальных token — накладные расходы 0.4% на
    500-token фрагменте, но уже 12.5% чистой потери на 16-token фрагменте
  * Добавление заголовка раздела в начало каждого фрагмента заметно улучшает его распознаваемость
</Tip>

## 3. Пороги нужно калибровать заново для каждой модели

Именно здесь миграция с OpenAI на `bge-m3` чаще всего даёт сбой. **Два диапазона оценок полностью различаются.**

Одинаковые вручную размеченные пары, оценённые всеми тремя моделями:

| Отношение                                                                                                                   | `bge-m3`  | `3-small` | `3-large` |
| --------------------------------------------------------------------------------------------------------------------------- | --------- | --------- | --------- |
| Не связанные («как включить потоковую передачу» ↔ «погода в Пекине сегодня»)                                                | **0.417** | 0.094     | 0.108     |
| Парафраз («получил 429» ↔ «API сообщает, что лимит запросов превышен»)                                                      | 0.552     | 0.420     | 0.360     |
| Межъязыковой парафраз                                                                                                       | 0.753     | 0.557     | 0.681     |
| Переставлены субъект и объект («пользователь отправляет модели изображение» ↔ «модель отправляет пользователю изображение») | 0.975     | 0.908     | 0.895     |

<Warning>
  У `bge-m3` **нижняя граница находится на 0.42**; у OpenAI — на 0.09.
  Копирование правила вроде «отбрасывать всё ниже 0.3» означает, что для `bge-m3` фильтрации вообще не будет;
  копирование правила «считать релевантными только значения 0.8 и выше» отбрасывает почти каждый корректный результат.
</Warning>

Лучшие одиночные пороги, измеренные для `bge-m3`:

| Сценарий                               | Рекомендуемый стартовый порог | Точность при этом пороге |
| -------------------------------------- | ----------------------------- | ------------------------ |
| Китайский корпус / китайские запросы   | **0.53**                      | 75%                      |
| Английский корпус / английские запросы | 0.51                          | 80%                      |
| Межъязыковой поиск                     | 0.53–0.55                     | 75%–82%                  |
| Многоязычный корпус                    | 0.62                          | 68%                      |

Для сравнения, лучший порог для китайского сценария — **0.45** для `text-embedding-3-small` и **0.33** для `3-large`.

<Tip>
  **На практике**: начните с **0.50**, считайте **0.45–0.60** серой зоной, требующей подтверждения, и заново калибруйте
  на основе 50–100 размеченных примеров из вашего собственного корпуса перед запуском в рабочую среду.
</Tip>

## 4. Сходство не может сказать вам, *правильно* ли что-то

Это верно для **любой** эмбеддинг-модели — это не недостаток какой-то одной из них, но вам нужно знать об этом заранее:

| Пара                                                                                        | `bge-m3` | `3-small` | `3-large` |
| ------------------------------------------------------------------------------------------- | -------- | --------- | --------- |
| "поддерживает вызов функций" ↔ "не **поддерживает** вызов функций"                          | 0.891    | 0.851     | 0.805     |
| "\$**2** за million tokens" ↔ "\$**20** за million tokens"                                  | 0.965    | 0.954     | 0.902     |
| "укажите base\_url на api.**apiyi**.com" ↔ "укажите base\_url на api.**openai**.com"        | 0.873    | 0.878     | 0.738     |
| "пользователь отправляет модели изображение" ↔ "модель отправляет пользователю изображение" | 0.975    | 0.908     | 0.895     |

Все три не срабатывают. **Косинусное сходство показывает, об одном ли и том же два текста, а не согласуются ли они.**

Отрицание, цены, номера версий и имена сущностей нельзя отделить на этапе поиска. Правильный
запасной вариант:

<Steps>
  <Step title="Векторный поиск, Top 50–100">
    Используйте `bge-m3`, чтобы быстро сузить круг. Порог лишь отфильтровывает явно нерелевантное.
  </Step>

  <Step title="Переранжирование до Top 3–5">
    Отправьте кандидатов в [`bge-reranker-v2-m3`](/ru/api-capabilities/rerank/overview). Это cross-encoder,
    который пропускает запрос и документ через модель вместе, и это **именно тот инструмент, который нужен для таких
    тонких различий** — и он относится к тому же семейству моделей, что и `bge-m3`.
  </Step>

  <Step title="Пусть LLM оценивает во время генерации">
    Передайте Top 3–5 вместе с исходным вопросом и явно укажите в prompt, что модель должна
    сказать, что ничего не найдено, если извлечённый контент не соответствует вопросу.
  </Step>
</Steps>

## 5. Размер батча и параллельные запросы

### Пакетная обработка: точка перегиба — 64–128

| Батч    | Общее время | На элемент |
| ------- | ----------- | ---------- |
| 16      | 1.67s       | 105ms      |
| 64      | 5.04s       | 79ms       |
| **128** | 7.45s       | **58ms**   |
| 256     | 14.6s       | 57ms       |
| 1024    | 54.4s       | 53ms       |

После 128 стоимость на элемент почти не улучшается (58ms → 53ms), тогда как один запрос становится в 7 раз дольше.
Длительность запроса напрямую влияет на риск тайм-аута клиента и на то, сколько работы будет потеряно при одном сбое.

### Параллельные запросы: 8 для работы в реальном времени, 32–48 для массовой индексации

| Параллельные запросы | Успешность | Общая пропускная способность | Самый медленный запрос |
| -------------------- | ---------- | ---------------------------- | ---------------------- |
| 8 (200 requests)     | **100%**   | 55 items/s                   | 5.6s                   |
| 48                   | 99.3%      | **133 items/s**              | 10.2s                  |
| 96                   | **88.2%**  | 65 items/s                   | 59.8s                  |

* **Используйте параллельные запросы 8 для получения в реальном времени**: 200 requests без единого сбоя
* **Для массовой индексации можно поднять до 32–48**: максимальная пропускная способность, но 429s начинают появляться, поэтому требуется экспоненциальный backoff
* **Не превышайте 64**: при 96 уровень сбоев составляет 11.8%, а запросы начинают зависать примерно на 60 seconds

<Warning>
  **Ваш клиент должен выполнять повторные попытки.** Даже при низкой параллельности примерно 0.8% запросов сталкиваются с разорванным соединением
  (`Connection aborted / Remote end closed connection`). Одна повторная попытка устраняет проблему; без повторной попытки в вашем индексе останется пробел.

  Установите тайм-аут клиента на **60–90 seconds** для массовой индексации, а не на несколько сотен — с зависшим запросом
  сложнее работать, чем с неудавшимся.
</Warning>

```python theme={null}
import time
from openai import OpenAI

client = OpenAI(api_key="sk-your-apiyi-key", base_url="https://api.apiyi.com/v1", timeout=90.0)

def embed_batch(texts, model="bge-m3", retries=3):
    """Batch embedding with backoff. Keep batches between 64 and 128."""
    for attempt in range(retries):
        try:
            resp = client.embeddings.create(model=model, input=texts)
            return [d.embedding for d in sorted(resp.data, key=lambda x: x.index)]
        except Exception:
            if attempt == retries - 1:
                raise
            time.sleep(2 ** attempt)          # 1s, 2s, 4s
```

## 6. Как фактически работает стоимость

Стоимость — это произведение двух вещей: **цена за unit** и **сколько token получается из данного фрагмента текста**.
Здесь различаются оба параметра.

`bge-m3` — **\$0.01 / 1M tokens** против **\$0.02** для `text-embedding-3-small` — то есть вдвое ниже unit price
еще до всего остального. Дополнительно сказывается эффективность tokenizer: `bge-m3` использует XLM-R SentencePiece и дает
около **2.1 Chinese characters per token**, тогда как cl100k от OpenAI — около **0.9**:

| Корпус                             | `bge-m3` tokens | OpenAI tokens | Соотношение token | **Фактические расходы** |
| ---------------------------------- | --------------- | ------------- | ----------------- | ----------------------- |
| Chinese technical doc (492 chars)  | **231**         | 552           | 0.42×             | **0.21×**               |
| Japanese (456 chars)               | **231**         | 480           | 0.48×             | **0.24×**               |
| Russian (810 chars)                | **213**         | 411           | 0.52×             | **0.26×**               |
| Mixed zh+en (760 chars)            | 413             | 360           | 1.15×             | 0.57×                   |
| English technical doc (1053 chars) | 210             | 182           | 1.15×             | 0.58×                   |
| Code snippet (1200 chars)          | 453             | 300           | 1.51×             | 0.76×                   |

**Китайские корпуса стоят примерно одну пятую от `text-embedding-3-small`.**
Английский текст и code потребляют больше token на `bge-m3`, но вдвое более низкая unit price все равно делает его дешевле OpenAI по
общим расходам — так что для этих двух корпусов решение зависит от **качества retrieval (English Recall\@1 70% vs
80%), а не от цены**.

Хранение тоже отличается. Векторы уже L2-нормализованы, а возвращаемые значения имеют точность fp16, поэтому
**хранение vector `bge-m3` в float16 не дает потерь в точности**:

| Формат                                  | На вектор | 1M векторов |
| --------------------------------------- | --------- | ----------- |
| `bge-m3` 1024-d float16                 | **2 KB**  | **2 GB**    |
| `bge-m3` 1024-d float32                 | 4 KB      | 4 GB        |
| `text-embedding-3-small` 1536-d float32 | 6 KB      | 6 GB        |
| `text-embedding-3-large` 3072-d float32 | 12 KB     | 12 GB       |

<Tip>
  Векторы возвращаются нормализованными (измеренные нормы L2 — 0.99992–1.00029), поэтому **dot product уже и есть cosine
  similarity**. `IP` и `COSINE` types индекса дают идентичные результаты в вашей vector database, а `IP` экономит
  один проход нормализации.
</Tip>

## 7. Режимы отказа, которые остаются незаметными

### 7.1 Значение по умолчанию в LangChain снижает Recall с 80％ до 15％

<Warning>
  `langchain_openai.OpenAIEmbeddings` по умолчанию использует `check_embedding_ctx_length=True`, который **сначала кодирует ваш текст
  в tiktoken token ids** и отправляет массив целых чисел в `/v1/embeddings`.

  Это нормально для моделей OpenAI, чьим tokenizer *и является* tiktoken. `bge-m3` использует tokenizer XLM-R, и
  эти два пространства id не имеют ничего общего.

  **Вызов по-прежнему возвращает 200, вектор по-прежнему имеет размерность 1024, `usage` по-прежнему выглядит нормально — только качество поиска тихо обрушивается.**
</Warning>

Измеренные затраты:

| Проверка                                                                              | Результат     |
| ------------------------------------------------------------------------------------- | ------------- |
| Сходство между одной и той же фразой, отправленной как текст и как tiktoken token ids | **0.282**     |
| Recall\@1 для китайского корпуса                                                      | **80% → 15%** |

Исправление:

```python theme={null}
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings(
    model="bge-m3",
    api_key="sk-your-apiyi-key",
    base_url="https://api.apiyi.com/v1",
    check_embedding_ctx_length=False,   # required, otherwise tiktoken ids go out instead of text
    chunk_size=64,
)
```

Тот же риск применим к **любому wrapper, который выполняет tokenization на стороне client перед отправкой**. При подключении
сторонней embedding-модели проверьте, отправляет ли ваш SDK исходный текст или token ids.

### 7.2 Пустая строка принимается как допустимый input

`input: ""` возвращает **200** на `bge-m3` (OpenAI здесь возвращает 400), формируя вектор размерностью 1024 и
тарифицируя 2 token.

Скрипт chunking, который не отфильтровывает пустые chunks, заполнит ваш index бессмысленными векторами, которые
будут случайно всплывать во время поиска. **Фильтруйте пустой текст перед индексацией.**

### 7.3 Параметр `dimensions` отклоняется

```json theme={null}
{ "model": "bge-m3", "input": "...", "dimensions": 512 }
```

Возвращает 400: `Model "bge-m3" does not support matryoshka representation, changing output dimensions will lead to poor results.`

`bge-m3` не был обучен с Matryoshka representation, поэтому **обрезание вектора заметно ухудшает качество**.
Используйте float16, чтобы уменьшить объем хранения, вместо того чтобы самостоятельно обрезать размерности.

### 7.4 Имя модели чувствительно к регистру, алиасов нет

Работает только `bge-m3`. `BAAI/bge-m3` и `BGE-M3` оба возвращают 503 «нет доступных каналов».

### 7.5 8192 — это лимит на один input

Один input длиной более 8192 token возвращает 400 и **никогда не обрезается молча** — что является более безопасным
поведением: вы не получите вектор, который выглядит нормальным, но тихо потерял вторую половину вашего текста.

Лимит применяется **к каждому item, а не к запросу**: один вызов, содержащий **1024 item / 102560 token**, в тестировании
возвращался нормально. Если любой один item превышает лимит, весь запрос завершается с ошибкой, а число token в
ошибке относится именно к **этому item**, а не к общему итогу.

## 8. Минимальная реализация, которую вы можете скопировать

```python theme={null}
"""Minimal working bge-m3 indexing + retrieval."""
import time
import numpy as np
from openai import OpenAI

client = OpenAI(
    api_key="sk-your-apiyi-key",
    base_url="https://api.apiyi.com/v1",
    timeout=90.0,
)

MODEL = "bge-m3"
BATCH = 96          # keep between 64 and 128
THRESHOLD = 0.50    # starting point; recalibrate on your own samples before launch


def embed(texts, retries=3):
    """Batch embedding with backoff. Returns float16 vectors."""
    texts = [t.strip() for t in texts if t and t.strip()]   # drop blanks, see 7.2
    out = []
    for i in range(0, len(texts), BATCH):
        chunk = texts[i:i + BATCH]
        for attempt in range(retries):
            try:
                resp = client.embeddings.create(model=MODEL, input=chunk)
                out += [d.embedding for d in sorted(resp.data, key=lambda x: x.index)]
                break
            except Exception:
                if attempt == retries - 1:
                    raise
                time.sleep(2 ** attempt)
    # already normalized, so float16 storage is lossless here, see section 6
    return np.array(out, dtype=np.float16)


def search(query, doc_vectors, docs, top_k=50):
    """Vectors are normalized, so the dot product is cosine similarity."""
    qv = embed([query])[0].astype(np.float32)
    scores = doc_vectors.astype(np.float32) @ qv
    order = np.argsort(-scores)[:top_k]
    return [(docs[i], float(scores[i])) for i in order if scores[i] >= THRESHOLD]


if __name__ == "__main__":
    docs = ["...your chunks, 200-500 tokens each..."]
    dv = embed(docs)
    for text, score in search("your question", dv, docs):
        print(f"{score:.4f}  {text[:60]}")
    # In production, hand this Top 50 to bge-reranker-v2-m3, see section 4
```

## Связанная документация

<CardGroup cols={2}>
  <Card title="API встраивания текста" icon="vector-square" href="/ru/api-capabilities/text-embedding">
    Параметры, формат ответа, быстрый старт
  </Card>

  <Card title="Переранжирование" icon="list-ordered" href="/ru/api-capabilities/rerank/overview">
    `bge-reranker-v2-m3`, подходящий инструмент для этапа повышения точности
  </Card>

  <Card title="Настройка RAG" icon="sliders-horizontal" href="/ru/api-capabilities/rerank/rag-best-practices">
    Двухэтапное извлечение: сколько кандидатов извлекать
  </Card>

  <Card title="Тарификация моделей" icon="tags" href="/en/models">
    Актуальная тарификация для каждой модели встраивания
  </Card>
</CardGroup>
