Все статьи
Статья
25 июня 2026 г.9 мин

RAG на эмбеддингах: как связать модель с базой знаний

Векторный поиск на bge-m3, POST /v1/embeddings и сборка контекста. Показываю рабочий пайплайн и места, где RAG обычно ломается.

Роман Дьяченко


RAG на эмбеддингах: как связать модель с базой знаний

RAG нужен, когда модель должна отвечать по вашим данным — документации, тикетам, договорам, — которых нет в её весах. Дообучать ради этого дорого и медленно. Дешевле находить релевантные куски текста и подкладывать их в промпт.

Пайплайн из четырёх шагов

  1. Режем документы на чанки.
  2. Считаем эмбеддинги через POST /v1/embeddings.
  3. Кладём векторы в хранилище (pgvector, Qdrant, даже numpy для прототипа).
  4. На запрос считаем эмбеддинг вопроса, ищем ближайшие чанки, отдаём их модели.

Эмбеддинги через bge-m3

bge-m3 — рабочая лошадка: мультиязычная (русский держит уверенно), размерность 1024, контекст до 8K токенов на вход. Для большинства баз знаний этого хватает.

from openai import OpenAI
client = OpenAI(api_key=KEY, base_url="https://api.yenisei.ru/v1")
def embed(texts: list[str]) -> list[list[float]]:
resp = client.embeddings.create(model="bge-m3", input=texts)
return [d.embedding for d in resp.data]

Отправляйте тексты пачками по 32–64 штуки — так меньше round-trip'ов. Эмбеддинги дешёвые (порядка 1–2 ₽ за 1M токенов), но за индексацию большой базы всё равно набегает, так что считайте заранее.

Чанкинг решает больше, чем модель

Главная ошибка новичков — резать по 512 символов «в лоб». Тогда предложение рвётся посередине и вектор получается бессмысленным. Что работает:

  • Режьте по структуре: заголовки, абзацы, пункты списка.
  • Держите чанк в 200–400 токенов с перекрытием 15–20%.
  • Храните рядом метаданные: источник, заголовок раздела, дату. Их потом покажете пользователю как ссылку.

Поиск и сборка контекста

Косинусная близость в pgvector:

SELECT chunk, source, 1 - (embedding <=> %(q)s) AS score
FROM kb
ORDER BY embedding <=> %(q)s
LIMIT 8;

Восемь чанков — разумный старт. Дальше отсекайте по порогу score (для bge-m3 обычно 0.5–0.6) и складывайте в промпт:

context = "\n\n".join(f"[{c['source']}] {c['chunk']}" for c in hits)
messages = [
{"role": "system", "content":
"Отвечай ТОЛЬКО по контексту ниже. Если ответа нет — скажи, что не знаешь."},
{"role": "user", "content": f"Контекст:\n{context}\n\nВопрос: {question}"},
]
resp = client.chat.completions.create(model="qwen-flash", messages=messages)

Где RAG ломается

  • Модель игнорирует контекст и отвечает из головы. Лечится жёстким system-промптом и явным требованием цитировать источник.
  • Похожие, но не те чанки. Чистый вектор путает синонимичные разделы. Добавьте гибридный поиск: BM25 по ключевым словам плюс вектор, и объедините ранги (reciprocal rank fusion).
  • Слишком длинный контекст. Не пихайте 50 чанков «на всякий случай» — модель тонет и растёт счёт. Лучше 6–8 точных.
  • Устаревшая база. Эмбеддинги не обновляются сами. Пересчитывайте изменённые документы по хуку на сохранение.

Какую модель ставить на генерацию

Ответ по готовому контексту — задача несложная, тяжёлый reasoning тут не нужен. Начните с быстрой и дешёвой модели (qwen-flash, deepseek-flash). Если ответы получаются поверхностными на сложных вопросах — поднимайте до deepseek-pro. Эмбеддинги при этом остаются на bge-m3: качество поиска от модели-генератора не зависит.

Соберите сначала минимальную версию на numpy и десяти документах, убедитесь, что поиск находит нужное, и только потом тащите Qdrant и гибридный поиск. Большинство проблем RAG — это проблемы чанкинга, а не инфраструктуры.