Як я будував AI Gateway: Архітектура системи
Коли я почав розробляти систему для трьох типів ботів, то з початку це був один проект який я взів за основу і почав переробляти його під конструктор ботів, думав так буде швидше, завжди кажу, що це в останнє не роби більше так. Але буває робиш одне тут, а що якщо спробувати... і вже робиш модуль. А далі думаєш о це заслуговує окремого проекту, клонуєш викидаєш зайве і починаєш доробляти, а потім, а потім ви знаєте що, думаю не тре пояснювати.
Тож мене попросили зробити бота для Телеграм, але до цього будучі на одному з проектів ми стикнулись із завданням реалізувати модуль на ларавел, який генерував би код для обробки певних задач на льоту і зберігався би в кеш, чесно я займався там іншим і не бачив як то реалізували. Тож подумав, а чому б не створити власну версію. Такий собі конструктор ботів. На сайті кодиш handler зберігаєш і він виконується, не тре лізти на сервер не тре ламати систему можна протестувати окремо.
І почав я робити конструктор та конструктор був дещо не простий, він повинен був автооризувати користувача та зберігати його сесію на сервері, одразу зауважу, що у деяких випадках це незаконно та заборонено політикою Телеграм та в тому випадку це було виправдано. І постала задача зробити, щось MTProto в моєму випадку MadelinoProto так і народився USerBot, а далі ідея зробити з цього щось корисне.
Отже у мене мав бути Integration Bot, який мав публікувати пости в канал. Constructor Bot, який мав обробляти користувацькі команди. І User Bot, який мав бути розумним з AI. Кожен мав свою логіку, свої потреби, свої способи спілкуватися з базою даних.
Спочатку все було як спагеті, творчий безлад, тож коли я змінював щось для одного бота, два інші ламалися. Потім я аналізував, пригадував та/чи читав щось про Clean Architecture, думав «а може так тре спробувати», й змінював, вообщем у процесі при ділі так сказать.
Очевидно мені потрібна архітектура, яка зробить три типи ботів незалежними, але зв’язаними через спільну AI систему. Мабуть це звучить як керування хаосом через необізнаність, можливо так але тут творчий зліт, я творю відійдіть і не заважайте.
Clean Architecture — це просто хороший порядок
Clean Architecture — це не якась магія, просто добра організація коду. Як я для себе уявляю, це: чим далі від центру, тим більше залежностей від реального світу.
Уявіть круги:
- Самий центр — ваша бізнес-логіка. Чиста, без залежностей, можна тестувати.
- Наступний круг — правила додатку. Як вони спілкуються.
- Третій круг — база даних, зовнішні сервіси, API.
- Зовнішній круг — інтерфейси, користувачі, те що змінюється частіше за все.
Залежності ідуть від зовні до центру, а не навпаки. Таким чином внутрішній код не знає про базу, про API, про те, який фреймворк використовується.
Це дає свободу можна змінити БД з MySQL на PostgreSQL — внутрішній код не знає. Можна додати Redis для кешування чи поки що прибрати і це не вплине на внутрішній код. Легко тестувати. Звісно я не закликаю робити саме так є і кращі способи, можливо хтось поділиться своїм баченням в коментарях
Як це виглядає у мене?
Є чотири окремих сервіси, кожен в своєму Docker контейнері, кожен зі своєю базою даних й своїми проблемами:
1. Laravel Backend (PHP)
Серце системи. Тут живе вся бізнес-логіка ботів, управління користувачами, сховище даних.
app/ ├── Domain/ ← чиста бізнес-логіка │ ├── Entities/ ← Bot, Message, User │ ├── Repositories/ ← інтерфейси для роботи з даними │ └── Services/ ← бізнес-правила ├── Application/ ← use cases │ ├── Commands/ ← що можна робити │ └── Queries/ ← що можна запитувати ├── Infrastructure/ ← робота з реальним світом │ ├── Repositories/ ← реалізація для MySQL │ ├── Services/ ← інтеграція з зовнішніми API │ └── Jobs/ ← асинхронні завдання └── Interfaces/ ← HTTP контролери, routes
Domain не знає про Laravel, Database не знає про HTTP. Це дозволило мені потім легко додавати нові речі без перестрахування.
2. AI Gateway (Node.js)
Окремий сервіс, який сидить між Laravel й AI провайдерами. Його робота — обрати найкращого AI помічника, побудувати контекст, відправити запит, повернути результат.
Чому окремо?
- Node.js краще обробляє асинхронні операції (не мусиш думати про потоки)
- Якщо Laravel впаде, AI Gateway продовжить працювати
- AI Gateway пможна використати в іншому проекті чи підключити інший проект сюди ж
- Можу запустити декілька інстансів, якщо потрібна більша пропускна спроможність
3. Content Parser (Go)
Мікросервіс на Go, який витягує контент з web сторінок, Reddit, Telegram каналів. Написаний на Go тому що мені потрібна швидкість та мінімальні ресурси, й я хотів спробувати щось нове.
Його робота: беру URL, витягую звідти головне, повертаю JSON. Просто і знов таки можна використати в інших проенктах.
4. React Frontend (TypeScript)
Вебінтерфейс для користувачів. Спілкується з іншими сервісами через Laravel API.
Як вони живуть разом?
Тут приходить цікава частина. У мене не один docker-compose.yml, а три окремих, які спілкуються через shared network. Неочікувано, але це працює. І для розробки цього достатньо
Три окремих docker-compose файли:
1. docker-compose-php83.yml (Laravel + Workers)
services: nginx: # веб сервер php-8: # PHP приложение environment: AI_GATEWAY_URL: http://ai-gateway-service:3002 CONTENT_PARSER_URL: http://content-parser:8080 mysql: # База для ботів redis: # Черга для jobs article-generation-worker: # Окремий контейнер для генерації статей article-publishing-worker: # Окремий контейнер для публікації laravel-cron: # Крон завдання networks: shared_network: external: true # ← Зовнішня мережа для спілкування!
2. docker-compose.yml (AI Gateway)
services: ai-gateway-service: ports: - "3002:3000" ai-database: # PostgreSQL з pgvector ai-redis: # Redis для кешування networks: shared_network: external: true # ← Та ж мережа!
3. docker-compose.yml (Content Parser)
services: content-parser: ports: - "8080:8080" environment: AI_GATEWAY_URL: http://ai-gateway-service:3002 ai-database: # Своя PostgreSQL ai-redis: # Свій Redis networks: shared_network: external: true # ← Та ж мережа знову!
Як це працює на практиці?
Крок 1: Створюємо shared network (робиться один раз)
docker network create shared_network
Крок 2: Запускаємо кожен docker-compose окремо (можна в різних терміналах)
# Terminal 1: AI Gateway cd ai-gateway && docker-compose up # Terminal 2: Content Parser cd content-parser && docker-compose up # Terminal 3: Laravel cd ./ && docker-compose -f docker-compose-php83.yml up
Результат: Три окремих системи в одній мережі shared_network. Вони бачать одна одну через hostname:
- Laravel обіцяет через
http://ai-gateway-service:3002 - Laravel обіцяет через
http://content-parser:8080 - Content Parser обіцяет до AI Gateway через
http://ai-gateway-service:3002
Це дає максимальну гнучкість:
- Запусти тільки AI Gateway для тестування парсера
- Запусти тільки Laravel для розробки фронтенду
- Запусти Content Parser окремо щоб дебагити парсинг
- Запусти все разом для тестування повної системи
На практиці часто запускаю тільки те, що тре змінити — економить час RAM, електроенергію, воду )).
Реальний флоу повідомлення
Щоб зрозуміти, як це працює, давайте відстежимо одне повідомлення від користувача до AI відповіді:
Крок 1: Користувач пише в Telegram
User Bot отримує повідомлення не через webhook від Telegram, бо то для ботів, а через власний метод який запускажться по розкладу та перевіряє, що там до нас прийшло.
Крок 2: Laravel обробляє
Cron → Use Case → Service
Laravel по Cron отримує повідомлення, перевіряє юзера, витягує контекст з бази, готує все для AI Gateway.
Крок 3: Запит іде до AI Gateway
Laravel відправляє HTTP запит до ai-gateway-service:3002/api/generate:
{
"user_id": 12345,
"bot_id": "user-bot-123",
"message": "Привіт! Як справи?",
"context": {
"recent_messages": [...],
"user_facts": [...],
"personality": {...}
}
}
Крок 4: AI Gateway обробляє контекст
- Вибирає найрелевантнішу інформацію (про це в наступній статті)
- Обирає AI провайдера (OpenAI? Google? Claude? чи кого залишилось?)
- Будує промпт
- Відправляє запит
Крок 5: Відповідь повертається
{
"response": "Привіт! Я добре, дякую за запитання!",
"provider": "google",
"tokens_used": 150
}
Крок 6: Laravel зберігає й відправляє
Результат зберігається в базі, відправляється назад боту в Telegram.
Все це — декілька сервісів, які спілкуються через HTTP.
Чому сервіси?
Якби я писав все в одному монолітному PHP додатку, я мав проблеми:
- Масштабування — якщо мені потрібна більше потужності для AI обробки, я мусив масштабувати весь додаток
- Технологія lock-in — якщо я захотів використати Go для парсера (тому що він швидший), мені довелось би переписувати все на PHP
- Падіння системи — якщо AI Gateway впадав, весь додаток зупинявся
- Development — змінюю код AI Gateway, перезапускаю весь Laravel
- Команда розробників — якщо у вас багато розробників, всі вони ходять один одному під ногами в одному монолітному кодбейсі
З мікросервісами:
- Запускаю більше AI Gateway інстансів, якщо потрібна більша пропускна спроможність
- Контент парсер на Go, AI Gateway на Node, backend на PHP — все мішається й (переважно) працює
- Одна частина впадає, інші продовжують роботу
- Змінюю AI Gateway, перезапускаю тільки його контейнер
- Кожен розробник може працювати на своєму мікросервісі або викорисати в іншому місці
Dependency Injection — як вони спілкуються?
Для когось це тригерить аскому, але все ж таки, як Laravel контролер знає, куди послати запит до AI Gateway? Як вона знає адресу, порт, які параметри?
Я передаю залежності через конструктор (Dependency Injection):
class SendMessageCommand {
private $aiGateway;
private $botRepository;
public function __construct(
AIGatewayService $aiGateway,
IBotRepository $botRepository
) {
$this->aiGateway = $aiGateway;
$this->botRepository = $botRepository;
}
public function execute($message) {
$bot = $this->botRepository->find($message->bot_id);
$response = $this->aiGateway->generate($message, $bot);
return $response;
}
}
У .env файлі я вказую:
AI_GATEWAY_URL=http://ai-gateway-service:3002 AI_GATEWAY_TIMEOUT=30
Код не знає де живе AI Gateway. Він просто знає, що є якийсь AIGatewayService, який робить роботу.
Це дозволяє мені:
- Тестувати — замісти реального AI Gateway, я можу передати mock який робить те ж саме, але без інтернету
- Міняти реалізацію — замісти HTTP запиту, я можу використати гря, або Redis queue
- Конфігурувати — змінити URL в
.env— й все переналаштовується без змін коду
Резюме: Як це все скадається
Рівень 1: Clean Architecture (код) Організую код так, щоб внутрішня логіка не знала про зовнішній світ. Domain не залежить від Laravel.
Рівень 2: Мікросервіси (сервіси) Розбиваю систему на чотири незалежних сервіси, кожен з своєю відповідальністю й своєю базою.
Рівень 3: Docker Compose (оркестрація) Все це живе в Docker контейнерах в трьох окремих docker-compose файлах, які спілкуються через shared network.
Результат:
— Модульність — кожна частина робить одне й робить (більш менш) добре
— Масштабованість — додаю нові інстанси без змін коду
— Стійкість — одна частина падає чи йде на техобслуговування, а основна система продовжує працювати
— Тестованість — можу тестувати кожну частину окремо
— Гнучкість — можу міняти технології без переписування
— Командна робота — розробники не заважають
А саме головне, що завтра я можу підєднати туди черговий сервіс (FFmpeg), який буде обробляти відео чи фото, чи Runpod де за допомогою comfyui генерую картинки і це буде легко зробити
Не кажу, що це супер правильно, ні в якому разі не закликаю робити так само, просто ділюсь досвідом, можливо комусь буде цікаво, хтось згадає власні рішення, а когось застереже від помилок.
Наступна стаття: Smart Context — як система розуміє контекст розмови без витрачання всіх грошей на токени.
1 коментар
Додати коментар Підписатись на коментаріВідписатись від коментарів