Статья четвертая из серии. Было исследование, личная история, продуктовый инсайт. Здесь будет продукт. Публикую манифест до того, как написана первая строчка кода — чтобы потом было честно сравнить, где я прав, а где разбился о реальность.
Большинство AI-ботов — это if-else вокруг GPT
Откройте любой «AI-коуч», «AI-психолог» или «AI-ассистент по саморазвитию». Под капотом почти всегда одно и то же: промпт с инструкцией «веди себя как коуч», пара-тройка условий на кнопки, и молитва, что модель не начнёт галлюцинировать диагнозы. Состояние — в лучшем случае в Redis с TTL на сутки. История — последние N сообщений в контексте. Логика — «спроси у Claude, что делать дальше».
Это работает ровно до первого серьёзного вопроса: а откуда взялся этот вывод обо мне? И тут выясняется, что никакого вывода нет — есть только последний ответ модели на последний запрос, сгенерированный заново, с нуля, без памяти о том, что было вчера.
Я строю продукт, где этот вопрос — центральный. Бот в мессенджере MAX, который ведёт адаптивный диалог с пользователем и строит многослойный профиль его личности. Не тест из 20 вопросов с готовым результатом. Не «спроси у ИИ, какой ты архетип». А система, в которой каждый ответ пользователя — это неизменяемое событие, профиль — read model поверх event log, а LLM — не ядро, а один из узлов, причём не самый важный.
В этой статье — спецификация до первого коммита. Почему event sourcing. Почему инварианты. Почему Stability Engine. И почему я начал с YAML-файла на 700 строк, а не с npm init.
Проблема: статичные тесты и амнезия чат-ботов
Человек не понимает своих паттернов. Это базовая неопределённость, которая сидит в нас почти всё время: как я думаю, что меня реально мотивирует, почему я снова и снова попадаю в один и тот же сценарий, где мои скрытые таланты, что мне на самом деле мешает.
Существующие инструменты отвечают на это плохо. MBTI и соционика — статичны: прошёл тест один раз, получил четыре буквы, забыл. Они не учитывают контекст жизни, не обновляются, не показывают динамику. Психотерапия — медленна и дорога, и даже там месяцы уходят на то, чтобы просто нащупать паттерны. А «AI-коучи» в телеграме страдают той самой амнезией: каждая сессия начинается с нуля, предыдущие разговоры — в лучшем случае в виде summary, который модель сама и галлюцинирует.
Что здесь на самом деле нужно — это система, которая:
— помнит каждый ответ как факт, а не как токены в контекстном окне;
— умеет показать, откуда взялся тот или иной вывод;
— меняет профиль только тогда, когда накопилось достаточно сигналов, а не после одной эмоциональной реплики в 3 часа ночи;
— различает «пользователь в поиске» и «пользователь в кризисе» — и во втором случае молча отходит в сторону, а не продолжает профилировать.
Всё это — не столько про ML, сколько про архитектуру. Поэтому я и начал со спеки.
Концепция: ÆON Map System
Профиль устроен как карта из семи слоёв. Каждый слой — это группа «карт» (cards), которые накапливают сигналы из диалога и в какой-то момент «назначаются» — когда уверенность достигает порога.
Слой I — Core. Когнитивный стиль и поведенческие паттерны. Как человек думает, как принимает решения, какие сценарии повторяет.
Слой II — Emotional & Motivational. Что реально мотивирует, а не что человек декларирует. Ценности-ядро.
Слой IV — Archetype. Архетипическая матрица — не юнговская поп-версия, а композиция из сигналов предыдущих слоёв.
В MVP — три этих слоя. Остальные четыре — стратегический, динамический, интеграционный и мета-слой — добавляются в v1 и v2.
Каждая завершённая сессия добавляет запись в Book of Consciousness — таймлайн трансформации. Финальный артефакт — глиф: уникальное визуальное изображение профиля, которое перегенерируется по мере того, как профиль уточняется.
Важно, что порядок разблокировки слоёв — линейный (сначала I, потом II, потом IV), но маршрутизация сигналов — сквозная: один ответ на вопрос из слоя II может дать сигнал в карту из слоя IV, если модель-классификатор увидит там релевантный паттерн. Это и есть разница между «пройди тест и получи результат» и «живая система, которая слушает всё».
Архитектурное решение: event sourcing, инварианты, стабильность
Теперь к интересному. Почему такая архитектура, а не очередной endpoint с промптом.
Event Core: ответ — это факт, а не состояние
Центральная сущность — Answer. Когда пользователь отвечает на вопрос, рождается событие answer.given с полями: session_id, question_id, answer_value, answered_at. Оно записывается в таблицу events в PostgreSQL, и это append-only: никаких UPDATE, никаких DELETE. Инвариант INV-02, зафиксированный в спеке:
> Answer неизменяем после записи. Пользователь не может редактировать ответ — только пройти новую сессию.
Почему это важно. Если пользователь вчера ответил «я экстраверт», а сегодня — «я интроверт», это не ошибка данных. Это факт: два разных ответа в двух разных контекстах. Профиль должен уметь это держать — не как противоречие, которое надо «исправить», а как сигнал о том, что человек меняется (или что вопрос был плох). Если разрешить редактирование, мы теряем историю, а вместе с ней — возможность видеть динамику.
Профиль (AeonProfile) — это read model, проекция над event log. Его можно пересобрать из нуля в любой момент, прогнав все события через aeon_engine. Это даёт главное: воспроизводимость. Баг в логике построения профиля — не катастрофа, а патч + пересборка проекции.
Инварианты живут в domain/, а не в контроллерах
В спеке — 10 доменных инвариантов и 2 safety-инварианта. Каждый — с полем enforcement: где именно в коде он держится. Несколько примеров:
— INV-03: карта назначается только при confidence ≥ CARD_CONFIDENCE_THRESHOLD (0.72). Проверка — в Stability Engine, не в LLM. Модель предлагает сигнал, решение принимает детерминированный код.
— INV-06: llm.called event пишется в базу до отправки ответа пользователю, а не после. Транзакционно. Если мы упали между вызовом LLM и ответом — у нас есть факт вызова, и мы можем разобраться.
— INV-07: одно сообщение пользователя = не более одного answer.given. Идемпотентность по max_update_id. Ретрай вебхука от MAX не должен давать дубль события.
— INV-09: карта не назначается, пока суммарный вес сквозных сигналов по её типу не перевалит порог. Даже если слой уже разблокирован.
Каждый инвариант покрывается property-based тестом на Vitest + fast-check. Это не unit-тест «проверим один случай», это «сгенерируй 1000 случайных последовательностей событий и проверь, что инвариант держится». Именно такие тесты ловят дыры в логике, которые на примерах не видны.
Stability Engine: где живёт здравый смысл
Отдельный модуль src/stability/, не размазанный по коду. Его работа — контроль порогов, лимитов и safety:
— DAILY_SESSION_LIMIT — 3 сессии в день, защита от злоупотреблений и LLM-расходов.
— CARD_CONFIDENCE_THRESHOLD — нельзя назначать карту на слабых сигналах.
— MIN_ANSWERS_PER_LAYER — 4 ответа минимум, иначе следующий слой не открывается.
— Safety Gate 1 — rule-based проверка на кризисные маркеры (суицид, самоповреждение, насилие) до любого дорогого вызова LLM.
Последнее — критично. Многие делают наоборот: отправляют сообщение в LLM, и уже он «решает», кризис это или нет. Но это значит платить за токены на каждом кризисном сообщении и доверять детекцию стохастической модели. Я делаю rule-based (словари маркеров + лёгкий классификатор) в детерминированном коде, с отдельными тестами. Если Gate 1 сработал — бот отвечает тепло и даёт телефон доверия 8-800-2000-122, и никакой LLM не вызывается. Gate 2 — в system prompt основного LLM, как второй рубеж для edge cases.
PDA как методология
Вся спека написана по методологии PDA — Possibility-Driven Architecture. В двух словах: сначала строится карта неопределённости (user-level и system-level), потом граф домена, потом инварианты, константы, события, архитектура, наблюдаемость. Код — в самом конце. Идея в том, что приложение — это машина снижения неопределённости: пользователь приходит с вопросами о себе, уходит с чуть более устойчивой моделью себя. Архитектура проектируется под это, а не под «как быстрее написать CRUD».
Отсюда — все решения выше. Event sourcing — чтобы любая итерация снижения неопределённости была обратимой. Инварианты — чтобы система не ломалась на входных данных, которые проектировщик не предвидел. Stability Engine — потому что есть класс решений, которые LLM принимать не должен никогда.
Почему LLM — периферия, а не ядро
Вот архитектурная схема, которая показывает поток: MAX webhook → адаптер → Stability Engine → Dialog Engine → Event Core. LLM вызывается параллельно, из Dialog Engine, и его вызов логируется как событие до отправки ответа. Aeon Engine работает только с event store — он не имеет доступа к LLM и не должен иметь.

