Ошибки Embedding API заметить сложнее, чем сбои чат-интерфейса. Если падает чат, пользователь видит это сразу; Embedding часто работает в офлайн-конвейере загрузки, и один ответ 400 или 429 может означать лишь несколько недостающих строк в пакете документов — проблема обнаружится через дни, когда качество поиска уже упало.

Начинать разбор не стоит с «попробуем другую модель». Порядок такой: сначала код состояния и тип ошибки, затем размер тела запроса и батч, и только потом сами векторы. Ниже разобраны 401, 413, 429, несовпадение размерности и пустые векторы, а также приведён код обработки, который можно переиспользовать.

Примечание: OpenAI, DeepSeek и другие названия являются товарными знаками их правообладателей. Ограничения интерфейса приведены по публичной документации OpenAI embeddings: не более 8192 токенов на один вход, до 2048 элементов в массиве и не более 300 тысяч токенов на запрос; у разных вендоров и шлюзов лимиты могут отличаться, ориентируйтесь на фактическую документацию.

1. Сначала сгруппируйте ошибки по типу

Ошибки Embedding можно условно разделить на семь групп. Сначала определите группу, потом решайте, менять ли код, размер батча или данные.

Симптом Частые коды Типичная причина Что проверить первым
Ошибка авторизации 401 или 403 Неверный ключ, нет заголовка, ключ отключён, нет прав или не тот регион Заголовок Authorization и источник ключа
Слишком длинный ввод 400 или 413 Один текст длиннее лимита модели, тело больше лимита шлюза Токены одного текста и байты тела
Слишком большой батч 400 или 413 Превышено число элементов или суммарные токены Число элементов и суммарные токены
Ограничение скорости 429 Резкий рост параллелизма, общий ключ, шторм повторов Скорость, параллелизм и Retry-After
Сбой upstream 5xx Перегрузка или временная неисправность сервера ID запроса и время
Несовпадение размерности 400 или исключение клиента Смена модели, изменение размерности, фиксированная размерность индекса Имя модели, размерность и конфигурация индекса
Пустой вектор или NaN 200, но данные некорректны Пустой текст, ошибка разбора, деление на ноль при нормализации Длина вектора, конечные значения и норма

В журнале фиксируйте как минимум время, код состояния, тип ошибки, имя модели, размер батча, длительность и ID запроса. Не записывайте полный ключ и не печатайте исходный текст целиком.

2. 401 и 403: сначала проверьте заголовок

401 означает, что идентичность не подтверждена; 403 — что идентичность подтверждена, но доступа нет. В обоих случаях не стоит сначала подозревать возможности модели.

Частые причины: заголовок не в формате Authorization: Bearer <key>; вокруг ключа пробелы или кавычки; переменная окружения не загружена; ключ удалён или отключён; Base URL и ключ относятся к разным платформам; reverse proxy отбросил заголовок авторизации. Для 403 дополнительно проверьте права на модель, права проекта, регион и белый список IP.

Сначала отправьте через curl минимальный запрос с коротким вводом и убедитесь, что приходит 200, затем постепенно возвращайте батч, параллелизм и прокси. Если 401 сохраняется, сравните с новым ключом — это поможет отделить проблему ключа от конфигурации шлюза.

3. 413 и слишком длинный ввод: разберитесь, чей это лимит

413 обычно означает, что тело запроса слишком велико; это может быть ограничение шлюза или reverse proxy, а не отказ модели. Ограничение длины входа модели может возвращать 400. Эти случаи нужно различать.

На примере OpenAI embeddings: не более 8192 токенов на один вход, до 2048 элементов в массиве и не более 300 тысяч токенов на запрос. Превышение суммарного лимита обычно даёт 400, даже если каждый элемент в норме. У шлюза может быть свой лимит размера тела, и 413 придёт ещё до модели.

При слишком длинном вводе рекомендуется такой порядок:

  1. Считайте токены токенайзером соответствующей модели, а не оценивайте по числу символов.
  2. Режьте по заголовкам, абзацам и смысловым границам, оставляя небольшое перекрытие.
  3. Для слишком длинных фрагментов используйте обрезку как крайнюю меру, фиксируя исходную длину и место обрезки.
  4. В батче ограничивайте одновременно число элементов и суммарные токены, оставляя 10–20% запаса.
  5. При 413 сначала делите тело запроса; при 400 сначала проверяйте отдельный текст и параметры.

