Что произойдёт с основным API если остановить Python сервис на котором выполняются вычисления ИИ?

В Enterprise GenAI Platform проверил это на работающем приложении. Пока Python был недоступен, запрос семантического сравнения вернул HTTP 503. Core API на .NET остался запущен. После пересоздания контейнера Python тот же запрос снова вернул HTTP 200 и результат вычисления без перезапуска .NET.

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

За этим результатом стоит вопрос шире выбора языка: где провести границу между бизнес логикой и машинным обучением и сколько распределённости действительно нужно для этой границы?

Почему Python отдельно, а .NET остаётся монолитом

Основной сервер Enterprise GenAI Platform это модульный монолит на .NET 10. В нём находятся публичные маршруты, проверка запросов, прикладная логика и доступ к PostgreSQL. Модули Chats и AI пока не требуют отдельных процессов: внутри Core API они остаются логически разделёнными частями одного приложения.

С вычислениями ИИ ситуация иная. Загрузка весов модели, зависимости PyTorch, работа с памятью и процессорная нагрузка имеют другой жизненный цикл, чем обработка запросов к чатам. Обновлять модель и управлять её ресурсами удобно независимо от основного сервера.

Разумеется, выполнить модель можно и в .NET например, через ONNX Runtime. Для одной неизменной модели такой вариант мог бы оказаться проще. Но платформа рассчитана на дальнейшее развитие обработки текстов, а Python даёт прямой доступ к инструментам этой области без переноса их зависимостей в Core API.

Поэтому отдельно развёрнут только специализированный вычислительный компонент, а не каждый прикладной модуль.

Однако такое разделение усложняет взаимодействие компонентов. Вызов Python теперь выполняется по HTTP, а значит необходимо согласовать контракты, учитывать задержки и обрабатывать недоступность сервиса. При этом независимое масштабирование остаётся архитектурной возможностью, а не обязательной необходимостью. Пока нет реальных данных о нагрузке, усложнять инфраструктуру дополнительными механизмами не имеет смысла.

Зачем эмбеддинги, если существует LLM

Для проверки взаимодействия сервисов выбрана небольшая, но реальная AI операция: сравнение двух текстов по смыслу.

Например, «The application stores data in PostgreSQL» и «The system persists information in a PostgreSQL database» написаны по разному, но описывают почти одно и то же. Сравнение строк не отражает эту близость.

Можно спросить генеративную языковую модель насколько похожи предложения. Но тогда простая оценка потребует запроса к LLM, времени генерации и разбора её ответа. Здесь достаточно модели векторных представлений sentence-transformers/all-MiniLM-L6-v2, которая работает локально на CPU.

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

В проекте нет отдельного вычислительного фреймворка для этой операции:

embeddings = self._model.encode([left, right])

return self._cosine_similarity(
    embeddings[0],
    embeddings[1],
)

Один вызов encode обрабатывает оба текста. Служба проверяет количество полученных векторов и их размерность, а при нулевой длине вектора исключает деление на ноль. Сам алгоритм можно проверять на тестовых данных, не загружая модель.

В сквозном испытании через Docker получились следующие значения:

Сравниваемые тексты

Косинусное сходство

Два описания хранения данных в PostgreSQL

0,7598

Описание PostgreSQL и фраза о солнечной погоде

0,0462

Это не оценка качества модели на полноценном наборе примеров, а проверка работы вычислительной цепочки. Значение score не является вероятностью истинности, и высокий показатель не означает, что два утверждения равнозначны. Например, противоположные фразы на одну тему могут иметь близкие эмбеддинги.

К тому же all-MiniLM-L6-v2 ориентирована прежде всего на английский язык. Для русскоязычных корпоративных документов модель и пороги близости придётся выбирать по отдельным проверочным данным. Вычислительный сервис возвращает показатель, но не решает за прикладную систему, какой показатель достаточен.

Как устроено взаимодействие .NET и Python

SentenceTransformer не должен создаваться для каждого запроса. В FastAPI модель загружается при запуске приложения через lifespan, после чего EmbeddingService хранится в состоянии приложения и передаётся обработчикам через внедрение зависимостей.

Адаптер SentenceTransformerModel изолирует библиотеку от расчёта сходства: EmbeddingService зависит от протокола модели, а не от её конкретной реализации. HTTP обработчик семантического сравнения объявлен синхронной функцией def, которую FastAPI выполняет в пуле потоков. Это не устраняет нагрузку на CPU, но не блокирует цикл асинхронных событий самим синхронным вызовом модели.

Между .NET и Python выбран обычный HTTP. Запросу нужен один результат в рамках текущего обращения; очередь сообщений потребовала бы хранить состояние задания и отдельно доставлять ответ. Для длительной обработки документов очередь может быть уместна, но для интерактивного сравнения двух фраз она только усложнила бы взаимодействие.

Внешний клиент вызывает POST /ai/semantic-similarity в Core API. Тот обращается к внутреннему POST /semantic-similarity на Python. Сетевое взаимодействие сосредоточено в AiOrchestratorClient, реализующем IAiOrchestratorClient. Типизированный HttpClient получает адрес Python и время ожидания из конфигурации.

Публичный модуль AI не знает ни адреса контейнера, ни устройства SentenceTransformer. Контракт ограничен двумя строками на входе и числом score на выходе. Входные значения проверяются на стороне .NET, Python дополнительно применяет ограничения Pydantic. Обязательность поля score проверяется и при десериализации ответа: отсутствие результата нельзя молча принять за ноль.

Такая граница даёт возможность менять внутреннюю реализацию модели без переписывания прикладного маршрута. Но она не устраняет необходимость согласовывать версии контрактов: проверка данных в двух разных языках сама по себе не гарантирует одинаковых правил.

Когда Python перестаёт отвечать

Отдельный процесс меняет характер отказа. Python может оказаться недоступен, превысить время ожидания или вернуть некорректный ответ. Сетевой вызов не устраняет эти проблемы он переносит их за границу процесса. Поэтому Core API должен различать причины сбоев, а не сводить всё к общей ошибке HTTP 500.

Что произошло

Ответ Core API

Python недоступен, соединение не установлено

503 Service Unavailable

Превышено время ожидания ответа

504 Gateway Timeout

Python ответил ошибкой или нарушил контракт

502 Bad Gateway

В AiOrchestratorClient сетевые исключения переводятся в понятные интеграционному слою типы:

catch (HttpRequestException ex)
{
    throw new AiOrchestratorUnavailableException(ex);
}
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
    throw new AiOrchestratorTimeoutException(ex);
}

Полученный от Python HTTP 500 при этом не считается потерей соединения: он классифицируется как ошибка ответа. Точно так же обрабатываются некорректный JSON и отсутствие обязательного score.

Отдельный AiServiceExceptionHandler превращает эти исключения в ответы ProblemDetails, не раскрывая клиенту внутренние сообщения зависимого сервиса. Core API записывает статус ошибки её категорию и идентификатор запроса, чтобы можно было сопоставить журналы.

Есть и важное различие в отмене: если запрос отменил сам вызывающий код это не следует выдавать за тайм аут Python. Проверка исходного CancellationToken сохраняет эту семантику. При этом отмена HTTP запроса ещё не означает, что уже начатое вычисление на стороне Python немедленно прекратилось.

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

Как найти один запрос в журналах .NET и Python

Когда сервисов больше одного, одного текста ошибки недостаточно. Core API принимает или создаёт X-Correlation-ID, возвращает его клиенту и передаёт Python через DelegatingHandler. FastAPI записывает тот же идентификатор в журнал.

Во время сквозной проверки один Correlation ID обнаружился и в .NET и в Python. Так можно связать обработку запроса по обе стороны HTTP границы без внедрения полноценной распределённой трассировки.

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

Проверка готовности не равна защите от отказов

В локальном окружении работают четыре сервиса Docker Compose: PostgreSQL с pgvector, одноразовый db-migrator, Python AI Orchestrator и Core API. При этом используются три разных образа: мигратор запускается из того же образа что и .NET API.

Python загружает модель до начала приёма HTTP запросов. Compose проверяет /health и не запускает Core API пока AI Orchestrator не перейдёт в состояние healthy. Для PostgreSQL действует собственная проверка а мигратор применяет недостающие изменения EF Core и должен успешно завершиться до старта API.

core-api:
  depends_on:

    postgres:
      condition: service_healthy

    db-migrator:
      condition: service_completed_successfully

    ai-orchestrator:
      condition: service_healthy

Веса модели сохраняются в именованном томе huggingface_cache: он переживает пересоздание Python контейнера. Это полезно для локального запуска, хотя само по себе не закрепляет версию весов для строгой воспроизводимости.

Но healthy означает только готовность в момент проверки. Если Python остановится позже то Compose не сделает запросы к нему успешными. Именно поэтому проверки готовности дополняют, а не заменяют обработку 503, 504 и 502 в Core API.

Здесь важны не только автоматические тесты, но и проверка настоящего отказа:

Действие

Наблюдение

Остановлен только AI Orchestrator

Core API остаётся запущенным; AI‑запрос возвращает 503

Python‑контейнер пересоздан

Модель загружается; сервис получает статус healthy

Запрос повторён без перезапуска .NET

200, score ≈ 0,7598

Кеш проверен до и после пересоздания

Те же 48 файлов, около 274 МБ

В тестовом наборе также проверяются контракты и отказные сценарии без запуска реальной модели: 21 тест на .NET и 9 на Python; фронтенд имеет ещё 2 теста. Сквозной вызов через Compose проверяет то, чего подмены зависимостей доказать не могут: реальный путь от HTTP запроса до вычисления SentenceTransformer и обратно.

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

Что даёт эта граница на практике

В Enterprise GenAI Platform пока нет полноценного RAG, обработки корпоративных документов, шлюза к генеративным моделям или агентов. Работающий AI сервис выполняет проверку текста и семантическое сравнение этого достаточно чтобы проверить границу технологий, контракт и поведение при отказе, но недостаточно чтобы объявлять готовой всю корпоративную AI платформу.

Само сравнение двух предложений несложно. Гораздо интереснее то, что для него удалось организовать отдельный жизненный цикл ML модели без превращения каждого модуля .NET в микросервис.

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

Отдельный Python сервис полезен не потому что в нём выполняется ИИ, а потому что он изолирует особый характер нагрузки. И ценность этой изоляции проявляется не в момент успешного запроса, а тогда, когда зависимость перестаёт отвечать.


Материалы проекта

Исходный код и конфигурация сервисов Enterprise GenAI Platform на GitHub

Подробнее о реализации: документация AI Orchestrator

Предыдущие публикации:

Комментарии (0)