Skip to main content
RAG (Retrieval-Augmented Generation) подставляет в контекст модели фрагменты вашей базы знаний. Ответ опирается на найденные документы: меньше галлюцинаций, данные можно обновлять без дообучения. Два эндпойнта Polza.AI закрывают пайплайн:
  • ЭмбеддингиPOST /v1/embeddings: векторы документов и запроса
  • ЧатPOST /v1/chat/completions: ответ по найденному контексту
В Polza.AI нет dedicated rerank-эндпойнта (cross-encoder). Для точного ранжирования используется LLM-as-judge через быструю модель — отдельный запрос к чату с просьбой оценить каждого кандидата. Это работает хуже настоящего cross-encoder rerank, но даёт заметный буст над голым cosine. Подробнее — в API Reference → Embeddings.

Как устроен пайплайн

  1. Индекс. Делите документы на чанки, считаете эмбеддинги батчем. В прототипе векторы можно держать в памяти, в проде — в векторной базе.
  2. Поиск. Эмбеддинг запроса, ближайшие чанки по косинусу. Top-10–20 кандидатов.
  3. Rerank. Быстрая модель оценивает релевантность каждого кандидата. Top-3–5 идут в контекст.
  4. Ответ. Эти фрагменты уходят в чат как контекст. Модель отвечает только по ним и ссылается на источники.

Требования

  • httpx — прямой вызов /v1/embeddings через кастомный класс (обход LangChain-токенизации)
  • numpy — cosine similarity
  • openai — OpenAI-совместимый клиент для judge-rerank и генерации (опционально, если не используете LangChain для чата)

Шаг 1. Индекс

Берём PolzaEmbeddings из LangChain-доки — это обёртка над httpx.post, которая обходит проблему LangChain-токенизации. Считаем эмбеддинги всех чанков одним батчем.
Та же модель для индекса и для запроса. Смешивать нельзя — пространства векторов разных моделей несовместимы.
Для продакшена с большими базами храните эмбеддинги в pgvector, Qdrant, Chroma или FAISS — см. секцию Продакшен-стек ниже.

Шаг 2. Поиск

Косинус similarity через numpy. Никакого евклидова расстояния — для эмбеддингов косинус канонический.

Шаг 3. LLM-as-judge rerank

Берём быструю и дешёвую модель (рекомендуем deepseek/deepseek-v4-flash-0731), просим оценить релевантность каждого кандидата от 0 до 10. Сортируем по оценке, отбрасываем слабые.
DeepSeek выбран вместо Gemini/Claude для judge потому что возвращает чистый JSON-массив без markdown-обёрток и ```json ``` форматирования. Это убирает необходимость в regex strip — экономит код и снижает риск ошибок парсинга. Gemini тоже работает, но оборачивает ответ в ```json ... ``` — нужен regex. Claude через Polza параметр response_format игнорирует.
DeepSeek — reasoning-модель. Внутренние reasoning_tokens идут сверх max_tokens и съедают видимый лимит. Для judge обязательно ставьте max_tokens >= 1500, иначе вернётся пустой content и JSON парсить будет нечего. Реальный расход reasoning — от 90 до 1000+ токенов в зависимости от сложности промпта.
Стоимость LLM-as-judge на DeepSeek-V4-Flash — порядка 0.03–0.05 ₽ за запрос (зависит от длины кандидатов и reasoning). Это в 30–100 раз дешевле последующего вызова claude-opus-4.8 для генерации. Для прототипа и небольших баз — приемлемый trade-off.
Когда LLM-as-judge не нужен:
  • База маленькая (< 50 чанков) — косинус уже хорошо попадает.
  • Критична минимальная задержка — judge добавляет один запрос.
  • Собираете прототип без метрик качества.
Когда стоит использовать:
  • База большая (1000+ чанков), косинус возвращает шум.
  • Важна точность: клиентский FAQ, юридические или медицинские тексты.
  • Берёте 10–20 кандидатов косинусом и хотите оставить 3–5 релевантных.

Шаг 4. Ответ по контексту

Top после judge (или сразу после cosine) — в системный промпт. Просим модель ссылаться на источники и молчать, если контекста мало.
Системный промпт «отвечай только по контексту, ссылайся на [n]» держите стабильным — его подхватит кеш промпта, и повторные запросы обойдутся дешевле.

Streaming ответа

Для UI-приложений важно показывать текст по мере генерации. Polza поддерживает стриминг через stream=True:
Токены появляются по мере ответа модели — пользователь видит текст сразу, не дожидаясь всего max_tokens.

Async RAG (параллельный retrieve + judge)

Judge добавляет ~1 сек к задержке. Если хочется быстрее — asyncio.gather для параллельных вызовов. Удобнее всего через httpx.AsyncClient, потому что openai.AsyncOpenAI для judge не даёт выигрыша по сравнению с прямым httpx.
При желании можно пойти дальше и запустить judge параллельно с подготовкой контекста через asyncio.gather.

Метаданные: фильтрация по разделу/источнику

Если чанки размечены (раздел документа, URL, автор, дата) — фильтруйте кандидаты до judge, чтобы не тратить токены на нерелевантные разделы:
Это особенно важно для больших баз: суджаете 10k чанков до 200 в нужном разделе до LLM-as-judge.

Multi-query RAG (переформулировка вопроса)

