Назад к articles
article

Интеграция Ollama с C# и .NET — локальные LLM, структурированный вывод и vision-модели

Интеграция Ollama с C# и .NET позволяет запускать open-source языковые и vision-модели локально — без облачных API-ключей, без оплаты за каждый вызов, без передачи данных на внешние серверы. В этой статье разбирается нативный HTTP API Ollama, структурированный вывод через JSON Schema, анализ изображений vision-моделями и типизированный .NET-адаптер, который генерирует схемы запросов из C# классов автоматически.

Два продакшн-проекта уже используют этот подход: агрегатор недвижимости применяет Ollama для оценки фотографий квартир по качеству ремонта; бот для изучения языков — для генерации словарного запаса. Оба работают локально без зависимости от сторонних API. Описание проекта агрегатора квартир.


Почему 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": "На фотографии изображена светлая гостиная с белыми стенами, ламинатом и большими окнами."
}

Структурированный вывод через 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"]
            }
        }
    }
}

Ответ соответствует схеме:

{
    "model": "qwen2.5vl:3b",
    "data": {
        "RenovationRating": 0.82,
        "Tags": ["clean", "bright", "new_windows", "modern_kitchen"],
        "Description": "Ухоженная квартира с недавним ремонтом, хорошим естественным светом и обновлёнными элементами."
    }
}

Проблема нативного API на практике

Писать format JSON Schema вручную для каждого типа ответа утомительно и чревато ошибками. Пропущенная обёртка "type", опечатка в имени свойства или неправильный уровень вложенности молча приводят к ответу, не совпадающему с вашим C# классом — и вы узнаёте об этом при десериализации, а не в точке вызова.

Вторая проблема — бойлерплейт: каждый вызов требует настройки HTTP-клиента, сериализации JSON, base64-кодирования, обработки ошибок и парсинга ответа. Ни одна из этих задач не является интересным кодом.


Laraue.Core.Ollama: типизированный .NET-адаптер

Пакет Laraue.Core.Ollama оборачивает нативный API типизированным интерфейсом. JSON Schema генерируется автоматически из вашего C# класса через рефлексию — изменения схемы подхватываются автоматически без правки кода запроса.

NuGet последняя версия
Загрузки загрузки
GitHub Laraue.Core.Ollama

Интерфейс

IOllamaPredictor предоставляет три перегрузки:

public interface IOllamaPredictor
{
    // Vision-модель: структурированный вывод + изображение
    Task<TModel> PredictAsync<TModel>(
        string modelName,
        string prompt,
        string base64EncodedImage,
        CancellationToken ct = default)
        where TModel : class;

    // Текстовая модель: структурированный вывод без изображения
    Task<TModel> PredictAsync<TModel>(
        string modelName,
        string prompt,
        CancellationToken ct = default)
        where TModel : class;

    // Сырая строка ответа — без схемы и десериализации
    Task<string> PredictAsync(
        string modelName,
        string prompt,
        CancellationToken ct = default);
}

Используйте generic-перегрузки, когда нужен структурированный вывод, разобранный в C# тип. Используйте сырую перегрузку, когда нужен текстовый ответ напрямую — для свободной генерации или когда парсинг реализован самостоятельно.

Установка

dotnet add package Laraue.Core.Ollama

Настройка

Зарегистрируйте предиктор в 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 сразу влияет на следующий запрос — правки схемы вручную не требуется.

Анализ текста

var result = await ollamaPredictor.PredictAsync<PredictionResult>(
    model: "gemma3:12b",
    prompt: "Классифицируй следующий текст и верни структурированный результат.",
    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>(
    model: "qwen2.5vl:3b",
    prompt: "Оцени качество ремонта, видимое на фотографии квартиры.",
    base64Image,
    ct);

Выбор модели

  • Для текстовых задач: gemma3:12b и qwen2.5:7b — хорошие отправные точки. Больше параметров — лучше рассуждения, меньше — быстрее инференс.
  • Для vision-задач: qwen2.5vl:3b хорошо справляется с анализом изображений при умеренных требованиях к железу. Проверьте поддержку vision на странице модели на ollama.com/library до загрузки.
  • Задержка первого вызова: Ollama скачивает модель при первом использовании, если она не закэширована локально. Последующие вызовы в пределах стандартного 5-минутного окна простоя загружают модель из памяти и значительно быстрее.
  • Смена модели: Измените строку параметра model — больше ничего в коде менять не нужно. Это главное преимущество Ollama перед хостингом дообученной модели: сравнение моделей без инфраструктурных затрат.

Применение в реальных проектах

Агрегатор недвижимости использует IOllamaPredictor с qwen2.5vl для оценки фотографий квартир по качеству ремонта. Каждое фото получает RenovationRating от 0 до 1, а также массивы Advantages и Problems, которые хранятся для отладки промптов. Среднее по всем фото объявления входит в итоговый рейтинг идеальности. Как работает формула ранжирования.


Часто задаваемые вопросы

На каком порту работает Ollama по умолчанию?

Ollama слушает порт 11434 по умолчанию. Базовый URL для всех API-вызовов — http://localhost:11434. Это можно изменить через переменную окружения OLLAMA_HOST, если Ollama запущен на отдельной машине или в контейнере.

Как работает структурированный вывод Ollama в C#?

Поле format в запросе Ollama принимает JSON Schema объект. Модель ограничивает свой вывод этой схемой перед возвратом. Адаптер Laraue.Core.Ollama генерирует эту схему автоматически из вашего C# класса через рефлексию — вы определяете форму ответа как C# record, адаптер делает остальное.

Можно ли использовать Ollama с vision-моделями в C#?

Да. Передайте base64-закодированные байты изображения в поле images запроса или используйте перегрузку PredictAsync<TModel>(modelName, prompt, base64EncodedImage, ct) в адаптере. Изображения обрабатывают только модели с поддержкой vision — подтвердите поддержку на странице модели на ollama.com/library до загрузки.

Как переключаться между моделями в Ollama?

Измените параметр model в API-вызове или вызове PredictAsync. Никаких других изменений в коде не требуется. Ollama автоматически управляет загрузкой и выгрузкой моделей. Это делает сравнение качества моделей для конкретной задачи простым без инфраструктурных изменений.

Подходит ли Ollama для продакшна?

Ollama хорошо работает в продакшне для нагрузок, нечувствительных к задержке и привязанных к GPU, где ограничения по приватности данных или стоимости исключают облачные API. Для latency-sensitive или высоконагруженных продакшн-систем тщательно оцените пропускную способность на целевом железе перед принятием решения. Архитектурный паттерн из проекта недвижимости — изолированный GpuWorkerHost, разбирающий очередь — практичный способ отвязать throughput инференса от остального приложения.