Содержание
Пролог. Краткость - сестра таланта...
Часть 1. Концепт
Часть 2A. Вайб-кодинг: философия и инструменты
Часть 2B. Вайб-кодинг: практика взаимодействия
Часть 3. Архитектура: первый блин комом
Часть 4. Релиз и фичи
Часть 5. Закат и рассвет
Часть 6. Мультитенантная архитектура
Часть 7: Сценарии — декларативная магия
Часть 8. Эволюция системы плагинов: от модулей к экосистеме
Часть 9. DevOps и развертывание: когда инфраструктура становится удовольствием
Часть 10. Производительность и масштабирование: от одного бота к платформе
Часть 11. LLM, RAG и AI-агенты: от нейросетей к интеллекту
Часть 12. Use Cases: Когда декларативность встречается с реальностью
Эпилог. Путешествие завершено... или только начинается?
Пролог. Краткость - сестра таланта...

Написание больших технических статей — штука коварная. Хочется сделать что-то реально классное: живое, полезное, не банальное, а главное — чтобы оно зашло. А уверенности, что получится, обычно как раз и не хватает. Особенно когда понимаешь, что текст вышел немного... длиннее, чем планировалось изначально. Если средняя статья на Хабре — это 5-10 минут чтения, то здесь можно смело закладывать пару часов. Или даже больше, если не пролистывать.
Но дело вот в чем. Изначально план был простой: написать про эволюцию проекта — от старого ядра для ботов Coreness Legacy до новой мультитенантной платформы. Показать трансформацию, рассказать про архитектурные решения, про то, как вайб-кодинг и работа с LLM изменили весь процесс разработки. Примерно на 20-30 тысяч знаков, ну может чуть больше.
Но потом стало понятно что в такие рамки не уложиться, хочется рассказать про одно, про другое... Не только про Clean Architecture, но и про то, почему она внезапно перестала работать. Не просто упомянуть YAML-конфигурации, а показать, как они стали сердцем всей платформы. Не ограничиться фразой "используем RAG", а объяснить, зачем это вообще нужно и как оно помогает AI-агентам не тупить в диалогах. Добавить DevOps, мультитенантность, производительность, кейсы использования... По итогу удалось раскрыть далеко не всё, что хотелось, но надеюсь хотя бы эта часть окажется увлекательной и интересной.
Можно сказать, что это большой рассказ. Про платформу, про вайб-кодинг, про жизнь с AI-инструментами в реальной разработке. Про путь от "хочу сделать ядро для ботов" до "работает мультитенантная платформа с RAG и десятками ботов в проде". Со всеми ошибками, переделками и выводами.
Но начнем с самого начала — с идеи и концепта.
TL;DR
Его нет. И это не шутка.
Хотя если очень нужна краткая версия, вот варианты:
Открыть содержание выше и прыгнуть сразу к интересующей части (архитектура, вайб-кодинг, мультитенантность, LLM/RAG или кейсы использования)
Скормить эту простыню ChatGPT, Perplexity, Claude или что нынче в моде — и получить саммари за минуту
Но если есть время — лучше прочитать целиком. Обещаю, будет интересно. (но это не точно)
P.S. Тем, кто читает это с мобильного — респект. Серьезно. Закладывайте пару свободных часов (и зарядку телефона поблизости).
P.P.S. Тем, кто решит прочитать за один присест — берите кофе. Много кофе.
Часть 1. Концепт
Изначально концепт был предельно прост — создать универсальное ядро для ботов, чтобы можно было собирать ботов разной сложности легко и быстро, без необходимости каждый раз писать код с нуля. В качестве основы для конфигурации и создания сценариев были выбраны YAML-файлы: удобный формат, понятная структура, достаточно легко читается и редактируется даже без IDE.
Сразу же можно парировать вопросом — а в чем разница с другими сервисами, которые позволяют через веб-интерфейс создавать ботов, имеют уже множество интеграций и всякие красивости из коробки?

Ответ — это коробочные решения с подпиской. А зачем платить за чужое решение, если можно сделать свое. Плюс если нужной функции нет в их наборе — ну извините, ждите следующего релиза (спойлер: не ждите). А развернуть на собственном сервере можно? Нет конечно.
Теперь есть представление о том, что же хочется сделать, но еще нет понимания как это сделать и с чего начать. А начать стоит с выбора стека.
Выбор стека технологий
Ну понятное дело, в качестве основного языка программирования был выбран Python. Кто бы сомневался, правда? Библиотеку для работы с Telegram берем aiogram — сразу заходим в серьезный мир асинхронного программирования. В качестве базы данных возьмем SQLite. Для стартер пака подойдет.
Но выбрать технологии недостаточно, нужно еще понять какую архитектуру использовать, как все это правильно организовать, и тут наступает интересный поворот — а зачем вообще писать код самому, если мы уже живем в эпоху AI?
Первый прототип
В то время понятие вайб-кодинга было мне еще не особо знакомо, но представление об AI-сервисах и агентах уже имелось. Первые несколько скриптов, накиданных с помощью ChatGPT, дали неплохие результаты и базу для старта, но достаточно быстро начались проблемы.
Основное ограничение пришло довольно скоро — потеря контекста. ChatGPT забывал, какие скрипты уже есть в проекте, какая структура, что где лежит. Контекст путался, модель начинала предлагать решения, которые конфликтовали с уже написанным кодом или просто не учитывали общую картину. Оно и понятно — модели для чата не заточены под написание больших объемов кода, они скорее для коротких советов и небольших фрагментов.
Поэтому начал искать более продвинутые инструменты, заточенные именно под разработку. Так я и познакомился с Cursor AI — в то время еще был на версии примерно 1.0, ламповый, простой и, что самое приятное, бесплатный (и такое бывает). Те самые времена, когда можно было спокойно работать без оплаты подписки, а лимиты были скорее теоретическим понятием. Верните мой 2007 2025. ?
И на этом моменте давайте подробнее поговорим про вайб-кодинг и что он из себя представляет. Разберёмся, что это такое на самом деле и почему это изменило весь процесс разработки.
Часть 2A. Вайб-кодинг: философия и инструменты
Думаю, уже только ленивый не слышал понятие вайб-кодинга и не видел шуток на эту тему. Но мемами сыт не будешь, поэтому посмотрим, что за зверь такой вайб-кодинг и чем он отличается от no-code и классического «пишем код ручками».

Стоит начать с того, что многие путают вайб-кодинг с zero-code или no-code. Кажется, что «ну это очередной способ не писать код самому», а значит можно смело проходить мимо. Но не все так просто.
Как ни странно, сами идеи no-code живут уже очень давно: конструкторы, визуальные редакторы, бизнес-процессы мышкой — все это с нами много лет. Вайб-кодинг же родился позже, когда стало понятно, что LLM умеют не только отвечать на вопросы, но и писать вполне рабочий код. Понятие выстрелило после поста Андрея Карпати (сооснователя OpenAI), где он, по сути, показал: можно просто описывать задачу, а код за вас будет писать модель - вам лишь остается просто нажимать кнопку "Принять все".
Zero-code vs Vibe-coding
Если совсем утрировать:
No-code — «делаем, чтобы вообще не видеть код»
Вайб-кодинг — «код есть всегда, просто пишет его не человек»
No-code предлагает собирать приложения и сервисы так, чтобы вообще не писать ни строчки кода. Раньше это было больше про простые сценарии и автоматизации, сейчас туда уже подтянулись AI и сложные пайплайны, можно строить весьма внушительные штуки из блоков и интеграций.
Проблема в том, что за всем этим чаще всего прячется черный ящик:
нельзя просто так залезть под капот и поменять пару строк
сложно сделать что-то нестандартное, чего не предусмотрели авторы
миграция на другую платформу — отдельный квест
Вайб-кодинг, наоборот, держится на том, что код есть, он открыт, его можно читать, менять и рефакторить. Просто вместо того, чтобы писать руками каждую функцию, вы:
формулируете задачу
задаете архитектурные рамки
проверяете результат
направляете модель, где она «поехала не туда»
То есть вы остаетесь владельцем кода и архитектуры, а модель — это очень быстрый исполнитель.
Классический вариант «AI-ассистента в IDE», который делает автодополнение или пишет маленькие подсказки, сюда тоже входит, но это такая лайтовая версия. Это не вайб-кодинг в полном смысле, это скорее удобная подсказка для пусеек для аккуратных разработчиков, которые пока не готовы отдать модели больше власти.
Адвокат дьявола
На этом месте обычно появляется внутренний (или внешний) скептик:
«Сейчас тут навайбкодят, а потом нормальным программистам всё разгребать»
«Это все не работает в реальных проектах»
«Код от моделей — мусор, его придется переписывать»

Частично скепсис понятен. Да, если просто кидать в модель абстрактные запросы вида «сделай мне тут микросервисы» и пушить всё в прод — это действительно хороший рецепт боли.
Но давайте посмотрим на это с другой стороны:
Если можно реализовать идею в разы быстрее, почему бы хотя бы не попробовать?
Если модель может снять с вас рутину, а вы в это время будете думать головой — это же плюс, а не минус.
Если подход правильно выстроен, то «разгребать» как раз будет меньше, а не больше.
Эволюция языков
Хорошая аналогия — эволюция языков программирования.
Когда-то почти всё писали на ассемблере. Потом пришли C и C++, где стало проще, но все еще больно. Сейчас же Python, Java, C#, JavaScript и прочие высокоуровневые языки чувствуют себя отлично, а ассемблер оставили тем, кому действительно нужна низкоуровневая магия.
Почему так произошло?
Высокоуровневые языки быстрее по разработке
Легче в поддержке
Позволяют вносить изменения и выкатывать фичи «на ходу»
Да, они проигрывают в скорости и потреблении ресурсов, но выигрывают во времени выхода фич и скорости итераций. А время разработчика и скорость вывода фич сейчас стоят дороже, чем лишние 10 МБ оперативки и 100 мс времени выполнения.
С вайб-кодингом похожая история:
для драйверов, real-time сложных штук и критически чувствительных систем он не нужен
для огромного количества «обычных» сервисов, бэкендов, API, автоматизаций — более чем уместен
Применимость вайб-кодинга
Если не планируется запускать ракету в космос, то при грамотном подходе вайб-кодинг:
ускоряет разработку в разы
сохраняет контроль над архитектурой и кодом
позволяет проще экспериментировать и менять решения по ходу
No-code прекрасно подходит для быстрых прототипов и простых задач: собрать лендинг, форму, простого бота, внутренний тул. Но до уровня сложной, долгоживущей системы, где важны архитектура, масштабирование, читабельность кода и возможность поддерживать проект годами, ему, мягко говоря, далеко.
Вайб-кодинг как раз про такие системы:
код есть
архитектура есть
ревью есть
просто большинство «рутинных» шагов делает модель
Сервисы для вайб-кодинга
Сейчас уже есть несколько заметных игроков, которые позволяют прочувствовать вайб-кодинг на практике. Каталог представлять не буду, просто пробежимся по ключевым.
Cursor AI
На мой взгляд, один из самых ярких представителей: редактор, который изначально проектировался под работу с целым проектом, а не только с текущим файлом. По сути, форк VS Code, который оброс своими фичами:
понимание структуры проекта
работа с файлами и деревом
умение вносить правки во множество модулей сразу
Идея простая: вы формулируете задачу, модель сама лезет в нужные файлы, читает контекст и предлагает изменения.
GitHub Copilot
Классический старожил. Начинался как ассистент для автодополнения: дописывал строки, предлагал куски кода, помогал с шаблонами. Это было круто, но быстро стало понятно, что этого мало, если хочется «вайб-кодить» целиком проект, а не просто добивать функции.
Со временем туда тоже начали завозить более «агентские» истории и работу с большим контекстом, но старт позиционирования у него был именно как «допиши мне вот тут».
Windsurf
Можно назвать «Cursor у нас дома». Очень похожий подход:
тоже форк VS Code
похожая философия
похожий UX
Если по какой-то причине Cursor AI не зашел или не подходит, Windsurf — один из логичных кандидатов для попробовать.
Claude Code
Интересный зверь: работает из консоли. С одной стороны — гибкость и скриптуемость, с другой — меньше визуальной наглядности и больший порог входа.
Фишка — мощные модели Anthropic под капотом. Но есть важная ремарка: региональные ограничения. В некоторых странах и регионах он просто не доступен, и тут во многом не вопрос вкуса, а скорее «работает/не работает».
Codex от OpenAI
Появился позже остальных и представляет собой полноценную экосистему инструментов от OpenAI: есть CLI-версия для работы из терминала, расширения для IDE (VS Code, Cursor, Windsurf) и облачный агент.
Как и в Claude Code - интегрированы собственные модели от OpenAI для работы с кодом. В остальном имеет схожий с другими сервисами функционал. К сожалению также имеет региональные ограничения на использование в некоторых странах.
Мой выбор: Cursor AI
В итоге выбор пал на Cursor AI. На тот момент у него по сути почти не было прямых конкурентов в своем классе, и уже на первых задачах стало понятно, что:
это не просто «умный автокомплит»
это инструмент, который меняет сам процесс разработки
Но тут хочется сделать важную ремарку.
С одной стороны, есть искреннее восхищение тем, какую революцию ребята сделали: первыми показали полноценный вайб-кодинг, а не очередной ассистент. С другой — моя горящая жопа от их подхода к разработке и продукту:
непрозрачный биллинг токенов
постоянные изменения прайсинга
минимальные или отсутствующие ченджлоги
добавить фичу, которую потом удалят
насыпать багов для остроты ощущений
а когда самостоятельно его починишь, накатить новый апдейт, который сломает фикс, но при этом не починит сам баг
и другие веселые истории и тихие обновления, которые могут внезапно что-нибудь поломать
Иногда это напоминает игру «угадай, что сегодня изменилось», без возможности откатить версию или отключить обновления. Живешь в постоянном напряжении: прилетела плашка «update available» — и ты уже хочешь ее развидеть.
Тем не менее, именно Cursor стал тем инструментом, с которого началось мое погружение в вайб-кодинг и понимание, как с этим всем вообще жить.
Часть 2B. Вайб-кодинг: практика взаимодействия
Развеиваем мифы

Если кажется, что вайб-кодинг выглядит как волшебная пилюля — написал пару слов, и готовый проект сам собой появился — это не так. Вайб-кодинг остается кодингом, просто с другим подходом и набором задач.
Теперь вы не пишете код, но зато:
ревьюите каждое изменение
выстраиваете архитектуру и держите её в голове
следите за документацией (и да, это критически важно — дальше поймёте почему)
пишете правильные промпты, которые дают нужный результат, а не что попало
Звучит просто? На самом деле нет. Нужно научиться понимать, как работают LLM-модели, как они обрабатывают контекст, что может привести к галлюцинациям и как этого избежать.
Условно, писать промпты сейчас — это как правильно гуглить было 20 лет назад. Кто умел искать информацию лучше — тот и был более эффективным разработчиком. С промптами сейчас та же история. Через 5-10 лет это станет нормой для всех.
Принцип работы
Давайте немного заглянем под капот того, как это всё работает изнутри.

Упрощённо схема выглядит так: вы пишете промпт, модель собирает контекст, выявляет намерения (intent), предлагает действия, вносит изменения в код. На практике под капотом идут множественные итерации: обработка промпта, сбор контекста из проекта, анализ, выполнение действий, рефлексия — и снова по кругу.
По итогу это приводит к изменениям в коде, которые, по хорошему, требуют ревью. Не просто «ок, накатили» и забыли, а реально посмотреть, что там модель натворила.
Давайте пройдёмся по ключевым аспектам работы с AI-ассистентом подробнее.
Контекст — ключ к успеху
Пока проект маленький, может казаться, что контекст и документация не имеют большого значения. Это ошибка. Для модели качественный контекст — это залог успеха.
Представьте, насколько сложно разбираться в легаси-коде без документации или выкатывать фичу, когда заказчик не может толком объяснить, что ему нужно. То же самое с моделью: без хорошего контекста она будет угадывать, что имел в виду разработчик, и высокий шанс сделать это не так, как хотелось бы.
С другой стороны, перегрузка контекстом тоже вредит. Особенно если контекст разнородный и слабоструктурированный. Каждая модель имеет своё контекстное окно — лимит на количество токенов, которые она может обработать. Новые модели уже умеют работать с миллионами токенов, но не стоит обольщаться: большое окно не гарантирует качества, а может сыграть злую шутку.
RAG и векторный поиск
Чтобы понять специфику работы, нужно немного погрузиться в RAG (Retrieval-Augmented Generation), векторные базы и семантический поиск. Сейчас пройдёмся очень поверхностно, техническую часть разберём в отдельной главе.
На текущем этапе достаточно понимать: модель берёт ваш промпт, обогащает его последними сообщениями из чата, периодически добавляет информацию из своей базы знаний и других источников. Следовательно, чем дольше диалог, тем больше информации уходит в модель (и тем дороже каждый запрос — за всё нужно платить).
Если в рамках одного диалога делается одна фича, а потом резко переключение на совсем другую задачу, это моментально увеличивает контекст и ухудшает качество результатов. Модель начинает получать «смешанные» данные, что приводит к размытости ответов и решений.

Саммаризация истории
При постоянном увеличении контекста все AI-ассистенты начинают делать саммари предыдущей истории и добавлять его в запросы. Чем дольше идёт диалог, тем более «агрегированные» и размытые данные будут в контексте.
Если думаете, что модель вспомнит, что вы писали ей 10 сообщений назад — скорее всего не вспомнит, а будет иметь только поверхностное представление, если вообще будет.
Также все действия модели сохраняются в памяти, а это достаточно большое количество данных. Поэтому даже огромное контекстное окно имеет свойство быстро «забиваться» и требовать регулярной очистки истории.
Документация и правила
Это частично вытекает из предыдущего аспекта, но стоит разобрать отдельно.
Документация
Это возможность на входе дать модели всю необходимую информацию для быстрого включения в работу и понимания деталей. А ещё это хорошая возможность вам самим потом вспомнить, что вы делали и как это работает. Да и в целом это просто правила хорошего тона.

Документация может и должна быть разноплановая: на проект в целом, на отдельные фичи, архитектуру, процессы и т.п. Чем лучше она структурирована, тем лучше модель понимает специфику работы и справляется со своими задачами. А ещё позволяет вам и коллегам легко погрузиться в детали, вспомнить нюансы и не упустить ничего важного.
И как ни странно, модели отлично умеют писать документацию и структурировать её, потому что обучены на этих же данных, в том числе «под себя». Если раньше было «лень писать документацию», то теперь это лишь вопрос привычки — не забывать при внесении изменений просить модель актуализировать доку. На самом деле это можно зашить прямо в правилах, в некоторых случаях модели сами будут это делать.
Совет: просто возьмите в привычку поддерживать документацию на хорошем уровне. Это окупится очень быстро.
Правила
Почти все современные агенты поддерживают функцию правил (rules). Раньше это была классная и нужная фича, но со временем она ушла на второй план, потому что всё больше системных правил поведения внедряется на уровне сервиса. Они могут конфликтовать с вашими правилами или вовсе их нивелировать.
Из личного опыта: на старте работы с Cursor AI потратил немало времени на попытки оптимизировать правила, научить модель сложным цепочкам и паттернам. Но каждое новое обновление разработчики меняли системные правила, которые конфликтовали с моими или начинали их перекрывать и ломать, ухудшая качество ответов модели.
Например, сейчас Cursor AI умеет самостоятельно:
запускать консольные команды
делать длинные цепочки действий (а не только закрывать одну задачу и останавливаться)
рефлексировать после действий и корректировать свои шаги
Раньше я пытался это воссоздать вручную с переменным успехом, теперь это работает из коробки.
Вывод: Проще сделать простые и базовые правила, не пытаясь изобретать велосипед — разработчики сервисов почти наверняка сделают это лучше вас. Все современные модели прекрасно знают и понимают архитектурные правила и принципы, поэтому нет смысла загонять это повторно и перегружать контекст. Достаточно указать базовые правила взаимодействия и структуру для своего удобства — этого будет достаточно.
Выбор модели
От самой модели зависит успех решаемой задачи. Как ни странно.
На текущий момент существует уже сотни разных моделей. Можно просто брать самые последние и работать на них, но достаточно быстро станет понятно, что это дорого, и как бы удивительно ни казалось — бесполезно и даже вредно.
Типы моделей
Например, при ответе на обычные вопросы или диалог хорошо подходят модели облегчённые или «общего» назначения, например GPT-4o. Но если планируется писать код, лучше подойдут модели с reasoning (размышлениями) и обученные специально под это: o3, Sonnet, DeepSeek-R1, GPT Codex и т.п.
Далее идут модели-гиганты — Opus, Gemini Pro, GPT Pro и подобные, которые могут решить действительно сложную задачу, требующую глубокого анализа логики и разработки технически «тяжёлого» решения.
Также стоит учесть, что новые модели дообучаются на новых данных. Нас волнуют не столько знания «о мире», сколько знания о процессах работы с моделями — они учатся лучше понимать промпты, получают новый функционал, изучают паттерны взаимодействия (например, могут получать и возвращать JSON в строгой форме). По сути, они просто имеют больше релевантных знаний.
Как можно заметить (если следите за трендами и бенчмарками) — новые модели всё больше заточены под программирование и решение инженерных задач в принципе. Наверное, это не просто так. Think about it.
Эволюция моделей

