smartaipack
Документация SmartAIPack

RAG

Как индексировать документы, искать и пересортировывать фрагменты и формировать ответ с источниками.

Чтобы собрать поиск с ответом по внутренним документам, приложение должно выполнить три вида запросов через AI Gateway: получить векторы, найти подходящие фрагменты, при необходимости пересортировать их и передать выбранный контекст модели генерации. RAG — генерация с поиском по своим данным — помогает обосновывать ответы источниками и снижать риск выдуманных сведений.

AI Gateway предоставляет пути к моделям, но не хранит ваш индекс, не загружает документы и не строит весь процесс автоматически. Разбиение текстов, векторное хранилище, поиск, права доступа, сбор контекста и ссылки на источники остаются в вашем приложении.

Как работает RAG

Рабочая схема состоит из четырёх этапов:

  1. Разбить документы на осмысленные фрагменты и получить для них векторы.
  2. Получить вектор вопроса и найти ближайшие фрагменты в своём хранилище.
  3. При необходимости отдельно пересортировать найденные фрагменты по соответствию вопросу.
  4. Передать лучшие фрагменты модели генерации и попросить отвечать только по этому контексту.

Для каждого вызова выберите в разделе Модели модель с нужным опубликованным путём и скопируйте точный request_model_id. Идентификаторы модели векторизации, модели пересортировки и модели генерации могут отличаться. Не подставляйте название из старого примера: каталог и доступность меняются.

Шаг 1: индексировать документы

Сначала сохраните для каждого фрагмента как минимум:

  • собственный идентификатор;
  • исходный текст;
  • ссылку или идентификатор документа;
  • версию документа;
  • вектор;
  • точный идентификатор модели векторизации.

Путь векторизации — POST https://route.smartaipack.ru/v1/embeddings. Минимальное тело содержит model и input:

curl https://route.smartaipack.ru/v1/embeddings \
  -H "Authorization: Bearer $AI_GATEWAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "<EMBEDDING_REQUEST_MODEL_ID>",
    "input": [
      "Фрагмент документа номер один.",
      "Фрагмент документа номер два."
    ]
  }'

Для подтверждённого совместимого поставщика input может быть строкой или массивом строк. Размер запроса и допустимое число элементов зависят от конкретной модели и поставщика, поэтому обрабатывайте документы порциями и проверяйте ограничения выбранной модели. Успешный совместимый ответ содержит массив data, а вектор каждого элемента находится в data[].embedding.

Для документов и будущих вопросов используйте одну и ту же модель векторизации. При смене модели постройте индекс заново: векторы из разных пространств нельзя корректно сравнивать как один набор.

Подробнее формат запроса разобран в статье «Векторные представления через AI Gateway».

Шаг 2: найти подходящие фрагменты

Когда приходит вопрос, отправьте его на тот же путь /v1/embeddings с тем же request_model_id. Затем приложение сравнивает вектор вопроса с сохранёнными векторами и выбирает ближайшие фрагменты.

Для небольшого учебного набора достаточно косинусного сходства в памяти. В рабочей системе индекс и поиск должны находиться в выбранном вами хранилище. AI Gateway не предоставляет встроенную векторную базу и не принимает на себя хранение исходных документов.

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

Шаг 3: пересортировать для точности

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

Путь — POST https://route.smartaipack.ru/v1/rerank. Проект подтверждает такое минимальное тело:

curl https://route.smartaipack.ru/v1/rerank \
  -H "Authorization: Bearer $AI_GATEWAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "<RERANK_REQUEST_MODEL_ID>",
    "query": "Как отозвать API-ключ?",
    "documents": [
      "Ключ можно отозвать в личном кабинете.",
      "Расход по моделям отображается в аналитике."
    ]
  }'

Здесь documents — массив строк. Формат успешного ответа может зависеть от модели и поставщика. До написания рабочего разбора ответа выполните обезличенный проверочный запрос, сохраните его ответ как образец для выбранной модели и адаптируйте разборщик именно к этой форме. Не стройте интеграцию на предположении о названиях полей или структуре элементов.

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

Шаг 4: сгенерировать ответ с контекстом

Финальный запрос приложение отправляет на POST https://route.smartaipack.ru/v1/chat/completions. Минимальное тело содержит точный идентификатор модели и массив messages:

curl https://route.smartaipack.ru/v1/chat/completions \
  -H "Authorization: Bearer $AI_GATEWAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "<CHAT_REQUEST_MODEL_ID>",
    "messages": [
      {
        "role": "system",
        "content": "Отвечай только по переданному контексту. Если данных недостаточно, так и скажи. Указывай ID использованных источников."
      },
      {
        "role": "user",
        "content": "Контекст:\n[doc-17] Ключ можно отозвать в личном кабинете.\n\nВопрос: Как отозвать API-ключ?"
      }
    ]
  }'

Контекст вставляет ваше приложение, а не шлюз. Передавайте вместе с текстом устойчивые ID или внутренние ссылки на источники. Инструкция модели должна запрещать добавлять сведения вне контекста и явно разрешать ответ «данных недостаточно». Это не устраняет ошибки полностью, но делает ответ проверяемее и снижает риск необоснованных утверждений.

Полный пример

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

