Локальные LLM в .NET: Ollama, приватность и цена вопроса
Когда локальная модель выгоднее облачной: приватность, предсказуемая стоимость и работа офлайн. Разбираем Ollama, Gemma 4 12B и подключение к .NET.
Когда облако не подходит
Облачные модели удобны, и именно поэтому их выбирают по умолчанию: нулевая настройка, свежие веса, оплата по факту. Но у этого выбора есть три ограничения, из-за которых в части проектов локальный запуск выигрывает не по удобству, а по существу.
Первое — данные. Внешний API означает, что содержимое запроса покидает контур компании. Для черновика в репозитории это безразлично; для договора, персональных данных или результата проверки регулятора — нет. Законодательные ограничения на передачу данных за периметр в ряде отраслей жёсткие, и их нельзя закрыть формулировкой в договоре с провайдером.
Второе — предсказуемость стоимости. При вызовах API вы платите за токены, и сумма зависит от числа вызовов, длины контекста и выбранной модели. В пилоте это незаметно. В потоке, где один запрос разворачивает в десятки вызовов агентов, счёт растёт пропорционально трафику, а не по плану.
Третье — доступность. Внешний сервис недоступен, когда нет сети, упирается в rate limit, отдаёт 529 при переполнении очереди, меняет поведение при обновлении модели. Локальный запуск даёт предсказуемую задержку и работу офлайн.
При этом локальная модель — не бесплатная замена. Покупается железо, и качество ответов ниже, чем у большой облачной модели. Поэтому вопрос не «локально или облако», а где проходит граница.
Граница проходит по данным и по нагрузке
Локальный запуск оправдан там, где выполняются два условия одновременно.
Данные не должны покидать контур. Это не про размер компании, а про содержимое: исходники с доступом, персональные данные, договорные документы, содержимое тикетов.
Нагрузка предсказуема и невелика. Локальная модель хорошо обслуживает задачи с фиксированным потоком: классификация, извлечение полей, черновик ответа по шаблону, локальный код-ассистент для разработчика. Она плохо обслуживает пиковые всплески и длинные рассуждения, где нужен большой контекст и сложный план.
Промежуточный вариант — гибрид: чувствительные данные не уходят из контура, потому что предварительный этап (конвертация документа, извлечение фактов, классификация) идёт локально, а финальный запрос к облачной модели уходит уже с обезличенной и усечённой выборкой. Такой расклад стоит проектировать заранее: если в облако уходит весь исходник, локальный этап не даёт ничего.
Gemma 4 12B как рабочая локальная модель
Для локального запуска на своём железе интересна не самая большая доступная модель, а та, что влезает в разумный бюджет памяти. Gemma 4 12B — 11.95 млрд параметров, 48 слоёв, словарь 262K токенов, лицензия Apache 2.0, что снимает вопрос о коммерческом использовании. В каталоге Ollama семейство опубликовано как gemma4 — теги 12b, 26b и 31b.
Заявленное контекстное окно — 256K токенов. Архитектура у модели необычная: в варианте 12B «Unified» энкодеры для изображений и аудио убраны, патчи изображения проецируются напрямую в декодер, поэтому весь вход обрабатывается одним проходом.
Объём памяти под загрузку определяется квантизацией. По расчётной таблице Google, для Gemma 4 12B получается:
| Точность | Память GPU/TPU под загрузку |
|---|---|
| BF16 (16 бит) | 26.7 ГБ |
| SFP8 (8 бит) | 13.4 ГБ |
| Q4_0 (4 бит) | 6.7 ГБ |
Сам Google оговаривает, что эти числа зависят от инструмента и окружения. И это ещё без KV-кэша: он растёт линейно с длиной контекста и оплачивается сверху. Поэтому «модель с окном 256K» и «модель, работающая на ноутбуке с 16 ГБ RAM» — это разные утверждения. Второе верно для Q4_0 и умеренного контекста, первое верно про возможности архитектуры.
Ollama: запуск и главная ловушка
Ollama — локальный рантайм моделей: скачивает веса, держит их в памяти и отдаёт HTTP-эндпоинт. Документация по API и моделям — docs.ollama.com. На macOS и Linux ставится скриптом, на Windows — установщиком OllamaSetup.exe:
curl -fsSL https://ollama.com/install.sh | sh
ollama run gemma4:12b
Команда скачивает веса на диск — для gemma4:12b это 7.7–8.0 ГБ. Дальше модель доступна по двум протоколам: родному /api/chat и совместимому с OpenAI /v1/chat/completions. Совместимость удобна: к ней подключается любой клиент, который умеет говорить с OpenAI. Сервер по умолчанию слушает 127.0.0.1:11434.
Ловушка в другом. По умолчанию Ollama использует контекстное окно 4096 токенов, а не все 256K, заявленные моделью. Значение меняется двумя способами: переменной окружения OLLAMA_CONTEXT_LENGTH при старте сервера или параметром options.num_ctx в теле запроса к /api/chat.
И вот здесь развилка: у OpenAI-совместимого эндпоинта /v1/chat/completions параметра num_ctx нет, и задать размер контекста через этот эндпоинт нельзя. Чтобы поднять его для OpenAI-клиента, нужен отдельный тег модели с зафиксированным параметром — его задаёт файл Modelfile:
FROM gemma4:12b
PARAMETER num_ctx 32768
ollama create gemma4-32k -f Modelfile
Практический смысл: если приложение ходит в локальную модель через OpenAI-совместимый клиент, размер контекста задаётся один раз в Modelfile, а не в каждом запросе. Тот, кто этого не учёл, получает модель, у которой контекст молча обрезан до 4K токенов, и диагностирует «модель не помнит» там, где проблема в конфигурации.
Подключение к .NET
Для .NET у Ollama есть клиенты, и выбор — между ними. Официальный пакет Microsoft.Extensions.AI.Ollama существует, но публикуется в preview-версиях (последняя доступная — 9.7.0-preview.1), тогда как стабильная ветка Microsoft.Extensions.AI уже дошла до 10.11.0. Для OpenAI-совместимых эндпоинтов есть отдельный пакет Microsoft.Extensions.AI.OpenAI — стабильный, с той же версией, — и он умеет говорить с любым сервером, реализующим /v1/chat/completions. Отдельно стоит комьюнити-пакет Ollama (OllamaSharp) — у него стабильные версии и поддержка IChatClient. Официальная линия интеграций с моделями в .NET собрана в .NET AI extensions.
Есть и готовая связка с оркестратором: пакет CommunityToolkit.Aspire.OllamaSharp регистрирует клиент Ollama в контейнере зависимостей одной строкой, а заодно — IChatClient и IEmbeddingGenerator из Microsoft.Extensions.AI:
builder.AddOllamaApiClient("ollama")
.AddChatClient(otel => otel.EnableSensitiveData = true);
Строка требует ещё и адреса сервера: ConnectionStrings:ollama в формате строки подключения (Endpoint=http://localhost:11434) либо секция Aspire:OllamaSharp с ключом Endpoint. Без этого хост поднимается, а первое обращение к клиенту падает с ошибкой о ненастроенном OllamaApiClient. Отдельный AddOllamaSharpChatClient из того же пакета помечен как устаревший — в актуальной версии правильная форма именно та, что выше.
Но ни один из них не обязателен: Ollama — это HTTP-сервис, а значит, обычный HttpClient. Совместимость с OpenAI-схемой позволяет переиспользовать тот же код-клиент, что и для облачного провайдера, меняя базовый адрес.
using System.Net.Http.Json;
using System.Text.Json.Serialization;
public sealed record ChatMessage(
[property: JsonPropertyName("role")] string Role,
[property: JsonPropertyName("content")] string Content);
public sealed record ChatRequest(
[property: JsonPropertyName("model")] string Model,
[property: JsonPropertyName("messages")] IReadOnlyList<ChatMessage> Messages,
[property: JsonPropertyName("temperature")] double Temperature = 0.2,
[property: JsonPropertyName("top_p")] double TopP = 0.9,
[property: JsonPropertyName("stream")] bool Stream = false);
public sealed record ChatResponse(
[property: JsonPropertyName("choices")] IReadOnlyList<ChatChoice>? Choices);
public sealed record ChatChoice(
[property: JsonPropertyName("message")] ChatMessage Message);
public sealed class OllamaChatClient(HttpClient http, string model)
{
public async Task<string> CompleteAsync(
string prompt,
CancellationToken cancellationToken = default)
{
var request = new ChatRequest(
Model: model,
Messages: [new ChatMessage("user", prompt)]);
var response = await http.PostAsJsonAsync(
"/v1/chat/completions", request, cancellationToken);
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<ChatResponse>(
cancellationToken: cancellationToken);
return payload?.Choices?.FirstOrDefault()?.Message.Content ?? string.Empty;
}
}
Регистрация клиента в Program.cs — единственное место, где различаются локальный и облачный варианты:
builder.Services.AddHttpClient<OllamaChatClient>((sp, client) =>
{
client.BaseAddress = new Uri(
sp.GetRequiredService<IConfiguration>()["Ollama:BaseUrl"] ?? "http://localhost:11434");
})
.AddTypedClient<OllamaChatClient>((http, sp) => new OllamaChatClient(
http,
sp.GetRequiredService<IConfiguration>()["Ollama:Model"] ?? "gemma4:12b"));
Деталь, на которой спотыкаются чаще всего: у клиента два параметра конструктора — HttpClient и string model. Контейнер внедрения умеет собрать HttpClient сам, а вот string он не знает откуда взять: без явной фабрики первое же разрешение падает с «Unable to resolve service for type ‘System.String’». Поэтому модель передаётся через конфигурацию и явный AddTypedClient, а не остаётся на волшебстве контейнера.
Два параметра стоит задавать явно, а не оставлять на волю рантайма. Первый — temperature. Модель-карта Gemma 4 рекомендует для большинства задач temperature=1.0, top_p=0.95, top_k=64, но для кодовых задач в статье Сахила Малика из журнала CODE предложены temperature: 0.2 и top_p: 0.9: код требует точности, а не свободы, и «креативная» догадка в коде обычно означает синтаксическую ошибку. Второй — размер контекста, о котором выше.
Когда локальный рантайм не нужен
Локальная модель не универсальна, и это ограничение стоит назвать прямо.
Если задача требует длинного рассуждения, широкого контекста или сложного многошагового планирования, локальная модель 12B проиграет большой облачной — и выигрыш в приватности придётся оплатить качеством. Если задача — разовая и не требует повторяемости, настройка рантайма, модели и Modelfile обойдётся дороже облачного вызова.
Есть и промежуточная форма: локальный рантайм на собственной машине разработчика для черновой работы и локальных экспериментов, облачный провайдер в продакшене. Один и тот же OpenAI-совместимый код клиента работает в обоих режимах — меняется адрес и имя модели. Когда в схеме становится больше одной модели, вопрос «какой ответ считать правильным» решается отдельно от выбора рантайма — в .NET это закрывает Delibera, где решения нескольких моделей сводятся к общему вердикту.
Порядок внедрения, который даёт результат без лишних затрат: сначала инвентаризация того, что вообще уходит из контура, потом выбор модели по памяти и лицензии, потом один работающий сценарий с замером качества и задержки, и только потом перенос остальных сценариев. Прыгать сразу к «перевести всё на локальную модель» — значит получить недовольных пользователей и неверный вывод о том, что локальные модели не работают.
О чём стоит помнить
Локальный запуск решает три задачи, которые облако решает плохо: данные не покидают контур, стоимость предсказуема, работа не зависит от внешнего сервиса. Платит за это железом, качеством модели и ответственностью за рантайм.
Практические выводы:
Q4_0— разумная отправная точка по памяти: около 6.7 ГБ под загрузку 12B-модели по расчётной таблице Google.- Окно 256K у модели и 4096 токенов у рантайма по умолчанию — разные вещи; размер задаётся явно.
- У OpenAI-совместимого эндпоинта нет параметра контекста — только Modelfile.
- Локальный сценарий оправдан, когда данные не должны уходить и нагрузка предсказуема.
- Клиенты для .NET есть: официальный
Microsoft.Extensions.AI.OpenAI(стабильный) для OpenAI-совместимых эндпоинтов,Microsoft.Extensions.AI.Ollama(preview) и комьюнити-пакетOllama(OllamaSharp) со стабильными версиями. - Всё, что уходит в облако, должно уходить в обезличенном и усечённом виде — иначе локальный этап не даёт эффекта.
Похожие статьи
- Архитектура enterprise AI: четыре решения, которые всё определяют
- .NET и Azure для AI-систем: референсная архитектура
- MCP для enterprise: протокол, безопасность и границы
- RAG на .NET: архитектура, которая переживает продакшн
CTA
Если часть ваших AI-сценариев упирается в требование «данные не покидают контур» или в непредсказуемый счёт за вызовы, начните с инвентаризации: что именно уходит наружу, с каким объёмом и в каком виде. Дальше подберём локальную конфигурацию под ваш контур или покажем, где облако всё равно остаётся правильным ответом.
Разобрать сценарии под ваш контур — напишите нам.
Contextum: конвейер документов в Markdown для AI-агента
AnetLab Contextum: конвейер из пяти стадий между агентом и LLM — конвертация в Markdown, сжатие контекста, идемпотентная доставка и честный реестр потерь.
Оценка качества RAG на .NET: Recall@5, MRR и гейт релиза
Как измерять качество RAG на .NET: Golden Set из 50–100 вопросов, расчёт Recall@5 и MRR, измерение faithfulness и eval-как-гейт-релиза в CI.