AI-инжиниринг

AI Backend на Node.js/NestJS: architecture patterns, best practices

Цели урока

  • Понять почему Node.js - идеальная платформа для AI backend и где её ограничения
  • Спроектировать NestJS модуль для AI-функциональности с правильной инкапсуляцией
  • Реализовать абстракцию LLM-провайдеров через injectable service
  • Построить middleware pipeline: валидация → rate limit → AI → валидация выхода
  • Настроить BullMQ для асинхронных AI-задач с приоритетами и retry
  • Освоить тестирование AI-компонентов: моки, snapshot-тесты, integration-тесты

Node.js - идеальный бекенд для AI: event loop отлично держит долгие LLM запросы, async/await нативный, ecosystem огромный. Пока GPT-4o генерирует ответ 800 мс, поток не спит - он обрабатывает ещё 50 запросов. Но есть нюанс: CPU-bound задачи блокируют event loop. И tokenization через tiktoken - CPU-bound. 2025 год, каждый второй стартап - AI-обёртка. 90% из них написаны как прототип: один файл, прямой вызов OpenAI, без rate limiting. Первый всплеск трафика - счёт на USD 10K, сервер лежит, пользователи видят 500. Разница между прототипом и production AI backend - не в модели, а в архитектуре вокруг неё.

  • ChatGPT Plus обрабатывает миллионы запросов через BullMQ-подобные очереди с приоритизацией - платные пользователи впереди
  • Vercel AI SDK - абстракция провайдеров именно с этим паттерном, позволяет переключиться между OpenAI и Anthropic одной строкой конфига
  • Linear использует очереди для AI-классификации тикетов - 100K+ задач в день без влияния на основное приложение
  • tiktoken в Worker Thread: на Node.js tokenization CPU-bound операция блокирует event loop, поэтому выносится в воркер

Как сложилась AI-экосистема JavaScript

Долгое время серьёзная работа с LLM подразумевала Python: там были SDK, примеры и фреймворки. JavaScript и TypeScript держали фронтенд и веб-бэкенд, но AI-инструментов под них почти не было. Это изменилось в 2022-2023 годах. В конце 2022 появился LangChain.js, портировав идеи оркестрации, цепочек и retrieval в экосистему Node.js, чтобы агентную логику можно было писать на TypeScript. В 2023 году Vercel, создатели Next.js, выпустили Vercel AI SDK (пакет ставится как npm i ai). Он закрыл частую задачу: стриминг ответа LLM в UI и работа с tool calling и структурированным выводом без громоздкого фреймворка, с готовыми хелперами под React, Next.js, Svelte. Параллельно официальные TypeScript SDK OpenAI и Anthropic сделали вызовы моделей нативными для Node. Так у JavaScript появился свой AI-стек: Vercel AI SDK для пользовательских стриминговых интерфейсов, LangChain.js для оркестрации и RAG. Node.js с его событийным циклом и нативным async/await оказался удобной средой для бэкенда, где много времени уходит на ожидание ответа модели.

Предварительные знания

  • AI System Design: production AI application architecture from zero to scale

NestJS module architecture для AI сервисов

Node.js - идеальный бекенд для AI. Event loop отлично держит долгие LLM запросы: пока GPT-4o думает свои 800 мс, поток не заблокирован - он обрабатывает следующий запрос. Async/await нативный, ecosystem огромный. Но есть нюанс: CPU-bound задачи блокируют event loop. И tokenization - CPU-bound. К этому вернёмся.

AI-функциональность в NestJS-приложении выделяется в отдельный **AiModule**. Один модуль инкапсулирует всё: провайдеров LLM, конфигурацию, rate limiting, логирование usage. Остальные модули получают AI-возможности через инъекцию сервиса - не зная деталей реализации. Это не формализм ради формализма: именно такая граница позволяет завтра заменить OpenAI на Anthropic за 10 минут.

  • **Один публичный сервис** - AiService. Все внутренние provider-ы скрыты
  • **@Global()** - AI нужен везде: чат, модерация, рекомендации. Один import в AppModule
  • **Конфигурация изолирована** - AiConfigService читает env, валидирует ключи, выбирает default model
  • **Очередь встроена** - BullMQ queue регистрируется внутри модуля, не снаружи
  • **Провайдеры взаимозаменяемы** - OpenAI, Anthropic, Ollama реализуют один интерфейс

**Валидация конфигурации при старте** - критична. Если API-ключ отсутствует, приложение должно упасть сразу при запуске, а не при первом запросе пользователя через 3 часа после деплоя.

Почему AiModule помечен декоратором @Global()?

Injectable AI Service: паттерн абстракции провайдеров

AiService - фасад, скрывающий детали работы с конкретным LLM-провайдером. Внешний код вызывает `aiService.complete()` - и не знает, OpenAI это или Anthropic. Провайдер выбирается на основе конфигурации, типа задачи или даже стоимости запроса. Модерация контента идёт через gpt-4o-mini (USD 0.15 per 1M токенов), сложный reasoning - через claude-3-5-sonnet (USD 3 per 1M). Разрыв в 20 раз, а пользователь ничего не замечает.

**Паттерн Strategy + Facade:** AiService выступает Facade (единый вход), а провайдеры - Strategy (взаимозаменяемые алгоритмы). Именно этот паттерн использует Vercel AI SDK: одна строка конфига меняет провайдера. Linear переключился с OpenAI на собственные модели - пользователи ничего не заметили.