Учитывайте пустые строки. Интерфейс embeddings OpenAI не принимает пустой ввод, поэтому абзацы, ставшие пустыми после очистки, нужно отфильтровать до отправки.

4. 429: лимиты, параллелизм и шторм повторов

429 означает превышение скорости, параллелизма или квоты, а не недостаток баланса. Частые triggers: несколько сервисов используют один ключ; батч-задачи стартуют одновременно в начале часа; повтор идёт без задержки; длинные запросы занимают большую часть квоты.

Сначала посмотрите, есть ли в заголовках Retry-After; если есть — ждите указанное время. Если нет — используйте экспоненциальную задержку со случайным разбросом и ограничьте число попыток. Повтор подходит только для временных ошибок: 401, 403, 400 и большинство 413 не исправятся повтором.

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

5. Несовпадение размерности: самая незаметная ошибка

Несовпадение размерности может проявиться как ошибка базы данных, например «expected dimension 1536, got 1024», как ошибка формы в numpy, как ошибка broadcasting при вычислении косинусного сходства или просто как внезапная потеря релевантности результатов поиска.

Частые причины: модель сменили, а старые векторы остались в индексе; изменили параметр dimensions; одноимённая модель у разных вендоров или версий имеет разную размерность; при создании индекса размерность зафиксировали; кто-то вручную обрезал или дополнил вектор нулями. Одинаковая размерность тоже не гарантирует совместимость: векторные пространства разных моделей несовместимы.

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

6. Пустые, нулевые векторы и NaN

Ответ 200 не означает, что данные пригодны. Пустые векторы, векторы из нулей и NaN ломают расчёт сходства и при этом часто не вызывают исключения.

Типичные источники: текст после очистки состоит только из пробелов; входной массив пуст; ошибка разбора ответа оставила embedding пустым списком; локальная модель вернула нулевой вектор на пустой ввод; нормализация разделила на ноль и дала NaN; base64 декодирован неправильно.

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

7. Переиспользуемый код обработки ошибок

Пример ниже рассчитан на OpenAI-совместимый интерфейс и покрывает задержку при лимитах, разбиение батча и проверку векторов. В рабочем коде подставьте свои имя модели, размерность и Base URL.

import logging
import math
import random
import time

from openai import OpenAI, APIStatusError, APITimeoutError, RateLimitError

logger = logging.getLogger("embedding")
client = OpenAI(
    api_key="sk-ваш-ключ",
    base_url="https://api.zhongshangqihang.com/v1",
    max_retries=0,  # отключаем авто-повторы SDK, чтобы не дублировать свои
)

MODEL = "text-embedding-3-small"
EXPECTED_DIM = 1536


def vector_is_valid(vec: list[float]) -> bool:
    if len(vec) != EXPECTED_DIM:
        return False
    if not all(math.isfinite(x) for x in vec):
        return False
    norm = math.sqrt(sum(x * x for x in vec))
    return norm > 1e-6


def embed_batch(texts: list[str], attempt: int = 0) -> list[list[float] | None]:
    # Пустые тексты не отправляем, ставим None, чтобы сохранить индексы
    pairs = [(i, t) for i, t in enumerate(texts) if t and t.strip()]
    if not pairs:
        return [None] * len(texts)
    indices = [i for i, _ in pairs]
    payload = [t for _, t in pairs]

    try:
        resp = client.embeddings.create(model=MODEL, input=payload, timeout=60)
        items = sorted(resp.data, key=lambda item: item.index)
        vectors = [item.embedding for item in items]
        if len(vectors) != len(payload):
            raise ValueError(f"Несовпадение количества: {len(vectors)} != {len(payload)}")
        for index, vec in enumerate(vectors):
            if not vector_is_valid(vec):
                raise ValueError(f"Некорректный вектор: index={index}, dim={len(vec)}")
        result: list[list[float] | None] = [None] * len(texts)
        for source_index, vec in zip(indices, vectors):
            result[source_index] = vec
        return result
    except (RateLimitError, APITimeoutError) as exc:
        if attempt >= 5:
            raise
        wait = min(2 ** attempt, 30) + random.random()
        logger.warning("Лимит или таймаут, повтор через %.1f с: %s", wait, exc)
        time.sleep(wait)
        return embed_batch(texts, attempt + 1)
    except APIStatusError as exc:
        if exc.status_code in (400, 413) and len(texts) > 1:
            mid = len(texts) // 2
            logger.warning("Батч слишком велик, делим на %d + %d", mid, len(texts) - mid)
            return embed_batch(texts[:mid], attempt) + embed_batch(texts[mid:], attempt)
        if exc.status_code >= 500 and attempt < 5:
            wait = min(2 ** attempt, 30) + random.random()
            logger.warning("Ошибка upstream, повтор через %.1f с: %s", wait, exc)
            time.sleep(wait)
            return embed_batch(texts, attempt + 1)
        raise

