MAATRIX / Блог / LocalAI: один API вместо зоопарка сервисов

LocalAI: один API вместо зоопарка сервисов

MAATRIX

Как только в проекте появляется не одна ИИ-задача, а несколько — генерация текста, распознавание речи, генерация картинок — начинается знакомая история: под каждую задачу свой сервис, у каждого свой API, свой формат запроса и своя логика ошибок. Ollama отвечает по-своему, сервер на faster-whisper — по-своему, Automatic1111 для картинок — по-третьему. В итоге приложение обрастает тремя клиентами вместо одного. LocalAI решает именно это: одна точка входа, один знакомый формат запросов, а какой движок отрабатывает конкретный запрос — уже не забота вашего кода.

Зоопарк сервисов: в чём на самом деле боль

Когда self-hosted ИИ-инфраструктура ограничивается одной задачей — например, только чат на локальной LLM через Ollama — жить относительно просто: один порт, один формат /api/generate, один клиент. Проблема начинается, когда задач становится несколько.

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

  • LLM-инференс — Ollama или llama.cpp со своим HTTP API;
  • распознавание речи — faster-whisper или whisper.cpp, поднятый как отдельный сервис со своим API;
  • генерация изображений — Stable Diffusion через Automatic1111 или ComfyUI, опять же со своим API и своим форматом ответа.

У каждого из них свой формат запроса, свой способ отдавать ошибку, свой способ описывать модель, свой порт. В коде приложения это превращается в три отдельных модуля интеграции, три набора тестов, три места, где что-то может сломаться при обновлении. Если раньше приложение уже было написано под облачный API (скажем, под OpenAI: chat/completions, audio/transcriptions, images/generations), при переезде на self-hosted это ещё и три разных чужих формата вместо одного знакомого.

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

Что такое LocalAI и как он устроен

LocalAI — открытый проект (автор Этторе Ди Джачинто, известный как mudler), который поднимает REST API, максимально совместимый с форматом OpenAI, и разворачивает под ним разные движки инференса в зависимости от того, какая модель и какая задача запрошены. Сам LocalAI переизобретением колеса не занимается — он оборачивает уже существующие движки:

  • llama.cpp — для текстовых GGUF-моделей (Llama, Mistral, Qwen и другие);
  • whisper.cpp — для распознавания речи;
  • бэкенды на базе diffusers / stable-diffusion.cpp — для генерации изображений;
  • piper, bark и другие — для синтеза речи (text-to-speech);
  • есть бэкенды для эмбеддингов, реранкинга, а через дополнительные бэкенды — интеграция с более специализированными движками вроде vLLM.

Ключевая идея: с точки зрения вашего приложения все эти разнородные движки выглядят как один и тот же сервис с одним и тем же набором эндпоинтов — /v1/chat/completions, /v1/completions, /v1/embeddings, /v1/images/generations, /v1/audio/transcriptions, /v1/audio/speech, /v1/models. Какая модель какому бэкенду в реальности соответствует — описывается в YAML-конфиге модели, а не в коде вашего приложения.

Это не значит, что LocalAI "умеет всё, что умеет vLLM плюс всё, что умеет ComfyUI" — он умеет ровно то, что реализовано в его интеграции с конкретным бэкендом, и не более. Но для типового набора задач (чат, базовая генерация изображений, транскрибация, эмбеддинги) этого чаще всего достаточно, а выигрыш — один API вместо трёх-четырёх.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть ИИ на сервере

Практический сценарий: было три интеграции, стал один base_url

Возьмём конкретный и частый случай: приложение изначально написано под облачный OpenAI API. Клиентский код выглядит примерно так:

from openai import OpenAI

client = OpenAI(
    api_key="sk-...",
    # base_url по умолчанию — https://api.openai.com/v1
)

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Составь план на день"}],
)

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

С LocalAI переезд для текстовой части часто сводится к смене одной строки — base_url:

from openai import OpenAI

client = OpenAI(
    base_url="http://your-server:8080/v1",
    api_key="not-needed",  # LocalAI по умолчанию не проверяет ключ,
                           # если явно не включена авторизация
)

