# Локальные LLM в .NET: Ollama, приватность и цена вопроса

Когда локальная модель выгоднее облачной: приватность, предсказуемая стоимость и работа офлайн. Разбираем Ollama, Gemma 4 12B и подключение к .NET.

> For the index of this section of the site, see [llms.txt](https://anetlab.ru/llms.txt)

22 октября 2026 г.8 мин чтения

# Локальные LLM в .NET: Ollama, приватность и цена вопроса

Когда локальная модель выгоднее облачной: приватность, предсказуемая стоимость и работа офлайн. Разбираем Ollama, Gemma 4 12B и подключение к .NET.

[Виктор Бузин](https://anetlab.ru/about.md) · Founder & Principal Engineer, AnetLab

16+ лет в enterprise-разработке: .NET, AI/LLM-системы, RAG, MCP. Автор материала и практик в студии.

## Когда облако не подходит

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

Первое — данные. Внешний API означает, что содержимое запроса покидает контур компании. Для черновика в репозитории это безразлично; для договора, персональных данных или результата проверки регулятора — нет. Законодательные ограничения на передачу данных за периметр в ряде отраслей жёсткие, и их нельзя закрыть формулировкой в договоре с провайдером.

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

Третье — доступность. Внешний сервис недоступен, когда нет сети, упирается в rate limit, отдаёт 529 при переполнении очереди, меняет поведение при обновлении модели. Локальный запуск даёт предсказуемую задержку и работу офлайн.

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

## Граница проходит по данным и по нагрузке

Локальный запуск оправдан там, где выполняются два условия одновременно.

Данные не должны покидать контур. Это не про размер компании, а про содержимое: исходники с доступом, персональные данные, договорные документы, содержимое тикетов.

Нагрузка предсказуема и невелика. Локальная модель хорошо обслуживает задачи с фиксированным потоком: классификация, извлечение полей, черновик ответа по шаблону, локальный код-ассистент для разработчика. Она плохо обслуживает пиковые всплески и длинные рассуждения, где нужен большой контекст и сложный план.

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

## Gemma 4 12B как рабочая локальная модель

Для локального запуска на своём железе интересна не самая большая доступная модель, а та, что влезает в разумный бюджет памяти. Gemma 4 12B — 11.95 млрд параметров, 48 слоёв, словарь 262K токенов, лицензия Apache 2.0, что снимает вопрос о коммерческом использовании. В каталоге Ollama семейство опубликовано как [gemma4](https://ollama.com/library/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](https://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](https://learn.microsoft.com/dotnet/ai/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](https://anetlab.ru/products/delibera.md), где решения нескольких моделей сводятся к общему вердикту.

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

## О чём стоит помнить

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

Практические выводы:

*   `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: четыре решения, которые всё определяют](https://anetlab.ru/blog/enterprise-ai-architecture.md)
*   [.NET и Azure для AI-систем: референсная архитектура](https://anetlab.ru/blog/dotnet-azure-ai-architecture.md)
*   [MCP для enterprise: протокол, безопасность и границы](https://anetlab.ru/blog/mcp-for-enterprise.md)
*   [RAG на .NET: архитектура, которая переживает продакшн](https://anetlab.ru/blog/rag-na-dotnet.md)

## CTA

Если часть ваших AI-сценариев упирается в требование «данные не покидают контур» или в непредсказуемый счёт за вызовы, начните с инвентаризации: что именно уходит наружу, с каким объёмом и в каком виде. Дальше подберём локальную конфигурацию под ваш контур или покажем, где облако всё равно остаётся правильным ответом.

Разобрать сценарии под ваш контур — [напишите нам](https://anetlab.ru/contact.md).

Читайте также

21 октября 2026 г.AI/LLM

### [Contextum: конвейер документов в Markdown для AI-агента](https://anetlab.ru/blog/contextum-document-pipeline.md)

AnetLab Contextum: конвейер из пяти стадий между агентом и LLM — конвертация в Markdown, сжатие контекста, идемпотентная доставка и честный реестр потерь.

[Читать разбор](https://anetlab.ru/blog/contextum-document-pipeline.md)

[.NET](https://anetlab.ru/blog/?tag=.NET)[RAG](https://anetlab.ru/blog/?tag=RAG.md)

20 октября 2026 г.AI/LLM

### [Оценка качества RAG на .NET: Recall@5, MRR и гейт релиза](https://anetlab.ru/blog/rag-evaluation-dotnet.md)

Как измерять качество RAG на .NET: Golden Set из 50–100 вопросов, расчёт Recall@5 и MRR, измерение faithfulness и eval-как-гейт-релиза в CI.

[Читать разбор](https://anetlab.ru/blog/rag-evaluation-dotnet.md)

[.NET](https://anetlab.ru/blog/?tag=.NET)[RAG](https://anetlab.ru/blog/?tag=RAG.md)[Оценка качества](https://anetlab.ru/blog/?tag=%D0%9E%D1%86%D0%B5%D0%BD%D0%BA%D0%B0%20%D0%BA%D0%B0%D1%87%D0%B5%D1%81%D1%82%D0%B2%D0%B0.md)