> ## 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.

# Настройка RAG на практике

> Как реально получить пользу от bge-reranker-v2-m3: структура двухэтапного поиска, сколько кандидатов извлекать, как фрагментировать длинные документы, как задавать пороги relevance_score, как обрабатывать отрицания, а также код интеграции для LangChain / LlamaIndex / Dify.

[обзор](/ru/api-capabilities/rerank/overview) описывает, что это за модель. На этой странице описано **как правильно использовать её**.

Каждая рекомендация основана на измерениях APIYI, полученных 2026-07-30 (UTC+8) — это не общие советы.

## 1. Правильно выстройте архитектуру: двухэтапный retrieval

Reranking — это не самостоятельный метод retrieval. Это второй этап pipeline.

<Steps>
  <Step title="Извлечение">
    Используйте **векторный поиск** или **BM25**, чтобы извлечь набор кандидатов из всего корпуса. Этот этап должен
    быть **быстрым** и должен **выбирать с запасом**: цель — «ответ где-то здесь», а не
    «ответ должен быть первым».
  </Step>

  <Step title="Реранжирование">
    Передайте набор кандидатов вместе с запросом в `bge-reranker-v2-m3`, чтобы оценить и переупорядочить их.
    Этот этап должен быть **точным**: цель — поднять правильный ответ на первое место.
  </Step>

  <Step title="Обрежьте и передайте в LLM">
    Возьмите переупорядоченные top 3–5 в свой prompt. **Чем точнее это сделано, тем меньше модель
    галлюцинирует и тем меньше контекста вы оплачиваете.**
  </Step>
</Steps>

<Info>
  **Почему нельзя пропустить recall и реранжировать все подряд**: cross-encoder нужен запрос,
  прежде чем он сможет что-либо вычислить, поэтому ничего нельзя предварительно вычислить офлайн.
  Оценивать миллионы документов по одному слишком дорого и по стоимости, и по задержке. Recall
  сокращает их с миллионов до сотен; reranking правильно сортирует эти сотни.
</Info>

### Полный рабочий пример

Это минимальный, но полный двухэтапный retriever — `text-embedding-3-small` для recall, `bge-reranker-v2-m3` для reranking, оба через один и тот же APIYI token:

```python theme={null}
import os
import numpy as np
import requests

BASE = "https://api.apiyi.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['APIYI_API_KEY']}"}


def embed(texts):
    """Batch embed. In production, document vectors live in a vector DB."""
    r = requests.post(f"{BASE}/embeddings", headers=HEADERS, timeout=60,
                      json={"model": "text-embedding-3-small", "input": texts})
    r.raise_for_status()
    data = sorted(r.json()["data"], key=lambda x: x["index"])
    return np.array([d["embedding"] for d in data])


def rerank(query, documents, top_n=5):
    """Rerank. documents must be a plain string array."""
    r = requests.post(f"{BASE}/rerank", headers=HEADERS, timeout=120,
                      json={"model": "bge-reranker-v2-m3", "query": query,
                            "documents": documents, "top_n": top_n})
    r.raise_for_status()
    return r.json()["results"]


def search(query, corpus, recall_k=50, final_k=5):
    """corpus: [{'id':..., 'text':..., 'meta':...}, ...]"""
    # ---- Stage 1: vector recall ----
    doc_vecs = embed([c["text"] for c in corpus])          # from your vector DB in production
    q_vec = embed([query])[0]
    doc_vecs /= np.linalg.norm(doc_vecs, axis=1, keepdims=True)
    q_vec /= np.linalg.norm(q_vec)
    recalled_idx = np.argsort(-(doc_vecs @ q_vec))[:recall_k]
    candidates = [corpus[i] for i in recalled_idx]

    # ---- Stage 2: rerank ----
    results = rerank(query, [c["text"] for c in candidates], top_n=final_k)

    # Key step: map back by index so you keep ids and metadata
    return [{**candidates[r["index"]], "score": r["relevance_score"]}
            for r in results]


corpus = [
    {"id": "kb-001", "text": "A 429 means you hit a rate limit. Reduce concurrency, implement "
                             "exponential backoff on the client, or ask for a higher RPM quota."},
    {"id": "kb-002", "text": "HTTP 4xx status codes indicate client errors: 400 bad parameters, "
                             "401 unauthorized, 403 forbidden, 404 not found."},
    {"id": "kb-003", "text": "To check your balance, call /v1/dashboard/billing/subscription."},
]

for hit in search("My API requests keep returning 429 — how do I fix it?",
                  corpus, recall_k=3, final_k=2):
    print(f"{hit['score']:.4f}  [{hit['id']}]  {hit['text'][:40]}")
```