Если в 2023 году выбирать было не из чего, в 2024 было понятно, какие лидеры, то в 2025 году реально конкурентных моделей — полно. Новые появляются уже не раз в квартал, а чуть ли не каждый месяц или неделю, с новыми возможностями и улучшениями.
Тут поможет только опыт взаимодействия, изучение и анализ трендов. Но в большинстве случаев все новые модели в своей ценовой категории примерно одинаково закроют задачу. А ещё в сервисах всё чаще есть «Авто» режимы, где модели выбираются самостоятельно — и вот этого будет хватать почти всегда (как минимум на старте).
Также можно заметить тренд: модели становятся мультимодальными, и не просто так. Чем больше модель имеет представления «о мире» в разной форме (текст, код, изображения), тем лучше она адаптируется под разные задачи. Сервисы стараются двигаться в сторону удобства и давать высокое качество прямо из коробки.
Промпт-инжиниринг
Основная составляющая вашего взаимодействия с AI-ассистентом
Чем лучше научитесь писать промпт без воды и по делу, тем лучше. Обычно агенты выстроены так, что из промпта выделяется один или несколько intents (намерений), что позволяет модели понять, что от неё хотят. Под это может быть сделан краткий промпт и саммари необходимой информации. Чем более чётко и по делу формулируете промпт, тем лучше и качественнее он будет обработан.
При вайб-кодинге нет смысла ждать от модели эмпатии, кричать на неё (писать капсом), шутить, ставить смайлики — всё это создаёт лишний шум, который только мешает.
По большей части умение писать хорошие промпты приходит с опытом, если внимательно смотреть и рефлексировать по результатам взаимодействия. В зависимости от ситуации ваши слова могут иметь разное влияние.
Общий совет: Воспринимайте модель как идеального исполнителя. Чем лучше опишете задачу, тем релевантнее результат. Чем лучше и точнее даёте контекст — тем выше качество.
Мультиагентные системы
Пару слов про мультиагентные системы.
Пока осваивал вайб-кодинг, уже появилось понятие Swarm Agentic Coding и в целом начали появляться системы для создания сложных мультиагентных систем. Представляют из себя систему, где один агент пишет код, второй проводит ревью, третий отвечает за бизнес-контекст и т.п.
На самом деле система достаточно сложная, а сам подход очень новый. А ещё дорогой, потому что требует в разы больше токенов на решение одной задачи. Также есть возможности запускать агентов «в фоне» для решения рутинных задач, но это имеет достаточно сомнительное применение, так как снижает уровень контроля.
В целом эти системы и подходы имеют хороший потенциал, и скорее всего мы к этому придём в будущем. Но пока это требует развития самих сервисов и доработки многих правил, паттернов, потому что это требует очень сложной координации и контроля за качеством. Иначе приводит к созданию «вакуума» и «чёрного ящика», где можно упустить важные моменты и получить бомбу, заложенную в коде.
Процесс разработки: реальность
Собственно, что же из себя будет представлять ваш процесс разработки.

На самом деле всё «просто»: нужно следить за архитектурой проекта, проводить ревью кода, следить за контекстом, обновлять документацию, подрабатывать DevOps, выступать в роли владельца продукта, аналитика, заказчика и, если повезёт, иногда делать себе кофе, пока модель идеально напишет код или решит задачу. ?
Конечно, немного утрирую, но вы должны понимать: вайб-кодинг не предполагает написание кода от слова совсем. Теперь ваша роль — это следить за идеальным исполнителем и правильно его направлять. Да, как ни странно, теперь у вас другие задачи — быть архитектором и ревьюером в первую очередь, что подводит к основной мысли: вайб-кодинг требует абсолютно иного подхода и навыков.
Если вообще не разбираетесь в коде и не имеете опыта в программировании и понимания принципов — это будет сложно. Хоть это и не приговор, но придётся научиться, если хотите получить качественный результат. Также если совсем нет опыта в проектировании систем и понимания архитектуры — при переходе на более сложные проекты начнёте сталкиваться с большими проблемами и не только масштабирования, а в целом взаимодействия, понимания, прозрачности и качества.
Системное мышление моделей
Главная проблема всех текущих моделей (пока что) — они очень плохо умеют в системное мышление.
Они прекрасно умеют писать код, как ни странно — они пишут его идеально. Модель никогда не напишет код, в котором будут опечатки или ошибки. Если правильно попросите, LLM внесёт все изменения, да ещё и корректно подменит все зависимые. Также по умолчанию модели выдают код в схожей структуре — добавляют комментарии, дают хорошую структуру, корректные названия функциям и переменным. То есть код, написанный с помощью модели, имеет очень высокую читаемость и понимание, даже с точки зрения алгоритма работы.
Казалось бы, в чём тогда подвох?
Возвращаемся к системному мышлению — модель прекрасно пишет код, но не всегда хорошо понимает его «логику». Если попросите сделать что-то простое или достаточно «строгое» действие, модель хорошо его реализует: посчитать факториал, написать калькулятор, реализовать регулярки для парсинга email и т.п. Это имеет достаточно хорошую конечную точку «понимания» для модели, она понимает ожидаемый результат, а сам паттерн решения обычно имеет схожую структуру и не требует сложных подходов и «креатива» (да, в программировании тоже есть креатив).
Но если попросите сделать что-то необычное или крайне сложное, где может возникать множество вариантов реализации — модель может пойти любым путём, который ей приглянется, и не всегда это будет то, что нужно. Тут начинает влиять качество контекста, точность задачи и её сложность.
Также модель не всегда может понять, что делает код, если он имеет высокую вложенность, плохо структурирован и/или имеет длинную цепочку действий. Модель не имеет мышления как такового и не видит всей картинки, поэтому если упущена даже маленькая важная часть, модель просто не поймёт, что от неё хотят, и будет трактовать по-своему, а в худшем случае уйдёт в галлюцинации.
Обычно модели действуют достаточно последовательно, поэтому нередко алгоритмы в коде могут быть более «длинными» или громоздкими, без оптимизации. Это даёт более чёткую структуру действий и детерминированность результата. Также иногда модели могут «лениться» или выбирать «лёгкий» путь, не пытаясь разобраться в деталях — тут сильно зависит от контекста и системных правил.
Реальный кейс: три варианта реализации
Вот пример того, как модель может себя вести на реальном кейсе, даже учитывая хороший контекст.

При реализации нового кода с запросами в базу модель может выбрать разные пути:
Вариант 1 (прямой вызов): Вместо того, чтобы использовать мастер-репозиторий, который является оберткой над всеми репозиториями, идёт напрямую в репозиторий тенантов, минуя общий флоу. Это может быть потому, что она внесла правки только что в него и просто упустила, что для остальных подобных решений используется иной флоу. Или просто выбрала «простое» решение без явного указания и контекста.
Вариант 2 (лазейка): Дав ей подсказку, она может пойти уже через мастер-репозиторий, но использовать лазейку и не вызывать метод мастер-репозитория, а использовать репозиторий тенанта напрямую через мастер-репозиторий. Тем самым снова нарушая чистую архитектуру.
Вариант 3 (целевой): В целевом варианте модель уже берёт соответствующий публичный метод мастер-репозитория, оставляя гибкость для дальнейших решений на уровнях ниже.
Пару слов, почему именно такой вариант был нужен — обёртка позволяет более гибко менять репозитории и алгоритмы реализации, никак не меняя способы взаимодействия и вызовы к данным методам.
С точки зрения работы кода — подобные решения никак не влияют на скорость работы или в целом на саму работу кода, но позволяют не накапливать «плохой» код. На самом деле в будущем модель скорее всего легко найдёт эти паттерны и обработает, в этом её отличие от человека. Но чем больше будет подобных решений и наслоений, тем быстрее будет деградировать качество кода и его понимание в будущем, как самой моделью, так и вами как ревьюером и архитектором.
Ну и ещё немного кровь из глаз будет течь, но, возможно, это только моя проблема. ?
Выводы
Следовательно, модель — это очень хороший исполнитель. Чем более простая и рутинная задача, тем лучше модель справится и больше времени сэкономит. Например, банальное переименование функции и небольшие фиксы у модели займут 2-3 минуты, когда у человека могут занять 10-20 минут (и более) в зависимости от объёмов и затрагиваемых зависимых. Где ещё можно по ходу словить опечатку или что-то забыть и получить «скрытую» ошибку и попасть в дебаг на часок-другой.
Скрытые ошибки
Небольшое отступление про «скрытые» ошибки, которые можно получить в неожиданных ситуациях.
При написании кода через вайб-кодинг, если всё делается грамотно, почти никогда такого не случается. Модели пишут ровно то, что им говорят, и если ловится ошибка, то скорее всего она «логическая», а не техническая (ну или промпт кривой).
Если сказали модели написать функцию валидации email, но забыли упомянуть, что email может иметь двойной домен в постфиксе, то это ваша проблема, если регулярка не пропустит данный паттерн. То же касается граничных значений и т.п. — скорее всего они могут быть «допущены» в варианте решения, если это явно не упоминалось.
То есть все подобные проблемы и ошибки — это скорее недостаток контекста или высокой сложности задачи, когда модель может что-то не учесть в силу перегруженности или недостаточных знаний.
Резюмируя: просто написание кода моделью экономит на написании чистого кода не просто N времени, а измеряется кратно в зависимости от задачи. Но это речь только про кодинг, а дальше вступает аспект стека и технологий.
Стек и технологии
Помимо написания кода, модель знает прекрасно все (ну или почти все) современные тренды, стеки, технологии. Следовательно, у неё не будет проблем с реализацией каких-то интеграций, понимания, что от неё хотят, а также она сама может предложить хорошие варианты и идеи, если давать ей соответствующую возможность.
Это не означает, что нужно всегда соглашаться с выводами и рекомендациями, это лишь означает, что у вас появляется отличный источник знаний и релевантных предложений, который может дать всю необходимую информацию для принятия решения за считанные секунды.
Вот вы понимаете, что хотите реализовать функцию работы с кэшами — модель сама предложит варианты: Redis, Memcached, локально и т.п., и если её ласково попросить, то учтёт специфику вашего проекта и даст рекомендации. За вами остаётся лишь дать оценку, выступить в роли архитектора и выбрать наиболее подходящее решение, учитывая весь контекст и видение проекта.
На самом деле это второй момент, который будет экономить время, так как не требуется изучать разные подходы, искать подходящие решения, а также учиться работать с ними напрямую — всё это будет у вас под рукой.
Резюме: роли при вайб-кодинге
Ну и собственно резюмируя блок вайб-кодинга. Так в чём же основная задача и что особенного в нём, и почему нужно заниматься всем понемногу, а не на чилле ждать, пока модель допишет очередные строчки кода? На самом деле можно, но недолго.
На маленьком проекте модель легко закроет почти любые потребности и деградация решения не будет заметна, но чем больше становится проект, тем больше проблем будет возникать. Поэтому давайте рассмотрим основные задачи при вайб-кодинге.
Ревью
Много... очень много... вам придётся проверять ВЕСЬ код, который пишет модель, если этот код идёт в продакшн (но можно «отпустить» написание тестов, каких-то DEV-модулей или кастомных скриптов).
Если хотите получить качественный и, что также важно, оптимальный код — нужно проверять его прямо на ходу и сразу же давать модели контекст, замечания и правки, пока не ушли слишком далеко. Да, это может показаться достаточно «муторным» занятием, но иначе значительно повышаются риски деградации кода. Дальше зависит от качества контекста, промптов, документации, архитектуры и качества самого проекта — чем лучше у вас всё это, тем проще это будет проходить.
Архитектура
Подробнее про разные подходы и выбор обсудим дальше, тут лишь отмечу: в зависимости от выбранной архитектуры проекта будут меняться и подходы к работе с AI. Ошибка с выбором может потом дорого обойтись, так как модели потребуется огромный контекст для внесения изменений, либо в целом проект упрётся в какие-то ограничения и придётся переделывать какую-то часть или даже всё. Тут никто не поможет, только вы решаете и несёте ответственность за принятые решения.
Документация
Нужно регулярно обновлять и поддерживать документацию в актуальном состоянии. Приятная часть — что её не нужно обновлять самому вручную, достаточно правильно и своевременно просить это сделать модель.
Бизнес-контекст
Никто, кроме вас, не знает, что нужно получить «на выходе», поэтому тут вы уже выступаете в роли заказчика и источника контекста. У вас должно быть видение того, что должно получиться в итоге, нужно давать соответствующее ТЗ модели, чтобы она корректно его реализовала. Чем сложнее и больше проект, тем больше контекста приходится держать «в голове». Именно поэтому и стоит поддерживать документацию в актуальном состоянии.
Стек и технологии
Аналогично предыдущему пункту. Только вам решать, какие инструменты использовать, что лучше подойдёт конкретно для вашего проекта, что уже есть, а что планируется. Всё также стоит это отражать в документации, чтобы в том числе этот же контекст можно было легко передать модели в любой момент времени.
Траблшутер
(Да, я из Англии) ??
Если что-то пошло не так, ну что ж, вам придётся решать и разбираться, что с этим делать. Это может быть как поломка всего проекта и кода внесением изменения с упущенной возможности своевременного коммита, неожиданные баги или ошибки, отвалившийся сервер в облаке, случайное разрешение запуска в консоли sudo rm -rf и прочее.
Ну тут можно сказать, что часть этих же проблем нас ждёт не только при вайб-кодинге — верно. Только просто не стоит забывать, что это никуда от нас не делось, и не всегда модель сможет вам «сходу» помочь. Вам самим придётся учиться работать в новых условиях, чтобы эффективно решать возникающие проблемы.
Общие рекомендации: проводить аудит проекта, очень осторожно работать при внедрении обратно несовместимых изменений, не откладывать изменения по качеству проекта, а внедрять сразу же. Чем чище и лучше ваш код и проект — тем проще вам и модели будет с ним работать, поэтому вариант накопления тех. долга плохо сказывается на дальнейшей работе, лучше потратить время и сделать всё сразу.
Финальная мысль
По итогу чем сложнее ваш проект, тем больше и глубже вам придётся погружаться в разные новые роли и аспекты взаимодействия с AI-агентами.
Но приятный бонус в том, что скорее всего на реализацию подобного проекта в одиночку без вайб-кодинга (или AI в целом) вы бы потратили в десятки, а то и сотни раз больше времени, а скорее всего при очень крупных проектах — просто бы не смогли реализовать в силу недостаточности знаний и опыта, который вам позволяет приобрести по ходу движения собственно сам процесс вайб-кодинга.
Вот мы и обсудили современные возможности разработки. Будем надеяться, кто-то ещё остался, ведь это был только разогрев. ?
Часть 3. Архитектура: первый блин комом
Собственно, что же из себя представляли первые версии моего ядра для ботов? Как ни странно, это был не скрипт на коленке (или даже несколько), это были уже полноценные, достаточно «непростые» проекты с десятками скриптов и модулей. Изначально ядро было рассчитано не под хардкод и быстрые фиксы, а под гибкую систему через конфигурации. Следовательно, чтобы выкатить первое решение, потребовались дни плотной работы.
Так как это было начало моего пути, я допустил фатальную ошибку — выбрал в качестве основной архитектуры модульную-слоистую архитектуру, что-то близкое к Clean Architecture.
Казалось бы, неплохой подход. Но проблема проявилась достаточно быстро — для вайб-кодинга это очень «плохое» решение. Модели требуется постоянно взаимодействовать с множеством файлов, переполняется контекст, много связей и зависимостей. Поэтому наступил этап рефлексии, изучения различных архитектурных подходов, тестирования и переработки проекта (и не один раз).
По итогу проект ядра дошёл до смешанной архитектуры на базе event-driven подхода с микросервисной архитектурой на базе вертикальных срезов в рамках общего монолита. И нет, это не просто набор слов, поэтому давайте сейчас уйдём на архитектурную паузу и разберёмся подробнее в этом, а потом поймём почему и как я к этому пришёл.
Что такое архитектура
Возможно, не все понимают, что есть архитектура, поэтому давайте синхронизируемся. Под архитектурой понимается высокоуровневая структура всего проекта, которая определяет, по каким принципам будут взаимодействовать компоненты системы. Можно, конечно, действовать по наитию и вообще не задумываться о таких вещах, но при росте сложности проекта достаточно быстро начинаешь осознавать, что без чёткого плана не обойтись.
Давайте теперь подробнее разберёмся в подходах.
Clean Architecture / Onion

Не сказал бы, что подробно ознакомился с данным подходом и много могу рассказать, поэтому в общих чертах.
Чистая архитектура — это, по сути, способ не дать коду превратиться в «спагетти» (плохо структурированный/хаотичный код). Идея простая: представьте систему как луковицу. В самом центре мы прячем самое ценное — логику того, как работает ваш бизнес (расчёты, правила, формулы). Всё остальное — базы данных, фреймворки и кнопочки интерфейса — уходит на внешние слои. Основной смысл в том, что центр «не в курсе», какие инструменты используются снаружи.
Плюсы:
Гибкость
Масштабируемость
Минусы:
Требует глубокого погружения и осознания
Большое количество файлов, классов и абстракций
Размытые границы ответственности, достаточно сложные пути обработки запросов
Очень сложно обеспечить реальную независимость слоёв
Поэтому подробно тут останавливаться не будем и пойдём дальше.
Vertical Slice Architecture (VSA)

И как неожиданно, дальше пойдёт речь не о микросервисах и иных вариантах, а о вертикальных срезах. Посмотрев со слезами на свой проект, пришло осознание того, что прыгать по слоям и вообще вот это всё мне не хочется и не нравится. Можно же взять и всё «обернуть» в сервис под ключ, и тут нам на помощь приходит VSA.
Вместо того чтобы делить приложение на горизонтальные слои (контроллеры, сервисы, репозитории), мы просто делим его на вертикальные срезы. Каждый срез — это отдельная бизнес-фича (например, «отправить сообщение» или «сохранить пользователя»). Суть в том, что при таком подходе каждый «срез» содержит внутри себя всё, что ему нужно для работы — логику, работу с базой, валидацию и т.п.
Плюсы:
Удобство и гибкость
Обособленность сервисов, низкая связность
Быстрое погружение, лёгкая разработка
Минусы:
Дублирование кода и функций
Можно создать монстра в срезе (слишком много функций и кода)
Проблемы работы с общими данными
Сам по себе подход очень даже удобный и гибкий, хорошо подходит под Agile и, кажется, в последнее время только набирает популярность. Для решения проблемы переиспользования функционала и работы с общими данными используется дальнейшее развитие подхода: общие «срезы» (сервисы) выносятся в слой shared, чтобы переиспользовать общий функционал. Тем самым связность системы растёт незначительно, но уменьшается дублирование кода и улучшается взаимодействие.
Event-Driven Architecture (EDA)

Наверное, здесь уже на уровне общего понимания понятно, что ожидается. Событийная архитектура — это подход, при котором системы общаются через получение событий. То есть каждый сервис подписывается ровно на те события, которые будет обрабатывать, следовательно, сервисы не знают друг о друге и просто обрабатывают свои действия, «вытаскивая» их из общей очереди.
Плюсы:
Практически нулевая связность системы
Высокая масштабируемость
Хорошая отказоустойчивость
Минусы:
Сложная отладка и мониторинг, тяжело разобраться, по какой причине запрос не прошёл ожидаемый путь
Требуется следить за согласованностью и синхронностью действий
Можно попасть в цикл или создать сложные хаотичные цепочки
В целом это достаточно хороший и интересный подход, именно он как раз лёг в основу уже следующей версии ядра, но, конечно же, со своими модификациями и особенностями, которые мы разберём далее.
Микросервисы

Ну про микросервисы наверное только ленивый не слышал. Просто делим систему на множество маленьких сервисов, как правило, работающих через взаимодействие через API или брокеры сообщений. В первой половине 2010-х годов начали активно обсуждать этот подход, а во второй половине начался бум и хайп — все «поехали» в микросервисы (Uber, Airbnb, Spotify, Netflix и др.).
Радость продлилась недолго. Уже в 2020-х годах, видимо, какой-то бухгалтер, сидя дома и со скуки решив ознакомиться со счетами за поддержку DevOps, потерял дар речи. Микросервисы превратились в невероятные огромные сети с сотнями, а то и тысячами точек и целой армией DevOps-инженеров.
Плюсы:
Полная независимость и практически бесконечное масштабирование
Технологическая свобода выбора решений и реализации
Изоляция ошибок, что позволяет сохранять частичную работоспособность
Удобство разработки и гибкость
Минусы:
Проблемы целостности данных — отсутствие понятия «единой транзакции», сложно (невозможно) синхронизировать обновление данных в нескольких сервисах сразу
Надежность системы: большое количество сервисов и систем — больше точек отказа
Операционные сложности мониторинга, деплоя и развёртывания
Дорого... Очень дорого... НЕВЕРОЯТНО ДОРОГО
Помимо требований к поддержке и развёртыванию командой DevOps, это также расходы на облака, трафик и т.п.
Можно сказать, что этот подход подойдёт тем, кто регулярно гоняет со своими друзьями DevOps-ами на феррари отдыхать на дачу на Рублёвке, где в подвале пылятся пару десятков старых серверов Google.
Монолит

Пока микросервисы успешно впаривали ручки топ-менеджерам крупных корпораций, монолит ждал своего часа. Напомню, что по своей сути монолит представляет единый целостный проект в цельной обёртке: просто, строго, понятно.
Плюсы:
Просто разрабатывать, легко деплоить и отлаживать
Высокая скорость работы, минимум накладных расходов
Дёшево... Достаточно просто сэкономить на обедах
Целостность данных, проще делать откат данных и операций
Минусы:
Либо всё работает, либо кто-то с горящим задом ищет ошибку в коде
Тяжело масштабировать, возможно, придётся экономить ещё и на завтраках, чтобы позволить себе вторую копию
Меньше гибкости, единый код, библиотеки и фреймворки
Сложность параллельной разработки, не избежать драки за гаражами между инженерами за право первого коммита
В целом понятное дело, что в нынешнее время уже редко используют монолит в его «классическом» представлении. Последний тренд — это смешанные архитектуры, где как раз монолит и показывает себя в новом свете.
Архитектурный микс
Собственно, кто бы мог подумать, что оказывается, если взять разные архитектуры и смешать вместе, можно получить на выходе неожиданный эффект. Нет — не серебряную пулю, но тот самый подход, в котором проект приобретёт подходящий уровень гибкости и возможностей и при этом «сгладит» минусы разных подходов. Всё-таки это не тот случай, где можно получить всё, не потеряв ничего, но при смешивании разных вариантов подходов можно получить неожиданные результаты.
Поэтому, набравшись знаний и воодушевлённый на новые свершения, ядро было переписано с нуля на новой архитектуре. В этот раз за основу были взяты вертикальные срезы и ивенты. Не совсем в классическом варианте, а с некоторыми изменениями и правками.
Особенности событийной архитектуры
Для ивентов не было брокера сообщений, и взаимодействие между сервисами шло через базу. То есть в базе была единая таблица
actions, которая создавала очередь для всех сервисов, которые её читали.В очередь писал только один сервис, который получал «входной» ивент и разбивал его корректно по разным действиям в зависимости от сценария обработки. Второй сервис был «ассистентом» и разблокировал действия по мере выполнения «цепочки» и переносил «метаинформацию» между действиями для сохранения контекста. Больше никто не мог ничего делать с базой в рамках «вне своего» действия. То есть каждый сервис читает своё действие, готовое к обработке, и возвращает результат, перезаписывая в нём данные, и всё.
Особенности вертикальных срезов
Имели более сложную структуру, чем обычно. То есть это не всегда были «единичные» скрипты до 500-1000 строк, а могли быть более сложные сервисы из нескольких подмодулей.
Очень большое количество
sharedсервисов.В дальнейших итерациях — несколько уровней
sharedсервисов в рамках нескольких групп (слоёв), аналогичное объединение бизнес-сервисов (срезов) также по слоям.
Если кто-то ещё с нами, то предлагаю взглянуть на картинку далее — возможно, она прояснит лучше, как это работало.