**Логирование каждого вызова** - не опционально. Без метрик `tokens`, `latencyMs`, `provider` невозможно оптимизировать расходы. 100K запросов в месяц через gpt-4o вместо gpt-4o-mini - это разница в `188.` Без логов это невидимо.

Для задачи модерации контента в примере используется GPT-4o-mini вместо GPT-4o. Почему?

Middleware Pipeline: request → validate → rate-limit → AI → validate-output → response

AI endpoint - не просто прокси к OpenAI. Это как граница с таможней: каждый слой делает своё дело. Валидация входа отсекает prompt injection. Rate limiting останавливает abuse. Маршрутизация выбирает модель. Валидация выхода ловит пустые ответы и oversized content. Без этого pipeline - первый злоумышленник с curl может сгенерировать счёт на тысячи долларов.

**Rate limiting для AI endpoints - обязателен.** Без него один пользователь может сгенерировать счёт в тысячи долларов за минуты. Redis/KeyDB даёт атомарные счётчики с TTL - единственный надёжный способ на многопоточном деплое.

Зачем нужна валидация ВЫХОДА модели (AiOutputValidationInterceptor), если модель и так генерирует текст?

BullMQ для асинхронных AI задач

Не все AI-задачи требуют мгновенного ответа. Генерация отчёта, batch-обработка 1000 текстов, создание эмбеддингов для RAG-системы - всё это **фоновые задачи**. BullMQ ставит их в очередь с приоритетами, retry-логикой и контролем concurrency. ChatGPT Plus работает именно так: платные пользователи получают jobs с priority=1, бесплатные ждут в конце очереди.

Параметр BullMQРекомендация для AIПочему
concurrency3-10Ограничено rate limits LLM-провайдера
attempts2-3LLM API может вернуть 429/503. Больше 3 - бессмысленно
backoffexponential, 5000msДаёт API время восстановиться
timeout60000msLLM может думать долго, но не бесконечно

Почему concurrency BullMQ для AI-очереди ставится 3-10, а не 100?

Тестирование AI Backend: моки, snapshots, integration tests

AI-компоненты сложно тестировать: ответы LLM недетерминированы, API-вызовы стоят денег, latency высокая. Но это не повод отказываться от тестов. Решение - **многоуровневое тестирование**: unit-тесты с моками закрывают 80% логики, snapshot-тесты фиксируют промпты, integration-тесты с реальным API запускаются раз в сутки в CI.

  • **Unit-тесты с моками** - 80% тестов. Мок LlmProvider, проверяется логика вокруг AI
  • **Snapshot-тесты промптов** - ловят случайные изменения system prompt. Промпт - это код
  • **Integration-тесты** - раз в сутки в CI с реальным API. Проверяют формат ответа, не содержание
  • **Contract-тесты** - проверяют что ответ парсится в ожидаемую структуру (JSON schema)
  • **Cost guard** - CI-проверка что integration-тесты не превысили лимит (например, USD 1 за прогон)

**Snapshot-тесты для промптов - недооценённая практика.** Случайное изменение system prompt может сломать поведение всего AI-функционала. `toMatchSnapshot()` гарантирует, что изменения промптов проходят через code review.

Зачем использовать snapshot-тесты для system prompts?

Node.js не подходит для AI из-за single-thread - всё будет тормозить

Event loop идеален для I/O-bound LLM вызовов: пока модель генерирует ответ, поток свободен. Проблема только в CPU-bound preprocessing

LLM вызов - это HTTP запрос с долгим ответом. Node.js держит тысячи таких запросов параллельно без блокировок. Настоящая проблема - tokenization (tiktoken), JSON parsing больших контекстов, embedding вычисления. Это CPU-bound операции, которые реально блокируют event loop. Решение - Worker Threads: `new Worker('./tokenizer.worker.js')` выносит тяжёлые вычисления в отдельный поток. Event loop остаётся свободным.

Итоги

  • Node.js - идеальная платформа для AI backend: event loop держит тысячи параллельных LLM запросов без блокировок
  • CPU-bound задачи (tokenization через tiktoken, embedding вычисления) выносятся в Worker Threads
  • AiModule с @Global() - единая точка входа, AiService - единственный публичный API, провайдеры скрыты
  • LlmProvider interface позволяет переключать OpenAI/Anthropic/Ollama без изменения клиентского кода - и экономить 20x на model routing
  • BullMQ: concurrency 3-10 (rate limits), exponential backoff, приоритеты (модерация > генерация)
  • Тестирование: 80% unit с моками, snapshot для промптов - промпт это код

Что дальше

AI backend построен. Теперь - как подключить AI к внешним системам стандартизированно, через MCP protocol, и как AI coding assistants используют эти же паттерны.

  • MCP (Model Context Protocol) — Стандартный протокол для подключения AI к внешним системам - tools, resources, prompts
  • AI Coding Assistants изнутри — Как Copilot, Cursor и Claude Code используют архитектурные паттерны AI backend

Связанные уроки

  • aie-42-ai-system-design — Backend реализует спроектированную AI-систему
  • aie-45-mcp-protocol — Backend отдаёт инструменты через MCP
  • aie-43-realtime-ai — Node-backend отдаёт streaming-ответы
  • aie-35-observability — Инструментируем backend для AI-трейсинга
  • sd-10-microservices — Та же декомпозиция сервисов для AI-фич
AI Backend на Node.js/NestJS: architecture patterns, best practices

0

1

Войти