Перейти к содержимому

9axis: Telegram-бот без ИИ, минута вечером, девять шкал

Константин Потапов
8 мин

Запустил @nineaxis_bot: ежедневное самонаблюдение по девяти шкалам. В рантайме нет LLM. Rule-based тексты в MDX, aiogram 3, FastAPI, Celery. Бот в проде, цифры пока не показываю.

9axis: Telegram-бот без ИИ, минута вечером, девять шкал

Контроль разгоняется неделями и однажды становится выгоранием. Границы проваливаются тихо, через месяц находится обида. Постфактум всё видно слишком ясно. В моменте нет. Между «всё ок» и «пора к специалисту» дыра, в которой ничего не происходит.

Туда я положил @nineaxis_bot на 9axis.ru. Минута вечером, одна практика на день, в воскресенье что вышло за неделю. Никакого ИИ, никаких диалогов «как психолог», никаких диагнозов. Карточка проекта здесь.

Бот в проде, первые пользователи есть. Слушаю и правлю мелочи. Цифр пока не будет: данных мало, а изображать урожай рано.

Эквалайзер, не термометр

Не «настроение 7 из 10». Девять ползунков. Пока все в середине, звучит. Кто-то уехал в край, звук начинает резать.

КодШкала1–34–78–10
self_worthСамоценностьСамоуничижениеЗдоровая уверенностьНарциссизм
trustДовериеИзоляция, гиперконтрольРазумная открытостьНаивность, зависимость
boundariesГраницыУгодничествоЧёткие и гибкиеЖёсткость, недоступность
exchangeОбменОтдаю всё, беру ничегоВзаимностьПотребляю, не отдаю
expressionПроявленностьНевидимостьУместное присутствиеПерформанс, навязчивость
radicalismРадикализмИзбыточная осторожностьВзвешенность«Всё или ничего», импульс
honestyЧестностьЛожь себеЯсность без жестокостиТравмирующая прямота
opennessОткрытостьРигидностьГибкость и любопытствоХаотичность
controlКонтрольБезответственностьУправляю чем могуГиперконтроль, держу всё руками

Раз в день человек отмечает кнопками от 1 до 10. Три шкалы по своей цели или все девять. Текстового ввода в пользовательских сценариях нет: только кнопки, только вниз. Бот считает отклонение от зоны 4–7, выбирает главный перекос дня, кладёт короткую интерпретацию и одно действие. Не «работай над доверием», а «делегируй одну задачу и намеренно не перепроверяй результат». Дальше две кнопки: взял или не сейчас.

Вечером follow-up: получилось? Да, частично, нет. Можно пропустить на любом шаге. Всё ложится в PracticeLog. Через месяц будет видно, какие практики стреляют, какие мимо.

Продукт не объясняет, почему ты такой. Не лечит. Одна задача: заметить раньше, чем накопится. Остальное в дисклеймере онбординга и в crisis-сценарии.

Три гипотезы

Вернётся ли человек завтра, если check-in короткий и бот не гундит.

Откроет ли воскресный обзор, чтобы увидеть свой график, а не получить «совет на сегодня».

Хватит ли правил, чтобы текст не отдавал гороскопом. В 2026 ткнуть LLM куда-нибудь рефлекс. Генеративный текст про психику это русская рулетка: сегодня умно, завтра стыдно. Плюс токены, латенси и невоспроизводимость одним пакетом.

Поэтому правила, заготовленные тексты и немного арифметики.

Почему без модели

Весь контент лежит в docs/content/ как MDX: 9 шкал, 27 интерпретаций диапазонов, 90+ практик, combined rules, онбординг, шаблоны weekly review. Изменение текста это pull request. CMS здесь мешала бы.

На одни и те же значения бот отдаёт одну и ту же интерпретацию, с точностью до выбора из пула практик зоны. Если во вторник и в четверг в одном состоянии приходят противоположные трактовки, доверие схлопывается.

Один check-in это запись в БД и пара условий. Ни токенов, ни «модель не ответила в пятницу вечером».

Тексты пишет человек. «У вас выраженный компенсаторный паттерн контроля» и «похоже, ты сейчас слишком удерживаешь всё вручную» это разная редполитика. Первое в промпт само заползает. Второе стабильно не зашьёшь.

Оборотная сторона: контент бутылочное горлышко. Около 145 единиц текста рукой. Если писать их после кода, релиз уезжает в «когда допишу». Бриф шёл параллельно с бэкендом. Валидатор ловил «забыл high_zone_label» до коммита. У практики жёсткая форма: одно действие, без обобщений.

Если бы делал сегодня, стартовал бы с пяти шкал, не с девяти. Гипотеза проверяется та же, контента вдвое меньше. Девять шкал были данью красоте модели, не требованием MVP.

Как собрано

Python 3.13, одна кодовая база, три процесса. Бот на aiogram 3. FastAPI поднимает лендинг на Jinja2, /health, /metrics, webhook и SQLAdmin на одном порту. Celery с Beat шлёт утренние реминдеры, вечерние follow-up, воскресный обзор и раз в сутки подметает удалённые аккаунты. Рядом PostgreSQL 16, Redis 7, SQLAlchemy 2 async, Alembic.