В кратце это можно описать так:
Система получает ивент
Он обрабатывается по триггерам, и находятся подходящие сценарии
Сценарий разбивается на ряд шагов, которые кладутся в очередь действий
actionsДействия связываются в цепочку, по мере их выполнения другой сервис разблокирует возможность выполнения следующих действий
Сервисы выполняют свою задачу и записывают результат в базу, как в таблицу действий, так и в иные таблицы
В целом система показала себя очень достойно — низкая связность, высокая устойчивость, хорошая гибкость и масштабируемость. Как вы могли заметить, сервисы не вызывают друг друга и действительно не знают друг о друге ничего, но на самом деле пишут в базу информацию и берут из неё — то есть по своей сути имеют «неявную» связь. Это осознанный шаг, который действительно может приводить к неожиданным сценариям и ошибкам, но с другой стороны буквально легко и дёшево позволяет всем сервисам жить в рамках одних общих данных и полностью исключить асинхронные действия между ними.
Иерархия слоёв
В дальнейшем система была улучшена и доработана — сначала сервисы объединились в группы (слои), а потом shared-сервисы превратились в несколько уровней сервисов, а именно level_0, level_1 и т.д. На самом деле это был очень важный шаг, который заложил основу для создания системы плагинов (но об этом чуть позже).
Система представляет из себя жёсткую иерархию наследования сервисов, а именно следующую цепочку:

То есть полноценный бизнесовый сервис может использовать Core слой (слой основных общих утилит, например работа с базой), далее идёт N слоёв (как правило их было 3), которые содержат в себе сервисы более низких уровней. Например, сервис на уровне level_2 может содержать в себе сервисы уровней 1 и 0 и Foundation. Сервисы слоя Foundation — это основные утилиты, которые не имели никаких зависимостей, то есть полностью независимые функции и методы.
Казалось бы — а зачем так сложно, ради чего всё это? Ну на самом деле всё просто — исключение циклических зависимостей и контролируемый поток зависимых и обработки ивентов. На самом деле это классический подход в дата-инженерии и ETL-процессах, где данные обрабатываются и проходят несколько слоёв для получения итогового результата — в принципе здесь был применён тот же самый подход.
Самые внимательные, и те, кто ещё не уснул, заметили стрелку внутри квадрата Core. Это тоже подход из дата-инженерии, который обозначает возможность внутренних потоков. В целом опасная штука — это означает, что сервисы уровня Core могут использовать другие сервисы уровня Core в своих зависимостях, что может создать циклические зависимости. Но опять-таки, чтобы не создавать слишком большое количество уровней, иногда проще сделать такое допущение, так как это не самый частый случай и достаточно легко отслеживаемый, если этим не злоупотреблять.
Собственно, пройдя большое количество трансформаций, проект наконец-то нащупал устойчивую почву под ногами и начал двигаться более уверенно в этом направлении.
Первые проблемы
На самом деле сложно назвать первые проблемы их как таковыми, потому что это были, по большей части, проблемы гипотетические. Можно заметить, что уже на текущем этапе проект является достаточно устойчивой системой и хорошо масштабируемой, но так уж получилось, что этого было мало, да и останавливаться на достигнутом не хотелось.
Поэтому далее можно выделить следующие направления, которые требовали внимания:
Проблемы масштабирования сервисов (возможных действий)
Деплой системы
Гибкость конфигурации
Давайте разберём каждую из этих частей подробнее.
Нужно больше СЕРВИСОВ
Когда сервисов десяток, ну максимум два — это ещё управляемое и понятное множество, но это не предел. Слоистая архитектура с событиями предполагала, что на каждое действие нужно создавать отдельный сервис плюс ещё множество промежуточных (общих) сервисов, которые могут реализовывать разный функционал: парсинг ивентов, работа с базой, нормализация данных, очистка очередей, логирование, настройки и т.п. Всё это приводит к тому, что количество начинает идти за второй десяток, потом за третий и так далее.
После очередной депрессии рефлексии пришло осознание и решение на будущее — а почему бы не сделать саму систему по схеме плагинов. Периодически бывали мысли, сделать из этого какой-то монетиризируемый сервис или может выложить в опенсорс, и как раз система плагинов бы позволила легко добавлять новые возможности другим разработчикам.
Что такое плагины
Плагины — это, по сути, те же сервисы ранее, только претерпевшие немного изменения концепта и работы с ними.
Первое — плагины разделились на 2 типа: утилиты и сервисы.
Сервисы — это полностью независимые сервисы, которые могут содержать в зависимостях любые утилиты и полностью самодостаточные. Вот тут как раз и выходит на передний план архитектура микросервисов. По своей сути предполагается, что сервисы — это микросервисы в монолите. Каждый сервис самодостаточный и спокойно работает в отрыве от других сервисов. На текущем этапе это ещё не так заметно, но в будущем эта особенность лишь будет усиливаться.
Утилиты — это «мини-сервисы», которые содержат часть общих или повторяющихся функций. Также утилиты могут наследовать другие утилиты.
По своей сути получается, что shared слой (состоящий из подслоёв Foundation, level_0 и т.д.) стал утилитами, а все бизнес-срезы стали просто сервисами. На самом деле может показаться, что вообще ничего не изменилось, но это не совсем так. Был проведён рефакторинг и реорганизация утилит и сервисов, и всё стало выглядеть чуть более просто в рамках флоу, а именно:

Вместо нескольких уровней количество наследования сократилось до трёх уровней — Core → Custom → Foundation, но добавилась возможность внутренних потоков зависимостей в пользовательских слоях. В целом это на дистанции показало достаточно хорошую структуру, так как слои объединялись по бизнес-смыслу и функциям, и потоки оставались управляемыми.
Внутренний контейнер
Но второй важной особенностью стал внутренний контейнер самого ядра. Честно сказать, считаю до сих пор одним из лучших решений, на которое было потрачено с десяток часов.
В кратце — внутренний контейнер управлял всеми зависимостями и при инициализации приложения получал метаинформацию обо всех плагинах:
определял, какие из них нужно запускать
какие зависимости необходимы
проводил инициализацию всех плагинов
запускал все сервисы
То есть не важно, в каких папках, подпапках, в какой структуре лежат плагины, не важно сколько зависимостей, где они находятся. Добавили новый плагин, убрали — всё автоматически соберётся корректно. Если не хватает зависимостей для запуска (удалили утилиту, она отключена и т.п.) — то все зависимые также не будут запущены. Технические детали рассмотрим позднее.
Окей, вот проблема масштабирования функционала решена, теперь хочется сделать так, чтобы сервис можно было взять и легко накатить.
Деплой системы

Недостаточно просто разработать удобный сервис, нужно его где-то развернуть. Не будем же мы держать постоянно включённым свой компьютер с запущенными скриптами. Поэтому далее нужно сделать возможным развёртывание прямо на облачном сервере. Но тут каждый может сказать — да просто возьми и скопируй код да запускай — и правда, в большинстве случаев будет работать, но это как-то несерьёзно, а как вы могли заметить, тут всё серьёзно.
На самом деле подробно про само решение я тут описывать не буду, опишу его в блоке технических решений далее, а сейчас дам просто краткую сводку и подход.
Для деплоя хотелось решить 2 проблемы — это работа с базой (миграция при обновлении), а также простой накат на сервер и развёртывание по запуску одного скрипта. Так вот, развернуть целую систему за счёт скачивания одного скрипта — это, конечно, был интересный челлендж. Для отладки данного решения ушёл не один десяток часов, но по итогу удалось достичь ожидаемого результата.
Можно было скачать всего один питоновский скрипт, и он прямо из коробки:
устанавливал все зависимости
скачивал с GitHub необходимые файлы
обновлял базу
А ну и, конечно, были разработаны уже внутренние скрипты (по сути утилита) для деплоя кода со среды разработки в соответствующие несколько репозиториев в зависимости от сборки ядра. Да, как ни странно, но были влажные мечты монетизировать проект и продавать разные сборки ядра с разным набором плагинов, но что-то пошло не так (никогда такого не было и вот опять).
Ну и собственно весь деплой и накат стал занимать не более 5 минут путём запуска пары скриптов с интерактивным меню.
Пресеты
В целом, хоть система и была достаточно гибкой, то есть, например, в ней присутствовали общие настройки, всё равно настройки на сервере (то есть в прод-среде) отличались от настроек в DEV. Также менять настройки сценариев и прочего было не очень удобно — нужно что-то скопировать, поменять, внести правки. Хоть это и отнимает не так много времени, но, как может уже кто-то догадался, я очень ленивый, как, кстати, и любой хороший программист.

Поэтому, конечно, была разработана система, которая позволяла создавать целые пресеты настроек. Например, есть test-пресет, prod и т.д. Можно было создать пресет нескольких ваших ботов и проверить их работу как в DEV-среде, так и на проде, легко переключаться. Короче, тоже очень удобно и в перспективе сэкономило множество времени.
Вообще, в целом спойлер на будущее — я очень люблю автоматизации и QoL-фишки, поэтому весь проект будет ими «обмазан» максимально.
Что дальше
Да, ещё я тут понял, что вообще не рассказал, как выглядит ядро, что оно из себя представляет, какие фичи и интересные решения там были. Пожалуй, на этом можно заканчивать нашу прелюдию и двигаться к следующей (технической) части.
Часть 4. Релиз и фичи
Для тех кто дочитал до этого блока — поздравляю, теперь переходим к технической реализации, необычным решениям и интересным деталям. Постараюсь не повторять особенности из предыдущей статьи про мой первый пет-проект, а расскажу про концепты и реализацию определенных фич, без углубления в мелкие детали.
Сначала напомню, что из себя представлял проект на тот момент — это готовое ядро для создания автоматизированных решений. Да, во многом был акцент на создание ботов для Telegram, но сама архитектура позволяла создавать любые сервисы, интеграции и сценарии обработки.
Ну а теперь давайте приступать к подробному разбору.
DI-контейнер
При словах о контейнере многие наверное думают про Docker, Kubernetes и т.п., но тут другая история — здесь речь пойдет про DI-контейнер самого приложения прямо внутри кода. Думаю все сталкивались с проблемой, что когда проект начинает разрастаться всё равно требуется переиспользование каких-то методов и функций, а прямой импорт — это путь к боли и страданиям.
Для создания автоматизированного процесса сборки и запуска приложения на помощь приходит DI-контейнер. Тут достаточно простые принципы:
Регистрация зависимостей — контейнер получает всю информацию об интерфейсах и конкретных реализациях (классах)
Определение способа создания экземпляров объектов (как правило через конструкторы)
Разрешение зависимостей — проходит рекурсивно по всем зависимостям и связывает их
Управление жизненным циклом — создает новые копии экземпляров по запросам или возвращает уже существующий, если это Singleton тип (единственный экземпляр)
По своей сути это гарантирует слабую связность объектов между собой, автоматическое разрешение связывания зависимых компонентов, легкий запуск и высокую гибкость.
В качестве примера для аналогии можно сказать так: если бы вы хотели прочитать статью на Хабре, то вам самостоятельно пришлось бы идти в магазин выбрать стол, потом позвонить провайдеру и договориться о подключении интернета, сделать себе кофе и т.п. Дак вот, DI-контейнер можно представить в данной ситуации как магическую кнопку "Подготовить место для чтения статьи", при нажатии которой будет обеспечено всё необходимое, где вам останется просто насладиться процессом. И если что-то изменится и у вас заменится кофе на чай — вам просто выдадут новое.
Singleton, transient и scoped
На самом деле тут всё очень просто: Singleton — это то, что требуется в одном экземпляре, как правило что-то требующее сохранения состояния и синхронизации. Transient — это создание новых экземпляров каждый раз. А Scoped — это создание экземпляров в рамках определенного контекста.
Для чего использовать каждый тип нужно решать исходя из задачи. Например, в рамках примера выше можно сказать, что:
Стол стоит один на весь день (Singleton)
Кофе приносится свежим на каждый запрос (Transient)
Интернет-сессия активна, пока сидишь за столом (Scoped)
Что использовать для каких целей нужно решать исходя из контекста задачи, но есть некоторые неочевидные моменты. На первый взгляд кажется удобным использовать новые экземпляры или просто non-singleton абстракции, но тут есть подвох — каждый раз создание экземпляра будет съедать оперативную память на создание, хранение и обработку. Поэтому если создать в моменте 1000 копий сервиса валидации email, который занимает 1 МБ памяти, то приложение сожрет целый гигабайт, что, думаю, ощутится очень быстро. Поэтому нужно внимательно следить где и как используются объекты, сколько они живут — можно очень легко создать неоптимальный паттерн и перегрузить систему.
На самом деле я бы рекомендовал смотреть в сторону singleton объектов, т.к. это гарантирует сохранение состояний и синхронизацию методов, а также решает проблему утечек памяти. Скорее всего этого будет достаточно для большинства задач и решений.
Важно понимать, что количество экземпляров никак не влияет на скорость работы и обработку очереди, т.к. распараллеливание задач и потоков не имеет никакого отношения к количеству созданных копий и решается на уровне асинхронных методов и мультипроцессинга.
Запуск приложения

В целом разобравшись что он из себя представляет, давайте посмотрим на картинку и разберемся с конкретной реализацией.
С точки зрения концепта всё достаточно просто:
Инициализация основных плагинов — при запуске приложения идет инициализация логгера и плагинов сбора метаинформации о настройках и плагинах из YAML-конфигов. Да, как можно понять, для инициализации самого контейнера и запуска придется делать прямой импорт и явную связь с некоторыми компонентами, но это не выглядит как проблема, т.к. остается неизменным уже много месяцев, да и в случае правок это нужно будет сделать лишь в одном месте.
Сбор информации о плагинах — плагин-менеджер проходится рекурсивно по всем папкам в директории
/plugins→services/utilities→ ... и ищет файлы с названиемconfig.yaml(сами конфиги разберем далее). Как только файл найден — значит мы находимся в "корне" плагина и далее погружаться не нужно. Парсим данные и сохраняем. По сути после прохода по всем папкам мы собираем информацию о всех доступных плагинах и их конфигурации, при этом плагины жестко разделяются на 2 типа: утилиты и сервисы.Обогащение настроек — Settings Manager получает на вход Plugin Manager, чтобы переиспользовать и постобработать собранную информацию, а именно: дообогатить конфигурации глобальными настройками, составить карту зависимостей (по сути функция контейнера скорее, но изначально было вынесено в подмодуль утилиты для возможности переиспользования при необходимости), разрешить переменные окружения и т.п. По сути на выходе мы получаем уже полностью готовые и описанные данные по всем сервисам и настройкам всего приложения.
Инициализация контейнера — контейнер получает все необходимые данные и реализует методы
вытаскивания из шляпыполучения любого плагина по имени, а также их инициализации и запуска.Запуск приложения — происходит запуск всего приложения через запуск всех сервисов.
Остановлюсь на последнем пункте чуть подробнее. Запуск приложения — это по сути простая обертка над контейнером, и логика запуска очень простая: нужно инициализировать все СЕРВИСЫ и запустить параллельно в них методы run(), а дальше уйти в бесконечный цикл ожидания завершения программы.
В чем особенность системы? В том что не нужно включать или выключать (а такая возможность есть) определенные утилиты или сервисы, следить за ними — система автоматически запустит ТОЛЬКО СЕРВИСЫ, не важно сколько утилит для этого потребуется. А если соответствующих утилит не будет в зависимых или они отключены, то сервисы просто не запустятся. Также это гарантирует, что будут запущены только необходимые плагины, не потребляя лишние ресурсы.
Вторая особенность — это метод run(). По сути это позволяет запускать сервисы в фоне и делать это гибко: если нужно, сервис выполняет какую-то работу и дальше уходит в ожидание, или например вообще ничего не выполняет (сидит и ждет пока вызовут его методы по аналогии работы API).
Давайте пару примеров для понимания:
# Пример сервиса с активным методом run()
class ActionUnlockerService:
async def run(self):
"""Регулярный пуллинг базы на новые действия"""
while True:
# Получаем новые заблокированные действия из БД
locked_actions = await self.db.get_locked_actions()
for action in locked_actions:
# Проверяем условия разблокировки
if await self.check_unlock_conditions(action):
await self.unlock_and_execute(action)
# Ждем перед следующим циклом
await asyncio.sleep(self.polling_interval)
# Пример сервиса без метода run() - работает по запросу
class ValidatorService:
def __init__(self, logger, settings):
self.logger = logger
self.settings = settings
# Настраиваем все необходимое при инициализации
self._setup_validators()
# Сервис просто ждет когда вызовут его методы
async def validate_email(self, email: str) -> bool:
"""Валидация email по запросу"""
return self._email_validator.is_valid(email)
async def validate_phone(self, phone: str) -> bool:
"""Валидация телефона по запросу"""
return self._phone_validator.is_valid(phone)
В первом случае можно заметить что сервис запускается и делает регулярный пуллинг базы на новые действия для их обработки. Во втором случае метод run() не требуется, т.к. этот сервис при инициализации настраивает все необходимые параметры и уходит в ожидание вызова своих методов.
Останавливаться еще более подробно не будем и перейдем к следующему блоку — конфигам.
Конфиги и структура проекта
Для удобства работы и гибкости изначально система закладывалась с возможностью вносить правки в настройки — как сценариев и триггеров, так и работы самой системы — не затрагивая код. В качестве основы конфигов были выбраны YAML-файлы, очень удобно и понятно. Далее нужно было проработать саму систему: что и как мы храним, как будем с этим работать, как свести хардкод к минимуму и т.п.
Плагины
Сама структура папки плагинов выглядит следующим образом:
plugins/
├── utilities/
│ ├── foundation/
│ │ ├── logger/ # Foundation утилита
│ │ └── plugins_manager/ # Foundation утилита
│ ├── custom_layers/
│ │ ├── telegram/ # Telegram утилиты
│ │ │ ├── tg_bot_api/ # Telegram Bot API
│ │ │ ├── tg_utils/ # Telegram утилиты
│ │ │ └── tg_permission_manager/ # Telegram права доступа
│ │ ├── api/ # API утилиты
│ │ └── database/ # Database утилиты
│ └── core/
│ ├── event_processor/ # Core утилита
│ └── database_service/ # Core утилита
└── services/
├── base/
│ └── tg_event/ # Сервис
│ └── validator/ # Сервис
└── core/
└── action_unlocker/ # Сервис
Структура каждого плагина выглядит следующим образом:
my_plugin/
├── config.yaml # ОБЯЗАТЕЛЬНО! Без него плагин не будет обнаружен
├── my_plugin.py # Основной код плагина
├── modules/ # Папка подмодулей плагина (опционально)
│ ├── core.py
│ └── validotor.py
└── tests/ # Юнит тесты (опционально)
В этой структуре есть несколько самых важных вещей, давайте их отметим:
Обязательно должен быть файл конфигурации
config.yamlОсновной скрипт содержит ТОЛЬКО один класс, который и будет инициализирован, а название скрипта совпадает с названием папки (на самом деле с названием плагина описанного в конфиге, но лучше не делать рассинхрона)
Все методы для "внешних" взаимодействий лежат в основном скрипте, всё остальное — это на усмотрение реализации плагина и может иметь любую структуру и подходы
Далее давайте погрузимся в сами конфиги, которые являются сердцем каждого плагина и задают его работу. Далее будет представлена облегченная ранняя версия плагина, а более подробную мы рассмотрим позднее.
name: "YourName" # Уникальное имя утилиты или сервиса (обязательно)
description: "Краткое описание утилиты"
singleton: false # (опционально) true если плагин должен быть в единственном экземпляре
dependencies: # (опционально, если утилита использует другие утилиты)
- "logger" # Формат: "utility_name" (уникальное имя утилиты)
- "database" # Система автоматически найдет утилиту по имени
- "cache" # Список необходимых утилит
settings: # (опционально, если есть настройки)
param_name:
type: string
default: "..."
description: "..."
interface: # ОБЯЗАТЕЛЬНО для утилит - публичные методы
methods:
method_name:
description: "..."
input:
param1:
type: ...
description: "..."
output:
type: ...
description: "..."
Возможно теперь картина становится более понятной и начинает проясняться (надеюсь). Особо внимательные читатели наверное заметили, что структура конфигурации содержит информацию о публичных методах, конфигах, описания и т.п., которые по своей сути никак не влияют на работу, но это не спроста.
На самом деле конфиги решают сразу несколько задач — это и настройка плагина, взаимодействия в системе, а также, что немаловажно — служат документацией. Да именно так, спустя столько времени мы снова вернулись к документации. Эта документация позволяет очень легко анпдейтить и получать контекст как самой модели LLM при решении задач, так и вам (как архитектору, ревьюеру и т.п.) легко вникнуть в контекст и вспомнить что тут происходит. Жесткая структура также дает дополнительную информацию и помогает в дальнейшей разработке, а еще дает возможность собирать метаинформацию и генерировать сводную документацию питоновскими скриптами без особых сложностей.
Глобальные конфигурации
Если вы думали, что на этом всё закончится с конфигами, то вынужден вас огорчить — это лишь начало.
Как вы уже заметили, я очень люблю автоматизацию, гибкость и удобство. А я уже упомянул, что ядро разрабатывалось так, чтобы не приходилось лазить в код? Дак вот, нужно сделать так, чтобы не приходилось лазить по проекту в целом, чтобы даже около-IT-специалист легко смог разобраться в системе.
Предположим вам хочется развернуть трех ботов с разной конфигурацией: например один будет отправлять какие-то напоминания, другой бот-помощник, третий — база знаний и т.п. Для этого могут потребоваться разные сценарии обработки действий, но и сама конфигурация настроек системы. В большинстве своем вас устроят базовые настройки, но если это уже полноценное решение и вы хотите выставить оптимальные настройки, то что делать? Верно, нужна возможность переопределения базовых настроек.
И тут мы переходим к блоку общих конфигураций, где структура выглядит следующим образом:
config/
├── settings.yaml # Глобальные настройки (active_preset, общие параметры)
└── presets/
├── default/ # Fallback пресет (используется если указанный пресет не найден)
│ ├── settings.yaml # Локальные настройки пресета
│ ├── triggers.yaml # Триггеры для этого пресета
│ └── scenarios/ # Сценарии для этого пресета
├── test/ # Пресет для тестирования
│ ├── settings.yaml # Локальные настройки пресета
│ ├── triggers.yaml # Триггеры для этого пресета
│ └── scenarios/ # Сценарии для этого пресета
├── dev/ # Пресет разработки (создается при необходимости)
└── prod/ # Пресет продакшна (создается при необходимости)
Ну теперь вы осознаете масштаб проблемы конфигурации. Давайте подробнее рассмотрим что же из себя представляют эти глобальные настройки в файле settings папки config/.
# === УПРАВЛЕНИЕ ПЛАГИНАМИ ===
plugin_management:
# Управление сервисами (основа запуска приложения)
services:
default_enabled: true # Сервисы по умолчанию включены
disabled_services: [] # Список отключенных сервисов
enabled_services: [] # Список включенных сервисов (пустой = все кроме отключенных)
# Управление утилитами (тонкая настройка вспомогательных компонентов)
utilities:
default_enabled: true # Утилиты по умолчанию включены
disabled_utilities: [] # Список отключенных утилит
enabled_utilities: [] # Список принудительно включенных утилит
# === ГЛОБАЛЬНЫЕ НАСТРОЙКИ ===
global:
active_preset: "test" # Текущий активный пресет
file_base_path: "resources" # Базовая папка для всех файлов (вложений, аудио и т.д.)
ssl_certificates_path: "data/ssl_certificates" # Путь к папке с SSL сертификатами
default_ssl_certificate: "russian_certs.pem" # Дефолтный SSL сертификат
enable_global_placeholders: true # Глобальное включение плейсхолдеров для всех действий
# === НАСТРОЙКИ СЕРВИСОВ С ТОКЕНАМИ ===
tg_bot_api:
token: "${TELEGRAM_BOT_TOKEN}" # Токен бота
tg_mtproto:
api_id: "${MTPROTO_API_ID}" # API ID
api_hash: "${MTPROTO_API_HASH}" # API Hash
# === НАСТРОЙКИ РОЛЕЙ И ПРАВ ДОСТУПА ===
tg_permission_manager:
roles:
admin:
description: "Администратор"
permissions:
- "test_roles"
- "test_permissions"
users: [1234567890] # ID (user_id) пользователей
moderator:
description: "Модератор"
permissions:
- "test_roles"
users: [1234567890, 9876543210] # ID (user_id) пользователей
tg_command_registry:
commands:
- command: "start"
description: "Основное меню"
scope: all_private_chats
- command: "help"
description: "Справка"
scope: all_private_chats
commands_clear:
chats: [] # Список чатов для очистки команд (scope: chat)
chat_members: [] # Список пользователей в чатах для очистки команд (scope: chat_member)
# === НАСТРОЙКИ ЛОГГЕРА ===
logger:
console_enabled: true # Управление выводом логов в консоль
file_enabled: false # Управление выводом логов в файл
# === НАСТРОЙКИ ОБРАБОТКИ СОБЫТИЙ ===
tg_event_bot:
max_event_age_seconds: 10 # Глобальный лимит возраста события (сек) до запуска бота для обработки
# === НАСТРОЙКИ СЕРВИСОВ ===
cache_cleaner:
older_than_with_file_hours: 240
older_than_without_file_hours: 2400
database_service:
database_preset: "sqlite"
Да, понимаю, что кажется сложно и не понятно, но попробую быстро и доступно всё объяснить.
Управление плагинами — блок для гибкой настройки используемых утилит и сервисов. Хорошо подойдет чтобы отключать ненужный функционал и экономить ресурсы.
Глобальные настройки — доступны через settings_manager, который собирает все конфигурации, чтобы обеспечить синхронные и одинаковые результаты для всех утилит, если они им потребуются. В основном там лежит папка сохранения и работы с ресурсами (файлами, картинками и т.п.), настройки сертификатов (часть российских сервисов требуют российский сертификат) и глобальная настройка работы плейсхолдеров.
Плейсхолдеры — это... будет сложно в двух словах, но давайте остановимся на том, что они позволяют по универсальной структуре подменять {placeholder} на соответствующие значения. Описать механику в двух словах будет сложно, но для понимания: это позволяет использовать любые данные доступные во всем сценарии, разной степени вложенности, проводить с ними дополнительные операции и т.п. Работает на базе регулярных выражений, рекурсий и т.п., что может потреблять повышенное количество ресурсов. На самом деле эта утилита была множество раз переработана, как и сам процесс ее использования.
Зачем вынесено глобально? Каждое действие (шаг в сценарии) по умолчанию будет пытаться обрабатывать всю конфигурацию шага на нахождение и подмену плейсхолдеров. В целом это очень быстрая операция, учитывая что утилита была оптимизирована до предела, но если вы хотите создать высоконагружаемую систему — есть опция отключить обработку по умолчанию и указывать ее явно в каждом шаге, что даст экономию ресурсов (но на самом деле 99,9% решений и систем это даже не почувствуют).
Настройки сервисов — дальше идет просто перечисление плагинов, для которых будут переопределяться часть настроек: начиная от токенов по умолчанию, команд регистрации для бота, частоты очистки базы, выбора самой базы и т.п. Здесь может быть что угодно. Система достаточно простая: нужно указать название сервиса, его настройку и значение, тем самым она переопределит значение по умолчанию указанное в конфиге самого плагина.
Ну что же, наверное некоторые читатели сейчас догадались, что файлы settings.yaml в пресетах имеют ровно ту же структуру и зачем это надо. Всё просто — чтобы переопределить набор глобальных настроек и используемый набор сценариев и триггеров. Достаточно внести правки в строку active_preset: "test" и готово. На картинке показано как применяется итоговая настройка для конфигурации плагина.

