RAG на дешёвой модели: поиск по базе знаний без векторной БД

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

5 мин · · разработка · экономия

Чтобы бот отвечал по вашей документации, не обязательно поднимать векторную базу и платить сервису за эмбеддинги. Для многих баз знаний хватает RAG на дешёвой модели: поиск по ключевым словам у себя на сервере и генерация ответа моделью за доли цента. Ниже — схема, рабочий код на Python с rank_bm25, расчёт стоимости запроса и признаки того, что пора переходить на модель посильнее.

Схема: поиск у себя, ответ у модели

  1. Документы режутся на фрагменты — абзацы или куски по несколько сотен токенов.
  2. По фрагментам строится индекс BM25 — прямо в памяти процесса.
  3. На вопрос пользователя индекс возвращает 10–30 самых подходящих фрагментов.
  4. Фрагменты и вопрос уходят модели с инструкцией отвечать только по ним и ссылаться на источник.

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

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

Индекс на rank_bm25

Пакет ставится командой pip install rank_bm25. Основной класс — BM25Okapi: он принимает список документов, каждый из которых — список токенов, и возвращает оценки через get_scores. Предобработку текста пакет не делает, это ваша задача.

bash
import re
from pathlib import Path

from rank_bm25 import BM25Okapi


def tokenize(text: str) -> list[str]:
    return re.findall(r"\w+", text.lower())


chunks = []
for path in Path("kb").glob("**/*.md"):
    text = path.read_text(encoding="utf-8")
    for i, part in enumerate(re.split(r"\n\s*\n", text)):

Код рассчитан на Python 3.9+. Пара замечаний:

  • Для русского языка с его падежами простое разбиение на слова теряет часть совпадений. Если это заметно на ваших вопросах, добавьте в tokenize стемминг — приведение слов к основе — любой библиотекой, которой вы доверяете. Главное — одинаково обрабатывать и документы, и вопрос.
  • Короткие абзацы лучше склеивать с соседними: фрагмент из одной строки без контекста модели мало помогает.

Ответ дешёвой моделью

Модель получает найденные фрагменты с пометкой источника и вопрос. Инструкция запрещает отвечать «из головы».

bash
import os

from openai import OpenAI

client = OpenAI(base_url="https://api.zukko.pro/v1", api_key=os.environ["ZUKKO_API_KEY"])

SYSTEM = (
    "Ты отвечаешь на вопросы по базе знаний компании. "
    "Используй только фрагменты из сообщения пользователя. "
    "После каждого утверждения укажи источник в квадратных скобках. "
    "Если во фрагментах нет ответа, так и скажи."
)

Почему контекст 1M важен: у deepseek-v4.1-flash и gemini-3.8-flash контекст 1M токенов, так что даже 50 фрагментов по 500 токенов (25 000 токенов) занимают малую часть окна. У GLM 5.3 и GLM 5.3 Flash контекст 128K — для этой схемы тоже хватит, но запаса меньше. Подробнее о клиенте — в документации по Python.

Если база маленькая, скажем 30–50 тысяч токенов, поиск можно не делать вовсе и отправлять её целиком. Но с ростом базы это быстро дорожает — расчёт ниже.

Сколько стоит RAG на дешёвой модели

Допущения на один запрос: 20 фрагментов по 500 токенов = 10 000 токенов, плюс инструкция и вопрос — около 350. Итого 10 350 входных токенов. Ответ — 400 выходных токенов.

Формула: стоимость = 10 350 × цена входа / 1 000 000 + 400 × цена выхода / 1 000 000.

МодельВход / выход, $ за 1MВходВыходЗа запросЗа 10 000 запросовИндекс
deepseek-v4.1-flash0.013 / 0.038$0.000135$0.0000152≈ $0.00015≈ $1.5039
gemini-3.8-flash0.025 / 0.088$0.000259$0.0000352≈ $0.00029≈ $2.9441
claude-sonnet-50.2 / 1.013$0.00207$0.000405≈ $0.0025≈ $24.75нет данных
gpt-6.1-sol0.27 / 1.32$0.00279$0.000528≈ $0.0033≈ $33.2352
claude-opus-5-50.4 / 2$0.00414$0.0008≈ $0.0049≈ $49.4058

Проверка первой строки: 10 350 × 0.013 / 1 000 000 = $0.00013455; 400 × 0.038 / 1 000 000 = $0.0000152; сумма $0.00014975, на 10 000 запросов — $1.4975.

Теперь вариант «вся база в контексте» для базы на 600 000 токенов на той же deepseek-v4.1-flash: 600 000 × 0.013 / 1 000 000 = $0.0078 за вход одного запроса. С ответом 10 000 запросов обойдутся примерно в $78.15 против $1.50 с поиском. Кэш промптов здесь не поможет: у DeepSeek чтение кэша стоит столько же, сколько вход. У моделей Claude в Anthropic-формате чтение кэша заметно дешевле входа, но это отдельная схема со своим расчётом.

Актуальный список и цены — GET https://api.zukko.pro/v1/models или страница «Модели» в кабинете.

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

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

  • Нужного фрагмента нет среди найденных. Это проблема поиска, и модель посильнее не поможет. Увеличьте k, добавьте стемминг, поменяйте нарезку на фрагменты.
  • Фрагмент найден, а ответ неверный или «нет ответа». Вот здесь имеет смысл попробовать модель с более высоким индексом: gpt-6.1-sol (52) или claude-opus-5-5 (58), на тех же вопросах.

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

Не обязательно переводить на неё весь поток. Рабочая схема — каскад с эскалацией (подробно — в статье о каскаде моделей): по умолчанию отвечает дешёвая модель, а вопрос повторяется на сильной, если пользователь нажал «не помогло» или если ответ не прошёл проверку (например, нет ни одной ссылки на источник). Посчитаем по таблице для 10% эскалаций на claude-opus-5-5: средний запрос = 0.00015 + 0.1 × 0.00494 ≈ $0.00064. Если отправлять на Opus 5.5 всё подряд — $0.0049 за запрос.

Больше о построении пайплайна — в статье про RAG-пайплайн на своих данных. Ключ для эксперимента создаётся в разделе API-ключи.

Частые вопросы

Почему не векторная база?

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

Сколько фрагментов передавать модели?

Начните с 20 и посмотрите на тестовом наборе, как часто нужный фрагмент оказывается в выдаче. Каждые дополнительные 10 фрагментов по 500 токенов на deepseek-v4.1-flash добавляют 5 000 × 0.013 / 1 000 000 = $0.000065 к запросу.

Подойдёт ли это для русского языка?

Да, с оговоркой про словоформы: без стемминга «возврат» и «возврата» считаются разными словами. Проверьте на своих вопросах, нужна ли нормализация.

Как обновлять индекс?

BM25Okapi строится заново из списка фрагментов. Для небольшой базы проще всего пересобирать индекс при запуске сервиса или по расписанию.

zukko

Платите в 25×
меньше

за Claude Code & ChatGPT

Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.