<Warning>
  **Всегда выполняйте сопоставление обратно по `index`; никогда не ищите документы по их возвращенному тексту.**
  Содержимое документов может повторяться — два одинаковых документа получают бит-в-бит идентичные оценки и
  неразличимы — поэтому поиск по тексту привяжет неправильные id и метаданные.
</Warning>

## 2. Сколько кандидатов следует извлекать?

Измеренная задержка (короткие документы, P50 по 5 прогонам на ступень):

| Кандидаты | P50    | P95    |
| --------- | ------ | ------ |
| 1         | 2.00 s | 2.58 s |
| 10        | 2.35 s | 2.68 s |
| 25        | 2.52 s | 2.81 s |
| 50        | 2.92 s | 3.39 s |
| 100       | 3.80 s | 4.32 s |
| 500       | \~15 s | —      |
| 1000      | \~33 s | —      |
| 2000      | \~68 s | —      |

Важнее всего здесь **фиксированные накладные расходы \~2 секунды**: даже один документ стоит 2 секунды. Переход от 1 до 100 кандидатов добавляет всего 1.8 секунды, а выигрыш в качестве извлечения стоит гораздо больше.

**На задержку на самом деле влияет общее число token, а не количество документов.** Для тех же 50 кандидатов:

| 50 candidates                           | Входные tokens | Задержка P50 |
| --------------------------------------- | -------------- | ------------ |
| Короткие документы (\~20 знаков каждый) | 673            | 2.48 s       |
| Длинные документы (\~480 знаков каждый) | 20,606         | 11.74 s      |

То же количество, но задержка выше в 4.7 раза. Именно поэтому chunking в следующем разделе не замедляет извлечение — chunking увеличивает количество документов, не увеличивая общее число token.

<Tip>
  **Рекомендуемое `recall_k`: 50–100.**

  * Ниже 20: список для извлечения уже короткий, поэтому мало что остается исправлять в неправильном ранжировании — Вы платите фиксированные накладные расходы почти ни за что
  * Выше 200: задержка начинает мешать интерактивной работе, а хвост списка для извлечения редко содержит ответ, поэтому предельная отдача стремится к нулю
  * **1000 или больше: гарантированно 429.** 1000 кандидатов — это 26,867 token, что превышает весь минутный бюджет TPM 20,000 (134%) — запрос обречен независимо от повторных попыток
</Tip>

Если Вам действительно нужно ранжировать много документов (например, для офлайн-пакетной обработки), **разделите задачу на несколько запросов по ≤100 документов** и ограничьте concurrency значением 4. Учтите, что разбиение не уменьшает общее число token — TPM 20,000 по-прежнему остается общим ограничением, поэтому пакетные задания нужно ограничивать по скорости в расчете на минуту. Эта квота расширяется; для больших пакетных нагрузок сначала проверьте текущий доступный запас с помощью поддержки APIYI.

## 3. Разбивайте длинные документы на чанки

Это самый эффективный шаг предварительной обработки. Причину объясняют два измерения.

**Во-первых, нерелевантный контент размывает релевантность.** Одно и то же совпадающее предложение с разным количеством добавленной воды:

| Общая длина   | Совпадение в начале | Совпадение в конце | Конец / начало |
| ------------- | ------------------- | ------------------ | -------------- |
| 528 символов  | 0.933               | 0.720              | 0.77           |
| 1028 символов | 0.931               | 0.500              | 0.54           |
| 2028 символов | 0.925               | 0.793              | 0.86           |
| 4028 символов | 0.907               | 0.351              | 0.39           |
| 8028 символов | 0.828               | 0.181              | 0.22           |

