Вы построили ИИ-агента. Он отлично работает на локальной машине. Запускаете в прод - и через час он начинает генерировать спам-письма клиентам, терять контекст беседы, а база данных Redis захлебывается от набегающих задач. Знакомо?
Я прошел этот путь трижды. Первый раз - когда мы писали SDR-агента на коленке из FastAPI и Celery. Второй - когда переписали на NestJS, но забыли про идемпотентность. Третий - когда все наконец-то взлетело. Эта статья - солянка из боли, кода и цифр. Без воды.
Проблема: почему голый LLM не тянет продажи
Типичный запрос: "Напиши агента, который будет слушать звонки, определять боли клиента, ставить задачу в CRM и отправлять follow-up через час". Звучит как один вызов к ChatGPT API. На практике - это конвейер из 5-7 шагов, каждый из которых может упасть, зависнуть или вернуть мусор.
Проблема №1: LLM нестабильны. Один и тот же промпт может дать разный результат. Проблема №2: транскрипция длится дольше самого звонка. Проблема №3: если агент ошибся и отправил клиенту не то письмо - это репутационная катастрофа.
Решение лежит на стыке асинхронных очередей, четкой архитектуры и идемпотентности. Разберем стек и грабли.
⚠️ Важное предупреждение: Все описанные подходы проверены на нагрузке 500+ звонков в день. Если у вас 10 звонков - можно и на коленке. Но привычка делать правильно спасет вас, когда вырастете.
Стек на 2026 год: что реально работает
Мы используем NestJS v11 (вышел в марте 2026 с нативной поддержкой декораторов для BullMQ v5), BullMQ v5 (Redis 7.4), Node.js 22 (стабильный LTS). Для LLM - GPT-5 от OpenAI (доступен через API c ноября 2025) и Whisper v4 (локально, для аудио).
Почему NestJS, а не FastAPI? Продакшен-агент - это не просто вызов LLM. Это десятки микросервисов, GraphQL, WebSockets, graceful shutdown. NestJS дает строгую модульную структуру, DI и встроенную поддержку очередей через @nestjs/bull.
Почему BullMQ? Потому что он умеет задержки, ретраи с экспоненциальным бэкоффом, приоритеты и, что критично для денежных операций - rate limiting. Celery и Redis Streams - тоже варианты, но BullMQ на Node.js работает нативнее.
Архитектура: не просто микросервисы, а обработочные цепочки
Весь пайплайн разбит на три слоя.
1 Слой приема (API Gateway + WebSocket)
NestJS Gateway получает аудиофайл (или ссылку на звонок через Twilio API). Сразу кладет задачу в очередь BullMQ и возвращает 202 Accepted. Клиент не ждет транскрипции. Вебхук отправляет статус через Socket.io.
2 Слой обработки (BullMQ очереди)
Три очереди с разными приоритетами:
- transcription.queue - самая тяжелая. Приоритет 10 (низкий). Держит аудио до 2 часов. Использует Whisper v4 с разделением на 30-секундные чанки. Каждый чанк - отдельная job для параллелизации.
- analysis.queue - средний приоритет 5. LLM (GPT-5) анализирует транскрипт: находит боли, теги, настроение, ключевые фразы. Результат - structured JSON.
- action.queue - высокий приоритет 1. Генерация письма, создание задачи в CRM, запись в базу. Проверка через сентинел-модель (Claude 4 Opus) на безопасность.
3 Слой стейта (Redis + Event Sourcing)
Каждая job пишет в Redis лог событий. При падении - читаем последний checkpoint и рестартуем с него. Это недетерминизм LLM мы приручаем через Event Sourcing - подробно описано в отдельном гайде.
Код: ключевые узлы
Модуль BullMQ (NestJS v11 @nestjs/bull v5)
// transcribe.processor.ts
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Job } from 'bullmq';
import { Injectable } from '@nestjs/common';
@Processor('transcription')
@Injectable()
export class TranscriptionProcessor extends WorkerHost {
constructor(private readonly whisperService: WhisperService) {
super();
}
async process(job: Job, token?: string): Promise<any> {
// Экспоненциальный бэк-офф: первая попытка 1s, вторая 2s, третья 4s...
await job.updateProgress({ state: 'transcribing' });
const result = await this.whisperService.transcribe(
job.data.audioUrl,
{ language: 'ru', model: 'whisper-4' }
);
// Сохраняем в Redis для Event Sourcing
await job.updateData({ status: 'done', text: result.text });
// Кидаем следующую задачу в очередь анализа
await job.addJob('analysis', {
transcriptionId: job.id,
text: result.text
});
return result;
}
}
❗ Грабли №1: Не забудьте установить concurrency = 2 для очереди транскрипции. Whisper жрет GPU. Если запустить 10 параллелей - OOM убивает процесс. Мы это поняли, когда прод упал в пятницу вечером.
Конфигурация очередей (для BullMQ v5)
// queues.config.ts
import { QueueOptions } from '@nestjs/bullmq';
export const transcriptionQueueConfig: QueueOptions = {
connection: { host: 'redis', port: 6379 },
defaultJobOptions: {
attempts: 5,
backoff: { type: 'exponential', delay: 2000 },
removeOnComplete: { count: 100 }, // храним только 100 успешных
removeOnFail: { age: 24 * 3600 }, // храним упавшие 24 часа
},
};
Грабли production: топ-5 ошибок, которые мы совершили
- Идемпотентность на уровне CRM. Дважды отправили follow-up одному клиенту -> менеджер скинул скриншот. Решение: deduplication key на основе callId + timestamp + user.
- Рассинхрон стейта. Redis упал, потеряли половину транскрипций. Без Event Sourcing восстановить невозможно. Читайте подробнее про Event Sourcing.
- Блокировка очередей. Долгая job по транскрипции блокировала весь worker. Решение: отдельный worker для тяжелых задач с низким concurrency.
- LLM-халлюцинации в action.queue. GPT-5 сгенерировал письмо с обещанием бесплатного продукта. Решение: второй LLM (Claude 4) как сентинел проверяет каждое письмо перед отправкой.
- Memory leak при частом создании Job. BullMQ v4 держал ссылки на завершенные задачи. Решение: явно удалять их через removeOnComplete.
Мониторинг: как не пропустить момент, когда агент сходит с ума
В продакшене критично видеть не только RPS, но и качество. Мы используем три метрики:
- Accuracy слепого теста. Раз в час random sample из 10 транскрипций сравниваем с эталоном (человек). Если accuracy падает ниже 85% - алерт.
- Job latency по каждому этапу. 95-й перцентиль для transcription не должен превышать 500ms на минуту аудио.
- Rate limit по action.queue. Не больше двух отправок в день одному контакту. Контролируем через BullMQ rate limiter.
Тестирование: когда мок LLM не спасает
Юнит-тесты мало что дают. Мы пишем интеграционные тесты на каждый процессор, поднимая Redis и BullMQ в Docker. Для LLM используем точные копии промптов с замороженными seed (OpenAI поддерживает seed параметр с GPT-5).
Самый полезный тест - chaos engineering: отключаем Redis, убиваем worker, перегружаем очередь. Так мы нашли баг с зависанием job при недоступности Redis.
❗ Грабли №2: Никогда не тестируйте на реальных клиентах без rate limit. На одной из нагрузок агент отправил 150 писем за час. Хорошо, что промпт был неправильным и письма были пустыми. Но осадочек остался.
Вместо заключения: одна мысль, которая сэкономит вам две недели
Самая частая ошибка начинающих - пытаться сделать агента "умным" сразу. Добавить RAG, долгую память, fine-tune модели. На деле 80% успеха - это надежная архитектура очередей, идемпотентность и грамотный мониторинг. Умный LLM бесполезен, если он кладет прод.
Поэтому мой совет: сначала разверните пустой пайплайн на NestJS + BullMQ, который принимает задачу и просто пишет в лог. Заставьте его работать стабильно под нагрузкой. А уже потом подключайте GPT-5.
И да - никому не рассказывайте, что ваш гениальный агент на самом деле спит три дня в очереди. (шутка)