Как собрать дешёвого мультимодального ИИ-ассистента на OpenRouter с русскими эмбеддингами

Как собрать дешёвого мультимодального ИИ-ассистента на OpenRouter с русскими эмбеддингами

 

Разберёмся, как собрать ИИ-ассистента на OpenRouter, который отвечает на русские вопросы по своей базе знаний, понимает картинки и стоит копейки. Это финальный кейс серии, где сходится всё пройденное: один ключ, дешёвая модель, русскоязычные эмбеддинги и мультимодальность. Соберём рабочий прототип, прогоним его на стенде и замерим стоимость и скорость. Весь код ниже проверен, числа настоящие.

 

Из чего собираем ассистента

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

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

Если вы не проходили серию, отдельные кирпичи разобраны раньше: старт и оплата, экономика токенов и мультимодальность. Здесь мы просто складываем их в один проект.

Показывает пайплайн: вопрос → эмбеддинг запроса → поиск ближайших фактов → дешёвая модель отвечает по контексту → ответ; отдельной веткой картинка → зрячая модель описывает → тот же ответный узел OpenRouter

Дальше собираем эту схему в один скрипт и разбираем по частям.

 

ИИ-агенты для оптимизации бизнес-процессов

Код курса
AGENT
Ближайшая дата курса
26 октября, 2026
Продолжительность
24 ак.часов
Стоимость обучения
66 000

 

Русскоязычные эмбеддинги

Эмбеддинг это числовой вектор, который передаёт смысл текста. Для русского языка важна мультиязычная модель, иначе качество поиска проседает. У OpenRouter есть такие модели прямо на эндпоинте эмбеддингов. Мы берём baai/bge-m3, она хорошо работает с русским и отдаёт вектор на 1024 числа.

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

Почему именно мультиязычная модель. Англоязычные эмбеддинги плохо ловят близость русских фраз. bge-m3 обучена на многих языках, поэтому русский вопрос и русский факт оказываются рядом в пространстве векторов. Есть и альтернатива, multilingual-e5, выбор между ними это вопрос качества и цены.

По традиции весь код используемый в статье 9 финального проекта выкладываем на наш GitHub репозиторий
Капстоун: дешёвый мультимодальный ассистент на русских эмбеддингах from airflow import DAG from airflow.operators.bash import BashOperator from datetime import datetime with DAG( dag_id="spark_submit_demo", start_date=datetime(2025, 1, 1), schedule="@daily", catchup=False ) as dag: run = BashOperator( task_id="run_job", bash_command="spark-submit app.py" ) GitHub code example Капстоун: дешёвый мультимодальный ассистент на русских эмбеддингах

 

Поиск и дешёвый ответ

Поиск это косинусная близость между вектором вопроса и векторами фактов. Берём два самых близких факта и отдаём их дешёвой модели как контекст. Модель отвечает строго по нему, а не по своей памяти, поэтому ответ опирается на вашу базу. Весь сценарий целиком лежит в файле assistant.py.

Ключевой момент это заземление ответа. Мы явно просим модель отвечать только по контексту. Так она опирается на вашу базу, а не фантазирует. Если нужного факта нет, честнее короткое не знаю, чем красивая выдумка.

# протестировано на EU-ноде (AWS Stockholm) 2026-08-04: Python 3.12.3, httpx 0.28.1, OpenRouter API
# Капстоун: дешёвый мультимодальный ассистент. RAG на русских эмбеддингах плюс зрение, с замерами.
import os, time, math, httpx

key = os.environ["OPENROUTER_API_KEY"]
H = {"Authorization": f"Bearer {key}"}
BASE = "https://openrouter.ai/api/v1"

def embed(texts):
    # Русскоязычные эмбеддинги через мультиязычную модель bge-m3.
    r = httpx.post(f"{BASE}/embeddings", headers=H, timeout=60,
                   json={"model": "baai/bge-m3", "input": texts}).json()
    return [d["embedding"] for d in r["data"]]

def cos(a, b):
    # Косинусная близость двух векторов.
    s = sum(x * y for x, y in zip(a, b))
    na = math.sqrt(sum(x * x for x in a)); nb = math.sqrt(sum(y * y for y in b))
    return s / (na * nb)

# Небольшая база знаний из фактов серии.
KB = [
    "OpenRouter это единый OpenAI-совместимый роутер к десяткам моделей одним ключом.",
    "Оплата картой РФ обычно не проходит, надёжный путь пополнения это криптовалюта USDC.",
    "Кэш промптов удешевляет повторный общий контекст, а session_id держит кэш тёплым.",
    "Через один ключ доступны текст, генерация картинок, транскрипция и синтез речи.",
]