**Во-вторых, длина повышает шумовые оценки.** Один и тот же нерелевантный документ получил 0.029 при короткой длине и 0.121 после 450 символов воды — в 4 раза выше.

Вместе это означает: **длинный документ с ответом в конце проигрывает длинному документу, который вообще не по теме.**

<Tip>
  **Стратегия разбиения на чанки**

  1. Разбивайте на чанки по **200–500 символов** с перекрытием 10–20%, чтобы ответы не обрезались на границе
  2. Отправляйте каждый чанк как отдельный элемент массива `documents`
  3. Берите **наивысшую оценку чанка** как оценку документа, затем удаляйте дубликаты по документу
  4. При формировании prompt отправляйте либо только совпадающий чанк, либо весь документ — в зависимости от того, какой бюджет контекста вы можете позволить
</Tip>

```python theme={null}
def chunk(text, size=400, overlap=60):
    step = size - overlap
    return [text[i:i + size] for i in range(0, max(len(text) - overlap, 1), step)]


def rerank_long_docs(query, docs, top_n=5):
    """docs: [{'id':..., 'text':...}] — score by chunk, aggregate to best chunk per document."""
    flat, owner = [], []
    for d in docs:
        for c in chunk(d["text"]):
            flat.append(c)
            owner.append(d)

    best = {}
    for r in rerank(query, flat, top_n=len(flat)):
        d = owner[r["index"]]
        if r["relevance_score"] > best.get(d["id"], (0, None))[0]:
            best[d["id"]] = (r["relevance_score"], flat[r["index"]], d)

    ranked = sorted(best.values(), key=lambda x: -x[0])[:top_n]
    return [{**d, "score": s, "best_chunk": c} for s, c, d in ranked]
```

<Warning>
  **8192 tokens — это жесткое ограничение**, применяемое к каждой паре запрос-документ. При превышении возвращается 400
  (`This model's maximum context length is 8192 tokens`) — оно **не обрезает данные молча**.

  Ограничение **не** относится к общему объему запроса: один запрос с 400 документами × 1000 символов
  (330K tokens) возвращается нормально. Поэтому разбиение на чанки защищает и качество, и от этой ошибки 400.
</Warning>

## 4. Настройка порогов (не просто выбирайте 0.5)

`relevance_score` отображается функцией sigmoid в диапазон 0–1, из-за чего он выглядит как значение уверенности. **Это не так.**

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

| Группа                                          | Диапазон оценок для действительно релевантных документов | Медиана |
| ----------------------------------------------- | -------------------------------------------------------- | ------- |
| Китайский запрос × китайские документы          | 0.289 – 1.000                                            | 0.948   |
| Одноязычные, не китайские (JA/KO/RU/FR/AR)      | 0.005 – 0.998                                            | 0.241   |
| **Межъязыковые (ZH ↔ EN)**                      | **0.0038 – 0.205**                                       | 0.008   |
| Нерелевантные отвлекающие документы (китайские) | достигал пика **0.945**                                  | 0.029   |

Фиксированный порог 0.5 **отбросил бы все корректные межъязыковые результаты** и большинство результатов, занявших второе место в не-китайских языках, при этом **пропустил бы** отвлекающий документ с сильным совпадением по ключевым словам (для запроса «Apple Inc. FY2024 revenue,» документ о ценах на выращивание яблок получил 0.945 и занял третье место).

Обратите внимание на последнюю строку: нерелевантный документ с наивысшей оценкой (0.945) опережает релевантный документ с наименьшей оценкой (0.289). Эти два распределения перекрываются — именно поэтому не существует «безопасного» абсолютного порога.