Контринтуитивный тезис: LLM в этой системе — не самый важный компонент. Самые важные — Event Core и Stability Engine. LLM можно заменить (Claude → OpenAI — fallback уже предусмотрен). Event Core и инварианты заменить нельзя — на них держится весь смысл.
Это не про «ИИ плохой» и не про «давайте всё делать на правилах». Это про разделение: что именно должен решать LLM, а что — нет.
LLM решает: как сформулировать следующий вопрос так, чтобы он был живым и адаптивным; как интерпретировать свободный текст ответа; как сгенерировать глиф (через DALL·E); как написать финальный текстовый профиль.
LLM не решает: когда назначать карту; когда прекращать сессию из-за кризиса; когда пользователь исчерпал лимит; какой слой разблокировать следующим; что такое «достаточная уверенность». Всё это — детерминированный код с тестами.
Такое разделение даёт две вещи, которые дороги в продукте. Первая — предсказуемость: поведение системы в edge-кейсах определено правилами, а не настроением модели. Вторая — дешевизна эксплуатации: если половина решений принимается до вызова LLM, расходы на API падают кратно.
Что дальше: трансляция сборки
План такой. Всего итераций — восемь, от нулевой (скелет: Docker, PostgreSQL, «привет» в MAX) до седьмой (safety, глиф, share-карточка). Каждая итерация — отдельная ветка feat/iter-N, закрытие итерации — merge в main и новая статья или короткий апдейт.
Формат трансляции — гибрид: первая из трёх больших статьей здесь, между ними — короткие апдейты со скриншотами и ссылками на коммиты. Репозиторий открытый, GitHub Issues и Projects — публичные.
Что будет во второй статье: итерация 1 и 2 — Event Core в PostgreSQL (append-only таблица с constraint-проверками, которые гарантируют INV-02 на уровне базы), идемпотентность по max_update_id, первый вопрос через Dialog Engine, property-based тесты на инварианты. С рабочим кодом, миграциями и разбором граблей.
Если вам резонирует подход «спека до кода, инварианты до фич» — подписывайтесь, будет подробно. Если считаете, что всё это overengineering для бота в мессенджере — тоже подписывайтесь, через пару итераций проверим.
Спека целиком лежит в репозитории в файле aeon-max-bot.vibepp.yaml — 700 строк машиночитаемого YAML, из которого Cursor-агент в принципе может восстановить половину кода. В репо уже есть что показать. Подписывайтесь, чтобы не пропустить.
Статус

—
Что думаете про такой подход? Если у вас есть опыт event sourcing в AI-продуктах — особенно интересно, где я ещё не вижу подводных камней. Критика приветствуется.
ссылка на оригинал статьи https://habr.com/ru/articles/1027210/