Если совсем коротко, то после переезда Картары на российский сервер у меня сломалась RAG-память, и сломалась потому, что эмбеддинги перестали считаться. OpenAI начал отдавать 403 (геоблок из РФ), а у OpenRouter, куда я к тому моменту увёл весь LLM-трафик, эндпоинта для эмбеддингов вообще нет. Решил это self-host'ом: подняли локальную модель intfloat/multilingual-e5-large через fastembed (это ONNX, гоняется прямо на CPU, без всякой видеокарты), вшили её в docker-образ, переехали с pgvector на 1024 измерения вместо 1536, пересоздали индекс и переэмбедили всю память, а это 1605 фактов на проде и 1614 на dev. Поиск проверил, точный факт ранжируется первым. И всё это копейки, потому что считается на том же сервере, где у меня и так всё крутится.
А теперь давайте по порядку, потому что тут интересна даже не сама развязка, а то, как одна вроде бы бытовая задача (переехали на другой сервер) вытаскивает за собой целую цепочку проблем, про которые ты заранее и не думаешь.
Откуда вообще взялась проблема
Тут надо понимать контекст. Раньше у меня весь AI-трафик в проектах шёл напрямую в OpenAI, а потом я увёл его на OpenRouter, и историю про то, почему так вышло и как два проекта у меня встали в один день не из-за бага, а потому что просто кончились деньги на балансе, я подробно рассказывал в отдельном посте. OpenRouter удобен тем, что это одна точка входа к куче моделей, можно гонять Claude, можно что угодно, и балансом рулишь в одном месте.
Но вот в чём фокус, про который никто не предупреждает. OpenRouter — это про чат-модели, про генерацию текста, про то что ты привык называть «нейросеть отвечает». А эмбеддинги это совсем другая история. Эмбеддинг это когда ты текст превращаешь в длинный список чисел, в вектор, чтобы потом искать похожее по смыслу, а не по словам. Без этого вся RAG-память не работает в принципе, потому что ты не можешь достать из базы нужный факт, ведь ты их туда складывал именно как векторы. И вот у OpenRouter эндпоинта /embeddings просто нет, то есть весь чат у тебя через прокси едет, а память считать нечем, и в эту же стенку я уже упирался в SceneX.
Можно было бы пойти обратно в OpenAI напрямую только за эмбеддингами. Но сервер теперь российский, и OpenAI на запрос из РФ вежливо отдаёт 403. Тот самый claude code 403 и request failed with status code 403, который вы наверняка видели, если пробовали что-то делать с американскими API из России, только тут он прилетел не на чат, а на векторы. И вот ты сидишь и понимаешь, что у тебя AI-агент с памятью внезапно стал агентом без памяти, а в логах ровно то, что я себе и написал в тот момент: «Эмбеддинги не залились... Пиздец надо решить вопрос то».
Почему я не стал городить прокси
Первая мысль у любого нормального человека такая: ну так пробрось эмбеддинги через VPN или прокси, у тебя же и так стоит свой VPN на сервере. И да, можно. Но это значит, что каждый раз когда юзер что-то говорит и это надо запомнить или найти, ты гоняешь запрос наружу, в чужой API, который завтра может опять что-то заблокировать, поднять цену или просто прилечь. А память это база, фундамент, она должна работать всегда, а не «пока прокси живой».
Поэтому решение, к которому я пришёл (точнее, к которому мы пришли, я командовал, а Claude Code в терминале это всё разворачивал), было такое: забрать эмбеддинги к себе целиком. Никакого внешнего API. Модель крутится на моём же сервере, на обычном CPU, и считает векторы локально. Это и есть та самая локальная RAG-память, когда данные не уезжают наружу вообще, и геоблок ей теперь до лампочки, ровно так же я до этого перенёс распознавание речи в Картаре на свой CPU.
Что конкретно подняли
Взяли fastembed, это библиотека, которая гоняет модели в формате ONNX, и ей не нужна видеокарта, всё считается на процессоре. Для соло-разработчика с одним сервером и бюджетом «копейки в месяц» это идеально, потому что не надо арендовать GPU-инстанс ради того, чтобы превращать текст в числа.
Модель выбрали intfloat/multilingual-e5-large, мультиязычную, и это важно, потому что Картара говорит по-русски, а многие эмбеддинг-модели заточены под английский и на русском плывут. Она выдаёт векторы на 1024 измерения. И вот тут был мой главный вопрос, который я честно задал вслух, потому что переживал за качество: «А эти эмбединги не будут сильно хуже тех что были? они же слабее наверно?» Ну логично же бояться, раньше была здоровая платная модель от OpenAI, а теперь какая-то локальная штука на CPU, и кажется что это шаг назад.
Чтобы модель не тянулась из интернета при каждом старте контейнера (а это и медленно, и опять же зависимость от внешней сети, которую могут заблокировать), мы её вшили прямо в docker-образ. Весит вендоренная модель примерно 2.1 гигабайта, образ от этого потяжелел, но зато он самодостаточный. Плюс выставили HF_HUB_OFFLINE=1, это флаг, который говорит библиотеке «не лезь в Hugging Face за моделью, бери локальную». Без него она на старте всё равно дёргает сеть проверить обновления, и на российском сервере это либо тормозит, либо падает.
Самое муторное это миграция базы
Вот тут начинается то, о чём в туториалах обычно пишут одной строчкой, а на деле это полдня возни. Старые векторы были на 1536 измерений (формат OpenAI), новые на 1024. Это разные размерности, их нельзя смешать в одной колонке, нельзя сравнивать между собой, и старый индекс под новые векторы просто не работает.
Значит, надо в pgvector пересоздавать колонку под vector(1024), пересоздавать сам индекс (у меня это ivfflat с lists=100) и, что критично, переэмбеживать вообще все факты заново. Потому что старые 1536-мерные векторы теперь мусор, искать по ним новым запросом бессмысленно. Прогнали переэмбеддинг: 1605 из 1605 фактов на проде, 1614 из 1614 на dev. Всё до единого, без пропусков, иначе память была бы дырявой.
И тут вылез баг, который сам бы я в жизни не заметил, а поймало его код-ревью. При создании индекса я указал, что искать надо по косинусной близости (это vector_cosine_ops в pgvector), а вот сам запрос на поиск использовал оператор <-> вместо <=>. Разница в одну стрелочку, а смысл противоположный, потому что <-> это евклидово расстояние, а <=> косинусное. То есть оператор поиска не совпадал с тем, под что заточен индекс. База бы такое съела молча, поиск как бы работал, но ранжировал по неправильной метрике и индексом толком не пользовался. Классический P1, который не падает с ошибкой, а просто тихо выдаёт херню. Поправили, привели оператор к косинусу под opclass индекса.
Качество я проверил, а не угадал
Помните мой страх, что локальная модель будет хуже? Я не стал верить ни своим переживаниям, ни обещаниям из README. Просто проверил ретрив на проде на реальном факте: спросил то, что точно лежит в памяти, и посмотрел, что вернётся. Точный факт встал на первое место, cosine 0.056 (это расстояние, и чем меньше, тем ближе совпадение, так что 0.056 это в упор). То есть e5-large на CPU нашёл нужное и поставил его номером один. Никакой деградации, которой я боялся, на моих данных не случилось.
И вот это, наверное, главный вывод из всей истории, причём не про эмбеддинги даже, а про привычку. Очень легко в 2026-м по умолчанию тащить всё в платные американские API, потому что «там же лучше», и так же легко словить от этого 403 и остаться у разбитого корыта, когда сервер переехал. А оказывается, что для RAG-памяти вполне хватает модели, которая бесплатно крутится на твоём же процессоре, не уезжает наружу ни единым байтом и не зависит ни от чьих геоблоков. Если вы делаете нейросети для бизнеса, где данные клиентов и не должны утекать на чужие сервера, то такой подход тем более стоит держать не как запасной, а как основной.
Так что если вы делаете AI-агента с RAG и упёрлись в то, что OpenAI вам из России не отвечает, а OpenRouter эмбеддинги не умеет в принципе, то не ищите третий платный прокси. Поднимите свои локально, это день работы вместе с миграцией базы, и спите спокойно. Только не забудьте проверить, что оператор поиска совпадает с тем, под что вы строили индекс, а то будете долго гадать, почему память вроде есть, а находит мимо.