response = client.chat.completions.create(
    model="llama-3.1-8b-instruct",
    messages=[{"role": "user", "content": "Составь план на день"}],
)

Остальной код — обработка ответа, стриминг, обработка ошибок — остаётся тем же самым, потому что формат ответа тот же самый (choices[0].message.content и так далее). То же самое верно для транскрибации и генерации изображений — меняется только base_url и, возможно, имя модели в теле запроса:

curl http://your-server:8080/v1/audio/transcriptions \
  -F file="@zapis.mp3" \
  -F model="whisper-base"

curl http://your-server:8080/v1/images/generations \
  -H "Content-Type: application/json" \
  -d '{
    "model": "sd-turbo",
    "prompt": "закат над пустыней, акварель",
    "size": "512x512"
  }'

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

Похожий подход, но для маршрутизации между облачными провайдерами (а не локальными движками), разобран в статье про единый шлюз к OpenAI, Claude и Gemini — там LiteLLM решает соседнюю, но другую задачу: не объединяет разные типы локального инференса, а маршрутизирует запросы между разными облачными API.

Установка: docker compose и первый запуск

Проще всего поднять LocalAI в Docker. Официальный образ публикуется под именем localai/localai (историческое имя было quay.io/go-skynet/local-ai — на момент установки сверяйтесь с актуальным названием в документации проекта, оно менялось). Минимальный docker-compose.yaml:

services:
  localai:
    image: localai/localai:latest
    container_name: localai
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - ./models:/models
    environment:
      - MODELS_PATH=/models
      - THREADS=4
    # для GPU-варианта нужен образ с суффиксом cuda/hipblas
    # и секция deploy.resources.reservations.devices, как для Ollama

Поднимаем:

mkdir -p models
docker compose up -d
docker compose logs -f localai

После старта проверяем, что API отвечает:

curl http://localhost:8080/readyz
curl http://localhost:8080/v1/models

Пока каталог models/ пуст, второй запрос вернёт пустой список — это ожидаемо, модели ещё не подключены. LocalAI поддерживает и загрузку моделей "по галерее" через встроенный каталог готовых конфигов, и ручное описание своих моделей YAML-файлами — на практике для рабочей инфраструктуры удобнее второй вариант, потому что он предсказуем и версионируется вместе с остальным конфигом.

Подключение разных типов моделей: текст, картинки, речь

Каждая модель в LocalAI описывается отдельным YAML-файлом в каталоге models/. Общий принцип: имя файла (без расширения) или поле name внутри становится значением model в запросах к API, а backend определяет, какой движок инференса реально обрабатывает запрос.

Текстовая модель на llama.cpp:

name: llama-3.1-8b-instruct
backend: llama-cpp
parameters:
  model: llama-3.1-8b-instruct.Q4_K_M.gguf
context_size: 8192
threads: 4

Генерация изображений (бэкенд может называться diffusers или stablediffusion в зависимости от версии — уточняйте в документации конкретного релиза):

name: sd-turbo
backend: diffusers
parameters:
  model: stabilityai/sd-turbo
f16: true

Распознавание речи на whisper.cpp:

name: whisper-base
backend: whisper
parameters:
  model: ggml-base.bin

Сами веса моделей (.gguf, ggml-*.bin, чекпоинты диффузионных моделей) кладутся в тот же смонтированный каталог models/, рядом с YAML-описанием, либо подтягиваются автоматически при первом обращении, если в конфиге указан URL. После того как три конфига лежат в models/, все три типа задач доступны через один и тот же префикс /v1/... — приложению не нужно знать, что за иллюстрацией стоит diffusion-модель, а за расшифровкой — whisper.cpp.

Отдельно от LocalAI, если интересен голый Whisper без обёртки — в статье про распознавание речи через Whisper разобрана прямая установка faster-whisper без унифицирующего слоя — это полезно сравнить, чтобы понять, что именно LocalAI берёт на себя, а что оставляет как есть.

Честные компромиссы: где унификация мешает, а не помогает

