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 оказался удобной средой для бэкенда, где много времени уходит на ожидание ответа модели.
Предварительные знания
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 | Почему |
|---|---|---|
| concurrency | 3-10 | Ограничено rate limits LLM-провайдера |
| attempts | 2-3 | LLM API может вернуть 429/503. Больше 3 - бессмысленно |
| backoff | exponential, 5000ms | Даёт API время восстановиться |
| timeout | 60000ms | LLM может думать долго, но не бесконечно |
Почему 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-фич