Эмбеддинги и RAG в домашней лабе: bge-m3 и поиск по памяти

Обложка

Есть момент, когда лаба перестаёт быть набором сервисов и превращается в архив собственной жизни. Дневники агента, черновики постов, конфиги роутеров, заметки «это я чинил в три часа ночи, не повторяй». Всё это копится, и однажды ты хочешь найти «ту самую мысль», а в руках у тебя только grep и смутное ощущение, что запись точно была. Вот тут и начинаются эмбеддинги.

Что такое эмбеддинги, если без маркетинга

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

Разница с grep принципиальная. Grep ищет точные совпадения: набрал «туннель к роутеру» — не найдёт запись, где написано «SSH-проброс до шлюза». А векторный поиск ловит смысл: вопрос своими словами тянет к нужному куску, даже если ни одно слово не совпало. Это и есть «семантический поиск», он же основа RAG.

Зачем это в self-host, а не в облаке

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

Поэтому критерии были жёсткие: локально, бесплатно, открыто и с нормальным русским. Эмбеддинг-модель должна понимать мои заметки, которые наполовину на русском, и не убегать с ними в интернет. Под это идеально легла bge-m3 — она же потом всплыла у меня в двух местах: в поиске по заметкам и в долговременной памяти агента.

Как устроен простой RAG без тяжеловесных стеков

Когда слышишь «RAG», воображение рисует Kubernetes, векторную базу, очередь и трёх инженеров на поддержку. На деле для домашней лабы хватает четырёх шагов и одного скрипта. Вот весь конвейер:

  1. Chunk — нарезка. Берёшь документ и режешь его на куски по несколько сотен символов, чтобы каждый кусок был самодостаточным куском смысла. Не по абзацам вслепую, а по границам, где смысл не рвётся пополам.

  2. Embedding — векторизация. Каждый кусок прогоняешь через эмбеддинг-модель и получаешь вектор. У bge-m3 это 1024 числа на кусок.

  3. Индекс. Все векторы складываешь в один файл или таблицу. Для десятков тысяч кусков хватит обычного JSON или SQLite — полноценная векторная БД нужна, только когда счёт идёт на миллионы.

  4. Поиск и подмешивание. Когда приходит вопрос, превращаешь его в такой же вектор, считаешь косинусную близость до всех кусков и берёшь ближайшие. Их — и только их — кладёшь в промпт модели: «вот что у нас есть по теме, отвечай опираясь на это».

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

У меня это завелось не ради академического интереса, а от реальной боли. Агент живёт в лабе и ведёт дневники: memory/2026-08-27.md и дальше по дням. Плюс один кураторский файл, куда вручную отбирается самое важное. Когда агенту нужно «вспомнить», что мы делали на прошлой неделе, он не перечитывает всю папку — он гоняет вопрос через векторный поиск.

За поиск отвечает отдельный инструмент — memory_search. Внутри всё тот же конвейер: вопрос → эмбеддинг → косинусная близость по индексу → несколько ближайших кусков в контекст. Никакого чуда, просто честная тригонометрия по смыслу.

Модель-эмбеддер крутится локально, на той же коробке с ollama и AMD-графикой. Отдельно у нас есть эмбеддинг-эндпоинт у локального провайдера — OpenAI-совместимый /v1/embeddings, к которому подключается плагин памяти как к обычному OpenAI. То есть архитектура простая: сам векторный поиск живёт у меня, а за эмбеддинги можно дёргать либо локальную модель, либо эндпоинт провайдера — что в моменте быстрее и дешевле.

Цифры, если коротко: модель bge-m3 весит чуть больше гигабайта и лежит в памяти видеокарты целиком; один эмбеддинг считается за десятки миллисекунд. Поиск по полусотне чанков — миллисекунды, индексация пары сотен заметок — пара минут. В контекст перед каждым ходом подмешивается несколько сотен символов релевантных воспоминаний вместо того, чтобы тянуть всю историю.

Где RAG окупается, а где проще grep

Честность дороже хайпа, поэтому признаю: RAG нужен далеко не везде. Вот простой критерий, который я для себя вывел.

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

Проще grep, когда знаешь точное слово: имя файла, IP-адрес, название интерфейса, конкретную команду. Тут grep быстрее, прозрачнее и не требует поднимать модель. Grep ищет строки, векторный поиск ищет смысл — это разные инструменты, и один не отменяет другой.

Хорошая связка: сначала точный поиск по ключу, а когда он не находит — догоняешь векторным. Смысл там, где точное совпадение не работает.

Грабли, о которых молчат в туториалах

Первая и главная: эмбеддинги ничего не понимают. Они не «осознали» твой текст, они просто посчитали близость векторов. Это очень тупая операция, просто на очень большом числе измерений. Из этого вытекают две неприятности.

Близость ≠ релевантность. Вектор может оказаться рядом не потому, что кусок отвечает на вопрос, а потому что там похожий набор тем. Модель нашла «про сеть», а тебе нужно конкретно «как я чинил BGP на прошлой неделе». Поиск по смыслу — это кандидаты, а не готовый ответ. Финальную оценку всё равно делает языковая модель, которой ты эти куски скормил.

Плохие чанки = халлюцинации. Если нарезать текст небрежно — кусок оборван на полуслове, в нём нет контекста, или в топ поиска вылезла запись двухлетней давности — модель честно «ответит», опираясь на мусор. И ответит уверенно, потому что ей дали «документ», а она не обязана сомневаться. Галлюцинация тут рождается не от глупости модели, а от кривой нарезки и грязного индекса. Лечится только дисциплиной: аккуратные чанки, свежий индекс, и не доверять результату поиска больше, чем он того заслуживает.

Вторая грабля из жизни homelab: модель-эмбеддер должна быть живой. Если контейнер с ollama остановлен или до него нет маршрута из соседней подсети — всё, что на него завязано, молча деградирует. Агент начнёт «не помнить», и ты не сразу поймёшь, что дело не в амнезии, а в том, что эмбеддинг-эндпоинт лежит. Проверять просто: дёрнуть эндпоинт напрямую и убедиться, что модель на месте и отвечает.

Итог

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

bge-m3 в связке с локальным ollama (или эмбеддинг-эндпоинтом провайдера) даёт русскоязычный поиск по смыслу бесплатно и без утечки данных наружу. Главное — не переоценивать результат: векторная близость — это кандидаты, а не истина, и плохие чанки всё так же превращаются в уверенные халлюцинации. Нарезай аккуратно, держи индекс свежим, и тогда grep останется для точных строк, а смысл найдёт вектор.

#llm #selfhosting