t0 = time.time()
kb_vecs = embed(KB)   # эмбеддинги базы считаем один раз
cost = 0.0

# 1. Текстовый вопрос: эмбеддинг запроса, поиск ближайших фактов, ответ дешёвой моделью.
q = "Как из России оплатить OpenRouter?"
qv = embed([q])[0]
ranked = sorted(range(len(KB)), key=lambda i: cos(qv, kb_vecs[i]), reverse=True)
ctx = "\n".join(KB[i] for i in ranked[:2])   # берём топ-2 факта как контекст
ans = httpx.post(f"{BASE}/chat/completions", headers=H, timeout=60, json={
    "model": "openai/gpt-4o-mini", "max_tokens": 80, "usage": {"include": True},
    "messages": [
        {"role": "system", "content": "Отвечай кратко и только по контексту."},
        {"role": "user", "content": f"Контекст:\n{ctx}\n\nВопрос: {q}"},
    ],
}).json()
cost += ans["usage"]["cost"]
print("вопрос:", q)
print("ответ :", ans["choices"][0]["message"]["content"].strip())

# 2. Мультимодальность: генерируем картинку и тут же спрашиваем о ней у зрячей модели.
img = httpx.post(f"{BASE}/chat/completions", headers=H, timeout=120, json={
    "model": "google/gemini-2.5-flash-image", "modalities": ["image", "text"],
    "messages": [{"role": "user", "content": "Синий круг на белом фоне"}],
}).json()
cost += img["usage"]["cost"]
data_url = img["choices"][0]["message"]["images"][0]["image_url"]["url"]
vis = httpx.post(f"{BASE}/chat/completions", headers=H, timeout=60, json={
    "model": "openai/gpt-4o-mini", "max_tokens": 40, "usage": {"include": True},
    "messages": [{"role": "user", "content": [
        {"type": "text", "text": "Что на картинке? Одним предложением."},
        {"type": "image_url", "image_url": {"url": data_url}},
    ]}],
}).json()
cost += vis["usage"]["cost"]
print("зрение:", vis["choices"][0]["message"]["content"].strip())

# Замеры: суммарная стоимость и время всего сценария.
print(f"итого стоимость: ${cost:.5f}, время: {time.time() - t0:.1f}s")
вопрос: Как из России оплатить OpenRouter?
ответ : Из России оплатить OpenRouter можно с помощью криптовалюты USDC.
зрение: На картинке изображён голубой круг на белом фоне.
итого стоимость: $0.04256, время: 10.6s

Ответ на вопрос собран из базы, а не выдуман. Модель нашла факт про оплату криптой и ответила по нему. Это и есть простой RAG, где эмбеддинги отвечают за поиск, а дешёвая модель за формулировку.

Такой подход легко расширяется. Добавили документов, пересчитали эмбеддинги, и ассистент знает больше. Логика поиска и ответа при этом не меняется. Для больших баз список векторов заменяют на векторную БД, но идея остаётся той же.

 

Зрение в том же ассистенте

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

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

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

 

Замеры стоимости и скорости модели в OpenRouter

Весь прогон вышел в 0.04256 доллара и 10.6 секунды. Львиную долю цены заняла генерация картинки, текстовая часть с эмбеддингами и ответом стоила доли цента. Значит, чисто текстовый ассистент на этой схеме почти бесплатен на запрос.

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

Скорость тоже поддаётся настройке. Здесь запросы шли по очереди, поэтому набежало десять секунд. Независимые вызовы можно пустить параллельно. А если важна отзывчивость, провайдера выбирают по латентности, как в статье про маршрутизацию.

 

ИИ-агенты для оптимизации бизнес-процессов

Код курса
AGENT
Ближайшая дата курса
26 октября, 2026
Продолжительность
24 ак.часов
Стоимость обучения
66 000

 

Итог серии

На этом кластер замыкается. Мы прошли путь от первого запроса и оплаты из России до маршрутизации, экономики, low-code, агентов, мультимодальности и приватности, а в финале собрали из этого рабочего ассистента. Один ключ OpenRouter закрыл текст, поиск по смыслу и зрение, а стоимость осталась копеечной.

Дальше эти навыки удобно докручивать на практике. Сборка агентов под реальные бизнес-задачи подробно разбирается в курсе про ИИ-агентов для оптимизации бизнес-процессов. Прототип из этой статьи это хорошая точка старта для своего проекта.

 

Референсные ссылки