Триггеры
Думаю все примерно понимают, что из себя представляют триггеры — они позволяют обрабатывать получаемые ивенты (telegram-сообщения, коллбеки, запросы API и т.п.) по соответствующим правилам и определять по какому сценарию следует их обрабатывать.
Сама структура выглядит следующим образом:
telegram:
text:
exact:
"/start": "command.start" # Точное совпадение → один сценарий
"/help": "command.help"
"/test": # Точное совпадение → несколько сценариев с условиями
- scenario: "command.test_private"
from_chat: ["private"] # Только для личных чатов
- scenario: "command.test_group"
from_chat: ["group"] # Только для групп
- scenario: "command.test_specific"
from_chat: [123456789] # Только для конкретного чата
state: # Обработка по состоянию пользователя
"feedback": "request_test.request_feedback" # Если пользователь в состоянии "feedback"
regex: # Обработка по регулярным выражениям
"^/start\\s+\\w+\\s+\\w+\\s+\\w+$": "trigger_test.regex_start_with_three_params"
"^/start\\s+\\w+\\s+\\w+$": "trigger_test.regex_start_with_two_params"
starts_with: # Обработка по началу строки
"/admin": "trigger_test.starts_with_admin"
contains: # Обработка по вхождению подстроки
"помощь": "command.help"
"*": # Universal handler - обрабатывает всё что не попало выше
- scenario: "universal_text_handler"
from_chat: ["all"]
continue: true # Продолжить обработку другими триггерами
callback: # Обработка callback-кнопок
exact:
"Назад": "main_menu"
"Подробнее": "show_details"
state:
"awaiting_confirm": "confirm_scenario"
"edit_mode": "edit_scenario"
contains:
"ещё": "more_details"
new_member: # Обработка новых участников группы
default:
- scenario: "group.welcome.welcome_default"
from_chat: ["group"]
group:
"Group": "group.welcome.welcome_group_main"
link:
"https://t.me/+aCdOwvjT5jYyNWUy": "group.welcome.welcome_from_link"
creator:
"@username": "group.welcome.welcome_from_creator"
initiator:
"username": "group.welcome.welcome_added_by_initiator"
Тут на самом деле тоже всё достаточно просто:
Указывается источник ивента, например
telegramДалее идут типы ивента:
text,callback,new_memberПотом уже правило обработки: в основном это поиск по совпадению, вхождению, регуляркам. Особые правила — это состояние пользователя (особый статус который позволяет отлавливать ивенты корректно, например если мы ожидаем ввода текста от пользователя в свободной форме). Ну и дальше можно настроить особые параметры в какие сценарии нужно идти для обработки с определенными правилами, в зависимости от типа чата или его ID например.
Сценарии
И вот мы дошли до бизнес-логики. Сценарии — по сути порядок действий для обработки ивента. Здесь техническая часть уходит на второй план и вперед выступает сама логика.
Как обычно начнем с обзора структуры сценария:
example_scenario: # Название сценария, должно быть уникальным для всех сценариев
actions:
- type: "send" # Тип действия (должен совпадать с action_type в сервисах)
text: "Пример приветствия"
inline: # Inline-клавиатура (опционально)
- ["Кнопка 1", {"Назад": "main_menu"}, "Кнопка 2"]
# "Кнопка 1" и "Кнопка 2" — обычные кнопки (callback_data = нормализованный текст)
# {"Назад": "main_menu"} — явный переход к сценарию main_menu (callback_data = :main_menu)
- ["Кнопка 3"]
- type: "scenario" # Вызов другого сценария (например, системного)
value: "system.api" # Путь к сценарию, который будет развёрнут на этом месте
- type: "send"
text: "Ещё одно сообщение"
reply: # Reply-клавиатура (опционально)
- ["Назад", "Меню"]
callback_edit: true # Редактирование исходного сообщения, которое создало событие
- type: "custom_action" # Можно добавить любые поддерживаемые действия
param1: "value1"
param2: "value2"
- type: "wait_for_reply" # Пример цепного действия (будет заблокировано до завершения предыдущего)
prompt: "Введите ответ"
chain: true # Это действие будет заблокировано (ожидает завершения предыдущего)
- type: "send" # Секретное сообщение только для администраторов с плейсхолдером {...}
text: "? Секретная информация для администраторов! {secret_info}"
required_role: ["admin", "moderator"] # Доступ к ролям admin ИЛИ moderator
chain: [completed] # Это действие будет заблокировано (ожидает упешного завершения предыдущего)
- type: "send" # Сверхсекретное действие только с нужными разрешениями
text: "? Сверхсекретная операция выполнена!"
message_reply: true # Отправить сообщение ответом на исходное
attachment: # Можно добавлять вложения
- "test.png"
- ["file1.pdf","file2.pdf"]
required_permission: ["manage_users", "system_commands"] # Нужны ВСЕ разрешения
Как вы можете заметить, сценарий представляет из себя просто набор последовательных шагов с их атрибутами, которые будут выполняться системой. Не будем подробно останавливаться на всех возможностях, а просто отметим основные особенности:
Можно задавать жесткую цепочку последовательных действий, также "сбрасывать" ее в случае провала (выполнение действий прервется), а также делать "развилки" (то есть одно действие может разветвляться в несколько потоков обработки)
Присутствует возможность ограничения выполнения действия в зависимости от роли пользователя
Сценариев может быть сколько угодно и на самом деле в любой вложенности и количестве в каждом файле, главное чтобы они лежали в исходной директории
scenarios/. Это позволяет самостоятельно выбирать и структурировать сценарии в удобном виде. Главное не дублировать названия сценария, т.к. это является уникальным ключом (но поддерживается возможность указания полного пути к файлу, чтобы избежать конфликта, но это не очень удобно)
DevOps и развертывание
На самом деле система действительно позволяла выдерживать огромный поток данных, где основным ограничением были не ресурсы системы, а системные ограничения пуллинга данных Telegram — до 100 событий в секунду.
Отдельно отмечу, что проблема нагрузки на базу тоже была решена достаточно необычным способом (но разбирать мы это уже не будем) через возможность выбора в качестве базы PostgreSQL, которая была поднята прямо в том же контейнере в котором работает основное приложение (то есть один контейнер на всю систему) и работает из коробки вместе со всем функционалом миграции, работы с базой и т.п. само-собой — мы же тут френдли-сервис делаем.
Вы конечно можете посмотреть на меня как на сумасшедшего — кто в здравом уме будет поднимать Postgres вместе с исполняемым кодом, не считая того, что сам Postgres требует PID 1 в контейнере. Но дайте мне возможность оправдаться.
Изначально уже была настроена система обновления и работы в одном Docker-контейнере еще на базе SQLite. Помимо настройки и установки также по выбираемому названию контейнера создавались команды для удобного управления ядром. Вот пример справки:
Команды
✅ Загружена конфигурация из /root/.dp_config
Command - Универсальное управление Docker контейнерами
Использование: dp [команда] [параметры]
Команды:
install Установить глобальные команды
start Запустить контейнер с текущими настройками
stop Остановить контейнер
restart Перезапустить контейнер
recreate Пересоздать контейнер (для применения конф)
Приложение через PM2:
core create pm2 start main.py --name core
core start pm2 start core
core stop pm2 stop core
core restart pm2 restart core
core recreate pm2 delete core && pm2 start main.py --name core
core reload pm2 reload core
core logs pm2 logs core
core status pm2 status core
core delete pm2 delete core
Системные команды:
dp [args] Работа с базой данных
update Обновление приложения
system [N] Системные логи приложения (N строк)
Управление:
pm2 [args] Прямая работа с PM2
logs Показать логи контейнера
status Показать статус контейнера
shell Запустить shell в контейнере
kill ? Убить ВСЕ Docker процессы
Настройки контейнера:
config show Показать текущие настройки
config memory Установить лимит памяти (512m=512MB, 1g=1GB)
config cpu Установить лимит CPU (0.5=50%, 1.0=100%)
config reset Сбросить все лимиты к дефолтным
help Показать эту справку
Примеры:
Контейнер:
dp start # Запустить контейнер
dp restart # Применить настройки
dp recreate # Пересоздать контейнер
Приложение core через PM2:
dp core create # Создать процесс core
dp core start # Запустить процесс core
dp core restart # Перезапустить процесс
dp core logs # Логи процесса core
dp core recreate # Пересоздать процесс
Системные команды:
dp dp --all --migrate # Миграция БД
dp update # Обновление приложения
dp system 50 # Системные логи (50 строк)
Прямая работа с PM2:
dp pm2 start main.py # Запустить через PM2
dp pm2 list # Список PM2 процессов
Конфигурация:
dp config cpu 1.0 # Установить лимит CPU (100%)
dp config memory 512m cpu 0.5 # Установить оба лимита (512MB, 50%)
dp config reset # Сбросить лимиты
Форматы значений:
Память: 512m (мегабайты), 1g (гигабайты), 2g (2 гигабайта)
CPU: 0.5 (50% от общего), 1.0 (100%), 2.0 (200% - 2 ядра)
Да, это была реально удобная система управления, которая позволяла одной командой обновлять приложение, управлять ресурсами, смотреть логи и делать всё что угодно. И всё это — не залезая внутрь контейнера.
Резюме
Давайте здесь остановимся и подведем итоги по проекту Coreness в его первой релизной версии — что он из себя представлял, какие возможности были.
Особенности и функционал:
Быстрое развертывание — возможность развернуть за 2 минуты полноценное ядро на облачном сервере (а можно и на домашнем ПК)
Гибкие настройки — простая отладка за счет системы пресетов, глубокого логирования
Полный контроль над данными — всё у вас под рукой
Модульность — возможность модификации и добавления функционала не затрагивая текущие сервисы, достаточно лишь создать новый плагин или доработать текущие
Высокая устойчивость — следствие выбранных принципов архитектуры
Способность выдержать высокую нагрузку — следствие тщательного ревью и регулярного аудита, рефакторинга и улучшения кода, вплоть до оптимизации количества транзакций в базу — только то что нужно и когда нужно, и сразу с расчетом на будущее
P.S. На этом заканчивается обсуждении проекта Coreness Legacy. Дальше нас ждет переход к новой эре — Coreness Reborn, но это уже совсем другая история...
Часть 5. Закат и рассвет
После релиза Legacy я написал пару статей на Хабр, получил неплохой отклик и... проект ушел в заморозку. Почему? Все просто: я сделал все, что планировал. Основной функционал работал, мои боты крутились, автоматизация радовала. А продолжать развивать проект без мотивации, без комьюнити, без фидбека от пользователей — это как бежать марафон в одиночку по пустыне. Скучно и бессмысленно.
Последний патч 6.2 вышел, и на этом история Coreness... закончилась. Точнее, история Coreness Legacy.
Триггер возрождения
Несколько месяцев я занимался другими проектами, отдыхал, наблюдал за тем, как меняется мир вокруг. И знаете что? Мир не стоял на месте, а ускорялся:
Что произошло за это время:
LLM-революция продолжилась — GPT-4, Claude 3.5, локальные модели стали мощнее и доступнее
RAG превратился в стандарт — семантический поиск и работа с базами знаний перестали быть экспериментами
AI-агенты вышли в прод — то, что было концептом, начало приносить реальную пользу
Вайб-кодинг укрепился — Cursor, Windsurf, Claude Projects стали привычными инструментами
Спрос на автоматизацию взлетел — все хотят ботов, агентов, ассистентов
И тут меня осенило: Coreness может стать не просто ядром для ботов, а платформой для AI-решений. Не инструмент для одного разработчика, а полноценная мультитенантная платформа, где каждый может создать своего бота или агента.
Амбициозно? Да. Безумно? Есть немного. Но почему бы не попробовать?
От ядра к платформе
Новый план был грандиозным: превратить Legacy из монолита для одного бота в платформу для множества независимых AI-решений.
Было (Legacy):
Один экземпляр приложения = один бот
Одна база данных = одна конфигурация
Один разработчик = локальное развертывание
Ручное управление = SSH и молитвы
Стало (Reborn):
Мультитенантность — множество изолированных ботов на одной платформе
Централизованное управление — все тенанты в одной БД с Row-Level Security
GitHub-синхронизация — Infrastructure as Code для конфигураций
RAG из коробки — pgvector, эмбеддинги, семантический поиск
AI-агенты — не просто боты, а умные ассистенты
SaaS-ready — готовность к масштабированию и монетизации
Звучит как полный редизайн? Потому что так и есть. Примерно 70% кода пришлось переписать с нуля.
Что такое мультитенантность
Для тех, кто слышит этот термин впервые: мультитенантность — это когда одно приложение обслуживает множество независимых клиентов (тенантов), а их данные полностью изолированы друг от друга.
Аналогия: Это как многоквартирный дом vs коттеджный поселок.
Коттеджный поселок (классический подход):
Каждому клиенту — отдельный сервер
Полная изоляция, но дорого
Сложное обслуживание — нужно следить за N серверами
Обновления — накатывай на каждый сервер отдельно
Многоквартирный дом (мультитенантность):
Все живут на одном сервере (в одной БД)
Используют общую инфраструктуру (электричество, вода = connection pool, очереди)
У каждого своя квартира с ключом (изоляция данных)
Сосед не может зайти к вам (Row-Level Security)
Обновления делаются один раз для всех
Экономия ресурсов в разы
Преимущества очевидны: меньше серверов, проще управление, быстрое масштабирование. Но есть нюанс — нужна железобетонная изоляция данных, иначе утечка информации между тенантами приведет к катастрофе.
Вызовы трансформации
Переход к мультитенантности — это не "добавил поле tenant_id в таблицы и готово". Это фундаментальная архитектурная перестройка, которая затронула каждый слой системы.
Что пришлось переосмыслить:
1. Изоляция данных
Каждый тенант видит только свои данные — ни байтом больше
PostgreSQL Row-Level Security как страховка на уровне БД
Защита от "забытого WHERE tenant_id" встроена в саму базу
Логическое разделение тенантов внутри одной инфраструктуры
2. Конфигурации 2.0
Полный отказ от пресетов — больше не нужны, каждый тенант изолирован
Революция триггеров и сценариев — каждый сценарий теперь содержит свои триггеры и полный набор для работы
Настройки тенантов и ботов — появились собственные конфигурации, облегчили глобальные настройки
Плагины адаптировались — формат ближе к API-стилю
Гибкость и удобство выросли на порядок
3. Окружения: тест и прод на одном сервере
Возможность раскатки двух изолированных окружений — staging и production
Два контейнера PostgreSQL на одном сервере (чего стоило это поднять!)
Настройки деплоя, docker-compose с монтированием образов
Система откатов и миграций для обоих окружений
Полная изоляция без дополнительного железа
4. CI/CD и автотестирование
GitHub Actions для автоматизации тестирования и деплоя
Юнит-тесты для критичных компонентов
Интеграционные тесты для проверки взаимодействия систем
Автоматический прогон тестов при каждом коммите
Continuous integration как часть процесса разработки
5. Управление тенантами
Системные тенанты (ID ≤ 100) для разработки и тестирования
Публичные тенанты (ID > 100) с GitHub-синхронизацией
Управление через преднастроеный мастер-бот (аналог @BotFather в телеграм)
Возможность добавить настройку квот и лимитов в будущем (если понадобится)
6. Синхронизация с GitHub
Infrastructure as Code для конфигураций тенантов
Push в репозиторий → автоматическое применение изменений
Версионирование, откаты, возможность code review
Централизованное хранилище всех конфигов
7. Производительность и масштабирование
Оптимизация запросов к БД (composite индексы по
tenant_id)Отказ от aiogram и переход на прямую работу с Telegram API
Переход на вебхуки вместо polling для снижения нагрузки
Кэширование на уровне платформы
Connection pooling для эффективной работы с множеством тенантов
Каждая из этих задач требовала не просто решения, а правильного решения, которое будет работать при масштабировании до сотен тенантов. На это ушли месяцы разработки, десятки рефакторингов и литры кофе пара килограммов кофейных зерен.
Coreness Reborn
И вот, спустя несколько месяцев интенсивной разработки, Coreness Legacy официально стал историей. На его месте появился Coreness Reborn — платформа нового поколения.
Ключевые изменения:
- Монолит для одного бота
+ Мультитенантная платформа для множества ботов
- Локальные конфигурации в одном месте
+ GitHub-синхронизация для каждого тенанта
- Ручное управление через SSH
+ Централизованное управление через систему
- Базовые триггеры и сценарии
+ RAG-система с pgvector и AI-агенты
- Один разработчик, один бот
+ Платформа для создания AI-решений
Проект трансформировался из "удобного инструмента для себя" в полноценную enterprise-ready платформу.
Звучит амбициозно? Безусловно. Но кто сказал, что нужно быть скромным, когда создаешь что-то действительно крутое?