<Tip>
  **Три рабочих стратегии фильтрации, в порядке предпочтения:**

  1. **Не фильтруйте — берите Top-N** (N = 3–5). Самый простой и труднее всего сделать неправильно. LLM спокойно переносят небольшой шум
  2. **Относительный порог**: оставляйте результаты, для которых `score >= top1_score × α`, с α около 0.2–0.3. Это автоматически подстраивается под низкий межъязыковой базовый уровень и при этом отсекает длинный хвост
  3. **Абсолютный порог**: только после того, как **вы размечали собственные примеры и измерили распределение на собственном корпусе** — и затем отдельно для каждого языка и каждого типа запроса. Никогда не используйте чужой порог
</Tip>

```python theme={null}
def filter_by_relative_threshold(results, alpha=0.25, max_n=5):
    if not results:
        return []
    top = results[0]["relevance_score"]
    return [r for r in results if r["relevance_score"] >= top * alpha][:max_n]
```

## 5. Отрицание требует подстраховки

Это явная слабость модели. Для запроса "Какие достопримечательности стоит посетить зимой?" два документа, которые **прямо говорят, что это не так**, заняли 2-е и 3-е места, сдвинув действительно релевантные результаты на 4-е и 5-е. nDCG\@3 оказался на уровне 0.47.

Модель сопоставила тему "winter + attractions + travel" и **так и не обработала отрицание**.

<Warning>
  Любой запрос, связанный с **отрицанием, исключением или условными конструкциями** — "gluten-free recipes", "офисы
  за пределами Пекина", "clauses not applicable to minors", "which regions do **не** support delivery" —
  не должен напрямую отдавать пользователям свой переранжированный Top-N.
</Warning>

**Исправление**: добавьте легковесный проход проверки с помощью LLM после reranking. Достаточно недорогой небольшой модели:

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

client = OpenAI(api_key=os.environ["APIYI_API_KEY"],
                base_url="https://api.apiyi.com/v1")

NEG_HINT = ("not ", "n't", "without", "except", "exclude", "free of", "non-")


def verify_if_negated(query, hits):
    """When the query carries negation, confirm each hit actually satisfies it."""
    q = query.lower()
    if not any(k in q for k in NEG_HINT):
        return hits
    kept = []
    for h in hits:
        resp = client.chat.completions.create(
            model="gemini-3.5-flash-lite",
            messages=[{"role": "user", "content":
                       f"Question: {query}\nDocument: {h['text']}\n"
                       f"Does this document positively satisfy the question's condition? "
                       f"Answer only YES or NO."}],
            max_tokens=4,
        )
        if "YES" in resp.choices[0].message.content.upper():
            kept.append(h)
    return kept
