LLM по своей природе — закрытая книга. Модель знает то, что видела в момент обучения, и ничего больше. Спросите её о вчерашних новостях — придумает. О внутренней архитектуре вашей системы — придумает тоже, причём уверенно. Это свойство, галлюцинация, не баг, а следствие того, что модель просто предсказывает следующий токен.
Но мы уже видели, как модель «учится действовать» в мире: в прошлой статье мы разбирали, как инструменты дают LLM руки — возможность вызывать функции, читать файлы, ходить в API. Сегодня — второй, не менее важный трюк: как дать модели глаза, то есть возможность находить релевантный контекст перед тем, как отвечать.
Этот паттерн называется RAG (Retrieval-Augmented Generation, генерация, дополненная поиском). И разбирать его мы будем не на абстрактном примере, а на реальном сервисе — System Design Wizard (sdw.ppid.ru), который помогает пошагово проектировать программные системы и при этом читает базу знаний из 285 глав по системному дизайну.
1. Проблема: модель, которая «знала, но забыла»
Представьте блестящего инженера, который 20 лет проектировал распределённые системы, а потом три года не читал ничего нового. Он по-прежнему силён в основах — CAP-теорема, шардирование, кэширование. Но спросите его о конкретной статье из свежего System Design Primer — и он начнёт путать детали, домысливать, уверенно ошибаться.
Именно так живёт «голая» LLM. Обучение (пре-тренинг, трансформер с миллиардами весов) закончилось в определённый момент, и всё, что появилось после, для модели не существует. А теперь представьте, что вы строите ассистента по системному дизайну, который должен опираться на конкретную базу знаний — те самые 285 глав. Есть три пути.
Первый: затолкать всё в контекст. 285 глав — это миллионы токенов. Даже у моделей с большим окном (128k–1M токенов) это съест весь бюджет и замедлит инференс до ползания. Каждая глава избыточна для конкретного вопроса; 99% контекста будет шумом.
Второй: дообучить модель (fine-tuning). Можно показать базе знаний модель и дообучить её. Но это дорого (каждое обновление базы — новый цикл), негибко (исправил опечатку в главе — пересобирай) и плохо для фактических данных: fine-tuning учит модель стилю, а не конкретным фактам. Модель не «запоминает» текст, она корректирует веса.
Третий путь — RAG. Не учить модель всей базе. А перед каждым запросом — находить три-пять самых релевантных глав и подкладывать их в контекст. Модель читает только то, что относится к вопросу, и отвечает на основе этого. Обновили базу — поиск мгновенно учитывает новую главу, без переобучения.
2. Главная метафора: архивариус с картотекой
Представьте архивариуса, у которого в подвале — миллион папок с документами. К нему приходит читатель с вопросом: «как проектировать кэш для ленты новостей?». Архивариус не пытается вспомнить всё прочитанное. Он делает два шага:
- Формулирует «поисковый образ» вопроса — выделяет ключевые понятия (кэш, лента, новости).
- Идёт в картотеку и находит 5 папок, чьи тематические карточки ближе всего к этому поисковому образу.
Только получив эти папки на стол, архивариус садится писать ответ — уже опираясь на документы, а не на память.
RAG работает ровно так же, и вся «магия» происходит на шаге 2 — в механизме поиска. Чтобы понять, как архивариус находит похожие папки, нужно разобраться с тем, что такое эмбеддинг.
3. Эмбеддинги: превращение текста в координаты
В статье про Self-Attention мы уже касались эмбеддингов: это способ представить текст числовым вектором так, что близкие по смыслу тексты получают близкие векторы. Если построить график, где каждая точка — это документ, то статьи про «кэширование» и «Redis» окажутся рядом, а статья про «auth» — далеко.
Как именно текст превращается в координаты? Отдельная нейросеть — эмбеддинг-модель — берёт фрагмент текста и выдаёт вектор фиксированной длины. В System Design Wizard используется BGE-M3 — открытая модель, дающая 1024-мерные векторы. Каждое из 1024 чисел кодирует какой-то абстрактный «аспект смысла» (никто не знает, какой именно — это выучивается в процессе обучения модели).
Ключевое свойство: векторы нормализуются (длина = 1), и тогда косинус угла между двумя векторами становится мерой их семантической близости. Косинус = 1 — тексты идентичны по смыслу. Косинус = 0 — никак не связаны.
«кэш для ленты новостей» → [0.21, -0.08, 0.44, ..., 0.12] (1024 числа)
«sharding для writes» → [0.03, 0.55, -0.2, ..., 0.31]
косинусное расстояние между ними ~0.3 — далековато, это разные темы
4. Векторный поиск и HNSW
Итак, каждый документ базы знаний превращён в вектор и лежит в векторной базе. Когда приходит запрос «как проектировать кэш для ленты новостей», мы:
- Пропускаем запрос через ту же эмбеддинг-модель → получаем вектор запроса.
- Ищем в базе ближайшие к нему векторы — это и есть релевантные документы.
Звучит просто, но тут прячется инженерная проблема: как искать ближайших соседей среди миллионов векторов за миллисекунды? Наивный подход — посчитать косинус между запросом и каждым вектором — это O(N) операций. Для 5000 чанков (как в SDW) ещё терпимо, для миллионов — катастрофа.
Решение — алгоритмы приближённого поиска ближайших соседей (ANN, Approximate Nearest Neighbors). Они жертвуют небольшой долей точности ради огромного выигрыша в скорости. Самый популярный сегодня — HNSW (Hierarchical Navigable Small World).
Идея HNSW изящна. Представьте социальный граф: чтобы найти человека «рядом» с вами по интересам, не обязательно перебирать всех 8 миллиардов — можно идти по «знакомым знакомых», постепенно приближаясь к цели. HNSW строит многослойный граф из векторов: верхний слой — редкий, из «хабов», нижний — плотный, со всеми точками. Поиск начинается с верхнего слоя (быстро перепрыгиваем через хабы в нужный регион), а затем «спускаемся» на нижние слои, уточняя позицию. Результат: при 5000 чанков поиск отдаёт top-5 за единицы миллисекунд, при точности ~99% от честного перебора.
В SDW этот граф живёт прямо в PostgreSQL через расширение pgvector — отдельная векторная БД не нужна, векторы хранятся в той же таблице, что и тексты. Колонка embedding_bge vector(1024) с HNSW-индексом по косинусному расстоянию. Прелесть подхода: одна база и для RAG, и для сессий пользователей, и для учётных записей.
5. Полный конвейер: от запроса до ответа
Теперь соберём всё вместе. Вот что происходит, когда пользователь System Design Wizard просит сгенерировать шаг проектирования — скажем, «опиши кэширующий слой для Twitter-подобной ленты»:
1. Запрос → эмбеддинг (BGE-M3, ~200 мс)
«опиши кэширующий слой для ленты» → [вектор 1024]
2. Векторный поиск (pgvector + HNSW, ~10 мс)
SELECT chunk FROM kb_chunks
ORDER BY embedding_bge <=> $query_vector -- косинусное расстояние
LIMIT 5
→ 5 самых релевантных чанков из 5132
3. Сборка промпта
system: «Ты — ассистент по системному дизайну...»
user: «опиши кэширующий слой для ленты»
context: [5 чанков из базы знаний, ~3000 токенов]
4. Генерация (LLM, стриминг)
модель читает запрос + контекст
→ поток токенов в браузер через SSE
Обратите внимание на шаг 3: RAG не заменяет промпт, он его дополняет. Модель по-прежнему решает задачу предсказания следующего токена — но теперь в её контексте есть конкретные факты из базы, на которые можно опереться. Self-Attention связывает слова вопроса со словами релевантных глав, и ответ «привязывается» к источникам, а не выдумывается.
Именно поэтому RAG так эффективен для фактических задач: вы не просите модель вспомнить системный дизайн. Вы кладёте перед ней конкретный учебник и просите прочитать нужную страницу.
6. Чанки: почему не ищут целыми главами
Тонкий момент, который легко упустить. В векторной базе лежит не 285 глав, а 5132 чанка. Почему?
Глава про кэширование может обсуждать и Redis, и мемоизацию, и CDN, и eviction-политики. Если превратить всю главу в один вектор, он «размажется» между темами — и запрос про eviction потянет к себе заодно ненужный кусок про CDN. Поэтому базу нарезают на чанки (фрагменты по несколько абзацев), и каждый чанк получает свой вектор. Тогда поиск находит именно тот кусок про eviction, без шума.
Размер чанка — баланс. Слишком мелкий — потеряем контекст, модель не поймёт, о чём фрагмент. Слишком крупный — снова размазывание. В SDW главы system-design.space нарезаны так, чтобы каждый чанк был самодостаточен: содержит одну законченную мысль + минимум рамок, чтобы модель понимала, к какой теме он относится.
7. Стриминг: как токены приходят в реальном времени
Когда вы видите, как ассистент печатает ответ слово за словом, — это не анимация для красоты. Это стриминг токенов через SSE (Server-Sent Events).
Без стриминга цикл выглядел бы так: дождаться, пока модель сгенерирует весь ответ целиком (десятки секунд) → отдать его разом. Пользователь смотрит на спиннер и не понимает, идёт ли вообще работа. Со стримингом — каждый сгенерированный токен немедленно летит в браузер, и вы видите текст по мере создания. Это меняет восприятие: как мы разбирали в статье про Шеннона, каждый новый токен несёт информацию — то есть снижает неопределённость. Стриминг показывает этот процесс «вживую»: вы буквально видите, как разрешается неопределённость.
Технически в SDW это Spring Boot-бэкенд, который держит HTTP-соединение открытым и шлёт события трёх типов: reasoning (рассуждение модели, если она его показывает) → token (очередное слово) → saved → done. Важная инженерная деталь: в nginx обязательно proxy_buffering off и proxy_read_timeout 300s — иначе прокси попытается накопить ответ в буфер и стриминг превратится в ту самую «отдачу разом».
8. Почему это критично для self-hosting
RAG — идеальный паттерн для self-hosted AI. Разберём почему.
Приватность без облака. Главная ценность: ваши документы (документация, переписка, спецификации) никогда не покидают вашу инфраструктуру. В SDW и LLM можно поднять локально (через Ollama или vLLM), и эмбеддинг-модель BGE-M3 работает прямо на CPU в Docker-контейнере. Вся обработка — внутри вашей сети. Для корпоративной документации или медицинских данных это не пожелание, а требование.
Обновляемость без переобучения. В классическом ML-мире обновление модели — это дорогой цикл: собрать данные, разметить, дообучить, валидировать, деплоить. В RAG база знаний — это просто таблица в PostgreSQL. Исправили главу? UPDATE kb_chunks SET .... Добавили новую? INSERT, посчитали эмбеддинг, готово. Переиндексация 5000 чанков на CPU занимает около 12 минут — это фоновая задача, не блокирующая работу. Модель вообще не трогается.
Стоимость и контроль. Облачные RAG-сервисы (GPT-4 + их vector store) удобны, но вы платите за каждый запрос и зависите от их прайсинга и доступности. Свой стек — pgvector + BGE-M3 + любая LLM — стоит ровно столько, сколько стоит ваше электричество. И вы точно знаете, где лежат ваши данные.
Защита от галлюцинаций. В системном дизайне выдумка критична: если ассистент предложит «выглядящий умно» антипаттерн, новичок может заложить его в архитектуру. RAG не исключает галлюцинации полностью, но радикально их снижает: модель опирается на конкретные главы проверенной базы знаний, а не на вероятностное продолжение.
9. Что RAG не решает
Честно об ограничениях, потому что RAG — не серебряная пуля.
Качество поиска — это всё. Если векторный поиск отдал 5 нерелевантных чанков, модель ответит плохо, как бы хороша она ни была. Проблема усугубляется, когда запрос сформулирован «не теми словами» (синонимы, жаргон, другой язык). Эмбеддинг-модели неплохо ловят семантику, но не всесильны. Отсюда — развитие переранжирования: быстрый векторный поиск отдаёт 20 кандидатов, а тяжёлая cross-encoder модель переоценивает их и отбирает лучший top-5. Это дорого, но заметно повышает точность.
Окно контекста — конечное. Даже если мы нашли 20 релевантных чанков, в контекст влезет не всё. Приходится жёстко ограничивать: top-5, скажем, 3000 токенов. Если правильный ответ «размазан» по 10 главам, часть отрежется. Большие окна (1M токенов) отчасти решают это, но платят скоростью и ценой.
Оценка качества — сложная. Как понять, что ваш RAG «стал лучше»? В отличие от классификации, тут нет готовой метрики. Вы либо читаете ответы глазами (медленно, субъективно), либо строите eval-пайплайн: генерируете тестовые вопросы, проверяете, нашёл ли поиск ожидаемые чанки (retrieval metrics), и насколько ответ соответствует источникам (faithfulness). Это отдельная инженерная дисциплина.
Сложность системы. RAG — это не «подключить API». Это конвейер: нарезка чанков, эмбеддинг-модель, векторный индекс, поиск, сборка промпта, генерация, стриминг. Каждый узел может сломаться. В SDW, например, был инцидент: общий эмбеддинг-сервис зависал (single-worker под нагрузкой), из-за чего поиск не отвечал и вся генерация падала. Фикс — выделить BGE-M3 в dedicated-контейнер, изолированный от других потребителей. Такие истории — норма, а не исключение.
Заключение
RAG — это второй столп (наряду с инструментами), на котором стоит практический AI. Если инструменты из прошлой статьи дают модели действовать в мире, то RAG даёт ей знать о мире — не из замороженного обучения, а из живой, обновляемой базы знаний.
Вся элегантность паттерна — в том, что мы не меняем саму модель. Мы меняем контекст, который модель читает тем же Self-Attention-механизмом, которым читает любой текст. Эмбеддинг превращает смысл в координаты, HNSW находит ближайших соседей в этих координатах за миллисекунды, а модель просто продолжает текст, опираясь на найденное. Архивариус из нашей метафоры работает именно так: он не умнеет от того, что у него есть картотека — но его ответы становятся точными, потому что теперь они основаны на документах, а не на памяти.
Если хотите увидеть это в действии — System Design Wizard работает именно так: каждый шаг проектирования генерируется с опорой на конкретные главы базы знаний, которые можно отследить. Это и есть RAG, видимый глазами пользователя: модель, которая не выдумывает, а читает.