01.08.2027 202 материалов

Локальный RAG без GPU, облака и Docker: пять уроков, каждый из которых стоил недели работы

Инженер из государственного сектора Коста-Рики собрал RAG-пайплайн на обычном «железе» без GPU и облака — и рассказывает, с какими подводными камнями столкнулся на каждом этапе.

Локальный RAG без GPU, облака и Docker: пять уроков, каждый из которых стоил недели работы

Каждый туториал по RAG неявно предполагает, что у вас есть GPU и доступ к облачному API. Если оба предположения не выполняются — а в корпоративном и государственном секторе это норма — придётся заново проектировать каждый слой стека.

Зачем это нужно

Системы RAG (Retrieval-Augmented Generation) — это архитектура, при которой языковая модель отвечает на вопросы пользователя не «из головы», а с опорой на релевантные фрагменты из базы документов. Сначала запрос превращается в вектор (эмбеддинг), затем по cosine-сходству из векторного хранилища достаются ближайшие чанки, после чего всё это подкладывается в промпт для LLM.

Такой подход позволяет создавать корпоративных ассистентов, которые не халлюцинируют про содержимое внутренних документов, — при условии, что архитектура собрана правильно. Проблема в том, что подавляющее большинство гайдов по RAG рассчитаны на «идеальный» стенд: Linux, GPU от NVIDIA, облачные API вроде OpenAI, Docker-контейнеры для каждого компонента. Автор оригинальной статьи — инженер, работающий с медицинскими информационными системами в государственном секторе Коста-Рики, — оказался в куда более типичных для enterprise условиях: Windows Server, только CPU, данные не покидают локальную сеть, контейнеризаторы не одобрены политиками ИБ.

Его проект rag-onpremise использует ASP.NET Core 9 для оркестрации, Ollama для локального инференса LLM, Qdrant в качестве векторной базы и Python-скрипт для импорта документов. В качестве языковой модели — Mistral 7B, для эмбеддингов — nomic-embed-text. Всё работает на CPU. Автор подчёркивает, что дизайн стека занял меньше времени, чем доведение его до production-состояния, — и делится пятью уроками, которые стоили ему недели кропотливой отладки.

Урок 1: забудьте про gRPC — используйте REST

Официальный .NET SDK для Qdrant выглядит элегантно: чистый API, хорошая документация, типизированные методы. Но под капотом он общается с сервером Qdrant по протоколу gRPC, а для gRPC требуется HTTP/2. В корпоративной сети с прокси, балансировщиками или шлюзами, которые принудительно понижают соединение до HTTP/1.1, SDK просто перестаёт работать — с неочевидными ошибками, указывающими куда угодно, кроме реальной причины.

Решение — отказаться от SDK и обращаться к Qdrant напрямую через REST API с помощью стандартного HttpClient. REST API у Qdrant покрывает все операции, необходимые для RAG-пайплайна: поиск, вставка, удаление точек. Цена — потеря типизации и некоторых удобств. Выгода — работоспособность на любом сетевом стеке без необходимости договариваться с сетевым отделом про поддержку HTTP/2 на каждом хопе.

Это типичный случай, когда «правильный» с инженерной точки зрения выбор (SDK с сильной типизацией) проигрывает «грубому» решению (прямые HTTP-вызовы) в реальных условиях развёртывания. Урок для инженеров: протокольные зависимости — это скрытая сложность, которую легко не заметить в лабораторных условиях.

Урок 2: таймаут HttpClient по умолчанию убьёт ваши ответы

В .NET стандартный таймаут HttpClient — 100 секунд. Для обычных веб-запросов это щедрый запас. Для LLM на CPU — катастрофически мало.

Автор измерил реальную латентность Mistral 7B на скромном сервере (4 виртуальных ядра, 16 ГБ ОЗУ): от 60 до 120 секунд на полный ответ. Первый запрос отработал, второй — тоже, а третий попал на чуть более длинную генерацию — и клиент обрезал соединение прямо посередине стриминга. Пользователь видит ошибку, а сервер продолжает генерировать ответ, который никто не прочитает.

Решение — две строчки кода: явно установить таймаут в 300 секунд (или больше, в зависимости от worst-case на вашем железе). Но важнее сам принцип: если вы вызываете локальную LLM на CPU, измерьте реальную латентность на вашем hardware и поставьте таймаут с запасом. А если над этим строится UI — добавляйте индикатор прогресса. 90 секунд тишины выглядят как сломанная система, даже когда всё работает штатно.

Урок 3: Ollama слушает только localhost

Ollama «из коробки» привязывается к 127.0.0.1:11434. Для разработки на локальной машине — нормально. Для развёртывания, когда приложение и модельный сервер находятся на разных машинах (или даже запускаются от разных учётных записей) — это неработающая конфигурация.

Исправление: установить переменную окружения OLLAMA_HOST = "0.0.0.0:11434" перед запуском сервера. Простое решение — если знаешь о нём заранее. Ловушка в том, что Ollama при недоступности выдаёт generic connection errors, а не конкретное сообщение о том, что она слушает только loopback. Автор признаётся, что потратил полдня на проверку файрвола, прежде чем разобрался с привязкой.

Дополнительный нюанс: если Ollama запущена как Windows-сервис, переменную окружения нужно задать на уровне сервиса, а не в текущей сессии PowerShell. Задание её в консоли не повлияет на уже работающий сервис. Мелочь, которая стоит реального времени.

Урок 4: Python-установщик не проходит через корпоративные GPO

Ингест-пайплайн (скрипт для извлечения текста из документов и формирования эмбеддингов) написан на Python. На защищённом Windows Server с групповыми политиками (GPO), ограничивающими запуск установщиков, стандартный Python MSI молча завершается с результатом «успешно», не установив ничего на диск. Или громко падает с ошибками про повышение привилегий, которые даже администратор не может обойти.

Решение — использовать embeddable Python package: ZIP-архив вместо установщика, который обходит большинство ограничений GPO. Настройка чуть более ручная: нужно скачать архив, распаковать, в файле python3xx._pth раскомментировать строку import site (иначе pip не работает), скачать get-pip.py и запустить его из папки. После этого pip install работает штатно.

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

Урок 5: качество ответов определяется промптом, а не ретривалом

Это, пожалуй, самый ценный инсайт статьи. Автор признаётся, что потратил недели на тонкую настройку параметров чанкинга (разбиения документов на фрагменты), эмбеддингов и количества извлекаемых кусков (top-K). Каждое улучшение давало единицы процентов прироста качества. А потом он переписал шаблон промпта — и получил качественный скачок, который сделал всю ретривал-оптимизацию «погрешностью округления».

Две крайности, между которыми пришлось лавировать:

  • Слишком строгий промпт («отвечай ТОЛЬКО на основе контекста, если ответа нет — скажи «не знаю»») — модель начинала отказываться отвечать на вопросы, частично покрытые контекстом, и сыпала «я не знаю» там, где любой человек, прочитав тот же документ, ответил бы уверенно.

  • Слишком свободный промпт («используй контекст, чтобы помочь ответить») — модель начинала уверенно галлюцинировать, заполняя пробелы в извлечённых фрагментах правдоподобным вымыслом. В регулируемой среде (медицина!) это не вопрос качества, а вопрос юридической ответственности.

Рабочий компромисс формулируется примерно так: отвечай на основе предоставленного контекста; если информация частично релевантна — используй её и явно укажи, что контекст говорит, а о чём молчит; только если ничего связанного с вопросом нет — скажи об этом прямо; не выдумывай данные, которых нет в контексте.

Ключевые формулировки — «частично релевантна» (разрешение модели рассуждать по неполному контексту) и «укажи, что контекст говорит, а о чём молчит» (принудительное разделение прочитанного и выведенного). Результат — модель отвечает, когда может, честно признаёт ограничения и явно разграничивает факты и инференции.

Реальная латентность на CPU

Автор приводит конкретные цифры, которых так не хватает в большинстве туториалов:

Конфигурация Модель Время ответа
4 vCPU / 16 ГБ RAM Mistral 7B 60–120 сек
16 vCPU / 32 ГБ RAM Mistral 7B 20–45 сек
4 vCPU / 8 ГБ RAM phi3:mini 15–30 сек
GPU 8 ГБ+ Mistral 7B 3–8 сек

Автор честно оговаривает: замеры CPU сделаны на реальных серверах развёртывания, а GPU-замер — с одолженного оборудования, reference point, а не production-данные.

Два практических вывода. Первый: phi3:mini на скромном железе по латентности конкурирует с Mistral 7B на значительно более мощном сервере. Если качество ответов устраивает — понижайте модель до того, как просить бюджет на апгрейд железа. Второй: разрыв между CPU и GPU — примерно 10×. Если есть возможность получить хотя бы одну 8-гигабайтную видеокарту — это кардинально меняет пользовательский опыт.

Чтобы смягчить проблему высокой латентности, автор реализовал семантический кэш перед LLM: cosine-сходство между входящим запросом и закэшированными запросами с порогом 0,92. При попадании в кэш ответ приходит менее чем за секунду. На умеренно загруженной внутренней системе hit rate кэша вырос достаточно, чтобы среднее время ответа ощущалось приемлемым, несмотря на worst-case в 90 секунд.

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