```

## 6. Мультиязычное и кросс-лингвальное использование

Китайский, английский, японский, корейский, русский, французский и арабский — а также кросс-лингвальный поиск ZH↔EN — все **корректно упорядочены** при тестировании. Одна модель покрывает мультиязычную базу знаний; вам не нужны отдельные развертывания для каждого языка.

Но следите за **величинами оценок**:

| Случай                                            | Измеренная оценка top-1 |
| ------------------------------------------------- | ----------------------- |
| Запрос на китайском × документы на китайском      | 0.97                    |
| Запрос на корейском × документы на корейском      | 0.87                    |
| Запрос на арабском × документы на арабском        | 1.00                    |
| **Запрос на китайском × документы на английском** | **0.20**                |
| **Запрос на английском × документы на китайском** | **0.011**               |

Правильные кросс-лингвальные ответы получают оценки на 1–2 порядка ниже — но **порядок при этом сохраняется**.

<Tip>
  Два правила для мультиязычных корпусов:

  * **Отдавайте предпочтение порядку, а не оценкам.** Относительные пороги (стратегия 2) здесь гораздо безопаснее, чем абсолютные
  * Если вам все же нужно использовать абсолютный порог, **калибруйте его для каждой пары язык запроса × язык документа, а не глобально**
</Tip>

## 7. Параллельные запросы, кэширование и идемпотентность

### Сначала заложите квоту в расчет, потом говорите о параллельных запросах

Upstream (Huawei Cloud MaaS) допускает **TPM 20,000 / RPM 120** для этой модели. Это ограничение чаще всего недооценивают — у модели эмбеддингов BGE-M3 на той же платформе 1,200,000 TPM, **в 60 раз больше**.

Тестирование (с 70-секундной паузой перед каждым случаем, чтобы окно квоты успевало сброситься) позволило разделить две **независимые** механики:

| Случай | Сценарий                                                           | Успех     | tokens как % от TPM | Примечание                                 |
| ------ | ------------------------------------------------------------------ | --------- | ------------------- | ------------------------------------------ |
| R1     | 10 параллельных × 10 запросов (мелкие)                             | 4/10      | 4%                  | Сбой, несмотря на ничтожное число tokens   |
| R2     | 20 параллельных × 20 запросов (мелкие)                             | 5/20      | 3%                  | Удвоение параллельности все равно дает 4–5 |
| R4     | **Полностью последовательные** 20 запросов (по 1.3K tokens каждый) | 11/20     | 97%                 | **429 при нулевой параллельности**         |
| R5     | **Полностью последовательные** 20 мелких запросов                  | **20/20** | 0%                  | Последовательный режим не затронут         |

**Вывод 1: примерно 4–5 одновременных in-flight слотов.** R1/R2 расходуют лишь 3–4% TPM, но массово дают сбои, а повышение параллельности с 10 до 20 все равно ограничивает число успешных запросов 4–5 — тогда как все 20 последовательных запросов в R5 проходят. Запросы сверх лимита слотов **отклоняются сразу, а не ставятся в очередь**.

**Вывод 2: TPM 20,000 применяется независимо от параллельных запросов.** R4 полностью последовательный, и 8-й запрос получает 429 при 16,093 накопленных tokens, причем до сдвига окна идет 9 подряд неудач:

```
#1–#7   [200]  cumulative tokens climbing to 16,093
#8      [429]  ← first rejection (quota 20,000)
#9–#16  [429]  nine consecutive failures
#17–#20 [200]  ← window slides, recovers
```

<Warning>
  Оба режима сбоя возвращают **идентичное** сообщение (`upstream load saturated`), поэтому по ответу
  невозможно понять, какой лимит вы достигли. Их нужно учитывать в расчете заранее — именно для этого
  и нужна таблица ниже на этапе проектирования.
</Warning>

### Сколько поисков в минуту

Используя измеренный `≈26.9 tokens/document`:

| Кандидатов на запрос | tokens на вызов | Теоретический потолок при TPM 20,000 |
| -------------------- | --------------- | ------------------------------------ |
| 50                   | 673             | \~29 / min                           |
| 100                  | 1,325           | **\~15 / min**                       |
| 200                  | 2,629           | \~7 / min                            |

**Количество кандидатов — это решение по capacity не меньше, чем по качеству**: удвоение числа кандидатов дает ограниченный прирост качества, но вдвое снижает пропускную способность.

Это также объясняет, почему один запрос не может содержать слишком много кандидатов — 1000 кандидатов это 26,867 tokens, **134% от всего минутного бюджета в одном вызове**, так что 429 гарантирован. При 2000 — уже 269%.

<Tip>
  **Чек-лист интеграции**: ограничьте параллельные запросы до 4; реализуйте exponential backoff; проверьте пиковый QPS
  по таблице выше; кэшируйте частые запросы (следующий раздел), чтобы вообще не расходовать квоту.
</Tip>

<Info>
  **Расширение квоты уже в процессе.** Указанный выше TPM 20,000 — это первоначальное выделение upstream, и
  APIYI уже подала заявку на его повышение. Если ваша нагрузка превышает то, что допускает таблица,
  **обратитесь в поддержку APIYI, чтобы пересмотрели upstream-квоту**, а не снижайте масштаб проекта
  под текущее значение.
</Info>

### Кэширование

**Повторение идентичного запроса 10 раз дает побитово идентичный результат** (нулевой drift). Drift около \~3.7e-4 появляется только когда порядок кандидатов перемешан (jitter пакетирования bf16), а порядок не меняется.

Значит, результаты **можно безопасно кэшировать**:

* Используйте в качестве ключа `normalised query + ordered hash of document contents`
* **Не используйте сам `relevance_score` в качестве ключа идемпотентности или дедупликации** — поменяйте порядок кандидатов, и его последние цифры изменятся
* Повторяющиеся запросы (FAQ, популярные поисковые запросы) обычно хорошо кэшируются, экономя и задержку, и нагрузку на upstream

## 8. Интеграция с существующими фреймворками

Имена параметров соответствуют Cohere Rerank v1 (`query` / `documents` / `top_n` / `return_documents`), но **`documents` принимает только массив строк** — `[{"text": "..."}]` возвращает 400 — и в ответе нет полей `id` / `meta`.

<Warning>
  **`cohere` SDK v2 не работает как есть**: он нацелен на `/v2/rerank`, а этот путь возвращает
  HTML главной страницы сайта с HTTP 200, поэтому SDK выдает ошибку разбора. Самый простой
  вариант — обернуть HTTP-вызов самостоятельно.
</Warning>

<CodeGroup>
  ```python LangChain theme={null}
  from typing import Sequence
  import os, requests
  from langchain_core.documents import Document
  from langchain_core.callbacks import Callbacks
  from langchain.retrievers.document_compressors.base import BaseDocumentCompressor


  class ApiyiRerank(BaseDocumentCompressor):
      model: str = "bge-reranker-v2-m3"
      top_n: int = 5
      base_url: str = "https://api.apiyi.com/v1"

      def compress_documents(self, documents: Sequence[Document], query: str,
                             callbacks: Callbacks = None) -> Sequence[Document]:
          docs = list(documents)
          if not docs:
              return []
          r = requests.post(
              f"{self.base_url}/rerank", timeout=120,
              headers={"Authorization": f"Bearer {os.environ['APIYI_API_KEY']}"},
              json={"model": self.model, "query": query,
                    "documents": [d.page_content for d in docs], "top_n": self.top_n},
          )
          r.raise_for_status()
          out = []
          for item in r.json()["results"]:
              d = docs[item["index"]]                    # map back by index, keeping metadata
              d.metadata["relevance_score"] = item["relevance_score"]
              out.append(d)
          return out
  ```

  ```python LlamaIndex theme={null}
  from typing import List, Optional
  import os, requests
  from llama_index.core.postprocessor.types import BaseNodePostprocessor
  from llama_index.core.schema import NodeWithScore, QueryBundle


  class ApiyiRerank(BaseNodePostprocessor):
      model: str = "bge-reranker-v2-m3"
      top_n: int = 5
      base_url: str = "https://api.apiyi.com/v1"

      def _postprocess_nodes(self, nodes: List[NodeWithScore],
                             query_bundle: Optional[QueryBundle] = None
                             ) -> List[NodeWithScore]:
          if not nodes or query_bundle is None:
              return nodes
          r = requests.post(
              f"{self.base_url}/rerank", timeout=120,
              headers={"Authorization": f"Bearer {os.environ['APIYI_API_KEY']}"},
              json={"model": self.model, "query": query_bundle.query_str,
                    "documents": [n.node.get_content() for n in nodes],
                    "top_n": self.top_n},
          )
          r.raise_for_status()
          out = []
          for item in r.json()["results"]:
              n = nodes[item["index"]]
              n.score = item["relevance_score"]
              out.append(n)
          return out
  ```

  ```python Универсальная обертка (с повторной попыткой) theme={null}
  import os, time, requests

  BASE = "https://api.apiyi.com/v1"


  def rerank(query, documents, top_n=5, tries=4):
      """Rerank with backoff. A 429 means upstream congestion and usually clears on retry."""
      delay = 5
      for attempt in range(tries):
          r = requests.post(
              f"{BASE}/rerank", timeout=120,
              headers={"Authorization": f"Bearer {os.environ['APIYI_API_KEY']}"},
              json={"model": "bge-reranker-v2-m3", "query": query,
                    "documents": documents, "top_n": top_n},
          )
          if r.status_code == 429 and attempt < tries - 1:
              time.sleep(delay)
              delay *= 2
              continue
          r.raise_for_status()
          return r.json()["results"]
  ```
</CodeGroup>

**Для Dify / RAGFlow / FastGPT**: в разделе Провайдер модели → Модель rerank выберите собственного провайдера, совместимого с интерфейсом Cohere/Jina, и заполните:

| Параметр        | Значение                             |
| --------------- | ------------------------------------ |
| Базовый URL API | `https://api.apiyi.com/v1`           |
| API-ключ        | Ваш APIYI token (начинается с `sk-`) |
| Имя модели      | `bge-reranker-v2-m3`                 |