Перед запуском задайте AI_GATEWAY_API_KEY, EMBEDDING_REQUEST_MODEL_ID и CHAT_REQUEST_MODEL_ID. Каждый идентификатор скопируйте из строки соответствующего пути в Моделях.

import json
import math
import os
import urllib.request

BASE_URL = "https://route.smartaipack.ru/v1"
API_KEY = os.environ["AI_GATEWAY_API_KEY"]
EMBEDDING_MODEL_ID = os.environ["EMBEDDING_REQUEST_MODEL_ID"]
CHAT_MODEL_ID = os.environ["CHAT_REQUEST_MODEL_ID"]

documents = [
    {
        "id": "doc-17",
        "source": "/support",
        "text": "Ключ можно отозвать в личном кабинете.",
    },
    {
        "id": "doc-23",
        "source": "/dashboard/analytics",
        "text": "Расход по моделям отображается в аналитике.",
    },
    {
        "id": "doc-41",
        "source": "/pricing",
        "text": "Актуальные цены опубликованы на странице тарифов.",
    },
]


def post(path, body):
    request = urllib.request.Request(
        BASE_URL + path,
        data=json.dumps(body).encode("utf-8"),
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Content-Type": "application/json",
        },
        method="POST",
    )
    with urllib.request.urlopen(request, timeout=60) as response:
        return json.load(response)


def cosine(left, right):
    numerator = sum(a * b for a, b in zip(left, right))
    left_norm = math.sqrt(sum(value * value for value in left))
    right_norm = math.sqrt(sum(value * value for value in right))
    if left_norm == 0 or right_norm == 0:
        return 0.0
    return numerator / (left_norm * right_norm)


document_result = post(
    "/embeddings",
    {
        "model": EMBEDDING_MODEL_ID,
        "input": [document["text"] for document in documents],
    },
)
document_vectors = [item["embedding"] for item in document_result["data"]]

question = "Где посмотреть расход по моделям?"
query_result = post(
    "/embeddings",
    {"model": EMBEDDING_MODEL_ID, "input": question},
)
query_vector = query_result["data"][0]["embedding"]

ranked = sorted(
    zip(documents, document_vectors),
    key=lambda pair: cosine(query_vector, pair[1]),
    reverse=True,
)
selected = [document for document, _vector in ranked[:2]]
context = "\n\n".join(
    f'[{item["id"]}] {item["text"]} Источник: {item["source"]}'
    for item in selected
)

chat_result = post(
    "/chat/completions",
    {
        "model": CHAT_MODEL_ID,
        "messages": [
            {
                "role": "system",
                "content": (
                    "Отвечай только по контексту. Если данных недостаточно, "
                    "сообщи об этом. В конце перечисли ID источников."
                ),
            },
            {
                "role": "user",
                "content": f"Контекст:\n{context}\n\nВопрос: {question}",
            },
        ],
    },
)
print(chat_result["choices"][0]["message"]["content"])

Чтобы добавить пересортировку, передайте тексты из ranked в /v1/rerank с отдельным RERANK_REQUEST_MODEL_ID. Сначала сохраните обезличенный проверочный ответ выбранной модели, затем напишите небольшой преобразователь, который возвращает документы в новом порядке. Сам пример намеренно не угадывает структуру ответа этого пути.

Когда нужна пересортировка

Рассмотрите дополнительный этап, если:

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

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

Стратегии разбиения

Качество поиска во многом определяется границами фрагментов. Практичные варианты:

  • по заголовкам и разделам — сохраняет структуру инструкции;
  • по абзацам и предложениям — подходит для коротких справочных материалов;
  • по смысловым границам — полезно для документов с неравномерными разделами;
  • с перекрытием соседних частей — помогает не потерять условие на границе, но увеличивает объём индекса и число дублей.

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

Рекомендации

  • Соберите проверочный набор из вопросов, ожидаемых документов и допустимых ответов до настройки порогов.
  • Храните ID источника, версию текста и модель векторизации рядом с каждым вектором.
  • Переиндексируйте изменившиеся документы, а при смене модели векторизации — весь индекс.
  • Сначала фильтруйте документы по правам доступа, затем выполняйте смысловой поиск.
  • Не отправляйте в модель секреты, персональные данные и документы, к которым у пользователя нет доступа.
  • Ограничивайте объём контекста и убирайте дублирующиеся фрагменты.
  • Разрешайте модели сообщить о нехватке данных и сохраняйте ID источников в итоговом ответе.
  • Сравнивайте качество, задержку и стоимость варианта с пересортировкой и без неё.
  • Проверяйте отдельные вызовы в Использовании, а итоговые расходы — в Аналитике.

Доступные модели

Не закрепляйте текущие названия моделей в коде или документации приложения. Для каждого этапа откройте Модели и выберите строку с нужным опубликованным путём:

ЭтапПутьКакое значение передать в model
Векторизация документов и вопросов/v1/embeddingsТочный request_model_id этой строки; один и тот же для индекса и вопросов
Пересортировка/v1/rerankОтдельный точный request_model_id строки пересортировки
Генерация ответа/v1/chat/completionsОтдельный точный request_model_id строки генерации

Перед рабочей нагрузкой отправьте короткий обезличенный запрос на каждый выбранный путь. Текущие цены смотрите на странице цен, а ограничения — в руководстве по лимитам.

Связанные материалы

На этой странице