Зачем ещё один Telegram-бот
Контроль разгоняется неделями — пока однажды не превращается в выгорание. Границы проваливаются тихо — пока через месяц не обнаруживается накопленная обида. Постфактум всё видно даже чересчур ясно. А в моменте — нет. И между «всё ок» и «пора к специалисту» зияет пустота, в которой ничего не происходит. Туда и захотелось положить хоть какой-то инструмент.
Так родился @nineaxis_bot на 9axis.ru. Минута вечером, одна практика на день, в воскресенье — что получилось за неделю. Никакого ИИ, никаких диалогов «как психолог», никаких диагнозов.
Это не победа и не кейс про деньги. Бот в проде, есть первые пользователи, я слушаю фидбек и правлю мелочи. Цифры покажу позже — пока смотрю.
Идея: эквалайзер вместо термометра
Метафора простая. Представьте эквалайзер в плеере: ряд ползунков, каждый отвечает за свою частоту. Пока все в разумной середине — звучит нормально. Кто-то уехал в крайность — звук начинает резать.
С состояниями человека я применил ту же оптику. Не «настроение 7 из 10», а девять отдельных осей, каждую из которых можно подвинуть влево или вправо. Каждая ось — отдельный аспект состояния, и каждая по-своему даёт о себе знать, когда уезжает в крайность.
| Код | Шкала | Перекос влево (1–3) | Баланс (4–7) | Перекос вправо (8–10) |
|---|---|---|---|---|
self_worth | Самоценность | Самоуничижение | Здоровая уверенность | Нарциссизм |
trust | Доверие | Изоляция, гиперконтроль | Разумная открытость | Наивность, зависимость |
boundaries | Границы | Угодничество | Чёткие и гибкие | Жёсткость, недоступность |
exchange | Обмен | Отдаю всё, беру ничего | Взаимность | Потребляю, не отдаю |
expression | Проявленность | Невидимость | Уместное присутствие | Перформанс, навязчивость |
radicalism | Радикализм | Избыточная осторожность | Взвешенность | «Всё или ничего», импульс |
honesty | Честность | Ложь себе | Ясность без жестокости | Травмирующая прямота |
openness | Открытость | Ригидность | Гибкость и любопытство | Хаотичность |
control | Контроль | Безответственность | Управляю чем могу | Гиперконтроль, держу всё руками |
Раз в день человек отмечает, где он по каждой шкале — кнопкой от 1 до 10. 1–3 — перекос влево, 4–7 — зона баланса, 8–10 — перекос вправо. Из этих оценок бот и собирает свою короткую «обратную связь дня»: где сегодня сильнее всего уехало, что это обычно значит, одно конкретное действие, которое можно взять в работу.
Продукт не объясняет, почему ты такой. Не пытается лечить. У него одна задача — помочь заметить раньше, чем накопится.
Гипотеза
Проверяю три вещи одновременно.
Ритуал. Вернётся ли человек завтра, если check-in правда короткий и бот не гундит. Полминуты, пять кнопок, ушёл.
Динамика. Захочет ли человек открыть воскресный обзор. Не ради «совета на сегодня», а чтобы увидеть свой график — где штормило, где выровнялось.
Смысл без ИИ. Хватит ли rule-based движка, чтобы интерпретация не отдавала гороскопом. Это самый спорный кусок: в 2026 ткнуть LLM куда-нибудь — рефлекс. Но генеративный текст про психику — это всегда немного русская рулетка: сегодня умно, завтра странно, послезавтра что-то, за что стыдно. Плюс токены, латенси и невоспроизводимость одним пакетом.
Поэтому — правила, заготовленные тексты и немного арифметики.
Цепочка одного дня
Что видит пользователь вечером.
Сначала нужно отметить состояние — три шкалы по своей цели или все девять, кнопками 1–10. Текстовый ввод в пользовательских сценариях отсутствует как класс: только кнопки, только вниз. Дальше бот считает взвешенное отклонение каждой шкалы от балансной зоны (4–7) и выбирает одну — главный перекос дня. Шкалы, приоритетные для цели, получают коэффициент ×1.3, чтобы «бесит начальник» не перебивало «падает самоценность», если человек пришёл сюда не за этим.
На выбранную шкалу и её соседей движок находит интерпретацию — сначала по combined rules (пары и тройки шкал, у каждого правила свой приоритет), и только если ничего не совпало, падает на интерпретацию одиночной зоны. Под интерпретацию подкладывается одна практика: не «работай над доверием», а «делегируй одну задачу и намеренно не перепроверяй результат». Дальше — две кнопки: взял или не сейчас.
Вечером приходит follow-up: «получилось?». Если «да» или «частично» — вопрос про пользу. Нет — тоже нормально, можно пропустить на любом шаге. Всё, что человек нажимает, ложится в PracticeLog: вот тут пригодится, когда через месяц захочется посмотреть, какие практики у него стреляют, а какие стабильно мимо.
Бот не лечит и не объясняет, почему у тебя так. Он помогает заметить раньше и среагировать. Всё остальное — в дисклеймере онбординга и в crisis-сценарии.
Почему без ИИ
Главный архитектурный выбор, к которому пришёл не за день.
Весь контент — 9 шкал, 27 интерпретаций диапазонов, 90+ практик, combined rules, сообщения онбординга, шаблоны weekly review — лежит в docs/content/ как MDX. Изменение текста — это pull request. История правок, code review контента, откат изменения одной командой. CMS мне тут не нужна и, если честно, мешала бы.
Второе — воспроизводимость. На одни и те же значения шкал бот отдаёт одну и ту же интерпретацию и одну и ту же практику (с точностью до выбора из пула практик зоны). Для продукта про психику это не «nice to have». Если человек во вторник и в четверг в одном и том же состоянии получит противоположные трактовки, доверие к инструменту схлопывается.
Третье — деньги и скорость. Один check-in = одна запись в БД и пара if-ов. Ни токенов, ни «давай ещё раз, модель не ответила», ни провайдер-даунов в пятницу вечером.
И четвёртое, самое недооценённое — редполитика. Тексты пишет и редактирует человек. Сравните:
— «У вас выраженный компенсаторный паттерн контроля, коррелирующий с базовым дефицитом доверия». — «Похоже, ты сейчас слишком удерживаешь всё вручную».
Второе в промпт стабильно не зашьёшь. А первое в продукте про состояние — это способ быстро потерять пользователя.
Оборотная сторона всего этого — контент превращается в бутылочное горлышко. К нему ещё вернёмся.
Архитектура
Питон, одна кодовая база, три процесса.
Процессов действительно три, но код у них общий. Бот на aiogram разговаривает с Telegram — в дефолте через polling, по флагу можно в webhook. FastAPI поднимает лендинг на Jinja2, /health, /metrics, webhook endpoint и SQLAdmin — всё на одном порту. Celery с Beat занимается всем, чему некому напомнить: отправляет утренние реминдеры, вечерние follow-up, собирает weekly review по воскресеньям и раз в сутки подметает за удалёнными аккаунтами.
Внутри — скучно и предсказуемо:
src/
bot/ # handlers, keyboards, middlewares, states
api/ # webhook, health, metrics, landing
admin/ # SQLAdmin
core/ # config, database, redis, logging, metrics, locks
models/ # SQLAlchemy ORM
repositories/ # единственное место, которое знает SQL
services/ # бизнес-логика — без aiogram и FastAPI
tasks/ # Celery
content/ # загрузчик и парсер MDX
Хэндлеры тонкие — ровно принять клик, дёрнуть сервис и вернуть клавиатуру. Бизнес-логика — в services/. Сервисы ничего не знают ни про aiogram, ни про FastAPI, поэтому покрываются обычным pytest без мок-бота и без специального бэкенда тестов.
Контент, которого нет в БД
Контент в БД не лежит совсем. Ни одной таблицы, ни одной миграции. При старте приложения content/loader.py проходит по docs/content/, парсит все MDX, валидирует структуру через типизированные dataclasses и собирает bundle. Дальше этот bundle живёт в dp["content_bundle"] и используется сервисами интерпретации и рекомендаций.
Источник правды один — Git. Между окружениями ничего не дрейфует, миграций контента не существует, а make content-check ловит косяк до того, как он доедет до пользователя. Единственный минус — любое изменение контента требует редеплоя. Для MVP, где я пока сам себе редактор, это скорее фича: никто не правит тексты на проде в три ночи с телефона.
Rule-based engine
Два слоя, оба скучные.
Сначала проверяются combined rules — условия на пары и тройки шкал, записанные в MDX, вроде «control ≥ 8 И trust ≤ 3». У каждого правила свой приоритет. Побеждает первое совпадение с наивысшим. Если ни одно не выстрелило — падаем на интерпретацию одиночной шкалы по её зоне.
Главный перекос считается вот так:
def deviation(value: int) -> int:
if 4 <= value <= 7:
return 0
if value < 4:
return 4 - value # 3 → 1, 2 → 2, 1 → 3
return value - 7 # 8 → 1, 9 → 2, 10 → 3
def weighted(value: int, scale_code: str, goal: str) -> float:
base = deviation(value)
return base * (1.3 if scale_code in GOAL_SCALE_MAP[goal] else 1.0)Побеждает шкала с максимальным weighted. При ничьей — приоритетная для цели. Ни ML, ни embeddings, ни «доверимся модели». Зато любую выдачу бота я могу разобрать построчно по логам.
Follow-up state machine
Самое скучное снаружи и самое нервное внутри.
Когда человек жмёт «возьму в работу», в PracticeLog пишется follow_up_due_at. Время считается по простому правилу: утренний check-in даёт +6 часов, дневной — ближайшее вечернее окно (по умолчанию 19:00–21:00, настраивается через /settings), поздний вечерний — съезжает на утреннее напоминание следующего дня. Celery Beat каждые несколько минут просыпается и забирает готовые follow-up-ы атомарно через SELECT ... FOR UPDATE SKIP LOCKED, помечая claimed_at и worker_id. Сверху — advisory lock на пользователя, чтобы два воркера не полезли одновременно.
Схема переживает рестарт воркера, двойной запуск Beat и разбор полётов с «а почему follow-up пришёл дважды». Последнее пока не случалось, но инфраструктура готова к тому, что случится.
Админка
SQLAdmin прикручен прямо к FastAPI, отдельного сервиса нет. Логин с паролем, аудит-лог, readonly dashboard, model views на User / CheckIn / PracticeLog / WeeklySummary и один action — включить/выключить is_premium. Автооплат в MVP нет сознательно: пока не видно устойчивого спроса на ручной upgrade, строить биллинг — это заранее проиграть гонку за фокус. Доступ к /admin закрыт access-листом на Nginx Proxy Manager, так что из интернета он просто не виден.
Privacy и crisis
Две вещи, на которых не хотелось ошибиться.
По /export_data бот отдаёт пользователю JSON со всем, что по нему есть — от timezone до каждой отметки в check-in. По /delete_account делается soft-delete: telegram_id зануляется, deleted_at проставляется, сообщения перестают уходить мгновенно. А через 30 дней отдельная Celery-задача вычищает данные физически. Тридцать дней — компромисс: достаточно, чтобы «удалил сгоряча, хочу вернуть», и недостаточно, чтобы это выглядело как «мы всё равно всё храним».
С кризисом — отдельный CrisisMiddleware в aiogram. Keyword-matching по фразам про суицид, самоповреждение и острую дезориентацию. При совпадении middleware прерывает любой текущий сценарий и отдаёт заготовленный emergency message с телефоном горячей линии. Ложные срабатывания я готов терпеть: лучше один раз лишний раз сказать «это не ко мне, пожалуйста, к специалисту», чем в неподходящую минуту предложить человеку «практику дня».
Делать «умную» детекцию кризиса я не собираюсь. Это работа специалистов, не middleware. Задача бота — чисто выйти из сценария и не мешать.
Деплой: два сервера и нюанс РФ
В проде два сервера. Внутренний гоняет docker-compose.prod.yml: API, bot, celery, Postgres, Redis — всё рядом. Внешний — Nginx Proxy Manager: SSL-терминация, проксирование на A:${API_PORT}, access-list на /admin. Так я получаю SSL и закрытую админку, не затаскивая Nginx в каждый контейнер и не разбираясь с certbot-ом.
Отдельная тема — работа с Telegram API из РФ. Webhook работает, когда есть стабильный публичный HTTPS, до которого Telegram спокойно дотянется. В текущих условиях «стабильный» — это громко сказано, поэтому основной режим в проде — polling через свой прокси: AmneziaWG поверх VPN-шлюза, microsocks сверху в роли socks5. Бот подключается к Telegram через него:
USE_WEBHOOK=false
TELEGRAM_PROXY_URL=socks5://tg-proxy:1080aiogram про прокси умеет через AiohttpSession(proxy=...) — один аргумент в create_bot(), больше код про это ничего не знает. Когда ситуация стабилизируется — флипну USE_WEBHOOK=true и сэкономлю на пинге. А пока polling спокойнее спит ночами.
Quality gate
В одиночном проекте дисциплина — это не добродетель, а инстинкт самосохранения: не поставил — через месяц читаешь собственный код как чужой.
Ruff, mypy и pyright собраны в make check — если горит хоть одна стадия, коммит не улетает. Pre-commit hook ставится на make install, поэтому обходить его лениво. Тесты бьются на две кучи: unit без БД и integration, который поднимается поверх настоящего Postgres из make up, накатывая схему через alembic upgrade head в conftest. make content-check валидирует MDX. Миграции — Alembic, autogenerate только как черновик: финал вычитываю руками, потому что async SQLAlchemy умеет вычудить странного. В GitLab CI крутится ровно тот же набор команд, что и локально — никаких «а на CI по-другому». Деплой в прод — последняя стадия с ручным триггером, чтобы не уехать в продакшен на автомате в пятницу вечером.
Контент — настоящее узкое место
В PRD это аккуратно названо «главным риском MVP». На практике — это ~145 единиц текста, которые нужно написать рукой, не ИИ. Шкалы, интерпретации, практики, combined rules, онбординг, шаблоны еженедельника, paywall, emergency message. Если начать писать их после того, как код уже готов — релиз сдвигается на неопределённое «когда допишу». Я это знал заранее и всё равно недооценил.
Что сработало. Во-первых, контент-бриф пошёл параллельно с бэкендом, а не после: пока я собирал rule-based движок, рядом в соседнем файле росли интерпретации и практики. Во-вторых, docs/content/_spec.md зафиксировал обязательные поля у каждого типа сущности, а валидатор ловил «забыл high_zone_label у шкалы X» ещё до коммита. В-третьих, у практики очень жёсткая форма: одно действие, без обобщений. «Делегируй одну задачу и не перепроверяй» — да. «Работай над доверием» — мусор, удалить. Это не только про качество текста, но и про скорость письма: когда форма ограничена, писать сильно быстрее.
Если бы делал сегодня — стартовал бы с 5 шкалами, а не с 9. Продуктовая гипотеза проверяется ровно той же, а контента вдвое меньше. Девять шкал — это была дань красоте модели, а не требование MVP. Записал на будущее.
Что уже видно
Цифр пока сознательно не даю — данных мало. Но фактура просматривается.
Кнопки 1–10 люди отжимают без жалоб. Я боялся, что после привычных «smile-meh-sad» десять позиций покажутся избыточными — нет, не кажутся. Похоже, когда человек действительно хочет что-то про себя отметить, ему проще ткнуть 7 из 10, чем выбирать из трёх смайлов.
Follow-up — самое слабое звено. Часть людей не отвечает на вечернее «как получилось?». Это ожидаемо: follow_up_response_rate как раз и задумывался метрикой дисциплины, а не инструментом выжимания 100%. Подозреваю, там можно отыграть несколько процентов просто нормальным таймингом — сейчас настройки вечернего окна у большинства дефолтные, а дефолт никогда не оптимален.
Combined rules отрабатывают лучше, чем я ожидал от «каких-то правил на if-ах». Когда бот ловит связку «контроль высокий + доверие низкое» и выдаёт «держишь всё вручную, потому что не веришь, что без тебя не развалится» — люди пишут «как будто обо мне». Это не магия, это внимательная работа над самими правилами. Rule-based движок не обязан быть тупым.
Онбординг живой ровно за счёт того, что короткий: 2–3 сообщения, дисклеймер, выбор цели, стартовый check-in. Всё, что длиннее, режу без сожаления. Каждое лишнее сообщение на старте — минус процент активаций.
Правлю по мелочи: тексты, тайминги follow-up, формулировки еженедельника. Архитектуру не трогаю — она тащит без напряжения.
Что дальше
Запас сценариев держу, но не разгоняюсь.
Автоплатежи подключу, когда увижу стабильный спрос на ручной апгрейд до premium. Пока спроса на ручной апгрейд нет — строить биллинг значит заранее проиграть фокусу. Темы и треки — пакеты практик под конкретные задачи вроде тревоги и выгорания — следующий логичный шаг, как только базовый контур устоится. Расширенный weekly review с трендами по шкалам имеет смысл, когда у аудитории накопятся данные хотя бы за 4 недели. OG-картинки и SEO лендинга висят в техдолге и дождутся своего вечера.
Чего точно не будет: свободного чата с ботом, LLM-«поговори со мной», уровней и достижений. Инструмент должен оставаться инструментом.
Что вынес
Rule-based в 2026 — нормальная стратегия, а не ретроградство. Особенно там, где важны воспроизводимость и редполитика. LLM-ом всё решать не надо, и уж точно не в продукте про психику.
Контент как код работает не только для технических курсов. Для психологического продукта — работает даже лучше CMS: каждое изменение текста проходит через твой же ревью и попадает в Git с историей.
Связка двух серверов + NPM — минимально разумный прод для одиночного проекта. Один сервер — страшно держать всё в одной точке отказа, Kubernetes — избыточно в три этажа.
Polling через свой прокси в нашей географии честнее вебхука: меньше движущихся частей снаружи, меньше «сегодня Telegram не достучался». Webhook дождётся своего релиза — когда это перестанет быть головной болью.
Узкое место — тексты, а не код. Если делаете что-то с контентом, начинайте писать его параллельно с бэкендом. Ждать «когда код будет готов» — это способ надёжно сдвинуть релиз на два месяца.
См. также
- Карточка проекта 9axis
- Терапия через код: два стартапа и ноль пользователей — честная история про самообман инженера
- MDX как альтернатива CMS
- FastAPI vs Django в 2025