Хэндлеры тонкие: принять клик, дёрнуть сервис, вернуть клавиатуру. Сервисы не знают ни aiogram, ни FastAPI, поэтому покрываются обычным pytest.

Контента в БД нет. При старте content/loader.py парсит MDX, валидирует dataclasses, кладёт bundle в dp["content_bundle"]. Источник правды Git. Минус один: любое изменение текста требует редеплоя. Для MVP, где редактор я сам, это скорее защита: никто не правит формулировки в три ночи с телефона.

Сначала combined rules, пары и тройки вроде «control ≥ 8 и trust ≤ 3». У правила приоритет. Если ничего не совпало, падаем на зону одиночной шкалы. Шкалы, приоритетные для цели, получают коэффициент 1.3, чтобы «бесит начальник» не перебивал «падает самоценность», если человек пришёл не за этим.

def deviation(value: int) -> int:
    if 4 <= value <= 7:
        return 0
    if value < 4:
        return 4 - value
    return value - 7
 
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. При ничьей приоритетная для цели. Ни embeddings, ни «доверимся модели». Любую выдачу можно разобрать по логам.

Когда человек жмёт «возьму в работу», в PracticeLog пишется follow_up_due_at. Утренний check-in даёт плюс 6 часов, дневной ближайшее вечернее окно (по умолчанию 19:00–21:00, настраивается в /settings), поздний вечерний съезжает на утро. Celery Beat забирает готовые follow-up атомарно через SELECT ... FOR UPDATE SKIP LOCKED. Сверху advisory lock на пользователя. Схема переживает рестарт воркера и двойной Beat. Двойной follow-up пока не случался. Инфраструктура ждёт, что случится.

SQLAdmin прикручен к FastAPI. Логин, аудит, readonly dashboard, User / CheckIn / PracticeLog / WeeklySummary, один action: включить is_premium. Автооплат нет: пока нет устойчивого спроса на ручной upgrade, биллинг это гонка за чужим фокусом. /admin закрыт access-листом на Nginx Proxy Manager, из интернета его не видно.

/export_data отдаёт JSON со всем, что есть по человеку. /delete_account делает soft-delete: telegram_id обнуляется, deleted_at ставится, сообщения прекращаются сразу. Через 30 дней Celery вычищает физически. Тридцать дней хватает, чтобы «удалил сгоряча» ещё можно было вернуть, и мало, чтобы это выглядело как «мы всё равно храним».

CrisisMiddleware ловит фразы про суицид, самоповреждение и острую дезориентацию. Прерывает любой сценарий и отдаёт заготовленный текст с телефоном горячей линии. Ложные срабатывания готов терпеть. Умную детекцию кризиса делать не буду. Это работа специалистов. Задача бота чисто выйти и не мешать.

Два сервера и Telegram из РФ

Внутри docker-compose.prod.yml: API, bot, celery, Postgres, Redis. Снаружи Nginx Proxy Manager: SSL, прокси на API, access-list на /admin.

Webhook хорош, когда до вашего HTTPS Telegram спокойно дотягивается. В текущих условиях «спокойно» сказано громко. Основной режим polling через свой прокси: AmneziaWG, сверху microsocks как socks5.

USE_WEBHOOK=false
TELEGRAM_PROXY_URL=socks5://tg-proxy:1080

aiogram умеет прокси через AiohttpSession(proxy=...). Один аргумент в create_bot(), дальше код про это не знает. Когда ситуация выровняется, флипну USE_WEBHOOK=true. Пока polling спокойнее спит.

Ruff, mypy и pyright в make check. Pre-commit ставится на make install. Юниты без БД, интеграция на живом Postgres из make up с alembic upgrade head. make content-check валидирует MDX. Alembic autogenerate только как черновик. В GitLab CI тот же набор команд. Деплой в прод ручной, чтобы не уехать в пятницу вечером.

Что уже видно

Кнопки 1–10 отжимают без жалоб. Я боялся, что после трёх смайлов десять позиций покажутся лишними. Когда человеку есть что отметить, ткнуть 7 проще, чем выбирать из «smile-meh-sad».

Follow-up самое слабое звено. Часть людей не отвечает на вечернее «как получилось?». follow_up_response_rate задумывался метрикой дисциплины, не станком на 100%. Подозреваю, несколько процентов лежат в тайминге: у большинства дефолтное окно, а дефолт редко оптимален.

Связка «контроль высокий, доверие низкое» и текст «держишь всё вручную, потому что не веришь, что без тебя не развалится» иногда встречает «как будто обо мне». Это не магия. Это внимательные правила.

Онбординг живой, пока короткий: 2–3 сообщения, дисклеймер, цель, стартовый check-in. Всё длиннее режу. Каждое лишнее сообщение на старте минус процент активаций.

Правлю тексты и тайминги. Архитектуру не трогаю, она тащит.

Свободного чата, «поговори со мной» и достижений не будет. Инструмент должен оставаться инструментом. Автоплатежи подключу, когда увижу спрос на ручной premium. Пока спроса нет.

Узкое место тексты, не код. Если делаете продукт с контентом, пишите его вместе с бэкендом. Ждать «когда код будет готов» надёжный способ сдвинуть релиз на два месяца.