Что дальше?
А дальше — самое интересное. Сейчас мы нырнем в технические детали и разберем:
Как устроена мультитенантная архитектура
Как работает изоляция данных через Row-Level Security
Как эволюционировала система конфигураций
Как GitHub превратился в центральное хранилище тенантов
И многое другое
Если вы думали, что часть про Legacy была технически насыщенной — вы еще ничего не видели, что нас ждет дальше.
Часть 6. Мультитенантная архитектура
Добро пожаловать в новый мир
Coreness Reborn — это не просто новая версия с парой новых фич. Это полное переосмысление того, чем должна быть платформа для создания AI-решений. Если Legacy был надежным автомобилем, то Reborn — это космический корабль с варп-двигателем.
Звучит пафосно? Может быть. Но когда вы увидите что под капотом, поймете почему.
Что осталось от Legacy:
Философия плагинов и событийной архитектуры
Конфигурации в YAML (хотя и серьезно переработанные)
DI-контейнер для управления зависимостями
Маниакальная одержимость автоматизацией и QoL фичами
Что изменилось фундаментально:
От монолита к мультитенантности
От одного набора таблиц к изолированным данным
От пресетов к тенант-специфичным конфигурациям
От "для себя" к "платформа для многих"
Погружаемся в детали.
Три пути мультитенантности
Когда стоишь перед выбором как реализовать мультитенантность, есть три классических подхода. Каждый со своими компромиссами.
Подход #1: Отдельная БД на каждого тенанта
Суть: Тенант A в database_a, тенант B в database_b, и так далее.
Плюсы:
Максимальная изоляция — данные физически разделены
Простые бэкапы — бэкапишь каждого клиента отдельно
Легко мигрировать клиента на другой сервер
Минусы:
Дорого — каждая БД жрет ресурсы (память, диск, CPU)
Сложное управление — следить за N базами данных
Миграции схемы — накатывать на каждую БД
Вердикт: Для enterprise-клиентов с толстым кошельком.
Подход #2: Отдельная схема на каждого тенанта
Суть: Одна БД, но каждому тенанту своя PostgreSQL schema. Тенант A в tenant_a, тенант B в tenant_b.
Плюсы:
Хорошая изоляция на уровне схем
Проще управление чем с отдельными БД
Один connection pool
Минусы:
PostgreSQL имеет лимиты на количество схем
Миграции все равно к каждой схеме
Производительность падает при росте количества схем
Вердикт: Компромисс для среднего количества тенантов.
Подход #3: Одна БД + tenant_id
Суть: Все тенанты в одной базе, в одних таблицах. Каждая запись помечена tenant_id. Изоляция на уровне запросов.
Плюсы:
Максимальная экономия ресурсов
Простые миграции — одна схема
Оптимальный connection pooling
Практически неограниченное количество тенантов
Минусы:
Нужна тщательная изоляция в коде (риск утечки)
Один "тяжелый" тенант может тормозить других
Индексы растут быстрее (composite индексы)
Вердикт: Для SaaS-платформ с большим количеством тенантов.
Выбор: tenant_id + явная изоляция
Для Coreness Reborn был выбран подход #3 — одна БД с явной изоляцией через tenant_id.
Почему именно он?
Масштабируемость — платформа должна поддерживать десятки и сотни тенантов без раздувания инфраструктуры.
Экономика — один мощный сервер вместо десятка слабых. Эффективное использование ресурсов.
Простота DevOps — одна БД проще мониторить, бэкапить, мигрировать. Меньше движущихся частей — меньше проблем.
Гибкость — новый тенант добавляется простой записью в таблицу. Никаких
CREATE DATABASEили схем.
Но это не просто "добавил tenant_id во все таблицы". Это архитектурное решение с несколькими уровнями защиты.
Типы тенантов: системные vs публичные
В Reborn не все тенанты равны. Существует разделение на два типа.
Системные тенанты (ID ≤ 100)
Назначение: Внутренние тенанты для разработки, тестирования, прототипирования.
Особенности:
Управляются локально
Не синхронизируются с GitHub
Для экспериментов, отладки и системных ботов
Примеры:
tenant_1— мастер-бот управления платформойtenant_2— тестовый бот для новых фичtenant_99— staging окружениеtenant_100— также системный тенант (граница включительная)
Публичные тенанты (ID > 100)
Назначение: Продакшн-тенанты, реальные боты пользователей.
Особенности:
Синхронизируются с GitHub — конфигурации в репозитории
Infrastructure as Code — все изменения версионируются в git
Автоматическое обновление — push в репозиторий → изменения применяются за секунды
Структура:
config/tenant/tenant_<ID>/
├── tg_bot.yaml # Конфигурация Telegram бота
├── config.yaml # Атрибуты тенанта (опционально)
├── storage/ # Key-value хранилище (опционально)
└── scenarios/ # YAML сценарии (опционально)
Важно: И системные, и публичные тенанты хранятся в одной папке config/tenant/. Различие только в синхронизации с GitHub.
Изоляция данных: два уровня защиты
Главный вопрос мультитенантности: как гарантировать, что тенант A никогда не увидит данные тенанта B?
В Reborn реализовано два независимых уровня изоляции. Это как швейцарский сыр наоборот — дыр нет, только защита.
Уровень 1: Явная изоляция в коде приложения
Все запросы к базе данных явно фильтруют по tenant_id:
# В репозиториях
async def get_scenarios(self, tenant_id: int) -> list[Scenario]:
stmt = select(Scenario).where(Scenario.tenant_id == tenant_id)
return await self.session.execute(stmt).scalars().all()
Как это работает:
Событие приходит от Telegram (webhook или polling)
EventParserвытаскиваетbot_idиз событияbot_id→tenant_idчерез маппинг в БД (с кэшированием)tenant_idдобавляется вevent['system']['tenant_id']Все репозитории явно фильтруют:
WHERE tenant_id = X
Никакой магии. Никакого "автоматически". Все явно и предсказуемо.
Уровень 2: VIEW-based access control
Второй уровень — для внешних пользователей БД (аналитики, мониторинг владельцев тенантов).
Работает только на PostgreSQL (SQLite — это просто файл, там не применимо).
Идея простая: создаются PostgreSQL VIEW, которые автоматически фильтруют данные по правам доступа.
-- VIEW автоматически фильтрует по view_access
CREATE VIEW v_tenant_user AS
SELECT tu.*
FROM tenant_user tu
WHERE EXISTS (
SELECT 1 FROM view_access va
WHERE va.login = current_user
AND (va.tenant_id = 0 OR va.tenant_id = tu.tenant_id)
);
Таблица view_access:
tenant_id = 0→ доступ ко ВСЕМ тенантам (админ)tenant_id = 123→ доступ только к тенанту 123
Роли:
user— только SELECT на VIEW (ограниченный доступ)admin— SELECT на таблицы + управлениеview_access
Важно: VIEW используются только для внешних пользователей БД, не для кода приложения. Код работает напрямую с таблицами.
Это как две двери в крепость: одна для своих (код приложения), другая для гостей (внешние пользователи). Обе закрыты на разные замки.
Структура базы данных
В мультитенантной архитектуре таблицы делятся на глобальные и тенант-специфичные.
Глобальные таблицы (без tenant_id)
Таких таблиц немного — только реестр тенантов и управление доступами:
-- Реестр тенантов
CREATE TABLE tenant (
id INTEGER PRIMARY KEY,
ai_token TEXT,
processed_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW()
);
-- Управление доступами к VIEW
CREATE TABLE view_access (
login VARCHAR(255) NOT NULL,
tenant_id INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (login, tenant_id)
);
Тенант-специфичные таблицы (с tenant_id)
Почти все остальные таблицы — от ботов до эмбеддингов для RAG:
-- Пример: инвойсы для оплаты звездами Telegram
CREATE TABLE invoice (
id INTEGER PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenant(id),
user_id BIGINT, -- Telegram user_id (64-bit)
title VARCHAR(200) NOT NULL,
description TEXT,
amount INTEGER NOT NULL, -- Количество звезд
link TEXT, -- Ссылка на инвойс
is_cancelled BOOLEAN DEFAULT FALSE,
telegram_payment_charge_id VARCHAR(100),
paid_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
INDEX idx_invoice_tenant_id (tenant_id),
INDEX idx_invoice_user_id (tenant_id, user_id)
);
Ключевые детали:
Composite индексы:
(tenant_id, ...)для быстрых запросовForeign keys:
tenant_id REFERENCES tenant(id)для целостностиVIEW: Автоматически создаются для всех тенант-специфичных таблиц
DI-контейнер: singleton как в Legacy
Вопреки распространенному мифу о scoped контейнерах, в Reborn DI-контейнер остался singleton.
Почему? DI-контейнер управляет плагинами, а не тенантами. Это разные уровни абстракции.
# При старте приложения — ОДИН контейнер
class Application:
async def startup(self):
self.di_container = DIContainer()
await self.di_container.initialize_all_plugins()
Tenant context передается через события:
event = {
'system': {
'tenant_id': 123, # Определяется по bot_id
'bot_id': 456,
},
'data': { ... }
}
# Репозитории принимают tenant_id явно
scenarios = await repo.get_by_tenant(tenant_id=event['system']['tenant_id'])
Логика проста:
DI-контейнер: один на все приложение (управляет плагинами)
Tenant context: передается через события (логическое разделение)
Изоляция: на уровне БД-запросов (явные WHERE)
Никакого scoped контейнера не требуется — тенанты изолированы логически, не физически.
Система конфигураций: 3 уровня + тенант
В Legacy была система пресетов (default, test, prod). В Reborn пресеты полностью исчезли.
Зачем? Каждый тенант теперь изолирован. У него своя конфигурация, свои данные, свои настройки.
Настройки плагинов: 3 уровня
Уровень 1: Defaults — в config.yaml плагина:
# plugins/services/ai_service/config.yaml
settings:
default_model:
type: string
default: "gpt-4o-mini"
temperature:
type: float
default: 0.7
Уровень 2: Global — в config/settings.yaml:
# config/settings.yaml
ai_service:
default_model: "claude-3-5-sonnet-20241022"
temperature: 0.5
Приоритет: Global > Defaults. Settings Manager мерджит автоматически.
Конфигурация тенанта: отдельная система
Файл: config/tenant/tenant_<ID>/config.yaml (опционально)
Хранение: Таблица tenant в БД
# config/tenant/tenant_101/config.yaml
ai_token: "your-token-xxx"
custom_attr: "value"
Доступ в сценариях через _config:
actions:
- type: "completion"
text: "{user_message}"
# Плагин автоматически использует _config.ai_token
Важно: Это две разные системы:
Настройки плагинов (Defaults + Global) — как работают плагины
Конфигурация тенанта (в БД) — атрибуты конкретного тенанта
Как говорится, "not my circus, not my monkeys" ("не мои проблемы") — плагины не лезут в конфигурацию тенантов, тенанты не переопределяют настройки плагинов.
Лайфхак с токенами
Как безопасно хранить токены, не коммитя их в GitHub?
Вариант 1: Использовать tenant_1 — системный тенант с расширенными правами (как @BotFather). Он может управлять другими тенантами, включая установку токенов.
Вариант 2: Закоммитить токен → он сохранится в БД → удалить из файла → закоммитить без токена. Токен остался в истории git, но если репозиторий приватный — для удобства сойдет.
Структура конфигурации тенанта
Каждый тенант имеет папку с четырьмя возможными блоками:
config/tenant/tenant_<ID>/
├── tg_bot.yaml # Конфигурация бота
├── config.yaml # Атрибуты тенанта (опционально)
├── storage/ # Key-value хранилище (опционально)
└── scenarios/ # YAML сценарии (опционально)
Для одного тенанта доступен только один бот Telegram, но архитектура заложена так, что позволяет разные источники событий — хоть несколько ботов, хоть другие платформы.
tg_bot.yaml — конфигурация бота
bot_name: "MyAwesomeBot"
bot_token: "123456:ABC-DEF..." # Опционально
is_active: true
commands:
- command: "start"
description: "Запустить бота"
- command: "help"
description: "Помощь"
config.yaml — атрибуты тенанта
ai_token: "token-api-key-xxx"
custom_settings:
theme: "dark"
language: "ru"
storage/ — key-value хранилище
YAML файлы для динамических данных:
# storage/settings.yaml
user_preferences:
notifications: true
auto_response: false
# storage/cache.yaml
daily_stats:
messages_count: 1523
active_users: 42
Когда использовать: пользовательские настройки, конфигурации, кэш, временное состояние.
Когда НЕ использовать: критичные данные, транзакции.
scenarios/ — бизнес-логика
YAML-сценарии с любой вложенностью:
scenarios/
├── commands/
│ ├── start.yaml
│ └── help.yaml
├── workflows/
│ └── onboarding.yaml
└── ai/
└── chat.yaml
Платформа рекурсивно загружает все .yaml файлы. Можно организовать все как будет удобно.
GitHub-синхронизация: Infrastructure as Code
Одна из killer-фич — автоматическая синхронизация публичных тенантов с GitHub.
Два механизма
1. Webhook (автоматически при push):
Developer → git push → GitHub → webhook → Platform → Tenant Updated
Endpoint:
/webhooks/githubВалидация: HMAC SHA256 через
X-Hub-Signature-256События: Только
pushПроцесс: GitHub → валидация → анализ файлов → синхронизация → graceful restart
2. Polling (периодически):
Интервал: Настраивается через
sync_intervalFallback: Если webhook не настроен
Процесс синхронизации
Создаешь папку
tenant_105/в репозиторииДобавление конфигураций
git pushПлатформа автоматически применяет изменения
Время от push до активации: не более 5 секунд.
Преимущества
Версионирование — вся история в git. Видно кто, когда, что изменил.
Откат —
git revert+git push→ откат к предыдущей версии.Code Review — можно делать Pull Requests для критичных изменений.
Бэкап — конфигурации в безопасности. Сервер сгорел? Восстанавливаем из git.
Прозрачность — весь процесс изменений аудируем.
Telegram Webhooks: вместо пулинга
В Legacy боты работали через polling — бесконечный цикл "спросить у Telegram, есть ли новые сообщения". Работает, но неэффективно и с задержками.
В Reborn — поддержка webhooks. Telegram сам отправляет обновления на сервер:
При запуске бота система генерирует самоподписанный SSL-сертификат
Устанавливает webhook с
secret_tokenдля валидацииTelegram шлёт обновления на
https://your-server:443/webhooks/telegramСервер проверяет токен, находит бота по
secret_token→bot_idОбрабатывает событие
Фишка: Для разных окружений разные порты:
prod: 443 (стандартный HTTPS)
test: 8443 (разрешённый Telegram)
Можно запускать test и prod одновременно на одном сервере — у каждого свой webhook endpoint. Если webhook недоступен (разработка на локалке без публичного IP) — fallback на polling. Также можно всегда выбрать подходящий вариант.
Storage: гибкое key-value хранилище
Не все данные нужно хранить в реляционных таблицах. Для гибкого хранения используется Storage.
Два типа хранилища:
Tenant Storage — данные уровня тенанта (настройки, кэш)
User Storage — данные уровня пользователя (персональные настройки)
Как работает
YAML файлы синхронизируются с БД:
# storage/settings.yaml → group_key = "settings"
settings:
notifications: true
theme: "dark"
В БД:
tenant_id | group_key | key | value
----------|-----------|---------------|-------
101 | settings | notifications | true
101 | settings | theme | "dark"
Использование в сценариях
# Получение
- type: "get_storage"
group_key: "settings"
key: "theme"
# Установка
- type: "set_storage"
group_key: "settings"
key: "theme"
value: "light"
Правило: Настройки и временное состояние → Storage. Критичные данные → таблицы БД.
Резюме: новый фундамент
Переход к мультитенантности — это смена парадигмы:
От "один бот" → к "платформа ботов"
От "локальный проект" → к "масштабируемый сервис"
От "я сам управляю" → к "Infrastructure as Code"
Ключевые решения:
Одна БД + tenant_id — экономия ресурсов, простое масштабирование
Два уровня изоляции — явные WHERE в коде + VIEW для внешних пользователей
Два типа тенантов — системные (локальные) и публичные (GitHub)
Singleton DI-контейнер — управляет плагинами, не тенантами
Отказ от пресетов — каждый тенант изолирован
3 уровня настроек — Defaults + Global для плагинов, отдельная конфигурация тенанта
GitHub-синхронизация — Infrastructure as Code через webhook и polling
Telegram-webhooks - больше возможностей масштабирования и скорости обработки
Storage key-value — гибкое хранилище без схемы БД
Это фундамент, на котором построено все остальное.
Часть 7: Сценарии — декларативная магия
В Legacy всё было разбросано по файлам. В Reborn — один YAML описывает всю историю.
Сценарии в Reborn — это не просто конфигурация. Это декларативное описание поведения, где каждый файл рассказывает полную историю: от триггера до финального действия. Никаких "найди файл X, потом загляни в Y" — всё перед глазами.
От хаоса к порядку
Legacy: файловый квест
triggers.yaml ← когда запускать?
scenarios/ ← что делать?
start.yaml
menu.yaml
Чтобы понять, что делает команда /start, нужно найти триггер, найти сценарий.

Reborn: всё в одном месте
start:
description: "Приветствие пользователя"
trigger:
- event_type: "message"
event_text: "/start"
step:
- action: "send_message"
params:
text: "Привет, {first_name}! ?"
Один файл. Одна история. Триггер → действия → готово.
?️ Анатомия сценария
Минимальная структура
scenario_name:
description: "Что делает сценарий" (опционально)
trigger: # опционально — можно вызвать сценарий напрямую
- event_type: "message"
event_text: "/start"
step: # всегда обязательно
- action: "send_message"
params:
text: "Привет!"
transition:
- action_result: "error"
transition_action: "jump_to_scenario"
transition_value: "default_error"
Три кита:
trigger — условия запуска (опционально)
step — последовательность действий (обязательно)
transition — управление потоком (внутри step, опционально)
Сценарий может вообще не иметь триггеров — он просто вызывается из другого сценария, например через действие или transition.
Триггеры: гибкая система запуска
Триггеры необязательны!
Сценарий может быть:
С триггерами — запускается по событиям
Без триггеров — вызывается программно из других сценариев
Scheduled — запускается по расписанию (вообще отдельная история)
# Сценарий с триггером — запускается автоматически
start:
trigger:
- event_type: "message"
event_text: "/start"
step:
- action: "send_message"
params:
text: "Главное меню"
# Сценарий без триггера — вызывается вручную
show_details:
# Нет trigger — запускается только напрямую из других сценариев
step:
- action: "send_message"
params:
text: "Детали..."
Как работают триггеры
Триггер — это набор условий. Каждый триггер может содержать:
Простые атрибуты —
event_type,event_text,callback_dataСложные условия — через поле
condition
Логика:
Между триггерами — ИЛИ (любой сработал → запуск)
Внутри триггера — И (все условия выполнены → сработал)
trigger:
# Триггер 1: команда /start
- event_type: "message"
event_text: "/start"
# Триггер 2: callback от кнопки
- event_type: "callback"
callback_data: "menu_main"
# Триггер 3: сложное условие
- event_type: "message"
condition: |
$event_text ~ "привет"
and $user_id > 100
Сработает, если:
Пришла команда
/startИЛИНажата кнопка с
callback_data: "menu_main"ИЛИСообщение содержит "привет" И user_id > 100
Операторы в условиях
condition: |
$event_text == "/start" # точное совпадение
$event_text ~ "привет" # содержит подстроку
$user_id > 100 # сравнение чисел
$username in ('@admin', '@mod') # вхождение в список
$event_text regex "^[0-9]+$" # regex
$user_state is_null # пустое значение
Scheduled сценарии
Отдельная фича — сценарии по расписанию:
daily_report:
description: "Ежедневный отчет в 9:00"
schedule: "0 9 * * *" # cron выражение
# Триггеры опциональны — сценарий запустится по расписанию
step:
- action: "send_message"
params:
target_chat_id: 123456789 # явно указываем чат
text: "? Доброе утро! Отчет за {scheduled_at|format:date}"
Важно: В scheduled сценариях нет данных с получаемых ивентов (например chat_id), поэтому нужно передавать их явно.
? Шаги и действия
Последовательное выполнение
Шаги выполняются строго по порядку, один за другим:
step:
# Шаг 1
- action: "send_message"
params:
text: "Привет!"
# Шаг 2
- action: "send_message"
params:
text: "Как дела?"
# Шаг 3
- action: "send_message"
params:
text: "Выбери действие:"
inline:
- [{"Меню": "menu"}]
Каждый шаг — это действие (action) + параметры (params).
Переходы: управление потоком
Внутри каждого шага можно указать transition — что делать после выполнения:
step:
- action: "validate"
params:
condition: "$user_role == 'admin'"
transition:
- action_result: "success"
transition_action: "jump_to_scenario"
transition_value: "admin_panel"
- action_result: "error"
transition_action: "continue" # продолжить обычный поток
Типы переходов:
continue— продолжить выполнениеbreak— прервать текущий сценарийabort— прервать всю цепочку (включая вложенные)stop— прервать обработку всего событияjump_to_scenario— перейти к другому сценариюjump_to_step— перейти к шагу по индексуmove_steps— сдвинуться на N шагов
Плейсхолдеры: динамика в статике
Всё подставляется автоматически
Все параметры в params обрабатываются через плейсхолдеры:
params:
text: "Привет, {first_name}!"
chat_id: "{chat_id}"
user_data: "{user_id}_{username}"
Примеры доступных данных
Из события:
{user_id} # ID пользователя
{first_name} # Имя
{username} # Username
{event_text} # Текст сообщения
{callback_data} # Данные callback
{message_id} # ID сообщения
{chat_id} # ID чата
Из конфигурации:
{_config.ai_token} # Токен AI из config.yaml
{_config.admin_chat_id} # Любые атрибуты тенанта
Из storage:
{_storage.settings.theme} # Tenant storage
{_user_storage.points} # User storage
Из предыдущих шагов:
{_cache.last_message_id} # ID последнего сообщения
{_cache.value} # Данные из предыдущего действия
Модификаторы
{username|fallback:Гость} # Значение по умолчанию
{event_text|upper} # В верхний регистр
{event_text|lower} # В нижний регистр
{price|*0.9|format:currency} # Умножить и форматировать
{event_text|regex:^([^\s]+)|lower} # regex + модификатор
? Примеры от простого к мощному
1. Простое приветствие
start:
trigger:
- event_type: "message"
event_text: "/start"
step:
- action: "send_message"
params:
text: "Привет, {first_name|fallback:друг}! ?"
inline:
- [{"? Меню": "menu"}, {"ℹ️ Помощь": "help"}]
2. Сценарий с условиями
check_access:
trigger:
- event_type: "callback"
callback_data: "admin_panel"
step:
# Проверяем права
- action: "validate"
params:
condition: "$user_role == 'admin'"
transition:
- action_result: "success"
transition_action: "jump_to_scenario"
transition_value: "admin_dashboard"
- action_result: "error"
transition_action: "jump_to_scenario"
transition_value: "access_denied"
3. AI-чат с RAG
ai_chat:
trigger:
- event_type: "message"
condition: "$event_text not is_null"
step:
# 1. Поиск в базе знаний
- action: "search_embedding"
params:
query: "{event_text}"
limit: 3
# 2. AI ответ с контекстом
- action: "completion"
params:
model: "openai/gpt-4o"
rag_chunks: # автоматически формируются messages из найденных чанков
- chunk_type: "knowledge"
limit: 3
messages:
- role: "user"
content: "{event_text}"
temperature: 0.7
# 3. Отправка ответа
- action: "send_message"
params:
text: "{response_data.content}"
4. Scheduled задача
morning_digest:
description: "Утренняя рассылка новостей"
schedule: "0 9 * * *" # каждый день в 9:00
step:
- action: "completion"
params:
model: "openai/gpt-4o"
system_prompt: "Составь краткий дайджест новостей"
prompt: "Новости за {scheduled_at|format:date}"
- action: "send_message"
params:
target_chat_id: "{_config.admin_chat_id}"
text: "{response_data.content}"
? Legacy vs Reborn
Аспект |
Legacy |
Reborn |
|---|---|---|
Где логика? |
Разбросана по 3+ файлам |
Один файл |
Триггеры |
Отдельный файл |
Часть сценария (или нет) |
Условия |
Сложные в каждом сервисе |
Общая логика процессором обработки |
RAG |
Отсутвует |
Нативная поддержка |
Scheduled |
Отсутствует |
Встроенная фича |
Часть 8. Эволюция системы плагинов: от модулей к экосистеме
Плагины 2.0: не просто улучшение, а философия
Помните, как в Legacy плагины были "удобным способом организовать код"? Рекурсивное сканирование папок, config.yaml, автоматическая регистрация — всё работало, все работало неплохо.
А в Reborn всё стало еще лучше. Плагины стали фундаментом платформы. Не просто способом структурировать код, а полноценной экосистемой, где каждый компонент — самодостаточный кирпичик.
Что осталось прежним:
Концепция утилит и сервисов
Автоматическое обнаружение через
config.yamlDI-контейнер для управления зависимостями
Иерархия слоёв (Foundation → Custom Layers → Core → Services)
Централизованное управление
Что стало лучше:
Optional dependencies — гибкость без жёстких требований
Access rules — безопасность на уровне конфигурации
Стандартизированные ответы — единый формат для всех
Public API — явный контроль доступности
Автогенерация документации — конфиг = документация
Разберемся на примерах.
Было vs Стало: config.yaml эволюционирует
Было (Legacy)
name: "notifications"
description: "Сервис уведомлений"
dependencies:
- "logger"
- "tg_bot_api" # ❌ Бот ОБЯЗАН быть настроен
- "email_service" # ❌ Email ОБЯЗАН быть настроен
Простая структура, но с проблемой: если бы хотелось запустить уведомления без email — облом. Плагин просто не запустится.
Стало (Reborn)
name: "notifications"
description: "Сервис уведомлений с гибкой конфигурацией"
dependencies:
- "logger" # Обязательно
optional_dependencies:
- "tg_bot_api" # Опционально — если есть, используем
- "email_service" # Опционально — если нет, не беда
Теперь плагин gracefully degraded — работает с тем, что есть.
В коде
class NotificationsService:
def __init__(self, **kwargs):
self.logger = kwargs['logger']
self.tg_bot = kwargs.get('tg_bot_api') # Может быть None
self.email = kwargs.get('email_service') # Может быть None
async def send(self, message):
# Пробуем Telegram
if self.tg_bot:
return await self.tg_bot.send_message(message)
# Пробуем Email
if self.email:
return await self.email.send(message)
# Fallback — логируем
self.logger.info(f"Notification: {message}")
Access rules: когда безопасность в конфиге
Представьте: пользовательский сценарий случайно вызывает delete_tenant и... ой. Нет тенанта, нет проблем (и нет данных).
В Reborn это декларативно в конфиге:
actions:
delete_tenant:
description: "Удаление тенанта"
access_rules: ["system_access"] # ? Система проверит автоматически проверяет правила доступов
public: false # Недоступно в пользовательских сценариях и не попадает в бизнес-документацию
Попытка вызвать из сценария → автоматический AccessDenied. Нельзя забыть проверить права, потому что проверка вшита в платформу.
Уровни доступа
system_access — системные операции:
Создание/удаление тенантов
Управление ботами
Изменение глобальных настроек
data_integrity — операции с данными:
Отправка сообщений в Telegram
CRUD в базе данных
Валидация пользовательского ввода
no_access — без ограничений:
Обработка событий
Чтение публичных данных
Вспомогательные утилиты
Фишка: всё прозрачно. Открываем config.yaml → сразу видно, какие действия критичны. И также легко добавлять новые правила.
Стандартизированные ответы
Единый формат для всех сервисов:
output:
result: # Обязательное поле
type: string
description: "success, error, timeout, not_found"
error: # Опционально
type: object
properties:
code: string # Код ошибки
message: string # Описание
details: array # Детали ошибки
response_data: # Опционально
type: object
properties: ... # Полезные данные
Примеры ответов
Успех:
{
"result": "success",
"response_data": {
"user_id": 123,
"username": "john_doe"
}
}
Ошибка валидации:
{
"result": "error",
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid email format",
"details": ["Field 'email' is required"]
}
}
Красота! Всегда понятно что произошло, всегда единообразная обработка.
Автогенерация документации
Вот у нас исходный конфиг:
actions:
send_message:
description: "Отправка сообщения пользователю"
access_rules: ["data_integrity"]
public: true
input:
data:
type: object
properties:
text:
type: string
description: "Текст сообщения"
chat_id:
type: integer
optional: true
description: "ID чата (если не указан — текущий)"
output:
result:
type: string
description: "Результат: success, error"
response_data:
type: object
optional: true
properties:
message_id:
type: integer
description: "ID отправленного сообщения"
Получаем готовую документацию:
# ACTION_GUIDE.md
## send_message
Описание: Отправка сообщения пользователю
Входные параметры:
- text (string) — Текст сообщения
- chat_id (integer) — ID чата (если не указан — текущий)
Выходыне параметры:
- result (string) - Результат: success, error
- response_data (object) — Данные ответа
- message_id (integer) - ID отправленного сообщения
И всё! Запускаешь скрипт генерации → получаешь полную документацию по всем действиям. Изменил конфиг → документация автоматически обновилась.
Бонус: LLM при генерации сценариев получает актуальную документацию прямо из конфигов.
Public флаг: не всё для всех
Некоторые действия должны быть только системными и недоступны из общей (бизнес) документации.
actions:
send_message:
public: true # ✅ Доступно в пользовательских сценариях и документации
internal_cleanup:
public: false # ❌ Только системные (внутренние) вызовы
Попытка использовать internal_cleanup в пользовательском сценарии → ошибка на этапе валидации.
Часть 9. DevOps и развертывание: когда инфраструктура становится удовольствием
От "а вдруг что-то сломается" к "обновляюсь и не парюсь"
Снова вспоминаем Legacy, там был один скрипт деплоя, Docker контейнер, PM2 для управления процессом. Работало? Да. А теперь работает еще лучше.
Философия проста: Если что-то делается дважды — автоматизировать. Если что-то делается один раз, но это критично — тоже автоматизировать. А потом автоматизировать автоматизацию. ?
Deployment Manager: один скрипт для всего
Интерактивное меню:
? Деплой в репозитории
? Обновление сервера
?️ Работа с БД
⏪ Откат Docker образа
? Очистка старых образов
?️ Удаление окружения

Всё в одном месте. Выбрал пункт, ответил на пару вопросов — готово.
Пример обновления сервера:
Выбираешь пункт 2, система спрашивает окружение (test/prod) — и дальше магия:
Клонирует нужную ветку из GitHub
Определяет версию из git tag
Делает бэкап БД (на всякий случай)
Применяет миграции (автоматически)
Обновляет файлы (кроме тех, что в исключениях)
Собирает новый Docker образ с версией
Перезапускает контейнеры (gracefully, без потери данных)
Время от запуска до готовности? 2-5 минут. И это включая миграции БД.
Docker Compose и команда dc: управление как в игре
В Legacy был один контейнер со всем внутри. В Reborn — multi-container setup: приложение, PostgreSQL. Каждый сервис в своём контейнере, каждый живёт своей жизнью.
Но управлять кучей контейнеров через docker-compose -f ... -f ... — это боль. Поэтому появилась команда dc:
dc start # Запуск всех сервисов
dc stop # Остановка
dc logs # Просмотр логов (с автоподстановкой сервиса)
dc shell app # Shell в контейнере приложения
dc stats # Кто сколько ресурсов жрёт
Внутри контейнера приложения работает Supervisor, управляющий процессами. И для него тоже есть команды:
dc sv status # Статус всех процессов
dc sv restart core # Перезапуск процесса core
dc sv logs core # Логи процесса core (без лезания в контейнер)
Фишка: Все compose-файлы лежат в глобальной директории ~/.docker-compose/. Это значит, что ты можешь запускать test и prod окружения на одном сервере одновременно, и команда dc сама разберётся, с чем работать, в зависимости от текущей директории.
Установка? При накате обновления автоматически, а также через dc install, если нужно обновить только команды. Работает из любой директории проекта.
Миграции БД: когда изменения схемы не пугают
Один из самых стрессовых моментов в разработке — миграции БД. Особенно в продакшне. Особенно когда у тебя мультитенантность и нельзя допустить, чтобы данные одного тенанта утекли к другому.
В Reborn миграции двухуровневые
Уровень 1: Универсальные миграции
Система сама понимает, что изменилось в схеме БД. Сравнивает текущую структуру с кодом (модели) и генерирует миграции автоматически:
Создание/удаление таблиц
Добавление/удаление колонок
Изменение типов данных
Пересоздание индексов (если структура изменилась)
Валидация JSON полей (для tenant_storage и других)
Всё в транзакции. Если что-то пошло не так — откат. Данные целы, можно спать спокойно.
Важная деталь: VIEW для access control удаляются перед миграцией и пересоздаются после, потому что PostgreSQL будет капризничать, если структура таблиц изменилась, а VIEW на них ссылаются. Проще пересоздать, чем разбираться с зависимостями.
Уровень 2: Версионированные миграции
Иногда нужны специфические изменения, которые система не может сделать сама. Например, миграция данных между таблицами, сложные трансформации, добавление специфичной логики.
Для этого есть папка tools/deployment/migrations/vX.Y.Z/:
migrations/
├── v0.18.0/
│ ├── add_tenant_isolation.sql
│ └── migrate_legacy_data.sql
├── v0.19.0/
│ └── add_rag_tables.sql
└── v0.20.0/
└── migrate_to_service_ai.sql
Создаёшь папку с версией, кидаешь туда SQL скрипты — и при обновлении система автоматически их применит. Один раз. Повторно не запустит (версия миграции отслеживается).
Бэкапы: Перед каждой миграцией система делает бэкап БД. Без напоминаний. Потому что бэкапы — это как страховка: кажется, что не нужна, пока не понадобится.
Graceful Shutdown: когда приложение умеет прощаться
Самая недооценённая фича в любой системе — это умение корректно завершать работу. В Legacy при обновлении контейнер просто убивался (docker stop), и надеялся, что ничего важного в этот момент не происходит.
В Reborn — многоуровневый graceful shutdown:
Docker → Supervisor → Application
Docker отправляет
SIGTERMи ждёт 15 секундSupervisor получает сигнал, передаёт приложению и ждёт 10 секунд
-
Приложение:
Останавливает принятие новых событий
Завершает текущие фоновые задачи (2 секунды на всё)
Последовательно выключает все плагины через DI-контейнер (до 5 секунд на всех)
Каждый плагин закрывает свои соединения, сохраняет состояние, освобождает ресурсы
Отчитывается в логах: "Graceful shutdown completed"
Если приложение не успело за отведённое время — его всё равно остановят (SIGKILL), но даже такой вариант приложение может аккуратно обработать.
Настраивается легко:
# config/settings.yaml
global:
shutdown:
di_container_timeout: 5.0s # На все плагины
plugin_timeout: 3.0s # На каждый плагин по умолчанию
background_tasks_timeout: 2.0s # На завершение фоновых задач
Результат? Обновления проходят без потери данных. Бот может обрабатывать сообщение — дождётся завершения и только потом выключится. Красота.
Версионирование: порядок в хаосе релизов
У каждой версии есть имя. И это имя хранится в одном месте:
# config/.version
version: "0.21.0"
release: false # true для продакшн-релизов
При обновлении сервера версия определяется из git tag и обновляется автоматически. При сборке Docker образа — версия вшивается в тег образа. При откате — выбираешь из списка доступных версий.
GitHub Actions автоматом:
При пуше в main запускается workflow:
Тестирование: Прогоняет pytest, создаёт детальную статистику (сколько тестов прошло/упало)
Релиз: Если в
config/.versionстоитrelease: true— создаёт git tag с версией
Если тег уже существует (кто-то забыл обновить версию), система автоматически инкрементирует: v0.21.0 → v0.21.0-1 → v0.21.0-2.
Всё логируется, всё отслеживается. Хочешь узнать, какая версия сейчас на сервере? cat config/.version. Хочешь откатиться? Deployment Manager покажет список доступных версий образов.
Webhooks: когда конфигурации обновляется самостоятельно
GitHub Webhooks: от коммита до обновления
Настройка webhook в репозитории тенантов, и дальше магия:
Пушим изменения в
tenant_123/scenarios/new_feature.yamlGitHub отправляет webhook на сервер
Сервер получает событие, проверяет подпись (HMAC SHA256)
Анализирует, какие файлы изменились: "О, это
tenant_123, обновляем его сценарии"Синхронизирует изменения
Перезапускает бота (gracefully)
Время от коммита до работающих изменений? Менее 5 секунд.
Безопасность:
Валидация подписи через secret (только настоящие события от GitHub)
Обработка только событий
push(остальные игнорируются)Асинхронная синхронизация (не блокирует работу платформы)
Откаты: кнопка "отменить" для продакшна
Нашел баг в обновлении и кажется ждем бессоная ночь, но нет! Так как образы монтируются на каждую версию можно всегда сделать откат на проверенный вариант.
python tools/deployment/deployment_manager.py
# Пункт 4: Откат Docker образа
Система показывает доступные версии (из локальных Docker образов), ты выбираешь нужную:
Доступные версии для отката:
1. prod-app:latest
2. prod-app:0.20.0
3. prod-app:0.19.5
Выбрал, подтвердил — и через пару минут работает старая версия. С бэкапом БД, если нужно откатить и данные.
Если набралось слишком много образов, то всегда можно через утилиту деплоя удалить лишние, еще и освободить немного места.
Конфигурация: гибкость через исключения
Для гибкой настройки обновления есть возможность заготовить пресеты, кастомные включения или исключения файлов при получении обновлений.
# Настройки фильтрации файлов (используется та же система пресетов, что и для деплоя)
deployment:
# Пресеты (подготовленный набор)
presets:
- "core_files"
# Дополнительные файлы для включения (имеют приоритет над пресетами)
custom_include:
- "LICENSE"
- "scripts/sqlite_to_postgresql_migration.py"
# Дополнительные файлы для исключения (имеют приоритет над пресетами)
custom_exclude:
- "config/settings.yaml" # Не обновляем, настраивается вручную на сервере
- "config/.version" # Не обновляем, локальный файл на сервере
- "docs/changelog/"
Резюме: DevOps как он должен быть
Время от последнего коммита обновления до наката - это буквально несколько кликов за 5 минут времени. Пока чайник закипает можно спокойно выкатываться на тестовый контур и обкатывать готовый релиз.
Что получилось:
Единая точка входа — deployment_manager для всех операций
Автоматизация — обновления, миграции, бэкапы без ручного участия
Безопасность — graceful shutdown, откаты, валидация, бэкапы перед миграциями
Версионирование — всегда знаешь, что где запущено
Webhooks — от коммита до обновления автоматически
Удобство — команда
dcдля всего Docker Compose
Push в GitHub → автоматическое тестирование → тег релиза → обновление на сервере → graceful restart → готово. Всё безопасно, всё логируется, всё можно откатить.
Инфраструктура настроена и работает как часы. Но как обеспечить, чтобы она работала быстро даже под нагрузкой? Об оптимизации и масштабировании как раз поговорим подробнее в следующей части.
Часть 10. Производительность и масштабирование: от одного бота к платформе
Когда платформа работает на одного тенанта — всё достаточно предсказуемо. Но стоит этому одному боту превратиться в десятки и сотни, как внезапно выясняется, что "и так норм работает" больше не прокатывает. Производительность перестаёт быть абстрактным словом и превращается в очень конкретные цифры в htop.
Почему производительность — это не только про скорость
Частая ошибка — думать о производительности как о "сделать быстрее". На уровне платформы важнее другое: как именно тратятся ресурсы и что происходит, когда количество тенантов растёт на порядок.
В Legacy всё работало вполне комфортно… пока жил один проект. Платформа стабильно ела 200+ МБ RAM и держала 5–10% CPU даже в спокойном состоянии, в основном из‑за фоновых задач и периодической работы с базой. Для одного бота это терпимо, но когда нужно держать десятки — начинается борьба за каждую сотню мегабайт.
В Reborn подход был другой: вместо "сначала запустим, потом оптимизируем" сразу закладывается экономное использование ресурсов. Асинхронный стек, локальные кэши, аккуратная работа с БД и отказ от тяжёлых библиотек вроде aiogram позволяют держать платформу в районе 100–150 МБ RAM и около 0% CPU в idle. То есть почти вдвое меньше памяти и на порядок меньше фоновой нагрузки.