В коде два компромисса. Первый: 400 тоже участвует в разбиении, потому что некоторые шлюзы сообщают о превышении суммарных токенов кодом 400; если и один текст не проходит, исключение будет выброшено. Второй: проверка вектора стоит до записи, поэтому плохие данные не попадут в индекс. В производстве дополнительно записывайте ID запроса, число повторов и неуспешный батч.

8. Порядок разбора и список мониторинга

При ошибке Embedding действуйте в таком порядке — обычно это быстрее, чем перебирать модели:

  1. Зафиксируйте код состояния, тип ошибки, ID запроса, имя модели, размер батча и длительность.
  2. При 401 или 403 проверьте заголовок, ключ, переменные окружения и платформу.
  3. При 400 проверьте токены одного текста, суммарные токены, пустые строки и формат параметров.
  4. При 413 проверьте размер тела в байтах и лимиты шлюза, затем уменьшите запрос и повторите.
  5. При 429 посмотрите Retry-After, снизьте параллелизм и проверьте, не используется ли общий ключ.
  6. При 5xx делайте ограниченное число повторов с задержкой; если ошибка повторяется, обратитесь к сервису, приложив ID запроса.
  7. Если пришёл 200, но результат некорректен, проверьте размерность, норму, NaN и нормализацию.
  8. Подключите эти метрики к мониторингу и регулярно прогоняйте фиксированный набор оценки.

Мониторинг считает по часам количество 401, 400, 413, 429 и 5xx, а также число пустых векторов и аномалий размерности. При резком росте ошибок сначала посмотрите, не сосредоточены ли они в одном проекте, ключе или пакете документов, и только потом решайте, снижать параллелизм, дробить батч, откатываться или исправлять данные.

Частые вопросы

И 413, и 400 означают слишком длинный ввод. В чём разница?

413 чаще приходит от шлюза или reverse proxy из-за размера тела, и запрос может не дойти до модели. 400 чаще связан с параметрами интерфейса или лимитом токенов. Посмотрите тело ответа и ID запроса, затем решайте, делить ли тело или текст.

Один текст не превышает 8192 токена, но всё равно приходит 400. Почему?

Возможно, превышен суммарный лимит на запрос, число элементов массива, есть пустая строка, ошибка формата параметров или неверное имя модели. Уменьшите батч, отфильтруйте пустые тексты и проверьте тело по пунктам.

Сколько раз повторять при 429?

Обычно три–пять раз с экспоненциальной задержкой и случайным разбросом, соблюдая Retry-After. После лимита отправляйте задачу в очередь или включайте оповещение, не повторяйте бесконечно. При сильных лимитах снижение параллелизма эффективнее увеличения числа повторов.

Можно ли исправить несовпадение размерности дополнением нулями?

Нет. Нули меняют направление вектора и сходство, результаты поиска становятся недостоверными. Правильно — привести модель и размерность к единым, создать новый индекс, полностью пересчитать векторы и проверить перед переключением.

Почему интерфейс вернул 200, а вектор пустой?

Возможно, ввод был пустым или после очистки остались только пробелы; также вероятны ошибка разбора ответа, декодирования base64 или локального инференса. Проверяйте длину, конечные значения и норму до записи, аномалии отправляйте в карантин.

Что дальше

Создайте тестовый ключ в ZSQH AI Gateway, прогоните небольшой массив текстов через авторизацию, батчи, лимиты и проверку векторов, а затем подключайте рабочий конвейер загрузки. Доступность моделей и цены смотрите на странице тарифов.

Читайте также: