03.08.2026 336 материалов

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

Инженер из Коста-Рики собрал RAG-пайплайн целиком на CPU и локальном железе — и выяснил, что все популярные туториалы молчат о пяти критических подводных камнях.

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

Большинство гайдов по RAG неявно предполагают наличие GPU и доступа к облачному API. В реальности корпоративного сектора и госорганизаций ни того, ни другого часто нет — и именно в этом сценарии начинаются настоящие проблемы.

Контекст: почему это важно

RAG (Retrieval-Augmented Generation) — архитектура, при которой языковая модель не генерирует ответ «из головы», а сначала ищет релевантные фрагменты в базе документов и подставляет их в промпт. Это позволяет получать ответы, основанные на конкретных данных, и снижает галлюцинации модели. Обычно RAG-системы строят на GPU с облачными API вроде OpenAI, но в ряде отраслей — здравоохранение, госсектор, финтех — это невозможно: данные не должны покидать сеть, а железо — то, что закупили два года назад.

Именно в таких условиях работает Хуберт Гарсиа Гордон, инженер информационных систем здравоохранения в государственном секторе Коста-Рики. Он собрал полностью локальный RAG-стек и опубликовал его как open-source проект rag-onpremise. Стек включает ASP.NET Core 9, Ollama для локального инференса, Qdrant для векторного хранилища, Python для пайплайна индексации документов, модель Mistral 7B и эмбеддинги на базе nomic-embed-text.

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

Архитектура системы

Прежде чем переходить к урокам, стоит обозначить общую схему. Документы (PDF, Word, Excel) проходят через Python-пайплайн, который извлекает текст, нарезает его на фрагменты по 500 токенов с перекрытием в 50, генерирует эмбеддинги и загружает в Qdrant. При запросе пользователя ASP.NET Core API эмбеддинги вопроса, ищет наиболее похожие фрагменты, собирает промпт с контекстом и отправляет в Mistral 7B через Ollama. Ответ возвращается вместе с указанием источников.

Всё это — стандартная архитектура RAG. Нестандартна среда выполнения.

Урок 1: Официальный SDK Qdrant для .NET использует gRPC — и это ловушка

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

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

Это важный момент для тех, кто работает в корпоративных сетях: gRPC — не «просто ещё один протокол», а зависимость от конкретной сетевой конфигурации, которую вы, возможно, не контролируете.

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

HttpClient в .NET по умолчанию ждёт 100 секунд. Для обычного веб-трафика этого с запасом. Для LLM на CPU — нет.

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

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

Урок 3: Ollama по умолчанию слушает только localhost

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

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

Отдельный нюанс: если Ollama работает как Windows-сервис (а в продакшене так и должно быть), переменная окружения должна быть задана на уровне сервиса, а не в пользовательской PowerShell-сессии.

Урок 4: Python MSI-установщик не работает под корпоративными GPO

Пайплайн индексации написан на Python. На защищённом Windows Server с групповыми политиками стандартный MSI-установщик Python отказывался устанавливаться — иногда молча, иногда с ошибками о привилегиях, которые не решал даже администратор.

Решение — встраиваемый пакет Python (embeddable package). Это ZIP-архив, а не установщик, поэтому большинство ограничений GPO его не касается. Настройка чуть более ручная: нужно распаковать архив, раскомментировать строку import site в конфиге, отдельно скачать и запустить 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-строка — с однократного теста на заёмном оборудовании, не sustained-замер, поэтому к ней стоит относиться как к ориентиру.

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

Для компенсации CPU-латентности автор реализовал семантический кэш перед LLM: косинусное сходство между входящим запросом и кэшированными, с порогом 0,92. При попадании в кэш ответ приходит менее чем за секунду. На умеренно загруженной внутренней системе доля попаданий оказалась достаточно высокой, чтобы средний пользовательский опыт был приемлемым.

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

Итог: что реально важно при построении RAG на ограниченном железе

Пять уроков автора сводятся к простым правилам:

  • Общайтесь с Qdrant по REST, а не через gRPC SDK — в корпоративных сетях меньше сюрпризов.
  • Выставляйте HTTP-таймауты явно — дефолты заточены под веб-трафик, а не под LLM на CPU.
  • Настраивайте привязку Ollama для сервера, а не для вашего ноутбука, и делайте это на уровне сервиса.
  • Используйте встраиваемый Python на защищённых Windows-машинах с GPO.
  • Настраивайте промпт раньше, чем ретривер. Чанкинг и top-K важны, но именно промпт определяет итоговое качество ответов.
  • Кэшируйте агрессивно, когда LLM медленная, и не забывайте сбрасывать кэш при смене модели.
  • Меняйте модель на более лёгкую раньше, чем наращиваете железо.

Ничего из этого не экзотика. Это та часть работы с RAG, которую опускают, когда туториал неявно предполагает GPU, облако и Linux-развёртывание. Когда у вас нет ни того, ни другого, ни третьего — строить приходится именно так.