Один и тот же вопрос можно задать тремя способами: «Как вернуть деньги?», «Возврат средств», «Как отменить заказ и получить refund?». Каждая переформулировка находит разные чанки. Multi-query расширяет покрытие:
Стоимость: +1 запрос на переформулировку (~0.01 ₽) → +5–10 уникальных кандидатов → выше recall.

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

Все четыре шага подряд, end-to-end:
Реальный прогон этого скрипта выдаёт структурированный ответ Claude со ссылками [1], [2], [3]. Стоимость одного шага 4 (Claude Sonnet) — порядка 0.2–0.5 ₽ для 7 чанков; для крупных баз с большим контекстом — до 0.9–1.5 ₽ за запрос. Judge на Gemini добавляет ещё ~0.02 ₽. Embed запроса — ~0.0001 ₽. Полный pipeline — обычно в пределах 1 ₽.

Нарезка документов

От нарезки зависит качество поиска не меньше, чем от модели:
  • По абзацам и заголовкам — сохраняет смысл, хорошо для структурированных текстов.
  • Фиксированный размер с перекрытием — 200–500 токенов и ~10–15% overlap, чтобы мысль не обрывалась на границе.
  • По смысловым границам — предложения, секции, разрывы глав.
Размер чанка:
  • Мелкие (200–300 токенов) — точнее попадают в запрос, но теряют окружение.
  • Крупные (500–1000) — держат контекст, но размывают сигнал.
Подбирайте на своих данных. Для русского текста используйте overlap 15–20% из-за богатой морфологии. Готовый splitter для русского текста (через langchain-text-splitters):

Продакшен-стек

Для баз больше нескольких сотен чанков in-memory хранилище не подходит. Варианты: langchain-postgres и langchain-qdrant реализуют интерфейс VectorStore из langchain-core, поэтому код шагов 2–4 не меняется — меняется только место хранения doc_vecs. Кеширование эмбеддингов. Эмбеддинг одного текста детерминирован — можно кешировать по hashlib.sha256(text).hexdigest() в Redis/Postgres. На повторной индексации (CI/CD, обновление одного документа) не пересчитываете тысячи чанков. Гибридный поиск (BM25 + vector). Для коротких запросов и точных терминов (названия моделей, ID, имена) одного cosine мало — добавьте BM25 и combine scores. Готовые движки: Qdrant и Weaviate имеют встроенный hybrid search; в Postgres — pg_bm25 расширение или ручной BM25 через rank_bm25.

Практика

  • Одна модель для индекса и запроса. Меняете модель → переиндексируете всё.
  • Пакет в input, не по одному тексту. До 100 чанков в одном /v1/embeddings — дешевле и быстрее.
  • 10–20 кандидатов косинусом → 3–5 после judge. Больше кандидатов на judge — больше расходы и задержка.
  • Метаданные в промпт. Заголовок, раздел, URL рядом с текстом — так проще цитировать.
  • Threshold релевантности после judge. Всё что ниже 5/10 — отбрасывайте, иначе модель начнёт «додумывать».
  • Следите за usage.cost_rub. RAG-pipeline = 3 запроса (embed + judge + chat). В прототипе это ~0.5–1 ₽ за запрос, в продакшене с большими базами — больше.
  • DeepSeek для judge, а не Gemini/Claude. DeepSeek-V4-Flash возвращает чистый JSON-массив без markdown-обёрток — regex strip не нужен. Gemini оборачивает в ```json ```, Claude через Polza параметр response_format игнорирует. Используйте DeepSeek с max_tokens >= 1500 (reasoning съедает лимит).

Решение проблем

LangChain пре-токенизирует тексты и шлёт массив токенов. Используйте класс PolzaEmbeddings через httpx (см. LangChain-доку).
google/gemini-2.5-flash часто оборачивает JSON в json … . Решения:
  • Переключиться на deepseek/deepseek-v4-flash-0731 — возвращает чистый JSON без обёрток (рекомендуется, см. шаг 3 выше)
  • Regex strip (см. judge_rerank выше) — fallback для Gemini
  • response_format={"type": "json_object"} работает только для Gemini через Polza. Claude этот параметр игнорирует
  • PydanticOutputParser из langchain-core для продакшена
  • Уменьшите размер чанка (200–300 токенов вместо 500+).
  • Попробуйте другую модель: openai/text-embedding-3-large (3072d) точнее, но дороже.
  • Для русского текста: qwen/qwen3-embedding-8b лучше text-embedding-3-small.
  • Добавьте BM25 и используйте гибридный поиск.
Усильте системный промпт: явно запретите «отвечать по памяти». Если не помогает — переключитесь на более послушную модель (например anthropic/claude-sonnet-4.6 вместо openai/gpt-5-mini).
Judge добавляет 1–1.5 секунды. Оптимизации:
  • Параллельный вызов judge и подготовки контекста через asyncio.gather (см. секцию «Async RAG» выше).
  • Пропустить judge для топ-5 cosine (если порог > 0.7).
  • Заменить judge на cross-encoder локально (sentence-transformers) — но это уже не через Polza.
qwen/qwen3-embedding-8b обучен на мультиязычных данных и для русского обычно лучше OpenAI-моделей. Если уже используете OpenAI — попробуйте intfloat/multilingual-e5-large (1024d, специально для кросс-языкового поиска).

Следующие шаги

LangChain

Подробнее про ChatOpenAI и embeddings-обёртку через Polza

API Reference: Embeddings

Полная справка по эндпойнту /v1/embeddings

Кеширование промпта

Как удешевить повторяющиеся запросы

Structured output

JSON по схеме из найденного контекста