Асинхронность по умолчанию: один стиль, максимум выхлопа
Одно из ключевых решений — асинхронность везде. Не отдельные "горячие" места, а общая философия: вся платформа говорит на языке async/await — от HTTP и Telegram до работы с БД и внешними API.
В синхронном подходе каждый запрос к БД, Telegram или AI блокирует поток. При сотнях одновременных запросов либо ставится очередь, либо заводятся десятки потоков и начинается борьба с контекст‑свитчингом и GIL. В асинхронном мире один event loop спокойно обслуживает тысячи корутин, пока те ждут I/O.
# Схематично синхронный подход
def handle_update(update):
user = db.get_user(update.user_id) # Блокируем поток
send_response(user) # Снова блокировка
# Асинхронный подход
async def handle_update(update):
user = await db.get_user(update.user_id) # Поток не блокируется
await send_response(user) # Другие корутины продолжают работать
У такого подхода несколько эффектов:
Платформа изначально готова к высокой конкурентности — тысячи "висящих" операций не превращаются в тысячу потоков.
Весь стек однородный: не нужно постоянно строить мосты между sync и async.
Накладные расходы на сам event loop минимальны по сравнению с выигрышем от отсутствия блокировок.
В связке с аккуратной работой с БД это даёт понятное поведение под нагрузкой: там, где Legacy упирается в CPU и блокирующие операции, Reborn продолжает спокойно переваривать запросы, пока железо действительно не закончится.
Локальный кэш вместо Redis
Кэш — один из самых дешёвых способов разгрузить базу и внешние API. Самый очевидный выбор — поднять Redis и жить спокойно. Но для этот путь кажется слишком тяжёлым относительно задач.
Что реально нужно кэшировать:
Конфигурации тенантов:
tg_bot.yaml,config.yaml, сценарии.Метаданные плагинов и утилит (которые меняются только при деплое).
Маппинги вроде "какой токен относится к какому боту/тенанту".
Все эти данные:
Меняются редко.
Прекрасно восстанавливаются из файлов или БД при перезапуске.
Не требуют жёсткой консистентности между несколькими инстансами в режиме "каждую миллисекунду".
Под это был сделан свой локальный in‑memory кэш на уровне Foundation — CacheManager. По сути это небольшой Redis в миниатюре:
Хранение данных в памяти процесса.
TTL для ключей, чтобы значения не жили вечно.
Ленивая очистка по обращению и фоновая "ленивая уборка" по алгоритму, похожему на Redis (случайная выборка и выкидывание просроченных).
Специализированные обёртки вроде
TenantCache,ScenarioCache,BotInfoManagerс разными TTL и неймспейсами.
Примерно так выглядит логика использования (по сути, Redis‑like API, только без сети):
Базовый паттерн cache-aside:
# Чтение с автоматической проверкой истечения TTL (ленивая очистка)
value = await cache.get("bot:123")
# Если ключ не найден или истек - загружаем из источника
if value is None:
value = await load_bot_config_from_files(tenant_id=123)
# Сохраняем с TTL 1 час (если не указан - используется default_ttl)
await cache.set("bot:123", value, ttl=3600)
return value
Инвалидация по паттернам (полезно при обновлении конфигов):
# Обновили конфиг тенанта - инвалидируем все связанные ключи
await cache.invalidate_pattern("tenant:123:*")
# Или все боты определенного типа
await cache.invalidate_pattern("bot:*:metadata")
# Затем заново кэшируем при следующем обращении
Механизм работы:
Ленивая очистка — при каждом
get()проверяется, не истек ли ключ. Если истек — удаляется сразу и возвращаетсяNone. Это гарантирует актуальность данных без лишних проверок.-
Фоновая очистка — каждую минуту запускается алгоритм как в Redis:
Берется случайная выборка из 50 ключей с TTL
Если >25% истекли — запускается полная очистка всех истекших
Если <25% — удаляются только найденные в выборке
Защита от утечек — если при
set()не указан TTL, используетсяdefault_ttl=3600(1 час). Это предотвращает накопление "вечных" ключей при ошибках в коде.Хранение
expired_at— как в Redis, хранится время истечения, а не остаток TTL. Это позволяет избежать обновлений TTL при каждой проверке.
Пример с разными TTL для разных типов данных:
# Конфиги тенантов - кэшируем долго (1 час)
tenant_config = await load_tenant_config(tenant_id)
await cache.set(f"tenant:{tenant_id}:config", tenant_config, ttl=3600)
# Метаданные ботов - средний срок (30 минут)
bot_meta = await load_bot_metadata(bot_id)
await cache.set(f"bot:{bot_id}:meta", bot_meta, ttl=1800)
# Временные токены - короткий срок (5 минут)
temp_token = generate_temp_token()
await cache.set(f"token:{temp_token}", user_id, ttl=300)
Почему это лучше, чем сразу тащить Redis:
Нет отдельного контейнера и оркестрации ещё одного сервиса.
Нет сетевых вызовов — чтение идёт напрямую из памяти Python‑процесса.
Нет лишней точки отказа в простых сценариях.
Нет дополнительный завимых для платформы
То есть кэш остаётся кэшем, а не отдельной подсистемой, которая сложнее, чем половина основной платформы.
База данных: правильные инстинкты вместо микро‑тюнинга
Корректная работа с БД — важная часть производительности, но здесь Reborn сознательно не превращается в "фетиш оптимизаций". Важно не то, сколько индексов и какой EXPLAIN ANALYZE, а то, что архитектура изначально дружит с мультитенантностью и нагрузкой.
Мультитенантность и индексы
Выбран подход "одна БД + tenant_id в таблицах", что сильно влияет на паттерны запросов. Почти все операции так или иначе фильтруются по конкретному тенанту, а значит:
В составных индексах
tenant_idпочти всегда идёт первым полем.Запрос очень быстро отсекает "чужие" данные и работает уже в небольшом подмножестве строк.
Это не выглядит как "вау‑оптимизация", но даёт ровную, предсказуемую производительность при росте количества тенантов и данных.
Пулы соединений и N+1
Дальше подключаются две базовые практики:
Пул соединений вместо открытия нового подключения под каждый запрос. Небольшой пул покрывает тысячи операций и экономит как память, так и ресурсы PostgreSQL.
Отслеживание N+1‑паттернов: если код делает 1 запрос за списком, а затем ещё N запросов за деталями по каждому элементу, это рано или поздно взорвётся под нагрузкой.
Вместо борьбы за каждый миллисекундный выигрыш по отдельности, платформа придерживается простого принципа: не делать с БД ничего "подозрительного", что ломает масштабирование при росте данных и числа тенантов.
Legacy vs Reborn: ощущение по ресурсам
Самый наглядный способ почувствовать разницу — сравнить, как ведут себя Legacy и Reborn "в обычной жизни", когда платформа просто запущена и ждёт событий.
Legacy
RAM: стабильно 200+ МБ на платформу даже при средней нагрузке.
CPU: 5–10% в спокойном состоянии за счёт фоновых задач, периодических операций с БД и тяжёлого стека.
Существенная часть объёма — это aiogram и его окружение (сама библиотека весит ощутимо и тянет за собой кучу зависимостей).
На одного бота это не катастрофа, но при росте числа проектов такая база становится дорогой в сопровождении.
Reborn
RAM: примерно 100–150 МБ на всю платформу с несколькими тенантами.
CPU: около 0% в спокойном состоянии, за счёт событийной архитектуры и отсутствия лишних фоновых процессов.
Заменён стек вокруг Telegram (вместо
aiogram— более лёгкий асинхронный подход), оптимизированы модули и убрано лишнее.
Разница ощущается не только в цифрах, но и в психологии: когда платформа спокойно живёт в 100–150 МБ и не греет CPU без причины, масштабировать её по количеству тенантов значительно спокойнее.
Логирование: видно всё, но не шумит
Производительность немыслима без нормальной наблюдаемости. При этом логирование легко само превращается в проблему: если писать всё и везде, это начинает есть диск и CPU, а главное — мешает увидеть главное.
Система логирования живёт в Foundation‑слое в виде отдельной утилиты, которая задаёт стиль и дисциплину сообщений.
Именованные логгеры и паттерны
Каждый модуль получает свой именованный логгер, что сразу даёт понятный контекст. Поверх этого используются простые паттерны:
[Tenant-137]/[Bot-1]/[Service-bot_hub]— контекст, который подсвечивается и позволяет фильтровать логи по сущностям.Краткие, самодостаточные сообщения уровня
INFO: без романов на три строки, только про текущую функцию.
Пример "здорового" лога:
self.logger.info(f"[Tenant-{tenant_id}] Обновление конфигурации...")
self.logger.info(f"[Tenant-{tenant_id}] Конфигурация обновлена ({groups_count} групп)")
self.logger.error(f"[Tenant-{tenant_id}] Ошибка синхронизации сценариев: {error}")

DEBUG‑уровень в продакшене не используется, чтобы не превращать логирование в отдельный источник деградации. Включается только когда действительно нужно "копать" конкретную проблему.
Масштабирование: когда одного процесса мало
Даже при аккуратном использовании ресурсов рано или поздно появится задача "хотим больше тенантов / выше RPS". Именно поэтому изначально спроектирован как платформа, которая спокойно живёт в нескольких инстансах.
Stateless‑подход и sticky‑тенанты
Ключевая идея проста:
Состояние (сессии, storage, результаты RAG, прогресс сценариев) хранится в БД и других внешних хранилищах, а не в памяти процесса.
Один инстанс — это просто "рабочая лошадка", которую можно добавить или выключить без изменения логики.
Для повышения эффективности локальных кэшей разумно "приклеивать" тенантов к конкретным инстансам — например, через хеширование tenant_id на количество инстансов.
Так платформа не только справляется с ростом нагрузки, но и остаётся довольно простой: вместо сложной сетевой магии — предсказуемое, прозрачно масштабируемое приложение.
Производительность как побочный эффект архитектуры
Самое интересное во всей этой истории — то, что производительность в Reborn — это не набор "героических оптимизаций", а побочный эффект архитектурных решений.
Асинхронный стек по умолчанию даёт конкурентность "из коробки".
Локальный кэш на уровне Foundation позволяет разгрузить БД, не усложняя инфраструктуру.
Мультитенантная схема с продуманными индексами делает запросы предсказуемыми.
Аккуратное отношение к зависимостям (тот же отказ от aiogram) экономит десятки мегабайт.
Логирование и stateless‑подход упрощают эксплуатацию и масштабирование.
В итоге получается платформа, которая:
В спокойном состоянии почти не тратит CPU.
Укладывается в 100–150 МБ RAM на несколько тенантов.
Не рассыпается при росте нагрузки и количестве ботов.
Часть 11. LLM, RAG и AI-агенты: от нейросетей к интеллекту
Введение: от инфраструктуры к интеллекту
Вот уже есть готовая платформа: мультитенантность, плагины, DevOps, производительность. Но это всё — инфраструктура. Без интеллекта это просто система обработки событий с YAML-конфигами.
AI в Coreness Reborn — это не просто "интеграция с ChatGPT". Это продуманная система от базовых completion запросов до полноценных AI-агентов с RAG, от промпт-инжиниринга до семантического поиска. Но прежде чем нырять в практику, давайте разберёмся, как это вообще работает под капотом.

Что мы разберём:
Анатомия LLM — как устроены большие языковые модели
Трансформеры и attention — механизм, который изменил всё
Токенизация — как модель видит текст
Completion запросы — структура, параметры, оптимизация
Промпт-инжиниринг — искусство формулировать запросы
RAG (Retrieval-Augmented Generation) — база знаний для AI
Векторный поиск — семантический поиск через pgvector
AI-агенты — автономные помощники
Блок A: Анатомия LLM — как это работает на самом деле
Главное: от последовательности к вниманию
Раньше (до 2017) нейросети читали текст последовательно, слово за словом. К концу длинного предложения они забывали начало.
Прорыв 2017 — архитектура Transformer. Главная идея: модель смотрит на весь текст сразу и определяет, какие части важны для понимания.
Почему это изменило всё:
Обработка текста параллельно (быстрее)
Нет проблемы "забывания"
Можно обучать на огромных данных
Четыре ключевые концепции
1. Attention (Внимание) — главная магия
Модель понимает связи между словами в контексте.
Пример:
"Кошка села на коврик, потому что она устала"
Когда модель видит слово "она" — механизм attention определяет: "она" = "кошка".
Ещё примеры:
"Я пошёл в банк у реки" → "банк" связан с "реки" (берег)
"Я пошёл в банк снять деньги" → "банк" связан с "деньги" (учреждение)
Модель динамически понимает контекст. Multi-Head Attention — несколько механизмов параллельно смотрят на разные аспекты: грамматику, смысл, дальние связи.
2. Токенизация — как модель "видит" текст
Нейросети работают только с числами. Текст разбивается на токены — не слова, а части слов.
Пример:
"платформа" → ["плат", "форм", "а"]
"привеееет" → ["прив", "е", "е", "е", "ет"]
В зависимости от языка получается разное количество токенов:
Английский: ~1 токен ≈ 0.75 слова
Русский: ~1 токен ≈ 2-3 символа ≈ 0.4 слова
Следствие: Русские промпты в 2 раза дороже в токенах!
3. Генерация текста — слово за словом
LLM = autoregressive модель — предсказывает следующий токен на основе всех предыдущих.
Процесс:
Промпт: "Кошка села на"
1. Модель видит: ["Кошка", "села", "на"]
2. Предсказывает вероятности:
- "коврик" (40%)
- "диван" (25%)
- "стул" (15%)
3. Выбирает токен (с учётом temperature)
4. Добавляет к входу, повторяет
Почему "печатает" ответ: Генерация последовательная, слово за словом.
4. Embeddings — слова как точки в пространстве
Каждое слово → вектор чисел (1024-12288 измерений). Близкие по смыслу слова = близкие векторы.
Классика:
вектор("король") - вектор("мужчина") + вектор("женщина") ≈ вектор("королева")
Два типа:
Token embeddings — смысл слова
Positional embeddings — позиция в предложении
Почему важно для RAG: Embeddings используются для семантического поиска!
Запрос: "как подключить базу данных"
Найдёт: "настройка соединения с БД" (близкие векторы, даже без общих слов)
Резюме блока A
Четыре ключевых концепта:
Attention — модель понимает связи между словами в контексте
Токенизация — текст разбивается на части (русский "дороже" в токенах)
Генерация — слово за словом, autoregressive
Embeddings — слова как векторы, основа для RAG
Главное: Не нужно понимать всю математику. Достаточно понимать концепты, чтобы эффективно использовать LLM в практике.
Теперь — к практике!
Блок B: Completion запросы — практическая работа с LLM
Провайдеры и агрегаторы
Платформа построена под использование агрегаторов моделей, таких как OpenRouter, CometAPI, Polza.AI и другие. Гораздо удобнее по одному API-ключу получать сразу весь набор доступных моделей, а не делать прямые интеграции с разными сервисами (не считая того, что они ещё могут иметь региональные ограничения).
Все агрегаторы работают по одинаковой схеме: достаточно указать endpoint и ключ — готово. Под капотом используется библиотека OpenAI, т.к. она стала стандартом индустрии и достаточно простая.
Для удобства в платформе создан AI Models Guide — справочник, который собирает данные агрегатора для выбора подходящих моделей, параметров, сравнения цен и возможностей. Не нужно держать всё в голове — открыл гайд, выбрал модель под задачу.
Что такое completion: "законченный" запрос
Главное непонимание: Многие думают, что модель "помнит" историю диалога. Это не так.
Completion = самодостаточный запрос. Каждый раз ты отправляешь модели всю историю заново в виде массива сообщений:
{
"messages": [
{"role": "system", "content": "Ты — помощник."},
{"role": "user", "content": "Привет!"},
{"role": "assistant", "content": "Привет! Как дела?"},
{"role": "user", "content": "Расскажи про Python"}
]
}
Модель не хранит историю. Она видит только то, что ты ей отправил в этом запросе. Хочешь "память" — сам управляй историей сообщений.
Роли в messages:
system — инструкции для модели (роль, правила, контекст)
user — сообщения пользователя
assistant — ответы модели (история)
Каждый запрос — чистый лист. Модель не знает, что было 10 запросов назад, если ты не отправишь эти сообщения снова.
Prompt cache: непрозрачная магия
Есть такая штука — prompt cache (кэш промптов). Провайдеры (Anthropic, OpenAI) могут кэшировать части промпта, чтобы не обрабатывать их повторно.
Проблема: Кэш очень непрозрачный и сложный в работе:
Непонятно, когда он сработает, когда нет
Условия кэширования меняются у провайдеров
Время жизни кэша неизвестно
Влияние на стоимость неочевидно
Вывод: Не стоит сильно полагаться на кэш. Лучше считать, что его нет, так будет надежнее.
Ключевые параметры completion
Не будем углубляться в детали, но основное:
temperature (0-2) — креативность vs детерминизм:
0 → строгие ответы (для фактов)
0.7-1 → баланс (для диалогов)
1.5-2 → креативность (для генерации контента)
max_tokens — ограничение длины ответа.
context window — сколько токенов модель может "видеть" за раз:
GPT-4: 8K-128K токенов
Claude 3.5: 200K токенов
Gemini: до 1M токенов
Проблема переполнения: Если история диалога больше context window → модель обрежет начало или выдаст ошибку.
Стратегии управления:
Truncation — обрезать старые сообщения
Sliding window — держать только последние N сообщений
RAG — не держать всю историю, а искать релевантные куски (об этом дальше)
Действие completion в платформе
Интеграция:
Универсальный
ai_tokenдля любого агрегатораPer-tenant токены — каждый тенант может иметь свой ключ
Базовый пример:
- action: completion
params:
prompt: "{event_text}"
system_prompt: "Ты — помощник компании."
rag_chunks: "{_cache.chat_history_chunks}"
model: "gpt-4o-mini"
temperature: 0.7
max_tokens: 1000
Платформа автоматически корректную структуру:
System prompt
История диалога (если передана)
RAG контекст (если передан)
Финальный user message
Промпты в платформе: динамика и переиспользование
Хранение в конфигах:
Промпты можно выносить в storage/ тенанта для переиспользования:
# config/tenant/tenant_101/storage/prompts.yaml
assistant:
system: "Ты — помощник компании {company_name}."
rules:
- "Отвечай кратко"
- "Используй базу знаний"
Плейсхолдеры:
Динамическая подстановка значений в промпты:
- action: completion
params:
prompt: "{event_text}"
system_prompt: "Привет, {user_name|fallback:Гость}! Сегодня {event_date|format:date}."
Главное из блока B
Ключевые моменты:
Агрегаторы удобнее — один ключ, все модели
Completion = самодостаточный запрос — модель НЕ хранит историю
Prompt cache непрозрачен — не стоит на него рассчитывать
Context window ограничен — нужно управлять историей самостоятельно
Думаю теперь стало понятно, почему RAG так важен при работе с AI-агентами!
Блок C: RAG (Retrieval-Augmented Generation) — база знаний для AI
Проблема: ограничения LLM
Ограничения LLM:
❌ Не знают специфики бизнеса (ваша документация, процессы)
❌ Устаревшие данные (cutoff date — модель обучена на данных до определённой даты)
❌ Галлюцинации (выдумывают факты, если не знают ответа)
❌ Нельзя засунуть всю документацию в промпт (лимит контекста)
Проблема контекста:
Документация компании: 1000+ страниц
Контекст модели: 200K токенов максимум (Claude 3.5)
Невозможно передать всю информацию
Fine-tuning не решает проблему:
Дорого (тысячи долларов)
Сложно (нужны данные, экспертиза)
Медленно (обучение занимает дни/недели)
Не подходит для часто меняющейся информации
Что такое RAG
Retrieval-Augmented Generation — подход, который решает проблему контекста.
Концепт: Поиск → Дополнение → Генерация
Схема работы:
Запрос пользователя: "Как настроить интеграцию с API?"
Поиск релевантной информации в базе знаний (векторный поиск)
Формирование промпта с найденной информацией
Генерация ответа моделью на основе найденного контекста
Почему работает лучше fine-tuning:
Дешевле (не нужно обучать модель)
Быстрее (добавление данных за минуты)
Актуальнее (можно обновлять базу знаний в реальном времени)
Прозрачнее (видно источники информации)

Векторные базы данных и эмбеддинги
Проблема поиска
Классический поиск (Full-Text Search): По ключевым словам
"настройка API" → найдёт только точные совпадения слов
Проблема: синонимы, перефразирование, опечатки
Нужен поиск "по смыслу", а не по словам.
Эмбеддинги
Что это: Преобразование текста в вектор чисел.
Как работает:
Текст → модель эмбеддингов → вектор (обычно 1024-1536 измерений)
Похожие тексты → похожие векторы
Близость векторов = близость смысла
Аналогия: Координаты точек в многомерном пространстве. Близкие точки = похожий смысл.
Векторный поиск (Semantic Search)
Семантический поиск:
Запрос → эмбеддинг → поиск ближайших векторов
Cosine similarity — мера близости (от -1 до 1, обычно 0.7-1.0)
Top-K результатов — возвращаем N ближайших
Пороговое значение (threshold) — минимальная схожесть (обычно 0.7)
Пример:
Запрос: "как подключить базу данных?"
Найдёт:
- "настройка соединения с БД" (similarity: 0.89)
- "конфигурация PostgreSQL" (similarity: 0.82)
- "подключение к MySQL" (similarity: 0.78)
Популярные решения
Специализированные БД:
Pinecone — managed сервис
Weaviate — open-source
Qdrant — быстрый, Rust-based
ChromaDB — простой, Python-based
Расширения для SQL БД:
pgvector (PostgreSQL) — нативное расширение
pgvecto.rs — альтернатива на Rust
Мой выбор пал на pgvector — не нужно поднимать новую базу, легко поддерживать.
Интеграция RAG в платформу
Архитектурное решение
Выбор pgvector:
✅ Одна БД для всего (PostgreSQL)
✅ Проще поддержка и deployment
✅ Меньше зависимостей
✅ Нативная поддержка в PG
Структура БД:
Таблица
vector_storageпо аналогии с другими сторейджами и key-value хранилищамиПоддержка векторных embeddings размерностью 1024
HNSW индекс для быстрого векторного поиска
JSONB для метаданных (улучшенная производительность)
Tenant isolation:
Фильтрация по
tenant_idв векторном поискеКаждый тенант видит только свои данные
class VectorStorage(Base):
__tablename__ = 'vector_storage'
tenant_id = Column(Integer, ForeignKey('tenant.id'), nullable=False) # FK к tenant
document_id = Column(String(255), nullable=False) # Уникальный ID документа
chunk_index = Column(Integer, nullable=False) # Порядок чанка в документе (0, 1, 2...)
document_type = Column(String(100), nullable=False) # Тип: 'knowledge', 'prompt', 'chat_history', 'other'
role = Column(String(20), nullable=False, default='user') # Роль для OpenAI messages: 'system', 'user', 'assistant' (по умолчанию 'user')
# Метаданные (JSONB для фильтрации: chat_id, username и др.)
chunk_metadata = Column(JSONB, nullable=True) # Метаданные чанка (chat_id, username и др.) - используется для фильтрации, не раскрывается в контексте AI
# Контент
content = Column(Text, nullable=False) # Текст чанка
embedding = Column(Vector(1024), nullable=True) # Векторное представление текста (опционально, может быть NULL для истории без поиска)
embedding_model = Column(String(100), nullable=True) # Модель, использованная для генерации embedding (например, "text-embedding-3-small", "text-embedding-3-large")
created_at = Column(TIMESTAMP, nullable=False, default=dtf_now_local) # Реальная дата создания (для правильной сортировки истории)
processed_at = Column(TIMESTAMP, nullable=False, default=dtf_now_local) # Дата обработки/обновления
# Метаинформация (НЕ используется для загрузки данных!)
tenant = relationship("Tenant", lazy='noload') # N:1 связь
__table_args__ = (
PrimaryKeyConstraint('tenant_id', 'document_id', 'chunk_index'),
Index('idx_vector_storage_tenant', 'tenant_id'),
Index('idx_vector_storage_document', 'tenant_id', 'document_id'),
Index('idx_vector_storage_type', 'tenant_id', 'document_type'),
# HNSW индекс для векторного поиска (размерность 1024 < 2000, поэтому поддерживается)
Index(
'idx_vector_storage_embedding_hnsw',
'embedding',
postgresql_using='hnsw',
postgresql_ops={'embedding': 'vector_cosine_ops'},
postgresql_with={'m': 16, 'ef_construction': 64}
),
)
Chunking: разбиение документов
Проблема реальная: У тебя документация на 50 страниц. Модель может "видеть" максимум 200K токенов (Claude), но это всё равно ~100 страниц текста. И главное — не нужно ей отдавать всю документацию для ответа на вопрос "как настроить webhook?".
Chunking — разбиение документов на небольшие фрагменты, которые можно искать и использовать независимо.
Подходы к разбиению:
-
По количеству символов — тупо режем через каждые N символов
Проблема: режет посреди предложения, теряется смысл
"Для настройки веб" | "хука используйте endpoint" ← сломанный контекст
-
По предложениям — группируем предложения до лимита
✅ Сохраняет смысл внутри чанка
❌ Но связи между предложениями теряются
-
По параграфам — для структурированных документов
✅ Логическая целостность
❌ Параграфы могут быть слишком большими/маленькими
-
Семантическое разбиение — по смыслу через embeddings
✅ Идеальная точность
❌ Дорого и сложно
Что выбрано в платформе: гибридный подход
Реализован умный split по предложениям с overlap (перекрытием):
# Пример логики (упрощённо)
chunk_size = 512 # символов в чанке
chunk_overlap = 100 # 20% перекрытия
Как работает:
Разбиваем текст на предложения (по
.,!,?, учитывая сокращения)Группируем предложения пока не достигнем
chunk_sizeДобавляем overlap — последние ~100 символов предыдущего чанка переходят в следующий
Почему overlap критичен:
Чанк 1: "...настройка webhook через config.yaml.
Укажите endpoint и secret_token."
Чанк 2: "Укажите endpoint и secret_token. ← overlap!
Валидация подписи через X-Hub-Signature..."
Без overlap: чанк 2 начинается с "Валидация подписи..." — теряется контекст про endpoint.
С overlap: чанк 2 видит предыдущее предложение — контекст сохранён.
Оптимальные параметры для платформы:
chunk_size: 512 # ~100-150 токенов (русский текст)
chunk_overlap: 100 # 20% перекрытия
Почему 512 символов:
Достаточно для 2-4 предложений
Не слишком маленький (много чанков = дорогой поиск)
Не слишком большой (релевантность страдает)
Хорошо вписывается в контекст completion (5-10 чанков = 2500-5000 символов контекста)
Почему 20% overlap:
Меньше 10% — контекст на границах теряется
Больше 30% — избыточность, дублирование информации
20% — sweet spot между контекстом и эффективностью
Обработка edge cases:
Слишком длинное предложение → жёсткий split по символам
Markdown форматирование → сохраняется структура
Пустые строки → используются как естественные границы
Результат: Чанки логически целостные, с сохранением контекста на границах. Модель получает осмысленные фрагменты, а не обрывки текста.

Metadata и фильтрация
Метаданные для чанков:
Источник (документ, URL)
Дата создания/обновления
Категория
Права доступа
Кастомные поля (
chat_id,username, и др.)
Фильтрация результатов:
- action: search_embedding
params:
query_text: "{event_text}"
document_type: "chat_history"
metadata_filter:
chat_id: "{chat_id}"
username: "{username}"
Комбинированный поиск: Векторный поиск (семантика) + Фильтры по метаданным (точность). Такой подход позволяет гибко управлять потоками данных и разрабатывать сложные решения.
RAG в действии: полный workflow
Пример: AI-чат с базой знаний:
ai_chat_with_rag:
description: "AI-чат с RAG"
trigger:
- event_type: message
steps:
# 1. Сохраняем сообщение пользователя
- action: save_embedding
params:
text: "{event_text}"
document_type: "chat_history"
role: "user"
metadata:
chat_id: "{chat_id}"
username: "{username}"
# 2. Ищем релевантные знания
- action: search_embedding
params:
query_text: "{event_text}"
document_type: "knowledge"
limit_chunks: 5
limit_chars: 2000
# 3. Получаем историю диалога
- action: get_recent_chunks
params:
limit_chunks: 10
document_type: "chat_history"
metadata_filter:
chat_id: "{chat_id}"
# 4. Генерируем ответ с контекстом
- action: completion
params:
prompt: "{event_text}"
system_prompt: "Ты — помощник. Используй базу знаний и историю диалога."
rag_chunks: "{_cache.chunks}" # Автоматическая группировка!
model: "gpt-4o-mini"
# 5. Отправляем ответ
- action: send_message
params:
text: "{_cache.response}"
# 6. Сохраняем ответ ассистента
- action: save_embedding
params:
text: "{_cache.response}"
document_type: "chat_history"
role: "assistant"
metadata:
chat_id: "{chat_id}"
username: "bot"
Что происходит под капотом:
save_embedding: Текст → chunking → embeddings → vector_storage
search_embedding: Query embedding → cosine similarity → top-5 chunks
get_recent_chunks: История из БД, сортировка по дате
-
completion: Формирование messages:
System prompt
Chat history (sorted by date)
Knowledge context (with similarity scores)
User query
send_message: Отправка в Telegram
save_embedding: Сохранение ответа для будущих диалогов
Автоматическая группировка rag_chunks:
Платформа группирует чанки по типам:
chat_history — история диалога (по дате)
knowledge — база знаний (по similarity)
other — прочий контекст
Блок D: AI-агенты и tool calling
Что такое AI-агенты
AI-агент — это LLM с возможностью использовать внешние инструменты (tools) для выполнения задач.
Отличие от обычного completion:
Обычный completion: Промпт → Ответ
AI-агент: Промпт → Анализ → Выбор инструмента → Выполнение → Ответ
Концепт tool calling:
Модель может "вызывать функции" через structured output:
{
"tool_call": "search_database",
"parameters": {
"query": "найти пользователя по email",
"email": "user@example.com"
}
}
Поддержка в платформе:
- action: completion
params:
prompt: "{event_text}"
tools:
- name: "search_database"
description: "Поиск в базе данных"
parameters:
type: "object"
properties:
query:
type: "string"
description: "Запрос для поиска"
tool_choice: "auto" # auto, required, none
Workflow агента:
Пользователь: "Найди информацию о клиенте ivan@example.com"
Модель анализирует → решает использовать
search_databaseВозвращает tool_call с параметрами
Платформа выполняет поиск
Результат передаётся обратно модели
Модель формирует финальный ответ
JSON mode и structured outputs
JSON mode:
Гарантирует, что модель вернёт валидный JSON.
- action: completion
params:
prompt: "Извлеки данные: {text}"
json_mode: "json_object"
system_prompt: "Верни JSON с полями: name, email, phone"
JSON Schema:
Детальная спецификация структуры:
- action: completion
params:
prompt: "{text}"
json_mode: "json_schema"
json_schema:
type: "object"
properties:
name:
type: "string"
age:
type: "integer"
required: ["name"]
Применение: Извлечение структурированных данных, tool calling, валидация ответов.
Резюме: AI как часть платформы
Вот мы пробежались от теории трансформеров до практического RAG.
AI в Coreness — это не просто интеграция API, это набор инструментов и возможностей для создания сложных сценариев с понимаем контекста, использованием базы знаний и генерирацией умных ответов.
Часть 12. Use Cases: Когда декларативность встречается с реальностью
Время разобрать практические кейсы — те ситуации, когда YAML-конфигурация превращается из абстрактного синтаксиса в работающее решение. Не буду копипастить километры конфигов, а разберу концептуальные паттерны и архитектурные решения для разных сценариев использования.

Небольшой пример от автора: @coreness_bot
Образовательный бот: Когда контекст живет в RAG
Представьте образовательную платформу, где студенты задают вопросы про материалы курса, а бот отвечает не общими фразами, а конкретно по содержанию лекций и учебников.
Архитектура решения
Загрузка базы знаний — при создании курса все материалы (лекции, конспекты, презентации) автоматически разбиваются на чанки и сохраняются в векторное хранилище с метаданными: название курса, модуль, тема. Это происходит через save_embedding с типом knowledge.
Семантический поиск — когда студент задает вопрос, система ищет релевантные фрагменты через search_embedding, фильтруя по курсу и теме. Найденные чанки автоматически передаются в AI completion как контекст.
Адаптивный контекст — система использует разные типы чанков: knowledge для материалов курса, chat_history для истории диалога конкретного студента. Это позволяет AI понимать не только общий контекст курса, но и персональную траекторию обучения.
Фича: персонализация через метаданные — каждый чанк помечается ID студента и курса, поэтому поиск находит только релевантные для конкретного пользователя фрагменты. Плюс можно фильтровать по датам, чтобы AI учитывал только недавние темы.
Применимость
RAG позволяет создать бота, который "знает" весь материал курса без необходимости вручную прописывать каждый вопрос-ответ. AI работает как адаптивный репетитор, который находит нужную информацию в контексте конкретного запроса студента.
E-commerce бот: Оплаты, корзина и подписки
Магазин цифровых товаров — классический кейс для автоматизации через бота. Пользователь выбирает товар, оплачивает через Telegram Stars, получает доступ.
Архитектура решения
Каталог через Storage — список товаров, цены, описания хранятся в Tenant Storage как JSON-структуры. Это позволяет менять ассортимент без изменения кода сценариев.
Процесс оплаты: трехактная драма
Акт первый: создание инвойса — пользователь нажимает "Купить", система создает инвойс через
create_invoiceи отправляет в чат.Акт второй: pre-checkout — пользователь нажимает "Оплатить", Telegram отправляет событие
pre_checkout_query. У бота есть 10 секунд, чтобы ответить через действиеconfirm_payment. Система автоматически проверяет статус инвойса (не отменен ли, не оплачен ли уже) и подтверждает или отклоняет платеж.Акт третий: финал — после успешной оплаты приходит событие
payment_successful. Система черезmark_invoice_as_paidфиксирует транзакцию, выдает доступ к товару и отправляет подтверждение.
Защита от дубликатов — если передать invoice_payload в confirm_payment, система автоматически проверит, не был ли инвойс уже оплачен или отменен. Это защищает от повторных списаний.
Подписки через Scheduled сценарии — для рекуррентных платежей используется связка: scheduled сценарий проверяет дату окончания подписки, создает новый инвойс, отправляет напоминание. Например, каждый день в 9:00 утра (schedule: "0 9 * * *") система проверяет, у кого заканчивается подписка, и автоматически инициирует процесс оплаты.
Применимость
Вся логика платежей инкапсулирована в сценарии — от создания инвойса до выдачи доступа. Нет необходимости вручную обрабатывать callback'и или мониторить статусы платежей. Система сама следит за жизненным циклом транзакции.
Корпоративный ассистент: Контекстная помощь в реальном времени
Внутрикорпоративный бот для сотрудников — помощник, который отвечает на вопросы про регламенты, процессы, документацию компании.
Архитектура решения
Многослойный RAG — база знаний состоит из нескольких типов документов: внутренняя документация (knowledge), история обращений сотрудников (chat_history), FAQ и регламенты и т.п. (other). Каждый тип помечается метаданными: отдел, категория, версия документа.
Департаментная фильтрация — через metadata_filter в search_embedding система находит только те чанки, которые относятся к отделу конкретного сотрудника. Например, HR видит регламенты по работе с персоналом, а IT-отдел — инструкции по инфраструктуре.
Персонализированный формат — через chunk_format настраивается отображение чанков в контексте AI. Для групповых чатов: chunk_format: {chat_history: "[$username]: $content"} — чтобы AI понимал, кто задавал вопрос. Для базы знаний: chunk_format: {knowledge: "[$category] $content"} — чтобы видеть категорию документа.
Scheduled обновления — каждую ночь scheduled сценарий проверяет изменения в внутренней документации (через Git или внешний API), обновляет RAG-хранилище. Это гарантирует актуальность информации без ручной синхронизации.
Статистика обращений — каждое взаимодействие сохраняется в User Storage: частота обращений, популярные темы, неразрешенные вопросы. Scheduled сценарий раз в неделю генерирует отчет для менеджмента.
Применимость
Бот становится единой точкой доступа к корпоративным знаниям. Вместо поиска по разрозненным документам и чатам сотрудник просто спрашивает бота. RAG находит релевантную информацию, AI формулирует ответ с учетом контекста отдела и роли пользователя.
Комьюнити-бот: Модерация и gamification
Бот для управления сообществом в Telegram — автоматическая модерация, система репутации, квесты и челленджи.
Архитектура решения
Автоматическая модерация через AI — каждое сообщение в группе проверяется через completion с заранее настроенным промптом: содержит ли спам, токсичность, запрещенные темы. AI возвращает JSON с оценкой, система через validate и transition принимает решение: пропустить, удалить, забанить.
Система репутации в User Storage — каждый пользователь имеет счетчик активности, рейтинг полезности, историю нарушений. Эти данные используются для принятия решений: новички проходят дополнительную проверку, проверенные пользователи получают больше прав.
Gamification через Scheduled сценарии — каждую неделю запускается сценарий, который подводит итоги активности: кто написал больше полезных сообщений, кто помог другим участникам. Победители получают бейджи (сохраняются в User Storage), доступ к закрытым чатам, специальные роли.
Динамические квесты — админы создают челленджи через команды бота (например, /create_quest "Напишите 10 полезных постов за неделю"). Система отслеживает прогресс через User Storage, автоматически уведомляет участников о выполнении, выдает награды.
Onboarding через transitions — новые пользователи проходят серию шагов: приветствие, правила, подтверждение согласия. Через transition система контролирует прохождение: если пользователь не согласился с правилами, доступ к функциям ограничивается.
Применимость
Вся логика модерации и взаимодействия с участниками автоматизирована. Админам не нужно вручную проверять каждое сообщение или считать активность — система сама следит за поведением пользователей и реагирует по заданным правилам.
SaaS-продукт: Multi-tenant архитектура
Платформа для создания ботов по подписке — каждый клиент получает своего бота с уникальными настройками.
Архитектура решения
Tenant изоляция — каждый клиент (тенант) имеет собственную конфигурацию: токен бота, набор сценариев, Storage. Данные разных тенантов полностью изолированы на уровне базы данных.
Шаблоны сценариев — базовые сценарии (приветствие, меню, FAQ) создаются как шаблоны и копируются новым тенантам при регистрации. Клиенты могут кастомизировать их через Web-интерфейс или напрямую редактируя YAML.
Динамическая конфигурация через Storage — лимиты использования (количество сообщений в день, доступ к AI, интеграции) хранятся в Tenant Storage. Система проверяет лимиты через validate перед выполнением действий.
Scheduled биллинг — каждый день scheduled сценарий проверяет использование ресурсов: сколько сообщений отправлено, сколько AI-запросов сделано. Если лимит превышен, система автоматически создает инвойс на оплату или приостанавливает бота до пополнения баланса.
Аналитика через RAG — все взаимодействия пользователей сохраняются в RAG с метаданными (тенант, пользователь, дата, тип действия). Клиенты получают дашборды с метриками: количество пользователей, популярные команды, конверсия в оплату.
Self-service управление — клиенты управляют своими ботами через мастер-бота: синхронизация сценариев, изменение настроек, просмотр статистики. Все операции выполняются через сценарии — не нужно предоставлять доступ к коду или серверу.
Применимость
Платформа масштабируется горизонтально без изменения архитектуры. Каждый новый тенант — это просто новая запись в базе и набор YAML-файлов. Обновления сценариев и функций распространяются автоматически через Git-синхронизацию.
AI-агент с Tools: Когда бот становится умнее
Бот, который не просто отвечает на вопросы, но и выполняет действия: отправляет email, создает задачи в Trello, создает ремайндеры, ищет информацию в интернете.

Архитектура решения
Function calling через JSON mode — AI модель настроена на генерацию структурированных ответов в JSON. Определяется набор доступных функций (tools): send_email, create_task, search_web. AI решает, какую функцию вызвать на основе запроса пользователя.
Цикл "запрос → решение → действие"
Пользователь задает запрос: "Создай задачу в Trello: разработать новый сценарий"
AI через
completionгенерирует JSON:{"tool": "create_task", "params": {"title": "Разработать новый сценарий", "board": "Backlog"}}Система через
validateпроверяет, что функция доступна, параметры корректныЧерез
transitionпереходит к сценарию выполнения функции (jump_to_scenario: "execute_create_task")Функция выполняется через HTTP action (запрос к Trello API), результат сохраняется в
_cacheСистема возвращается к AI, передает результат выполнения как контекст
AI формулирует финальный ответ пользователю: "Задача создана в доске Backlog"
Fallback на человека — если AI не может выполнить действие (нет подходящей функции, непонятный запрос), система отправляет уведомление администратору через отдельный сценарий. Администратор может вручную обработать запрос или добавить новую функцию.
Логирование через RAG — каждое взаимодействие с tools сохраняется в векторное хранилище: какая функция вызвана, с какими параметрами, какой результат. Это позволяет AI учиться на прошлых действиях и не повторять ошибки.
Применимость
Бот перестает быть пассивным FAQ-ботом и становится активным агентом. Пользователь общается на естественном языке, AI понимает намерение и выполняет действие без необходимости запоминать команды или синтаксис.
Выводы: от простого к сложному
Один язык для разных миров — триггеры, шаги, переходы и плейсхолдеры работают одинаково что для простого FAQ-бота, что для AI-агента с автономными действиями. Образовательные платформы, e-commerce, корпоративные ассистенты, модерация сообществ — всё это собирается из одних и тех же базовых блоков без изменения кода системы.
RAG как переломный момент — векторное хранилище превращает статичные сценарии в динамическую систему. Бот перестает быть набором жестко прописанных правил и начинает опираться на контекст: материалы курса, историю диалогов, корпоративную документацию. Это не просто база данных, а способ научить бота понимать смысл запросов.
Multi-tenant по умолчанию — платформа изначально спроектирована для работы с множеством независимых ботов. Один движок, но каждый тенант живет в своей изолированной среде: собственные сценарии, Storage, лимиты, биллинг. Это не доработка «для масштабирования», а базовая архитектура с первого дня.
Декларативность без ограничений — YAML-конфигурация здесь не "упрощенный режим для новичков", а полноценный способ описывать сложные системы. Scheduled сценарии для автоматизации, transitions для управления потоком, интеграции через HTTP actions — всё это доступно без написания кода, но не теряет в гибкости.
Результат: быстрые эксперименты, безопасные изменения, переиспользование готовых решений. Код живет внутри платформы, разнообразие поведения — в конфигурациях.
Эпилог. Путешествие завершено... или только начинается?
Если вы дочитали до этого места — вы настоящий герой. Без шуток. Это тысячи строк текста, десятки тысяч слов, и далеко не каждый решится пройти весь этот путь от пролога до последней точки. Но раз вы здесь — значит, было интересно (надеюсь). И за это отдельное огромное спасибо.

Что получилось в итоге
Когда всё начиналось, план был простой: рассказать про проект. Показать эволюцию от старого ядра до новой платформы, добавить немного технических деталей — и всё. Но в процессе накопилось так много всего, что статья разрослась до... ну, до того, что вы только что прочитали.
Вместо короткого "вот мой проект" получилось целое путешествие:
От Clean Architecture, которая перестала радовать, до прагматичного микса, который реально работает
От ChatGPT в браузере до полноценного вайб-кодинга с Cursor AI
От одного бота до мультитенантной платформы с десятками клиентов
От простых команд до AI-агентов с RAG, способных понимать контекст и принимать решения
От хардкода до YAML-конфигураций, в которых живёт вся бизнес-логика
И самое крутое — это не то, что получилась платформа (хотя это тоже приятно), а сам процесс. Путь от "хочу сделать ядро" до "работает мультитенантная платформа с RAG" оказался интереснее, чем можно было предположить на старте. Были ошибки, были переделки, были моменты "вот тут всё внезапно сложилось", были и те самые душевные кризисы, о которых было в прологе.
Главные выводы
Вайб-кодинг — это не просто модное слово
Вайб-кодинг оказался не хайпом, а реальным способом делать больше за меньшее время. Да, пришлось учиться формулировать промпты, держать контекст, проводить ревью и не доверять модели слепо. Но это стоило того.
Без вайб-кодинга этот проект либо занял бы в разы больше времени, либо пришлось бы упрощать и отказываться от части идей. Cursor AI, плагинная архитектура, декларативные конфигурации — всё это складывалось в систему, где AI реально помогает, а не просто генерирует код, который потом приходится выбрасывать.
Архитектура — это компромисс, а не догма
Было протестировано много разных архитектурных подходов — от классических слоистых до event-driven и микросервисов. Каждый имел свои сильные стороны, но и свои ограничения. Особенно в контексте вайб-кодинга, где важна не только правильность, но и читаемость для AI-моделей.
В итоге система сошлась на архитектурном миксе: событийная архитектура + вертикальные срезы + плагинная система в рамках монолита. Не самое "учебниковое" решение, зато работающее. И это главное — архитектура должна помогать решать задачи, а не создавать дополнительную сложность. Если подход перестал работать — его нужно менять, даже если это значит переписать половину проекта.
Декларативность открывает новые возможности
YAML-конфигурации стали сердцем платформы. И это не просто "удобный способ настройки" — это философия:
Логика отделена от кода
Изменения безопасны (не сломаешь ядро случайным коммитом)
Тестировать можно прямо в проде (изменил конфиг — проверил — откатил, если что)
AI понимает структуру лучше, чем Python-код со всеми его нюансами
Сценарии, триггеры, transitions, placeholders — всё это превратилось в конструктор, где можно собирать ботов любой сложности без правки основного кода.
Мультитенантность — это не "фича на потом"
Multi-tenant архитектура изначально казалась избыточной. Зачем усложнять, если пока всего один клиент? Но решение сделать это с самого начала оказалось верным.
Когда появился второй тенант, третий, десятый — не пришлось ничего переделывать. Каждый клиент живёт в своей изолированной среде: собственные сценарии, Storage, лимиты, биллинг. И всё это на одном движке, без костылей и хардкода.
RAG превращает ботов в агентов
RAG (Retrieval-Augmented Generation) — не просто модное слово из мира AI. Это то, что превращает статичного бота в интеллектуального агента:
Вместо жёстко прописанных ответов — контекст из векторного хранилища
Вместо "команда → действие" — понимание намерений пользователя
Вместо "не знаю" — поиск по базе знаний и генерация релевантного ответа
PostgreSQL + pgvector, интеграция с агрегаторами моделей, semantic search — всё это превратило платформу из "ядра для ботов" в фреймворк для AI-агентов.
Что дальше?
Эта статья появилась в тот момент, когда Coreness Reborn дошёл до версии 1.0. После сотен патчей, десятков рефакторингов и нескольких архитектурных переработок проект наконец-то реализовал всё, что планировалось на старте:
Мультитенантность работает
Поддержка PostgresSQL и SQLite из коробки
RAG и AI-сценарии интегрированы
GitHub-синхронизация стабильна
Поддержка вебхуков и возможность перехода на API-формат
Scheduled сценарии выполняются по расписанию
Производительность оптимизирована
Документация написана
Платформа работает в проде, обрабатывает запросы, справляется с нагрузкой. Можно выдохнуть.

Планы на ближайшее время — просто отдохнуть. А дальше уже посмотрим, возможно придут новые идеи по фичам или совершенно новым проектам.
На этом всё
Для тех, кто дочитал до конца — спасибо за терпение. Статья получилась большой, но, надеюсь, не скучной.
Если в процессе чтения что-то зацепило — здорово. Если появились вопросы или идеи — ещё лучше. Всегда интересно обсуждать прикладные решения в реальных проектах.
Ссылки
Связь с автором: @vensus137
Вопросы, предложения, сотрудничество
Telegram-канал проекта: t.me/coreness
Иногда автор там что-то пишет
GitHub: https://github.com/Vensus137/Coreness
Подробнее о проекте
Бот: @coreness_bot
Для ознакомления****