Интеграция Ollama с C# и .NET позволяет запускать open-source языковые и vision-модели локально — без облачных API-ключей, без оплаты за каждый вызов, без передачи данных на внешние серверы. В этой статье разбирается нативный HTTP API Ollama, структурированный вывод через JSON Schema, анализ изображений vision-моделями и типизированный .NET-адаптер, который генерирует схемы запросов из C# классов автоматически.
Два наших проекта используют этот подход: агрегатор недвижимости применял Ollama для оценки фотографий квартир по качеству ремонта (новые объявления он больше не собирает); бот для изучения языков — для генерации словарного запаса. Оба работают локально без зависимости от сторонних API. Описание проекта агрегатора квартир.
Краткое содержание
Как вызвать локальную модель Ollama из C#:
- Запустите Ollama. По умолчанию он слушает
http://localhost:11434. - Отправьте
POSTна/api/generateчерезHttpClient:model,promptиstream: false. Для vision-модели добавьтеimages— массив строк base64. Для структурированного вывода добавьтеformat— JSON Schema. - Ответ лежит в поле
response. При структурированном выводе это JSON-строка, которую вы десериализуете сами. - Либо отдайте эту рутину библиотеке: в статье показан наш типизированный адаптер
Laraue.Ollama.NET, который строит схему из C# класса, и его сравнение с OllamaSharp и Microsoft.Extensions.AI.
Почему Ollama, а не облачный API
Облачные API (ChatGPT, DeepSeek, Gemini) — очевидный первый выбор, пока не сталкиваешься с ограничениями. Типичные причины запускать модели локально:
| Ситуация | Почему облако не подходит |
|---|---|
| Обработка персональных данных | Передача данных пользователей внешнему провайдеру может нарушать 152-ФЗ, GDPR или внутреннюю политику |
| Высокие требования к доступности | Uptime не может зависеть от SLA стороннего сервиса |
| Санкционные ограничения | Часть AI-провайдеров недоступна в ряде стран из-за экспортных ограничений |
| Высокая нагрузка | Поцентрично-платная модель становится дорогой при большом объёме вызовов |
| Офлайн или изолированная среда | Нет доступа в интернет по условиям инфраструктуры |
Для MVP и проверки гипотез предобученные open-source модели через Ollama — самый быстрый путь: смените модель изменением одной строки конфига, без переобучения. Если MVP докажет жизнеспособность, модель можно заменить на дообученную — интеграционный код не изменится.
Ollama предоставляет единый HTTP API для разных моделей. При запуске Ollama поднимает локальный сервис на порту 11434 по умолчанию. Любой HTTP-клиент может его вызвать — никакого SDK не требуется.
Нативный HTTP API Ollama
Генерация текста
Отправьте POST на /api/generate:
{
"model": "gemma3:12b",
"prompt": "Переведи следующий текст на французский язык: 'Квартира имеет две комнаты и большой балкон.'",
"stream": false
}
Ответ:
{
"model": "gemma3:12b",
"response": "L'appartement a deux pièces et un grand balcon."
}
Для передачи изображения добавьте поле images с base64-строкой. Только vision-модели его обрабатывают — для текстовых моделей поле игнорируется:
{
"model": "qwen2.5vl:3b",
"prompt": "Опиши, что изображено на фотографии.",
"stream": false,
"images": ["<base64EncodedImageBytes>"]
}
Ответ:
{
"model": "qwen2.5vl:3b",
"response": "На фотографии изображена светлая гостиная с белыми стенами, ламинатом и большими окнами."
}
Вызов API из C# через HttpClient
Официального .NET-клиента у Ollama нет (официальные библиотеки — для Python и JavaScript), но API — это обычный JSON поверх HTTP, так что HttpClient достаточно. Вот вызов vision-модели с изображением:
using System.Net.Http.Json;
var client = new HttpClient { BaseAddress = new Uri("http://localhost:11434/") };
var imageBase64 = Convert.ToBase64String(await File.ReadAllBytesAsync("photo.jpg"));
var response = await client.PostAsJsonAsync("api/generate", new
{
model = "qwen2.5vl:7b",
prompt = "Describe what you see in this photo.",
images = new[] { imageBase64 },
stream = false,
});
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<GenerateResponse>();
Console.WriteLine(result!.Response);
record GenerateResponse(string Response);
Для текстового запроса уберите images. stream = false заставляет Ollama вернуть один JSON-объект вместо потока частей, так читать проще.
Структурированный вывод через JSON Schema
Поле format включает структурированный вывод — модель возвращает JSON-объект по вашей схеме вместо произвольного текста. Это необходимо в любом сценарии, где ответ нужно парсить программно:
{
"model": "qwen2.5vl:3b",
"prompt": "Проанализируй фотографию квартиры и верни оценку.",
"stream": false,
"images": ["<base64EncodedImageBytes>"],
"format": {
"type": ["object"],
"properties": {
"RenovationRating": {
"type": ["number"]
},
"Tags": {
"type": ["array"],
"items": { "type": ["string"] }
},
"Description": {
"type": ["string"]
}
},
"required": ["RenovationRating", "Tags", "Description"]
}
}
Список required важен. Когда мы запускали схему с одним свойством и без required на gemma3:4b, модель оба раза отвечала пустым объектом ({ }); с "required": ["RenovationRating"] оба раза возвращалось { "RenovationRating": 7 }. Схема ограничивает форму ответа, но без required пустой объект тоже считается допустимым.
Структурированный ответ приходит в поле response как JSON-строка, которую ваш код должен разобрать:
{
"model": "qwen2.5vl:3b",
"response": "{ \"RenovationRating\": 0.82, \"Tags\": [\"clean\", \"bright\", \"new_windows\", \"modern_kitchen\"], \"Description\": \"Ухоженная квартира с недавним ремонтом, хорошим естественным светом и обновлёнными элементами.\" }"
}
В C# десериализуйте response через JsonSerializer.Deserialize<T>(...).
Проблема нативного API на практике
Писать format JSON Schema вручную для каждого типа ответа утомительно и чревато ошибками. Пропущенная обёртка "type", опечатка в имени свойства или неправильный уровень вложенности молча приводят к ответу, не совпадающему с вашим C# классом — и вы узнаёте об этом при десериализации, а не в точке вызова.
Вторая проблема — бойлерплейт: каждый вызов требует настройки HTTP-клиента, сериализации JSON, base64-кодирования, обработки ошибок и парсинга ответа. Ни одна из этих задач не является интересным кодом.
Laraue.Ollama.NET: типизированный .NET-адаптер
Пакет Laraue.Ollama.NET оборачивает нативный API типизированным интерфейсом. JSON Schema генерируется автоматически из вашего C# класса через рефлексию — изменения схемы подхватываются автоматически без правки кода запроса.
Примечание: ранее пакет публиковался как
Laraue.Core.Ollama. Он был переименован вLaraue.Ollama.NET— обновите командуdotnet add package/PackageReferenceи using-директивы при обновлении со старой версии.
| NuGet | |
| Загрузки | |
| GitHub | Laraue.Ollama.NET |
Интерфейс
IOllamaPredictor (пространство имён Laraue.Ollama.NET) предоставляет три перегрузки:
public interface IOllamaPredictor
{
// Vision-модель: структурированный вывод + изображение
Task<TModel> PredictAsync<TModel>(
string modelName,
string prompt,
string base64EncodedImage,
Dictionary<string, object>? additionalParameters = null,
CancellationToken ct = default)
where TModel : class;
// Текстовая модель: структурированный вывод без изображения
Task<TModel> PredictAsync<TModel>(
string modelName,
string prompt,
Dictionary<string, object>? additionalParameters = null,
CancellationToken ct = default)
where TModel : class;
// Сырая строка ответа — без схемы и десериализации
Task<string> PredictAsync(
string modelName,
string prompt,
Dictionary<string, object>? additionalParameters = null,
CancellationToken ct = default);
}
Используйте generic-перегрузки, когда нужен структурированный вывод, разобранный в C# тип. Используйте сырую перегрузку, когда нужен текстовый ответ напрямую — для свободной генерации или когда парсинг реализован самостоятельно. additionalParameters добавляет поля на верхний уровень запроса к Ollama, поэтому можно передать то, чего адаптер не моделирует, например options или keep_alive, не переходя на нативный HTTP API. Параметры модели вроде temperature или top_p нужно класть внутрь options: new Dictionary<string, object> { ["options"] = new Dictionary<string, object> { ["temperature"] = 0.2 } }. Сам ключ temperature передавать не нужно: адаптер уже записывает этот ключ, и повторное добавление выбросит исключение.
Установка
dotnet add package Laraue.Ollama.NET
Настройка
Зарегистрируйте предиктор в DI-контейнере:
services.AddHttpClient<IOllamaPredictor, OllamaPredictor>((serviceProvider, client) =>
{
client.BaseAddress = new Uri("http://localhost:11434/");
// Замените на адрес вашего Ollama, если запущен на отдельной машине
});
Определите контракт ответа
Подходит любой C# класс или record. Свойства маппятся в генерируемую JSON Schema:
public record PredictionResult
{
public required double RenovationRating { get; set; } // → "number"
public required string[] Tags { get; set; } // → "array" of "string"
public required string Description { get; set; } // → "string"
}
Адаптер рефлектирует PredictionResult в момент вызова, строит format JSON Schema, отправляет запрос и десериализует ответ обратно в PredictionResult. Добавление нового свойства в record сразу влияет на следующий запрос — правки схемы вручную не требуется.
Что содержит сгенерированная схема
Адаптер читает публичные свойства вашего класса и сопоставляет их типы:
| Тип C# | Тип JSON Schema |
|---|---|
string, DateTime |
string |
int, long, float, double, decimal |
number |
bool |
boolean |
| массивы и списки | array (с сопоставленным типом элемента, вложенные классы тоже) |
| остальные классы, словари | object с их свойствами |
Есть два ограничения, о которых нужно знать. В сгенерированной схеме нет списка required, поэтому модель вправе пропустить свойства, а отсутствующее свойство десериализуется в значение по умолчанию (например, оценка 0). Прогон на gemma3:4b выше показывает, что так бывает на самом деле, поэтому проверяйте результат или вызывайте нативный API со своим required. Enum не превращаются в строки; используйте свойство типа string.
Анализ текста
var result = await ollamaPredictor.PredictAsync<PredictionResult>(
modelName: "gemma3:12b",
prompt: "Классифицируй следующий текст и верни структурированный результат.",
ct: ct);
Console.WriteLine(result.RenovationRating); // 0.74
Console.WriteLine(string.Join(", ", result.Tags)); // "clean, bright, good_location"
Анализ изображений
var imageBytes = File.ReadAllBytes("apartment.jpg");
var base64Image = Convert.ToBase64String(imageBytes);
var result = await ollamaPredictor.PredictAsync<PredictionResult>(
modelName: "qwen2.5vl:3b",
prompt: "Оцени качество ремонта, видимое на фотографии квартиры.",
base64EncodedImage: base64Image,
ct: ct);
Другие варианты для .NET
Laraue.Ollama.NET намеренно небольшая. Вот другие распространённые способы вызвать Ollama из .NET:
- OllamaSharp. Сообщественный .NET SDK, который указан для .NET в README самого Ollama. Он умеет chat и generate, потоковый вывод, изображения и структурированный вывод и реализует
IChatClientиIEmbeddingGeneratorот Microsoft. - Microsoft.Extensions.AI. Абстракция от Microsoft (
IChatClient), позволяющая менять провайдера. Отдельный пакетMicrosoft.Extensions.AI.Ollamaустарел, и NuGet указывает на OllamaSharp как замену: используйте OllamaSharp как реализацию Ollama заIChatClient. - Обычный
HttpClient, как показано выше, когда нужно один-два вызова и не нужна лишняя зависимость.
Свой адаптер мы написали под одну узкую задачу: структурированный вывод со схемой, сгенерированной из C# класса. Если вам нужны потоковый вывод, история чата или не привязанный к провайдеру интерфейс, берите OllamaSharp.
Выбор модели
- Для текстовых задач:
gemma3:12bиqwen2.5:7b— хорошие отправные точки. Больше параметров — лучше рассуждения, меньше — быстрее инференс. - Для vision-задач:
qwen2.5vl:3bхорошо справляется с анализом изображений при умеренных требованиях к железу. Проверьте поддержку vision на странице модели на ollama.com/library до загрузки. - Задержка первого вызова: Ollama скачивает модель при первом использовании, если она не закэширована локально. Последующие вызовы в пределах стандартного 5-минутного окна простоя загружают модель из памяти и значительно быстрее.
- Смена модели: Измените строку параметра
model— больше ничего в коде менять не нужно. Это главное преимущество Ollama перед хостингом дообученной модели: сравнение моделей без инфраструктурных затрат.
Применение в реальных проектах
Агрегатор недвижимости использует IOllamaPredictor с qwen2.5vl:7b. Все фото квартиры склеиваются в один коллаж и уходят одним запросом, а модель возвращает оценку ремонта от 0 до 10, флаг HasNoRenovation и список положительных и отрицательных особенностей. Особенности хранятся для отладки промптов. Как работают конвейер и формула ранжирования.