Унифицирующая прослойка по своей природе — это компромисс между удобством и полнотой доступа к возможностям конкретного движка. У LocalAI это проявляется предсказуемо:

  • Не все параметры конкретного бэкенда пробрасываются через единый API. Специализированный инструмент вроде ComfyUI даёт узловой редактор workflow для генерации изображений с произвольными цепочками обработки — через /v1/images/generations такого контроля нет, доступен упрощённый набор параметров (промпт, размер, иногда negative prompt и количество шагов).
  • Тонкая настройка производительности инференса тоже урезана. Прямая работа с vLLM даёт контроль над батчингом запросов, квантованием на лету, PagedAttention-параметрами — через унифицированный слой доступна лишь часть этих рычагов, ровно та, что реализована в интеграции конкретной версии LocalAI с этим бэкендом.
  • Отставание от апстрима. Новая функциональность конкретного движка (свежая версия квантования в llama.cpp, новый режим в whisper.cpp) появляется в LocalAI не мгновенно — сначала в оригинальном проекте, потом в интеграции.
  • Отладка на один слой сложнее. Когда ответ странный, приходится смотреть не только логи бэкенда, но и то, как LocalAI транслирует запрос и ответ между своим API и внутренним форматом движка.

Практический вывод простой: для типовых сценариев — чат-бот, транскрибация голосовых, генерация иллюстраций по тексту без экзотических настроек — унификация того стоит: меньше кода интеграции, один формат ошибок, один сервис для мониторинга. Для узкоспециализированных продвинутых требований (тонкий контроль батчинга под нагрузкой, сложные графы обработки изображений, кастомные фичи конкретного форка whisper.cpp) разумнее развернуть специализированный инструмент напрямую и не пытаться получить его возможности через универсальный слой. Про выбор между Ollama и более "тяжёлым" vLLM в похожем разрезе — в статье Ollama против vLLM: что выгоднее и когда.

Если задача — не объединить разные типы локального инференса, а именно перейти с облачного API на локальную модель того же типа (только текст), возможно, унифицирующий слой вообще избыточен — тогда полезнее почитать про миграцию с OpenAI API на локальную модель: там разобрано, что при таком переезде меняется не только эндпоинт, но и качество ответов, и это не всегда решается сменой одной строки конфига.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть ИИ на сервере

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

LocalAI — это то же самое, что LiteLLM?

Нет, хотя обе идеи звучат похоже. LocalAI объединяет разные *локальные* движки инференса (llama.cpp, whisper.cpp, diffusers и другие) под одним API. LiteLLM решает соседнюю задачу — маршрутизацию и биллинг между разными *облачными* провайдерами (OpenAI, Claude, Gemini). Они не взаимоисключающие: можно поставить LiteLLM перед LocalAI как ещё один уровень маршрутизации, если нужно сочетать локальные и облачные модели.

Нужен ли GPU для LocalAI?

Нет, LocalAI умеет работать и на CPU — как и большинство движков, которые он оборачивает (llama.cpp, whisper.cpp прекрасно работают на CPU, просто медленнее). GPU ускоряет инференс, особенно для генерации изображений, но не обязателен для тестового или лёгкого продакшен-сценария.

Можно ли использовать существующий клиент OpenAI SDK без изменений?

В большинстве случаев да, для основных эндпоинтов (chat, completions, embeddings, images, audio) — совместимость достаточно высокая. Но экзотические параметры конкретных облачных функций (например, самые новые фичи Structured Outputs или специфичные параметры function calling) могут поддерживаться LocalAI не полностью — стоит явно протестировать именно те вызовы, которые использует ваше приложение.

Что произойдёт, если модель, которую я указал в запросе, не сконфигурирована в LocalAI?

API вернёт ошибку, что модель не найдена — LocalAI не подгружает произвольные модели "на лету" без предварительного описания в YAML-конфиге (за исключением явно настроенной автозагрузки из галереи).

Подходит ли LocalAI для продакшен-нагрузки с высоким RPS?

Для умеренной нагрузки и типовых задач — да. Для по-настоящему высокой конкурентной нагрузки на LLM-инференс специализированные движки вроде vLLM с их батчингом обычно дают лучшую пропускную способность — в этом случае прямая работа с vLLM (в том числе за отдельным шлюзом, а не за LocalAI) может быть оправданнее.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →