1. Архитектура умного API: интеграция FastAPI, PostgreSQL и LLM-провайдеров
Архитектура умного API: интеграция FastAPI, PostgreSQL и LLM-провайдеров
Перенос локального Python-скрипта, вызывающего нейросеть, в production-среду обнажает фундаментальный архитектурный конфликт. LLM-провайдеры отвечают медленно, генерация может занимать десятки секунд. Веб-фреймворк, напротив, спроектирован для миллисекундной обработки тысяч конкурентных запросов. База данных требует коротких транзакций для предотвращения блокировок. Попытка связать эти три компонента напрямую в одном обработчике маршрута неизбежно приводит к исчерпанию пула соединений, потере данных при сетевых сбоях и деградации пользовательского опыта.
Чтобы система работала стабильно, API должно выступать не просто прокси-сервером, а интеллектуальным оркестратором состояний.
Топология слоистой архитектуры
Надежное ИИ-приложение строится на основе строгой изоляции зон ответственности. Монолитный код, где внутри FastAPI-маршрута происходят SQL-запросы и HTTP-вызовы к OpenAI, невозможно масштабировать и тестировать. Архитектура разбивается на четыре независимых слоя.
Транспортный слой (FastAPI Routers). Отвечает исключительно за прием HTTP-запроса, валидацию входящего JSON через Pydantic-модели и возврат HTTP-ответов. Здесь не принимаются решения о том, как формировать промпт или в какую таблицу писать данные. Этот слой использует механизм Dependency Injection для получения готовых к работе сервисов.
Слой бизнес-логики (Service Layer). Ядро системы. Сервисный класс (например, ChatOrchestrator) инкапсулирует сценарий работы: он запрашивает историю диалога из базы, формирует контекст, вызывает LLM, обрабатывает ошибки провайдера и инициирует сохранение результата.
Слой доступа к данным (Repository / Data Layer). Изолирует работу с SQLAlchemy и драйвером asyncpg. Сервисный слой не должен знать о существовании таблиц или SQL-синтаксиса. Он вызывает методы вроде repository.save_message(), а репозиторий уже управляет транзакциями, flush() и commit().
Слой интеграции (LLM Gateway). Абстракция над внешними API нейросетей. Поскольку провайдеры (OpenAI, Anthropic, локальная Ollama) имеют разные форматы запросов и ответов, шлюз приводит их к единому внутреннему контракту.
Эта топология гарантирует, что при замене PostgreSQL на MongoDB или переходе с GPT-4 на Llama 3, изменения затронут только один конкретный слой, не ломая транспортный интерфейс или бизнес-логику.
Паттерн LLM Gateway: абстракция над провайдерами
Прямое использование официальных SDK (например, библиотеки openai) внутри бизнес-логики жестко привязывает архитектуру к одному вендору. В корпоративных системах часто применяется роутинг запросов: простые задачи уходят в дешевую локальную модель, сложные — в тяжелую проприетарную.
Для реализации этой гибкости проектируется паттерн Gateway (Шлюз). В его основе лежит единая внутренняя Pydantic-модель UnifiedChatRequest и UnifiedChatResponse.
Шлюз реализуется как абстрактный базовый класс или протокол BaseLLMProvider, содержащий метод generate_response(). От него наследуются конкретные адаптеры: OpenAIAdapter, AnthropicAdapter и т.д. Внутри адаптера происходит маппинг внутренних моделей в специфичный JSON-payload конкретного API с использованием глобального асинхронного клиента httpx.AsyncClient.
Критически важный аспект на этом уровне — управление таймаутами. В отличие от классических REST API, где таймаут обычно устанавливается в пределах 5-10 секунд, авторегрессионная генерация требует гранулярного подхода. В httpx.Timeout задается жесткий лимит на установку соединения (connect) и отправку данных (write), но лимит на чтение (read) расширяется до 60-120 секунд, чтобы дождаться завершения генерации длинного ответа.
Двухфазная запись: транзакционная модель ИИ-вызова
Главная ошибка при проектировании чат-API — попытка выполнить весь цикл в памяти и записать результат в базу данных только после успешного ответа от LLM.
Если генерация длится 20 секунд, а на 19-й секунде у клиента обрывается интернет или сервер перезапускается (например, при деплое Kubernetes), данные теряются. Промпт пользователя не сохранен, списание токенов не произведено, контекст диалога рассинхронизирован. Кроме того, удержание открытой транзакции БД на протяжении всего вызова к внешней сети приведет к мгновенному исчерпанию пула соединений (Transaction pooling в PgBouncer не спасет, так как транзакция активна).
Правильный паттерн — двухфазная запись (Two-Phase Write) с промежуточным состоянием.
pending (или generating). Транзакция фиксируется (COMMIT), соединение возвращается в пул.completed (или failed).Этот подход гарантирует консистентность эпизодической памяти. Даже при фатальном сбое контейнера в базе останется след: сообщение со статусом generating, зависшее во времени. Фоновый процесс (Worker) может периодически находить такие «осиротевшие» записи и переводить их в статус ошибки, уведомляя пользователя.
Потоковая передача (SSE) и рассинхронизация I/O
Ожидание полного ответа от LLM перед отправкой HTTP-ответа клиенту неприемлемо для UX. Метрика TTFT (Time-to-First-Token — время до первого токена) является ключевой. Пользователь должен увидеть начало текста уже через 0.5–1 секунду, даже если полная генерация займет минуту.
!Сравнение блокирующей генерации и потоковой передачи
Для этого используется Server-Sent Events (SSE) в связке с StreamingResponse из FastAPI. Однако потоковая передача ломает классический жизненный цикл запроса. В блокирующей модели мы получаем строку, пишем её в базу и возвращаем клиенту. В потоковой модели мы передаем токены клиенту по мере их поступления прямо из сетевого сокета httpx, минуя оперативную память сервера.
Возникает проблема: как сохранить сгенерированный текст в PostgreSQL, если мы отдаем его клиенту по частям?
Решение заключается в паттерне накопления состояния (State Accumulation) внутри асинхронного генератора.
Функция-генератор, переданная в StreamingResponse, выполняет двойную работу:
yield (отдает чанк клиенту).Когда цикл чтения завершается (LLM прислала стоп-последовательность), генератор имеет на руках полный собранный текст. Именно в этот момент, перед закрытием соединения с клиентом, инициируется вторая фаза записи в базу данных.
Для предотвращения задержки закрытия HTTP-запроса, финальное обновление базы данных часто делегируется механизму BackgroundTasks в FastAPI. Генератор передает собранный текст и ID сообщения фоновой задаче, которая выполняет SQL UPDATE уже после того, как клиент получил последний токен и закрыл соединение.
Идемпотентность и защита от состояния гонки
В распределенной среде пользователи часто ведут себя непредсказуемо. Распространенный сценарий: пользователь отправляет длинный промпт, не видит моментальной реакции, нервничает и нажимает кнопку «Отправить» еще три раза подряд.
Если API спроектировано наивно, оно запустит четыре параллельных процесса генерации. Это приведет к:
Для защиты на уровне бизнес-логики применяется концепция идемпотентности операций. При поступлении запроса сервисный слой проверяет статус последней активности в сессии. Если в базе уже есть сообщение со статусом generating для данного диалога, API должно немедленно отклонить новый запрос с кодом HTTP 409 Conflict, блокируя параллельную ветку вычислений.
Альтернативный подход — использование пессимистичной блокировки SELECT ... FOR UPDATE при чтении сессии, однако в высоконагруженных чат-системах это может создать бутылочное горлышко. Оптимальным решением становится использование уникальных ключей идемпотентности (Idempotency Keys), генерируемых на клиенте, или проверка хэша последнего промпта в рамках короткого окна времени.
Сведение воедино слоистой топологии, двухфазной записи и потоковой обработки превращает простой прокси-скрипт в отказоустойчивое ядро ИИ-системы. Это ядро способно выдерживать сетевые штормы, корректно тарифицировать использование токенов и гарантировать, что ни один контекст диалога не будет утерян из-за таймаута внешнего провайдера.