Создание Telegram-бота кажется тривиальной задачей. Разработчик регистрирует токен у BotFather, настраивает вебхук и пишет первый if ($text === '/start'). Через два часа бот бодро отвечает на десяток жестко заданных команд. Создается иллюзия продуктивности.
К вечеру воскресенья реальность берет свое. Код превращается в неподдерживаемого монстра. Контроллер обрастает лапшой из вложенных условий, проверок базы данных и глобальных состояний. Пользователь отвечает «Да», а бот ломается, потому что потерял контекст и не понимает, к какому вопросу относится согласие. Попытка добавить нейросеть для связного диалога окончательно добивает проект: контексты запросов смешиваются, медленные ответы API обрывают вебхуки по тайм-ауту, отладка превращается в пытку.
Проблема не в Telegram API и не в ограничениях PHP. Проблема в отсутствии архитектуры. Стейт-машина (конечный автомат) и быстрое хранилище контекста — это грань, отделяющая поделку от стабильного продукта.
Проектируем архитектуру
Мы создадим бота на фреймворке Laravel. Цель — построить систему, которая корректно маршрутизирует команды, хранит историю диалогов и общается с пользователем через LLM.
Ключевые компоненты:
- WebhookController: принимает JSON от Telegram и немедленно ставит задачу в очередь.
- Стейт-машина: определяет текущее состояние пользователя (idle, registration, chatting) и направляет запрос в специализированный обработчик.
- Redis: сверхбыстрое хранилище истории сообщений.
- RouterAPI: единый шлюз доступа к нейросетям, интегрированный через пакет
openai-php/client.
Изоляция вебхука и очереди
Телеграм требует, чтобы сервер отвечал на вебхук HTTP-статусом 200 в течение нескольких секунд. Запрос к тяжелой языковой модели легко занимает 5–15 секунд. Если вебхук зависнет, Telegram начнет повторять доставку сообщения. Бот сойдет с ума и заспамит пользователя дублями ответов.
Контроллер обязан быть тонким. Он только принимает данные и передает их воркеру.
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Jobs\ProcessTelegramMessage;
final class TelegramWebhookController extends Controller
{
public function handle(Request $request)
{
$message = $request->input('message');
if ($message && isset($message['chat']['id'], $message['text'])) {
// Мгновенно ставим задачу в очередь
ProcessTelegramMessage::dispatch(
(int) $message['chat']['id'],
$message['text']
);
}
// Отпускаем сервер Telegram
return response->json(['status' => 'ok']);
}
}
Логика переезжает в фоновый процесс, который не боится сетевых задержек.
Диспетчеризация состояний
Воркер извлекает задачу из очереди и передает ее стейт-машине. Вместо парсинга текста регулярными выражениями диспетчер проверяет текущее состояние пользователя.
namespace App\Services\Bot;
use Illuminate\Support\Facades\Redis;
use App\Services\Bot\Handlers\ChatHandler;
use App\Services\Bot\Handlers\IdleHandler;
final class StateDispatcher
{
public function process(int $chatId, string $text): void
{
// Читаем состояние. Если его нет — пользователь в режиме idle
$state = Redis::get("bot:user:{$chatId}:state") ?? 'idle';
$handler = match($state) {
'chatting' => app(ChatHandler::class),
default => app(IdleHandler::class),
};
$handler->handle($chatId, $text);
}
}
Эта структура изолирует контекст. Обработчик ChatHandler уверен, что пользователь ведет диалог с нейросетью. Ему не нужно проверять системные команды регистрации или настройки профиля — они обрабатываются в других классах.
Управление памятью через Redis
Нейросети не обладают встроенной памятью. Чтобы LLM понимала суть беседы, с каждым новым запросом ей необходимо отправлять историю предыдущих сообщений. Реляционная база данных для этого избыточна: слишком много операций записи и чтения на каждый запрос. Redis справляется с этим мгновенно за счет операций над списками (Lists).
Реализуем сервис памяти:
namespace App\Services\Bot;
use Illuminate\Support\Facades\Redis;
final class ChatMemoryService
{
private const MAX_MESSAGES = 20;
public function addMessage(int $chatId, string $role, string $content): void
{
$key = "bot:chat:{$chatId}:messages";
$message = json_encode(['role' => $role, 'content' => $content], JSON_UNESCAPED_UNICODE);
// Добавляем реплику в конец списка
Redis::rpush($key, $message);
// Отрезаем старые сообщения, оставляя только свежий контекст
Redis::ltrim($key, -self::MAX_MESSAGES, -1);
// Обновляем время жизни ключа
Redis::expire($key, 86400);
}
public function getContext(int $chatId): array
{
$key = "bot:chat:{$chatId}:messages";
$raw = Redis::lrange($key, 0, -1);
return array_map(fn($msg) => json_decode($msg, true), $raw);
}
}
Код работает за доли миллисекунды. Приложение всегда имеет актуальный массив реплик, готовый к отправке в API.
Интеграция RouterAPI
Для генерации текста мы применим RouterAPI. Шлюз позволяет обращаться к мощным моделям вроде Claude 3.5 Sonnet или GPT-4o, используя полностью совместимый с OpenAI транспорт. Это освобождает от написания собственных HTTP-клиентов под каждого провайдера.
Устанавливаем официальный пакет: composer require openai-php/client
Настраиваем клиент в сервис-провайдере AppServiceProvider. Главный шаг — переопределить базовый URL и увеличить таймаут, так как ответы сложных моделей могут генерироваться долго.
use OpenAI;
use OpenAI\Client;
public function register: void
{
$this->app->singleton(Client::class, function {
return OpenAI::factory
->withApiKey(config('services.routerapi.key'))
->withBaseUri('https://api.routerapi.net/v1')
->withHttpClient(new \GuzzleHttp\Client(['timeout' => 60.0]))
->make;
});
}
Остается связать компоненты в ChatHandler. Этот класс принимает сообщение, обновляет контекст в Redis, отправляет запрос в нейросеть и возвращает результат пользователю.
namespace App\Services\Bot\Handlers;
use App\Services\Bot\ChatMemoryService;
use OpenAI\Client;
final class ChatHandler
{
public function __construct(
private readonly ChatMemoryService $memory,
private readonly Client $llmClient,
private readonly TelegramApiService $telegram
) {}
public function handle(int $chatId, string $text): void
{
$this->memory->addMessage($chatId, 'user', $text);
$messages = $this->memory->getContext($chatId);
array_unshift($messages, [
'role' => 'system',
'content' => 'Ты — технический ассистент. Отвечай точно и без лишней воды.'
]);
$response = $this->llmClient->chat->create([
'model' => 'claude-3-5-sonnet',
'messages' => $messages,
'max_tokens' => 1500,
]);
$reply = $response->choices[0]->message->content;
$this->memory->addMessage($chatId, 'assistant', $reply);
$this->telegram->sendMessage($chatId, $reply);
}
}
Обратите внимание на чистоту логики. Здесь нет if / else, проверок базы данных или ручной маршрутизации. Класс решает ровно одну задачу.
Итоги
Проектирование системной архитектуры окупается в первые же часы активной разработки. Внедрение очередей предотвращает падение вебхуков, подключение Redis обеспечивает мгновенную работу с памятью диалога, а применение стейт-машины гарантирует изоляцию логики.
Бот перестает забывать контекст диалога. Платформа масштабируется: добавление нового функционала требует написания нового изолированного хэндлера, а не раздувания старого контроллера. Подключение новых LLM через RouterAPI и пакет openai-php сводится к смене конфигурационной строки без вмешательства в транспортный слой.
Выходные подошли к концу. Вместо монолитного легаси-скрипта на сервере развернуто расширяемое production-ready решение.