- Эмбеддинги —
POST /v1/embeddings: векторы документов и запроса - Чат —
POST /v1/chat/completions: ответ по найденному контексту
В Polza.AI нет dedicated rerank-эндпойнта (cross-encoder). Для точного
ранжирования используется LLM-as-judge через быструю модель — отдельный
запрос к чату с просьбой оценить каждого кандидата. Это работает хуже
настоящего cross-encoder rerank, но даёт заметный буст над голым cosine.
Подробнее — в API Reference → Embeddings.
Как устроен пайплайн
- Индекс. Делите документы на чанки, считаете эмбеддинги батчем. В прототипе векторы можно держать в памяти, в проде — в векторной базе.
- Поиск. Эмбеддинг запроса, ближайшие чанки по косинусу. Top-10–20 кандидатов.
- Rerank. Быстрая модель оценивает релевантность каждого кандидата. Top-3–5 идут в контекст.
- Ответ. Эти фрагменты уходят в чат как контекст. Модель отвечает только по ним и ссылается на источники.
Требования
httpx— прямой вызов/v1/embeddingsчерез кастомный класс (обход LangChain-токенизации)numpy— cosine similarityopenai— OpenAI-совместимый клиент для judge-rerank и генерации (опционально, если не используете LangChain для чата)
Шаг 1. Индекс
БерёмPolzaEmbeddings из LangChain-доки — это обёртка над httpx.post, которая обходит проблему LangChain-токенизации. Считаем эмбеддинги всех чанков одним батчем.
Та же модель для индекса и для запроса. Смешивать нельзя — пространства
векторов разных моделей несовместимы.
Шаг 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 игнорирует.- База маленькая (< 50 чанков) — косинус уже хорошо попадает.
- Критична минимальная задержка — judge добавляет один запрос.
- Собираете прототип без метрик качества.
- База большая (1000+ чанков), косинус возвращает шум.
- Важна точность: клиентский FAQ, юридические или медицинские тексты.
- Берёте 10–20 кандидатов косинусом и хотите оставить 3–5 релевантных.
Шаг 4. Ответ по контексту
Top после judge (или сразу после cosine) — в системный промпт. Просим модель ссылаться на источники и молчать, если контекста мало.Streaming ответа
Для UI-приложений важно показывать текст по мере генерации. Polza поддерживает стриминг черезstream=True:
max_tokens.
Async RAG (параллельный retrieve + judge)
Judge добавляет ~1 сек к задержке. Если хочется быстрее —asyncio.gather для параллельных вызовов. Удобнее всего через httpx.AsyncClient, потому что openai.AsyncOpenAI для judge не даёт выигрыша по сравнению с прямым httpx.
asyncio.gather.
Метаданные: фильтрация по разделу/источнику
Если чанки размечены (раздел документа, URL, автор, дата) — фильтруйте кандидаты до judge, чтобы не тратить токены на нерелевантные разделы:Multi-query RAG (переформулировка вопроса)
Один и тот же вопрос можно задать тремя способами: «Как вернуть деньги?», «Возврат средств», «Как отменить заказ и получить refund?». Каждая переформулировка находит разные чанки. Multi-query расширяет покрытие:Полный пример
Все четыре шага подряд, end-to-end:[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) — держат контекст, но размывают сигнал.
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 съедает лимит).
Решение проблем
Ошибка 400 при эмбеддингах: 'input должен быть непустой строкой либо непустым массивом непустых строк'
Ошибка 400 при эмбеддингах: 'input должен быть непустой строкой либо непустым массивом непустых строк'
LangChain пре-токенизирует тексты и шлёт массив токенов. Используйте класс
PolzaEmbeddings через httpx (см. LangChain-доку).Judge выдаёт невалидный JSON или текст с пояснениями
Judge выдаёт невалидный JSON или текст с пояснениями
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для продакшена
Cosine возвращает мусор — релевантные чанки на 5–10 месте
Cosine возвращает мусор — релевантные чанки на 5–10 месте
- Уменьшите размер чанка (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).Задержка RAG-pipeline > 3 секунд
Задержка RAG-pipeline > 3 секунд
Judge добавляет 1–1.5 секунды. Оптимизации:
- Параллельный вызов judge и подготовки контекста через
asyncio.gather(см. секцию «Async RAG» выше). - Пропустить judge для топ-5 cosine (если порог > 0.7).
- Заменить judge на cross-encoder локально (sentence-transformers) — но это уже не через Polza.
Embeddings работают, но качество низкое на русском
Embeddings работают, но качество низкое на русском
qwen/qwen3-embedding-8b обучен на мультиязычных данных и для русского обычно лучше OpenAI-моделей. Если уже используете OpenAI — попробуйте intfloat/multilingual-e5-large (1024d, специально для кросс-языкового поиска).Следующие шаги
LangChain
Подробнее про ChatOpenAI и embeddings-обёртку через Polza
API Reference: Embeddings
Полная справка по эндпойнту /v1/embeddings
Кеширование промпта
Как удешевить повторяющиеся запросы
Structured output
JSON по схеме из найденного контекста