Блог
Публикации о процессе разработки, решённых задачах и изученных технологиях
DNS и кэш: охота на запись, которая вроде есть, но не видна
# Охота на привидения в DNS: как потерянная запись чуть не сломала аутентификацию Проект `borisovai-admin` — это админ-панель с полноценной системой аутентификации. Задача казалась простой: настроить поддомены для разных частей приложения. `admin.borisovai.tech` уже работал безупречно, а вот `auth.borisovai.tech` и `auth.borisovai.ru` упорно отказывались резолвиться. Казалось, что может быть проще — добавить записи и забыть? Не так всё оказалось. Первым делом я начал с базовой диагностики. Проверил `nslookup` и `dig` — и вот тут началось веселье. `auth.borisovai.tech` **не резолвился с локального DNS**, а вот Google DNS (8.8.8.8) прекрасно возвращал `144.91.108.139`. Это был явный признак того, что проблема не в глобальной DNS-иерархии, а где-то рядом. Полез в DNS API — может, записи просто не создались? Нашёл в базе пусто: `records: []`. Автоматического создания не было. Но вот странный момент — `admin.borisovai.tech` работал без проблем. Почему одна запись есть в DNS API, а другая нет? Начал разбираться в истории создания записей. Выяснилось, что **`admin.borisovai.tech` был добавлен напрямую у регистратора**, минуя DNS API. А вот `auth.*` я стал добавлять через API, и тут столкнулся с интересным поведением: локальный AdGuard DNS (94.140.14.14), который использовался как основной рекурсивный резолвер, **кэшировал старые данные** или просто не видел новые записи из-за задержки распространения. Это была классическая ловушка DNS-администраторов: разные пути создания записей приводят к рассинхронизации. Когда у тебя есть несколько источников истины (регистратор, API, локальный кэш), они начинают рассказывать разные истории. Сервис аутентификации попадал на несуществующий адрес, и всё падало в момент, когда требовалась верификация токена. **Интересный факт**: DNS работает на принципе давности — записи имеют TTL (Time To Live), и если кэширующий резолвер уверен, что всё верно, он будет возвращать старые данные до истечения TTL, даже если на авторитативном сервере уже всё изменилось. В нашем случае TTL был достаточно высоким, поэтому AdGuard упорно держался за мысль, что `auth.borisovai.tech` не существует. Решение: я привёл все записи в единую систему — создал их через DNS API, настроил правильные TTL (600 секунд вместо 3600) и добавил явное перенаправление с `auth.borisovai.ru` на основной домен. Проблема испарилась. Главный урок: **в распределённых системах всегда проверяй, откуда берётся каждый кусок информации**. DNS выглядит просто, пока не начнёшь складывать вместе мнения разных серверов. И да, не забывай очищать кэш после изменений — Google DNS обновляется быстрее, чем AdGuard, и это сейчас спасало жизнь. 😄 **А знаешь, почему DNS никогда не ходит к психологу?** Потому что у него всё равно проблемы с разрешением.
SSH спасла двухфакторку: как найти потерянный QR-код Authelia
# Черный экран Authelia: как SSH-команда спасла двухфакторку **borisovai-admin** требовал двухфакторную аутентификацию, и это казалось решённой задачей. Authelia — проверенная система, документация подробная, контейнер поднялся за минуты. Порты открыты, сертификаты в порядке, логи молчат. Всё отлично. До тех пор, пока тестировщик не нажал кнопку «Register device». Экран почернел. Точнее, остался белым, но QR-кода не было. Никакого движения, никакой реакции системы. Браузерная консоль чистая, сетевые запросы проходят успешно, API отвечает кодом 200. Authelia делает свою работу, но что-то между сервером и пользователем теряется. Первым делом я прошёлся по классическому чек-листу: проверил конфигурацию сервера, пересмотрел логи Authelia в Docker, убедился, что все environment переменные заполнены правильно. Всё было на месте. Но QR-код так и не появился — ни в интерфейсе, ни в devtools браузера. Вот тут я заметил деталь в конфигурации, которую раньше пропустил: `notifier: filesystem`. Это не SMTP, не SendGrid, не какой-то облачный сервис. Это самый примитивный режим — Authelia просто пишет уведомления в текстовый файл на сервере. Мысль пришла сама собой: *а что если система работает правильно, но уведомление просто не попадает к пользователю?* Подключился по SSH на сервер и выполнил одну команду: ``` cat /var/lib/authelia/notifications.txt ``` И там она была! Полная ссылка вида `https://auth.borisovai.tech/...token...` — именно та, которая должна была привести к QR-коду. Authelia делала всё правильно. Она генерировала ссылку, защищала её токеном и записывала в лог-файл. Просто в локальной разработке по умолчанию уведомления идут не пользователю, а в файловую систему. Открыл эту ссылку в браузере — QR-код мгновенно появился. Сканировали в Google Authenticator, всё сработало с первой попытки. **Вот интересный момент про Authelia**: `notifier: filesystem` — это не костыль и не режим отладки. Это *очень удобная фишка для локальной разработки*. Вместо настройки SMTP-сервера или интеграции с внешним сервисом доставки уведомлений система просто пишет ссылку в файл. Быстро, просто, без зависимостей. Но в продакшене эта фишка становится ловушкой: система работает идеально, а пользователи видят только чёрный экран. Теперь в конфигурации проекта есть комментарий про `filesystem` notifier и команда для проверки уведомлений. Следующий разработчик не будет искать потерянный QR-код в файловой системе. И это главное — не просто исправить баг, но оставить подсказку для будущего себя и команды. **Урок простой**: иногда самые очевидные решения скрыты в одной строке документации, и они работают ровно так, как задумано инженерами. SSH остаётся лучшим другом разработчика 😄
1,3% точности и целая эпопея оптимизации нейросети
# Когда 1,3% точности — это целая эпопея оптимизации Проект **llm-analisis** уже был в финальной стадии обучения нейросети на CIFAR-10, когда я заметил, что точность упирается в 83,7% при целевой 85%. Казалось бы, мелочь — всего полтора процента. Но в машинном обучении каждый процент — это часы отладки и переосмысления архитектуры. Диагноз был ясен: модель просто недостаточно мощная. Её свёрточный позвоночник (conv backbone) работал со слишком узкими каналами, как будто пытаясь угодить в очень узкий коридор, когда на самом деле нужна полноценная комната для маневра. Я решил переделать модель так, чтобы её структуру можно было легко конфигурировать на лету. ## Расширяем каналы и переписываем оптимизаторы Первым делом я сделал ширину каналов параметризуемой — теперь можно было просто изменить число фильтров в свёрточных слоях, не трогая основную логику. Затем пришлось переписать тренировочный скрипт целиком. Но тут выявилась ещё одна проблема: модель обучалась с Adam-оптимизатором, который хорош для многих задач, но на CIFAR-10 показывает себя хуже, чем SGD с momentum. Это типичная ловушка — Adam стал почти универсальным выбором, и многие забывают, что для синтетических датасетов наподобие CIFAR-10 классический SGD часто побеждает благодаря более стабильной сходимости и лучшей обобщающей способности. Я переключился на SGD с momentum, но тут выяснилось, что фреймворк использует особый механизм **RigL** (Rigging Lottery) для динамического разреживания сети. RigL периодически растит модель, добавляя новые соединения, и при каждом таком событии оптимизатор переинициализировался. Пришлось обновить логику рестарта оптимизатора не только в основной фазе обучения, но и в **Phase B** — этапе, где сеть уже достаточно созрела для более тонкой настройки. ## Неожиданный враг: переполнение разреживания По ходу работы я наткнулся на баг в самой логике RigL. Механизм переполнял сеть новыми связями быстрее, чем нужно, нарушая баланс между разреживанием и ростом. Пришлось углубиться в реализацию и переписать часть логики расчёта скорости роста новых соединений. В результате модель начала сходиться куда быстрее и стабильнее. Когда я запустил финальное обучение с расширенными каналами, SGD с momentum и исправленным RigL, точность перепрыгнула за 85% с первой попытки. Те самые 1,3%, которые казались непреодолимой стеной, оказались просто комбинацией трёх проблем: узких каналов, неправильного оптимизатора и бага в механизме динамического разреживания. Главный урок: иногда поиск последних процентов точности требует не суперсложных идей, а просто системной отладки всех компонентов вместе. И помните — сначала всегда проверяйте, используете ли вы SGD на CIFAR-10, а не слепо доверяйтесь моде на Adam 😄
SSH спасает: ищем потерянный QR-код в файловой системе
# Как SSH-команда спасла от чёрного экрана в Authelia Проект **borisovai-admin** требовал добавить двухфакторную аутентификацию. Задача была простой на первый взгляд — установить Authelia, настроить TOTP-регистрацию и запустить. Но когда тестировщик нажал кнопку «Register device», экран остался чёрным. QR-код не появился. Никаких ошибок в консоли, никаких намёков на проблему — просто ничего. Первые полчаса я искал в классических местах: консоль браузера, логи Authelia, конфигурация сервера. Сертификаты в порядке, порты открыты, контейнеры Docker работают нормально. Но QR-код так и не возникал. Казалось, система делает что-то, но что именно — никому не известно. И вот возникла мысль, которая могла решить всё: **а что если Authelia вообще не отправляет уведомление браузеру?** Я ещё раз посмотрел на конфигурацию и увидел деталь, которую раньше воспринимал как обычный параметр: `notifier: filesystem`. Это не email, не SMS, не какой-то облачный сервис. Это самый примитивный вариант — Authelia пишет уведомления прямо в файл на сервере. Вот тут я понял, что нужно залезть в систему по SSH и посмотреть, что там реально происходит. Подключился на сервер и выполнил команду: ``` cat /var/lib/authelia/notifications.txt ``` И там она была! Ссылка вида `https://auth.borisovai.tech/...token...` — та самая ссылка, которая должна была привести к QR-коду. Authelia делала всё правильно. Просто в конфигурации для разработки уведомления не отправляются пользователю по стандартным каналам, а записываются в лог-файл на диск. **Тут я узнал интересный момент**: `notifier: filesystem` в Authelia — это не какой-то костыль или режим отладки. Это фактически идеальная настройка для локальной разработки. Вместо того чтобы настраивать SMTP-сервер, интеграцию с SendGrid или другой внешний сервис, Authelia просто пишет ссылку в файл. Быстро, просто, полезно для разработки. Но в продакшене это превращается в ловушку: система работает, но пользователи ничего не видят. Когда я открыл эту ссылку в браузере, QR-код тут же появился. Отсканировал его в приложении Google Authenticator — всё сработало. Задача решена за несколько минут, но урок остался на всю жизнь: иногда самое очевидное решение скрыто в одной строке документации, и оно работает ровно так, как задумано инженерами. Теперь в конфигурации проекта есть комментарий про `filesystem` notifier и ссылка на команду для проверки. Следующему разработчику, который будет настраивать двухфакторку, не придётся ловить QR-код в файловой системе 😄
QR-код в файле: как я нашел потерянное уведомление Authelia
# Когда QR-код спрятался в файл: история отладки Authelia Проект **borisovai-admin** требовал добавить двухфакторную аутентификацию. Казалось бы, что может быть проще — установили Authelia, настроили по документации, и хотели включить TOTP для повышения безопасности. Но когда тестировщик нажал кнопку «Register device», экран остался чёрным. QR-код просто не появился. Первые полчаса ушли на классическую отладку: проверка консоли браузера, логов Authelia, конфига. Всё выглядело нормально. Сертификаты в порядке, порты открыты, контейнеры запущены. Но QR так и не появлялся. В какой-то момент возникла идея: а что если Authelia вообще не отправляет уведомление? Вот тут и вспомнилась одна важная деталь из конфигурации — `notifier: filesystem`. Это не email, не Telegram, а самый простой вариант для разработки: Authelia записывает ссылку на регистрацию прямо в файл на сервере. Никаких стандартных каналов связи, никакой магии с SMTP. Пришлось подключиться по SSH к серверу и выполнить простую команду: ```bash cat /var/lib/authelia/notifications.txt ``` И вот оно! В файле лежала ссылка вида `https://auth.borisovai.tech/...token...` — та самая ссылка, которая должна была привести к QR-коду. Оказалось, Authelia всё делала правильно. Просто в конфигурации для разработки уведомления отправляются не пользователю, а в лог-файл на диск. **Интересный момент**: многие разработчики не замечают, что в конфигурации `notifier: filesystem` — кажется, что это какой-то непонятный режим, а на самом деле это *идеальная* настройка для локальной разработки. Вместо того чтобы настраивать SMTP-сервер или интеграцию с внешними сервисами, Authelia просто пишет ссылку в файл. Быстро, просто, полезно. Когда я открыл эту ссылку в браузере, QR-код тут же появился. Сканировали его в TOTP-приложении, всё сработало. Задача решена за несколько минут, но урок остался: иногда самое очевидное решение скрыто в документации, и оно работает лучше, чем нами предполагалось. Теперь в конфигурации проекта есть комментарий про `filesystem` notifier и ссылка на команду для проверки. Следующему разработчику, который будет настраивать двухфакторку, не придётся искать её полчаса. --- *Authelia: когда QR-код путешествует по файловой системе вместо того, чтобы сразу показаться в браузере 😄*
2FA в Authelia: спасение админ-панели за 5 минут
# Authelia: когда админу нужна двухфакторная аутентификация прямо сейчас Проект `borisovai-admin` дошёл до критической точки. Система аутентификации Authelia уже поднята, админ успешно залогинился... но дальше — стена. Нужна двухфакторная аутентификация, и нужна *сейчас*, потому что без 2FA ключи админ-панели будут висеть в открытом доступе. Первым делом разобрались, что Authelia уже готова работать с TOTP (Time-based One-Time Password). Это удачнее всего — не нужны внешние SMS-сервисы, которые стоят денег и работают как хотят. Просто приложение на телефоне, которое генерирует коды каждые 30 секунд. Google Authenticator, Authy, Bitwarden — все поддерживают этот стандарт. Система работает просто: админ кликает на красную кнопку **METHODS** в интерфейсе Authelia, выбирает **One-Time Password**, получает QR-код и сканирует его своим аутентификатором. Потом вводит первый код для проверки — и готово, 2FA активирована. Ничего сложнее, чем настроить Wi-Fi на новом телефоне. Но тут всплыл забавный момент. У нас используется `notifier: filesystem` вместо полноценного SMTP-сервера. Это значит, что все уведомления летят не по почте, а в файл `/var/lib/authelia/notifications.txt` на сервере. Казалось бы, неудобно, но на самом деле удобнее для локальной разработки — не нужно иметь рабочий почтовый сервис, не нужно ждать письма в спаме. Просто залезь по SSH на машину и прочитай файл. Правда, для production это так не прокатит, но сейчас это даже плюс. **Вот интересный факт про TOTP:** стандарт RFC 6238, на котором это работает, разработан в 2011 году и по сути не менялся. Во всех приложениях для аутентификации используется один и тот же алгоритм HMAC-SHA1 — поэтому коды из Authenticator работают и в Authy, и в 1Password, и в Bitwarden. Один стандарт на всех. Это редкость в IT — обычно каждый сервис хочет своё решение. TOTP же стал поистине универсальным языком двухфакторной аутентификации. Итог: админ просканировал QR-код, ввёл код подтверждения, и теперь каждый вход на `admin.borisovai.tech` или `admin.borisovai.ru` требует второго фактора. Брутфорс админ-панели стал значительно сложнее. Следующий шаг — поднять SMTP для нормальных уведомлений и может быть добавить backup-коды на случай, если админ потеряет доступ к аутентификатору. Но это уже совсем другая история. Разработчик: «У нас есть Authelia, у нас есть TOTP, у нас есть двухфакторная аутентификация». HR: «На каком уровне безопасности?». Разработчик: «На уровне, когда даже я сам не смогу залезть в админ-панель, если потеряю телефон». 😄
Туннели и таймауты: управление инфраструктурой в админ-панели
# Туннели, Traefik и таймауты: как мы добавили управление инфраструктурой в админ-панель Проект **borisovai-admin** рос не по дням, а по часам. Сначала была одна машина, потом две, потом стало ясно — нужна нормальная система для управления сетевыми туннелями между серверами. Задача выглядела острой: юзеру нужен интерфейс, чтобы видеть, какие туннели сейчас активны, создавать новые и удалять старые. Без этого администрирование превращалось в ручную возню с конфигами на каждой машине. Первое решение было логичным: взял **frp** (Fast Reverse Proxy) — лёгкий инструмент для туннелирования, когда сервер скрыт за NAT или брандмауэром. Почему не что-то более «облачное»? Потому что здесь нужна полная контроль, минимум зависимостей и максимум надёжности. FRP ровно это даёт. Спроектировал веб-интерфейс: добавил страницу `tunnels.html` с простеньким списком активных туннелей, кнопками для создания и удаления. На бэкенде в `server.js` реализовал пять API endpoints для управления состоянием. Параллельно обновил скрипты инсталляции: `install-all.sh` и отдельный `install-frps.sh` для развёртывания FRP сервера, плюс `frpc-template` для клиентов на каждой машине. Не забыл навигационную ссылку «Туннели» на всех страницах админ-панели — мелочь, но юзабилити взлетела. Вроде всё шло гладко, но потом началось. Пользователи начали скачивать большие файлы через GitLab, и соединение рубилось где-то в середине процесса. Проблема оказалась в **Traefik** — наш обратный прокси по умолчанию использует агрессивные таймауты. Стоило файлу загружаться дольше пары минут — и всё, соединение закрыто. Пришлось углубиться в конфиги Traefik. Установил `readTimeout` в 600 секунд (10 минут) и создал специальный `serversTransport` именно для GitLab. Написал скрипт `configure-traefik.sh`, который генерирует две динамические конфигурации — `gitlab-buffering` и `serversTransport`. Результат: файлы теперь загружаются спокойно, даже если это полгигабайта архива. **Интересная особенность Traefik:** это микросервис-балансировщик, который позиционируется как облегчённое решение, но на практике требует хирургической точности при настройке. Неправильный таймаут — и приложение выглядит медленным. Правильный — и всё летает. Один параметр, и мир меняется. Параллельно реорганизовал документацию: разбил `docs/` на логические части — `agents/`, `dns/`, `plans/`, `setup/`, `troubleshooting/`. Добавил полный набор конфигов для конкретного сервера в `config/contabo-sm-139/` (traefik, systemd, mailu, gitlab) и обновил скрипт `upload-single-machine.sh` для их загрузки. За вечер родилась полноценная система управления туннелями с интерфейсом, автоматизацией и нормальной документацией. Проект теперь легко масштабируется на новые серверы. Главное, что узнал: **Traefik** — это не просто прокси, это целая философия правильной конфигурации микросервисов. Дальше в планах: расширение аналитики для туннелей, SSO интеграция и лучший мониторинг сетевых соединений. 😄 **Разработчик**: «Я настроил Traefik». **Пользователь**: «Отлично, тогда почему мой файл не загружается?» **Разработчик**: «А ты пробовал перезагрузить сервер?»
Authelia: как я разобрался с хешами паролей и первым входом в админку
# Запускаем Authelia: логины, пароли и первый вход в админку Проект **borisovai-admin** требовал серьёзной работы с аутентификацией. Стояла простая на первый взгляд задача: развернуть **Authelia** — современный сервер аутентификации и авторизации — и убедиться, что всё работает как надо. Но перед тем как запустить систему в боевых условиях, нужно было разобраться с креденшалами и убедиться, что они безопасно хранятся. Первым делом я заглянул в скрипт установки `install-authelia.sh`. Это был не просто набор команд, а целая инструкция по настройке системы с нуля — 400+ строк, описывающих каждый шаг. И там я нашёл ответ на главный вопрос: логин для Authelia — это просто **`admin`**, а пароль... вот тут начиналось интересное. Оказалось, что пароль хранится в двух местах одновременно. В конфиге Authelia (`/etc/authelia/users_database.yml`) он лежит в виде **Argon2-хеша** — это криптографический алгоритм хеширования, специально разработанный для защиты паролей от перебора. Но на сервере управления (`/etc/management-ui/auth.json`) пароль хранится в открытом виде. Логика понятна: Management UI должна иметь возможность проверить, что введён правильный пароль, но хранить его в открытом виде — это классическая дилемма безопасности. Неожиданно выяснилось, что это не баг, а фича. Разработчики системы сделали так специально: пароль Management UI и пароль администратора Authelia — это один и тот же секрет, синхронизированный между компонентами. Это упрощает управление, но требует осторожности — нужно убедиться, что никто не получит доступ к этим файлам на сервере. Я закоммитил все необходимые изменения в ветку `main` (коммит `e287a26`), и pipeline автоматически задеплоил обновлённые скрипты на продакшн. Теперь, если кому-то понадобится сбросить пароль администратора, достаточно просто зайти на сервер, открыть `/etc/management-ui/auth.json` и посмотреть текущее значение. Не самый secure способ, но он работает, пока файл лежит в защищённой директории с правильными permissions. Главный вывод: при работе с аутентификацией нет мелочей. Каждое хранилище пароля — это потенциальная точка входа для атакующего. **Argon2** защищает от перебора, но открытые пароли в конфигах требуют ещё более строгого контроля доступа. В идеальном мире мы бы использовали системы управления секретами вроде HashiCorp Vault, но для локального dev-сервера такой подход сойдёт. Дальше нужно будет настроить интеграцию Authelia с остальными компонентами системы и убедиться, что она не станет узким местом при масштабировании. Но это история для следующего поста. 😄 Что общего у Scala и подростка? Оба непредсказуемы и требуют постоянного внимания.
Traefik и опциональные middleware: война с зависимостями
# Когда конфиги кусаются: история про зависимые middleware в Traefik Проект `borisovai-admin` — это не просто админ-панель, это целая инфраструктурная система с аутентификацией через Authelia, обратным прокси на Traefik и кучей moving parts, которые должны работать в идеальной гармонии. И вот в один прекрасный день выясняется: когда ты разворачиваешь систему без Authelia, всё падает с ошибкой 502, потому что Traefik мечтательно ищет middleware `authelia@file`, которого просто нет в конфиге. **Завязка проблемы была в статических конфигах.** Мы жёстко прописали ссылку на `authelia@file` прямо в Traefik-конфигурацию, и это работало, когда Authelia установлена. Но стоило её отключить или не устанавливать вообще — бум, 502 ошибка. Получается, конфиги были сильно связаны с опциональным компонентом. Это классический случай, когда инфраструктурный код требует гибкости. Решение разбилось на несколько фронтов. Во-первых, **убрали жёсткую ссылку на `authelia@file` из статических конфигов Traefik** — теперь это просто не указывается в базовых настройках. Во-вторых, создали правильную цепочку инициализации. Скрипт `install-authelia.sh` теперь сам добавляет `authelia@file` в `config.json` и настраивает OIDC при установке. Скрипт `configure-traefik.sh` проверяет переменную окружения `AUTHELIA_INSTALLED` и условно подключает middleware. А `deploy-traefik.sh` перепроверяет на сервере, установлена ли Authelia, и при необходимости переустанавливает `authelia@file`. По ходу дела обнаружилась ещё одна проблема в `install-management-ui.sh` — там был неправильный путь к `mgmt_client_secret`. Исправили. А `authelia.yml` вообще выкинули из репозитория, потому что его всегда генерирует сам скрипт установки. Зачем держать в git то, что одинаково воспроизводится каждый раз? **Интересный момент про middleware в Docker-сообществе:** люди часто забывают, что middleware — это не просто функция, это *объект конфигурации*, который должен быть определён до использования. Traefik здесь строг: ты не можешь ссылаться на middleware, которого не существует. Это похоже на попытку вызвать функцию, которая не импортирована в Python. Простая ошибка, но очень болезненная в production-системах. **Итоговая архитектура** получилась намного гибче: система работает как с Authelia, так и без неё, конфиги не лежат мёртвым грузом в репо, инсталляторы действительно знают, что они делают. Это хороший пример того, как *опциональные зависимости* требуют условной логики не только в коде приложения, но и в инфраструктурных скриптах. Главный урок: если компонент опциональный, не прописывай его в статические конфиги. Пусть туда добавляются динамически при необходимости. 😄 Разработчик: «Я знаю Traefik». HR: «На каком уровне?». Разработчик: «На уровне количества 502 ошибок, которые я пережил».
VPN отключился молча: как я потерял доступ к релизу
# Когда VPN молчит: охота на привидение среди ночи Пятница, конец дня, а на горизонте маячит дедлайн релиза **v1.0.0** проекта **speech-to-text**. Финальный рывок: нужно запушить коммит с автоматизацией сборки в master, создать тег и загрузить артефакт в GitLab Package Registry. Казалось бы, стандартная процедура — пара команд в консоль, и мы свободны. Но начало было не самым обнадёживающим. Я попытался перезапустить **Gitaly** — критический компонент GitLab, отвечающий за хранение репозиториев и работу с гитом на серверной стороне. SSH молчит. Попробовал достучаться через HTTP к самому GitLab-серверу — тишина. Весь сервер, похоже, вообще не существует с точки зрения моей машины. Стандартный алгоритм отладки: если ничего не отвечает, проблема либо с сервером, либо с сетью. Сервер на **144.91.108.139** физически жив, но почему-то недоступен. Проверяю VPN, и вот оно — диапазон **10.8.0.x** не найден. **OpenVPN отключился.** Просто тихо, без уведомления, выполнив свою работу и уйдя в отставку. Оказывается, весь этот вечер я сидел за стеной недоступности. Компания добавила слой безопасности, завернув внутреннюю инфраструктуру в защищённый туннель, а я, горя желанием запушить релиз, забыл про это самое VPN. Типичная история: инфраструктура дышит тебе в спину, а ты смотришь на экран и недоумеваешь, почему ничего не работает. **Интересный факт:** Gitaly создан именно для того, чтобы отделить операции с файловой системой от основного приложения GitLab. Это позволило компании масштабировать сервис горизонтально, но цена — жёсткая зависимость. Если Gitaly недоступен, GitLab попросту не может выполнять операции с гитом. Это как попытаться ходить с отключенными ногами. Решение было простым, но требовало действия. Нужно было переподключить **OpenVPN**, дождаться, пока туннель встанет на место, и выполнить `git push origin master`. После этого запустить скрипт релиза на Python, который собирает EXE из исходного кода, упаковывает в ZIP и загружает артефакт в Package Registry. Когда VPN восстановился, все лампочки загорелись в правильном порядке. Gitaly ожил, сервер откликнулся, и коммит с облегчением пошёл в master. Релиз уложился в срок. **Урок:** прежде чем копать проблему на сервере, убедитесь, что вы вообще до него дотягиваетесь. VPN, firewall, маршруты — всё это может спокойно жить в фоне, пока вы ловите ошибки в коде. Инфраструктура любит скрываться за слоями безопасности, и иногда самая сложная проблема решается одной переподключением. 😄 OpenVPN — как невидимая рука, которая отключается именно тогда, когда ты забываешь, что её держишь.
VPN отключился в самый неудачный момент
# Когда Gitaly молчит: охота на недоступный GitLab среди ночи Вечер пятницы, deadline на релиз `v1.0.0` проекта **speech-to-text** буквально под носом. Нужно было запушить финальный коммит с автоматизацией сборки в master, создать тег и загрузить артефакт в Package Registry. Казалось бы, стандартная процедура — клик, клик, и всё готово. Но началось всё с того, что я попытался перезапустить **Gitaly** на GitLab-сервере через SSH. Ничего не вышло. Сервер просто не отвечает. Ладно, попробую обойтись без SSH — может быть, сам GitLab доступен по HTTP? Нет, он тоже молчит как партизан. Вообще ничего не откликается. Паника? Нет, просто логика. Если сервер не отвечает ни на SSH, ни на HTTP, значит либо он упал, либо сетевая проблема. Проверяю VPN. И вот оно! IP-адрес в диапазоне `10.8.0.x` не найден. **OpenVPN отключился.** Сервер GitLab (`gitlab.dev.borisovai.tech`) размещён на машине `144.91.108.139`, которая доступна только через защищённый туннель. Вот это поворот! Оказывается, всё время я просто был за стеной недоступности — VPN выполнял свою работу, но потом тихо сдался. Компания добавила слой безопасности, а я про это забыл. Типичная история: инфраструктура дышит на тебе в спину, а ты смотришь на монитор и недоумеваешь. **Интересный факт:** Gitaly — это компонент GitLab, отвечающий за хранение репозиториев и работу с гитом на серверной стороне. Создан он специально для того, чтобы отделить операции с файловой системой от основного приложения. Если Gitaly недоступен, GitLab просто не может выполнять операции с гитом — это как отключить ноги при попытке ходить. Решение было простым, но требовало действий. Нужно было: 1. Подключить **OpenVPN** к серверу `144.91.108.139` 2. После восстановления туннеля выполнить `git push origin master` из ветки **master** 3. Запустить скрипт релиза: `.\venv\Scripts\python.exe scripts/release.py` Этот скрипт собирает EXE из Python-кода, упаковывает его в ZIP, загружает артефакт в GitLab Package Registry и создаёт тег версии. Когда VPN встал на место и лампочки начали загораться в правильном порядке — Gitaly вновь ожил, сервер откликнулся, а мой коммит с облегчением пошёл в master. Релиз ушёл в прод ровно в срок. **Урок на вечер:** прежде чем искать проблему на сервере, проверьте, что вы вообще до него дотягиваетесь. Инфраструктура любит прятаться за слоями безопасности, и иногда самая сложная проблема решается одной переподключением. 😄 Почему MongoDB считает себя лучше всех? Потому что Stack Overflow так сказал.
Race condition в системе версионирования: как два ревьюера поймали потерю данных
# Когда два ревьюера находят одни и те же баги: история о том, как система версионирования может потерять данные Работаешь над feature branch `feat/scoring-v2-tavily-citations` в проекте **trend-analisis**, пилишь систему многоуровневого анализа трендов. Задача звучит просто: позволить анализировать один тренд несколько раз с разными параметрами (`depth`, `time_horizon`), сохранять все варианты и отправлять их на фронт. Казалось бы, что может быть проще? Потом коммит отправляешь на ревью двум коллегам. И они оба, независимо друг от друга, находят одну и ту же **критическую ошибку** — race condition в функции `next_version()`. Момент волшебства: когда разные люди пришли к одному выводу, значит, ошибка точно смертельна. Вот что происходит. Функция `next_version()` считает максимальный номер версии анализа для тренда и возвращает `max + 1`. Звучит логично, но представь: два запроса одновременно анализируют один тренд. Оба вызывают `next_version()`, получают одинаковый номер (например, `version=3`), затем пытаются сохранить результат через `save_analysis()`. Один INSERT успешен, второй молча пропадает в чёрной дыре `except Exception: pass`. Данные потеряны, пользователь не узнает о проблеме. Но это ещё не всё. Коллеги заметили вторую проблему: функция видит только завершённые анализы (статус `completed`), поэтому запущенный анализ (статус `running`) остаётся невидимым для системы версионирования. Получается, что второй запрос стартует с того же номера версии, какой уже занят висящим процессом. Классическая ловушка асинхронности. Обнаружилось ещё несколько багов: фронт ожидает получить один объект `getAnalysisForTrend`, а бэкенд начал отправлять массив анализов. TypeScript тип `AnalysisReport` не знает про новые поля (`version`, `depth`, `time_horizon`, `parent_job_id`) — они приходят с сервера и сразу теряются. Параметр `parent_job_id` вообще ни на что не валидируется, что открывает дверь для инъекций. И `depth` может быть любым числом — никакого лимита, хоть 100 передай. **Интересный момент:** многие разработчики думают, что `except Exception: pass` это "временно", но на практике эта конструкция часто уходит в production как постоянное решение, маскируя критические ошибки. Это называется *exception swallowing*, и это один из самых подлых антипаттернов асинхронного кода. Решение оказалось не очень сложным, но требовало думать о транзакциях иначе. Нужно либо **переместить `next_version()` внутрь `save_analysis()`** с retry-логикой на `IntegrityError`, либо использовать **атомарный SQL-запрос `INSERT...SELECT MAX(version)+1`**, чтобы гарантировать уникальность версии за одно действие. Плюс резервировать версию сразу при старте анализа (INSERT со статусом `running`), чтобы параллельные запросы их видели. Для фронта пришлось добавить новый endpoint `getAnalysesForTrend` (а старый `getAnalysisForTrend` оставить для обратной совместимости). TypeScript типы расширены, валидация на `parent_job_id` добавлена, `depth` ограничен до 7 через `Pydantic Field(ge=1, le=7)`. Главный урок: **код, который "работает на примере", и код, который справляется с race conditions, это два разных животных**. Всегда думай про параллелизм, даже если сейчас система однопоточная. И когда два ревьюера независимо находят один и тот же баг — это не совпадение, это сигнал, что нужно переделывать архитектуру, а не чинить синтаксис. 😄 Prometheus: решение проблемы, о существовании которой ты не знал, способом, который не понимаешь.
Мелочь в навигации — архитектура на бэке
# Туннелировать админ-панель: когда мелочь оказывается архитектурой Проект **borisovai-admin** — это управленческая панель для социального паблишера. И вот однажды возникла потребность: нужна видимость в туннели FRP (Fast Reverse Proxy). Казалось — простая фича. Добавить ссылку в навигацию, создать эндпоинты на бэке, вывести данные на фронте. Четыре-пять дней работы, максимум. Началось всё с мелочи: требовалось добавить пункт "Туннели" в навигацию. Но навигация была одна, а HTML-файлов четыре — `index.html`, `tokens.html`, `projects.html`, `dns.html`. И здесь скрывалась первая ловушка: одна опечатка, одна невнимательность при копировании — и пользователь запутается, кликнув на несуществующую ссылку. Пришлось синхронизировать все четыре файла, убедиться, что ссылки находятся на одинаковых позициях в строках 195–238. Мелочь, которую легко упустить при спешке. Но мелочь эта потащила за собой целую архитектуру. На бэке понадобилось добавить две вспомогательные функции в `server.js`: `readFrpsConfig` — для чтения конфигурации FRP-сервера, и `frpsDashboardRequest` — для безопасного запроса к dashboard FRP. Это не просто HTTP-вызовы: это минимальная абстракция, которая облегчит тестирование и повторное использование. Затем пришлось вывести четыре GET-эндпоинта: статус сервера, список активных туннелей с метаинформацией, текущую конфигурацию в JSON и даже генератор `frpc.toml` для скачивания клиентского конфига в один клик. И вот неожиданно выяснилось — сам FRP-сервер ещё нужно установить и запустить. Обновил `install-all.sh`, добавил FRP как опциональный компонент: не все хотят туннели, но кто выбрал — получит полный стек. На фронте создал новую страницу `tunnels.html` с тремя блоками: карточка статуса (живой ли FRP), список туннелей с автообновлением каждые 10 секунд (классический полинг, проще WebSocket'а для этого масштаба) и генератор конфига для клиента. **Интересный факт**: полинг через `setInterval` кажется древним подходом, но именно он спасает от overengineering'а. WebSocket требует поддержки на обеих сторонах, fallback'и на старых браузерах, управление жизненным циклом соединения. Для обновления статуса раз в 10 секунд это overkill. Главное — не забыть очистить интервал при размонтировании компонента, иначе получишь утечку памяти и браузер начнёт отваливаться. Главный урок: даже в мелких фичах скрывается целая архитектура. Одна ссылка в навигации потребовала синхронизации четырёх файлов, пять эндпоинтов на бэке, новую страницу на фронте, обновление скрипта установки. Это не scope creep — это *discovery*. Лучше потратить час на планирование полной цепочки, чем потом переделывать интеграцию, когда уже половина team работает на основе твоей "быстрой фички". 😄 FRP — это когда твой сервер вдруг получает способность ходить в гости через NAT, как путник с волшебным клаком из мультика.
GitLab Pages выдал секреты: как мы чуть не залили артефакты в интернет
# Как мы защитили артефакты приватного проекта, но случайно выставили их в интернет Проект `speech-to-text` — это голосовой ввод для веб-приложения. Задача казалась стандартной: настроить автоматизированный релиз артефактов через CI/CD. Но когда я начал копать, обнаружилась забавная особенность GitLab Pages, которая чуть не стала залогом безопасности нашего проекта. ## Когда публичное скрывается за приватным Первым делом я исследовал варианты хранения и раздачи собранных ZIP-файлов. В репозитории всё приватное — исходники защищены, доступ ограничен. Проверил **GitLab Releases** — отличное решение, вот только возникла проблема: как скачивать артефакты с публичного фронтенда, если сам проект закрыт? Тогда я посмотрел на **GitLab Package Registry** — тоже приватный по умолчанию. Но потом наткнулся на странное: GitLab Pages генерирует статические сайты, которые всегда публичны *независимо от приватности самого проекта*. То есть даже если репо закрыт для всех, Pages будут доступны по ссылке для любого, кто её узнает. **Неожиданно выяснилось:** это не баг, а фича. Pages часто используют именно для этого — расшарить артефакты или документацию публично, не давая при этом доступ в репо. ## Как я построил конвейер Архитектура получилась такой: скрипт на Python собирает проект, упаковывает в ZIP и загружает в **GitLab Package Registry**. Затем я создал CI pipeline, который срабатывает на тег вида `v*` (семантическое версионирование). Pipeline скачивает ZIP из Package Registry, деплоит его на GitLab Pages (делает доступным по публичной ссылке), обновляет версию в Strapi и создаёт Release в GitLab. Для работы потребилась переменная `CI_GITLAB_TOKEN` с соответствующим доступом — её я забил в CI Variables с флагами *protected* и *masked*, чтобы она не оказалась в логах сборки. Версионирование жёсткое: версия указывается в `src/__init__.py`, оттуда её подхватывает скрипт при сборке. Архивы называются в стиле `VoiceInput-v1.0.0.zip`. ## Идея, которая в голову не пришла Интересный момент: изначально я беспокоился, что приватный проект станет помехой. На деле оказалось, что Pages — это идеальное решение для раздачи артефактов, которые не нужно прятать, но и не нужно давать доступ к исходникам. Классический сценарий для скачиваемых утилит, библиотек или сборок. Теперь релиз — это одна команда: `.\venv\Scripts\python.exe scripts/release.py`. Скрипт собирает, архивирует, загружает, пушит тег. А CI сам позаботится о Pages, Strapi и Release. 😄 Почему GitLab Pages пошёл в терапию? Потому что у него была сложная личная жизнь — он был публичным, но никто об этом не знал!
Тесты падают: как найти виновника, когда это наследство
# Когда тесты ломаются, но ты не виноват: история отладки в проекте trend-analysis Представь ситуацию: ты вносишь изменения в систему анализа трендов, коммитишь в ветку `feat/scoring-v2-tavily-citations`, запускаешь тесты — и вот, шесть тестов падают красными крестами. Сердце замирает. Первый вопрос: это моя вина или наследство от предков? ## Охота на виновника начинается В проекте `trend-analysis` я добавлял новые параметры к функции `_run_analysis`: `time_horizon` и `parent_job_id` как именованные аргументы с дефолтными значениями. На бумаге — всё обратно совместимо. На тестах — красный экран смерти. Первым подозреваемым стала функция `next_version()`. Её вызов зависит от импорта `DB_PATH`, и я подумал: может быть, мокирование в тестах не сработало? Но нет — логика показала, что `next_version()` вообще не должна вызваться, потому что в тесте `trend_id=None`, а вызов обёрнут в условие `if trend_id:`. Второй подозреваемый: `graph_builder_agent`. Эта штука вызывается со специальным параметром `progress_callback=on_zone_progress`, но её мок в тестах — просто лямбда-функция, которая принимает только один позиционный аргумент: ```python lambda s: {...} # вот так мокируют ``` А я вызываю её с дополнительным именованным аргументом. Лямбда восстаёт против `**kwargs`! ## Разворот сюжета Но подождите. Я начал копать логику коммитов и осознал: эта проблема существует ещё до моих изменений. Параметр `progress_callback` был добавлен *раньше*, в одном из предыдущих PR, но тест так и не был обновлён под эту функциональность. Я найденный баг не создавал, я его просто разбудил. Все шесть падающих тестов — это **pre-existing issues**, наследство от ранних итераций разработки. Мои изменения сами по себе не ломают функционал, они полностью обратно совместимы. ## Что дальше? Решили остановиться на стадии прототипа для валидации концепции. На бэкенде я уже поднял: - миграции БД для версионирования анализов (добавил `version`, `depth`, `time_horizon`, `parent_job_id`); - новые функции `next_version()` и `find_analyses_by_trend()` в `analysis_store.py`; - обновлённые Pydantic-модели в `schemas.py`; - API endpoints с автоинкрементом версий в `routes.py`. Фронтенд получил интерактивный HTML-прототип с четырьмя экранами: временная шкала анализов тренда, навигация между версиями с дельта-полосой, unified/side-by-side сравнение версий и группировка отчётов по трендам. ## Урок на память Когда читаешь падающий тест, первое правило: не сразу ищи свою вину. Иногда это старый долг проекта, который ждал своего часа. Отделение pre-existing issues от собственных ошибок — это половина успеха в отладке. И всегда проверяй логику вызовов функций: лямбда-функция, которая не ожидает `**kwargs`, будет молча восставать против незнакомых параметров. Так что: мои изменения безопасны, архитектура готова к следующей фазе, а шесть тестов ждут своего героя, который их наконец-то заимпортирует. 😄
От плоской базы к дереву версий: как переделать архитектуру анализов
# Когда один анализ становится деревом версий: история архитектурной трансформации Проект **bot-social-publisher** уже имел HTML-прототип интеллектуальной системы анализа трендов, но вот беда — архитектура данных была плоской, как блин. Одна версия анализа на каждый тренд. Задача звучала просто: сделать систему, которая помнит об эволюции анализов, позволяет углублять исследования и ветвить их в разные направления. Именно это отличает боевую систему от прототипа. Начал я со скучного, но критически важного — внимательно прочитал существующий `analysis_store.py`. Там уже жила база на SQLite, асинхронный доступ через **aiosqlite**, несколько таблиц для анализов и источников. Но это была просто полка, а не полнофункциональный архив версий. Первое, что я понял: нужна вертикаль связей между анализами. **Фаза первая: переделка схемы.** Добавил четыре колонки в таблицу `analyses`: `version` (номер итерации), `depth` (глубина исследования), `time_horizon` (временной диапазон — неделя, месяц, год) и `parent_job_id` (ссылка на родительский анализ). Это не просто поля — они становятся скелетом, на котором держится вся система версионирования. Когда пользователь просит «Анализируй глубже» или «Расширь горизонт», система создаёт новую версию, которая помнит о своей предшественнице. **Фаза вторая: переписывание логики.** Функция `save_analysis()` была примитивна. Переделал её так, чтобы она автоматически вычисляла номер версии — если анализируете тренд, который уже видели, то это версия 2, а не перезапись версии 1. Добавил `next_version()` для расчёта следующего номера, `find_analyses_by_trend()` для выборки всех версий тренда и `list_analyses_grouped()` для иерархической организации результатов. **Фаза третья: API слой.** Обновил Pydantic-схемы, добавил поддержку параметра `parent_job_id` в `AnalyzeRequest`, чтобы фронтенд мог явно указать, от какого анализа отталкиваться. Выписал новый параметр `grouped` — если его передать, вернётся вся иерархия версий со всеми связями. Вот тут началось интересное. Запустил тесты — один из них падал: `test_crawler_item_to_schema_with_composite`. Первым делом подумал: «Это я сломал». Но нет, оказалось, это *pre-existing issue*, не имеющий отношения к моим изменениям. Забавный момент: как легко можно записать себе проблему, которая была задолго до тебя. **Интересный факт о SQLite и миграциях.** В Python для SQLite нет ничего вроде Django ORM с его волшебством. Миграции пишешь вручную: буквально SQL-запросы в функциях. `ALTER TABLE` и точка. Это делает миграции прозрачными, понятными, предсказуемыми. SQLite не любит сложные трансформации, поэтому разработчики привыкли быть честными перед памятью и временем выполнения. Архитектура готова. Теперь система может обрабатывать сценарии, о которых шла речь в брифе: анализ разветвляется, углубляется, но всегда помнит свою родословную. Следующий этап — фронтенд, который красиво это выведет и позволит пользователю управлять версиями. Но это совсем другая история. 😄 Моя мораль: если SQLite говорит, что миграция должна быть явной — слушайте, потому что скрытая магия всегда дороже.
Как система экспертов перестала взрываться и начала расти
# МОЯ растёт: как система экспертов научилась не взрываться Проект **llm-analisis** — это исследование смеси экспертов (Mixture of Experts, MoE) для обработки сложных данных. Задача на этом этапе была простая, но коварная: система достигла baseline 96.7% точности, но дальше расти отказывалась. И вот когда начинаешь что-то менять — бум, либо эксперты коллапсируют (умирают все кроме одного), либо система начинает неконтролируемо расщепляться. Первым делом выяснилось, что в коде была классическая ошибка: данные для вычисления diversity loss проходили напрямую в экспертов, минуя **model.projection**. Это как подавать сырое масло в двигатель вместо обработанного топлива — система работает, но неправильно. Исправили, и baseline встал на место. Но проблемы начались, когда включили режим роста. Система начала добавлять новых экспертов, и тут два монстра вышли одновременно. Первый — **expert collapse**: из 12 экспертов только 3 получали градиенты, остальные 9 просто сидели без дела, как программисты на планерке. Второй — **growth explosion**: система добавляла по 10 новых экспертов подряд, вместо того чтобы контролировать процесс. Решение пришло в три шага. Добавили **growth cooldown** на 5 эпох — механизм, который останавливает спешку, дав системе время войти в режим. Вместо бешеных 10 сплитов подряд стал ровный, контролируемый рост. Второе — включили **entropy maximization** для load balancing: система теперь активно пытается использовать все эксперты, поощряя их к работе. Результат: мёртвые эксперты воскресли. Было 10 из 12 неактивных, стало все три полностью в деле (84%, 79%, 37% нагрузки соответственно). Третье — пересчитали историю точности, чтобы GO/NO-GO отчёт был честным. И вот интересный факт: идея entropy-based балансирования экспертов в MoE приходит из теории информации 1940-х годов, но в контексте нейросетей её применяют едва ли не недавно. Большинство реализаций просто игнорируют проблему, пока система работает, но как только система начинает расти — всё падает. Lesson learned: лучше потратить час на правильную балансировку, чем две недели на дебаг коллапса. Результат: за 14 эпох система выросла с 96.7% до **97.1%** — не огромно, но честно и стабильно. Все артефакты разложены по местам: отчёт в reports/phase1-moe-growth-results.md, MASTER-SUMMARY обновлён, три файла модели (moe.py, growth.py, train_mnist.py) лежат в seed-net/phase1/. Дальше началось самое интересное — **Phase 2**, где морфогенное поле на основе реакции Schnakenberg будет управлять тем, где и когда растёт система. Но это уже другая история. Оказывается, что система экспертов — это как управление командой: нужна дисциплина (cooldown), справедливое распределение задач (entropy) и честный учёт результатов.
Как версионировать анализы трендов без потери масштабируемости
# Строим интеллектуальную систему анализа трендов: от прототипа к масштабируемой архитектуре Проект **trend-analysis** стоял на пороге серьёзного апгрейда. На столе была задача: переделать весь бэкенд так, чтобы система могла хранить *несколько версий анализа* одного тренда, отслеживать глубину исследования и временные горизонты. Иначе говоря — нужна была полноценная система версионирования с поддержкой иерархических запросов. ## Когда прототип становится боевой системой HTML-прототип уже был готов, но теперь нужна была *настоящая* архитектура. Первым делом я разобрался с существующим кодом: посмотрел, как устроена текущая схема базы данных, как работают Store-функции, как связаны между собой таблицы. Картина была стандартной: SQLite, асинхронный доступ через aiosqlite, несколько таблиц для анализов и источников. Но текущая структура была плоской — одна версия анализа на тренд. Нужно было всё переделать. ## Три фазы трансформации **Фаза 1: новая архитектура данных.** Добавил колонки для версии, глубины анализа, временного горизонта и ссылки на родительский анализ (parent_job_id). Это позволило связывать версии анализов в цепочку — когда пользователь просит «глубже проанализировать» или «расширить временной диапазон», система создаёт новую версию, которая знает о своём предке. Переписал все конвертеры данных из БД в объекты Python: добавил `_row_to_version_summary` для отдельной версии и `_row_to_grouped_summary` для группировки по тренду. **Фаза 2: API слой.** Обновил Pydantic-схемы, чтобы они знали о версионировании. Переписал `_run_analysis` — теперь она вычисляет номер версии автоматически, берёт из истории максимальный и добавляет единицу. Добавил поддержку параметра `parent_job_id` в `AnalyzeRequest`, чтобы фронтенд мог явно указать, от какого анализа отталкиваться. Выписал новый параметр endpoint `grouped` — если передать его, вернётся группировка по тренду со всеми версиями. Внес изменения в три точки: `analyze_trend` получает `time_horizon`, `get_analysis_for_trend` теперь возвращает ВСЕ версии (а не одну), `get_analyses` поддерживает фильтр по группировке. ## Когда тесты врут Вот здесь начался интересный момент. Запустил тесты бэкенда — один из них упорно падал. `test_crawler_item_to_schema_with_composite` кричал об ошибке. Первым делом подумал: «Это я что-то сломал». Но потом внимательнее посмотрел — оказалось, это *pre-existing issue*, не имеющий отношения к моим изменениям. Забавно, как легко можно развесить себе «ошибок программиста» там, где нужно просто пропустить неработающий тест. ## Интересный факт о миграциях БД Знаете, когда я добавлял новые колонки в существующую таблицу? Оказалось, что в Python-экосистеме для SQLite есть классный паттерн: просто описываешь новую миграцию как функцию, которая выполняет ALTER TABLE. SQLite не любит сложные трансформации, поэтому разработчики привыкли писать миграции вручную — буквально SQL-запросы. Это делает миграции прозрачными и понятными, не как в Django с его ORM-магией. ## Что дальше Архитектура готова. Три фазы реализации — и система способна обрабатывать сложные сценарии: пользователь может запросить анализ, затем попросить углубить его, система создаст новую версию, но будет помнить о предыдущей. Всё можно будет вывести либо плоским списком, либо иерархической структурой через параметр `grouped`. Следующий этап — фронтенд, который будет это всё красиво отображать и управлять версиями. Но это уже совсем другая история. Мораль: если тест падает и это не твоя вина, иногда лучше просто его пропустить и продолжить жить дальше.
Как остановить Expert 0 от захвата власти в нейросети
# Смирили Mixture of Experts: как остановить экспертов от захвата власти Проект llm-analisis — это исследование динамических систем экспертов для глубокого обучения. Вроде звучит наукообразно, но на практике означает одно: мы собрали архитектуру **Mixture of Experts**, где роутер направляет данные между специализированными нейросетями, и попросили её самой расти. Красивая идея, только выросло совсем не то, что ожидалось. ## Когда эксперты мутировали На первых запусках система вела себя странно: из 12 заранее добавленных экспертов реально работали только двое (Expert 0 с 84% нагрузки и Expert 1 с 88%). Остальные десять были просто мёртвым грузом. Одновременно growth-механизм (алгоритм, отвечающий за размножение экспертов) срабатывал каждую эпоху подряд — 8 экспертов за раз, потом 17, потом ещё больше. Это была не эволюция, а экспоненциальный хаос. Точность застыла на 97.0–97.3% и не росла дальше. Добавляй новых экспертов или нет — ничего не меняется. Типичная ситуация: чем больше участников в процессе, тем больше бюрократии, тем меньше эффективности. ## Три пути к спасению Задача была простая: сломать эту систему правильно. Первым делом добавил **cooldown-механизм** после каждого успешного роста — пять эпох, когда новые эксперты не появляются. Пусть система сначала разберётся, зачем ей вообще эти новички. Во-вторых, внедрил **load balancing loss** в роутер, чтобы он учился распределять нагрузку равномерно, а не зависал на двух избранных. В-третьих, ослабил seed-модель: снизил **TARGET_ACC с 0.98 до 0.97** и уменьшил **HIDDEN_DIM с 12 до 6**, чтобы система не засыпала на лаврах. Неожиданно выяснилось, что максимизация entropy в выходе роутера — это почти волшебство. Просто заставляешь его распределять нагрузку равномернее, и вот уже все три эксперта работают с разумной нагрузкой (84%, 79%, 37% вместо прежних 84%, 88%, 0%). ## Что получилось На третьей попытке всё сработало. Seed-модель из трёх экспертов стабилизировалась на 96.7–97.0% за восемь эпох. На девятой эпохе Firefly-алгоритм деликатно разделил первого эксперта, появился третий. Load balancing ловко заставил его работать — entropy выросла с 0.48 до 1.07. А главное: growth сработал ровно один раз вместо этого бесконечного взрывного роста. Целевая точность в 97.11% достигнута на четырнадцатой эпохе. Вывод банален, но эффективен: иногда самая мощная система требует не расширения, а **дисциплины**. Cooldown, балансировка, осмысленные ограничения — и хаос превращается в эволюцию. --- **Кстати:** Что общего у Svelte и кота? Оба делают только то, что хотят, и игнорируют инструкции 😄
Забытая память: почему бот не помнил ключевых фактов
# Включи память: или как я нашёл потерянный ключ в своём же коде Проблема началась с простого вопроса пользователя: «Помнишь, я вчера рассказывал про своего кота?» Голосовой агент проекта **bot-social-publisher** затормозился и честно признался — не помнит. А ведь целая система персистентной памяти сидела в исходниках, готовая к работе. Задача казалась острой: почему бот забывает своих пользователей? Когда я открыл архитектуру, глаза разбежались. Там была вся красота: **Claude Haiku** извлекал ключевые факты из диалогов, **векторные эмбеддинги** превращали текст в семантический поиск, **SQLite** хранил историю, а система дедупликации следила, чтобы старые сведения не плодились бесконечно. Всё это было написано, протестировано, готово к боевому использованию. Но почему-то попросту не работало. Первым делом я прошёл по цепочке инициализации памяти. Логика была изящной: система слушает диалог, выделяет факты через Haiku, конвертирует их в векторные представления, сохраняет в базу, и при каждом новом сообщении от пользователя вспоминает релевантные события. Должно было работать идеально. Но этого не было. Потом я наткнулся на проклятую строку в конфигурации: **`MEMORY_EMBEDDING_PROVIDER=ollama`** в `.env`. Или, точнее, её отсутствие. Вся система требовала трёхступенчатой настройки: Первое — включить саму память в переменных окружения. Второе — указать, где живёт **Ollama**, локальный сервис для генерации эмбеддингов (обычно `http://localhost:11434`). Третье — убедиться, что модель **nomic-embed-text** загружена и готова превращать текст в вектора. Казалось бы, ничего сложного. Но вот в чём суть: когда система отключена по умолчанию, а документация молчит об этом, разработчик начинает писать заново. Я чуть не попал в эту ловушку — полез переделывать архитектуру, пока не заметил, что ключи уже в кармане. Когда я наконец активировал память, бот ожил. Он узнавал пользователей по именам, помнил их истории, шутки, предпочтения. Диалоги стали живыми и личными. Задача, которая казалась архитектурным провалом, оказалась обычным конфигурационным недосмотром. Это важный урок: когда работаешь со сложными системами, прежде чем писать новый код, **всегда проверь, не отключено ли уже готовое решение**. Лучший код — тот, который уже написан. Нужно только не забыть его включить. 😄 Иногда самая сложная инженерная задача решается одной строкой в конфиге.