<Warning>
  **Путь должен включать `/v1`.** И `/rerank`, и `/v2/rerank` возвращают **HTTP 200 с HTML главной
  страницы сайта** вместо JSON 404 — что в платформах проявляется как «невозможно разобрать
  ответ» или «модель недоступна» и трудно диагностируется. Проверьте, что итоговый URL запроса
  — `https://api.apiyi.com/v1/rerank`, прежде чем искать причину в чем-то еще.

  Если платформа сама добавляет `/v1` (формируя `{base}/v1/rerank`), задайте базовый URL API как
  `https://api.apiyi.com`, чтобы не получить дублированный `/v1`. Один `curl` быстрее всего подтверждает
  правильность пути.
</Warning>

## 9. Контрольный список перед запуском

<AccordionGroup>
  <Accordion title="Корректность">
    * [ ] Обратное сопоставление через `results[].index`, а не по совпадению текста
    * [ ] Длинные документы разбиваются на фрагменты (200–500 символов) и агрегируются по лучшему score фрагмента
    * [ ] Ни одна пара запрос-документ не превышает 8192 token
    * [ ] Для запросов с отрицанием или исключением выполняется дополнительная проверка через LLM
  </Accordion>

  <Accordion title="Надежность">
    * [ ] Наборы кандидатов ограничены 100
    * [ ] **Параллельные запросы ограничены 4** (измерено: upstream допускает примерно 4–5 одновременных запросов, лишние отклоняются сразу, а не ставятся в очередь)
    * [ ] **Пиковая пропускная способность проверена относительно TPM 20,000** (\~15 поисков/мин при 100 кандидатах) и подтверждено, что ее достаточно
    * [ ] Для 429 реализован backoff-retry (перегрузка upstream носит временный характер)
    * [ ] Timeout установлен на 60 с или больше (100 коротких документов — это около 4.3 с на P95, но длинные документы или большие наборы доходят до десятков секунд)
    * [ ] Путь запроса подтвержден как `/v1/rerank` — неверный путь возвращает 200 + HTML, а не 404
    * [ ] Имя модели указано правильно и в нижнем регистре — опечатка возвращает 503, что легко принять за сбой
  </Accordion>

  <Accordion title="Качество">
    * [ ] Фильтрация использует Top-N или относительный порог, а не угаданное фиксированное значение
    * [ ] Проверены величины scores для многоязычных и межъязыковых сценариев, чтобы низкие scores не отфильтровывались
    * [ ] Небольшой размеченный набор запросов был A/B-тестирован (rerank включен/выключен), чтобы подтвердить, что метрики действительно изменились
  </Accordion>

  <Accordion title="Стоимость и кэширование">
    * [ ] Учитывается, что `usage.input_tokens` / `output_tokens` всегда равны 0 — см. `prompt_tokens` / `total_tokens`
    * [ ] Учитывается, что ни `top_n`, ни `return_documents` не снижает расход — оценивается каждый кандидат
    * [ ] Учет затрат основан на счете из консоли (`prompt_tokens` и `total_tokens` отличаются примерно на 45% при 100 кандидатах, и в этой проверке не удалось подтвердить, какой из них выставляется к оплате)
    * [ ] Настроено кэширование результатов для часто повторяющихся запросов
  </Accordion>
</AccordionGroup>

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

* [Обзор bge-reranker-v2-m3](/ru/api-capabilities/rerank/overview)
* [Песочница Rerank API](/ru/api-capabilities/rerank/rerank-api)
* [Текстовые embeddings](/ru/api-capabilities/text-embedding)
