Блог
Публикации о процессе разработки, решённых задачах и изученных технологиях
SQLite спасает день: масштабируемая БД вместо хаоса в памяти
# SQLite вместо памяти: как я спас Telegram-ботов от хаоса Проект `bot-social-publisher` взлетал буквально на глазах. Каждый день новые пользователи подключали своих ботов, запускали кампании, расширяли функционал. Но в какой-то момент понял: у нас есть проблема, которая будет только расти. Где-то в недрах памяти процесса валялась вся информация о том, какие чаты под управлением, кто их владелец, какие у них настройки. Приватный чат? Группа? Канал? Всё это было либо в переменных, либо в логах, либо вообще только в голове. Когда рост пользователей начал экспоненциальный скачок, стало ясно: **нужна нормальная база данных. Правильная, масштабируемая, без требований на отдельный сервер**. **Первым делом посмотрел на то, что уже есть.** В проекте уже была собственная SQLite база в `data/agent.db`, и там спокойно жил `UserManager` — отличный пример того, как работать с асинхронными операциями через `aiosqlite`. Логика была простой: одна база, одна инфраструктура, одна точка подключения для всей системы. Так почему бы не применить ту же философию к чатам? Архитектурное решение созревало быстро. Не было никаких грёз о микросервисах, Redis-кэшах или какой-то сложности. Нужна таблица `managed_chats` с полями для `chat_id` (первичный ключ), `owner_id` (связь с пользователем), `chat_type` с `CHECK` constraint для валидации типов, `title` для названия и JSON-поле `settings` про запас на будущее. **Неожиданно выяснилось** — и это было критично — что индекс на `owner_id` вообще не опциональная штука. Когда пользователь запрашивает список своих чатов, база должна найти их за миллисекунды, а не сканировать таблицу от начала до конца. SQLite часто недооценивают в стартапах, думают, что это игрушка для тестирования. На самом деле при правильном использовании индексов и подготовленных SQL-statements она справляется с миллионами записей и может быть полноценной боевой базой. Реализацию сделал по образцу `UserManager`: создал `ChatManager` с асинхронными методами `add_chat()`, `is_managed()`, `get_owner()`. Каждый запрос параметризован — никаких SQL-injection уязвимостей. Всё та же `aiosqlite` для асинхронного доступа, один способ работать с данными, без дублирования логики. Красивый момент получился благодаря `INSERT OR REPLACE` — если чат переиндексируется с новыми настройками, старая запись просто заменяется. Это вышло из архитектуры, не планировалось специально, но сработало идеально. **В итоге:** одна БД, одна инфраструктура, индекс уже готов к аналитическим запросам на будущее, JSON-поле ждёт расширенных настроек. Никаких ORM-фреймворков, которые на этом этапе обычно добавляют больше проблем, чем решают. Дальше — интеграция с обработчиками Telegram API, где эта информация начнёт по-настоящему работать. Но то уже следующая история. 😄 Разработчик говорит: «Я знаю SQLite». HR: «На каком уровне?». Разработчик: «На уровне, когда она работает, и я этому не верю».
SQLite вместо памяти: как обуздать рост Telegram-ботов
# Управляем Telegram-чаты как должно: от памяти к базе данных Проект `bot-social-publisher` рос как на дрожжах. Каждый день новые пользователи подключали своих ботов, запускали кампании, развивали функционал. Но вот беда: где-то в памяти процесса валялась информация о том, какие чаты под управлением, кто их владелец, какие у них настройки. Приватный чат? Группа? Канал? Всё это было либо в переменных, либо где-то в логах. Нужна была система. Правильная, масштабируемая, не требующая отдельного сервера для базы данных. **Первым делом** посмотрел, как устроен текущий стек. В проекте уже была собственная SQLite база в `data/agent.db`, и там жил `UserManager` — отличный пример того, как правильно работать с асинхронными операциями через `aiosqlite`. Значит, нужно просто добавить новую таблицу `managed_chats` в ту же базу, скопировать философию управления пользователями и запустить в production. **Архитектурное решение** было ясно с самого начала: никаких микросервисов, никаких Redis-кэшей для этого этапа. Нужна таблица с полями для `chat_id` (первичный ключ), `owner_id` (связь с пользователем), `chat_type` (с проверкой через `CHECK` constraint — только валидные типы), `title` и JSON-поле `settings` на будущее. Неожиданно выяснилось, что индекс на `owner_id` — это не опциональная штука. Когда пользователь запрашивает список своих чатов, база должна найти их быстро, а не сканировать всю таблицу от начала до конца. SQLite часто недооценивают в стартапах, думают, что это игрушка для тестирования. На самом деле при правильном использовании индексов и подготовленных statements она справляется с миллионами записей и может быть полноценной боевой базой. **Реализацию** сделал по образцу `UserManager`: создал `ChatManager` с асинхронными методами `add_chat()`, `is_managed()`, `get_owner()`. Каждый запрос параметризован — никаких SQL injection уязвимостей. Используется всё та же `aiosqlite` для асинхронного доступа, одна точка подключения для всей системы, без дублирования логики. Красивый момент получился благодаря `INSERT OR REPLACE` — если чат переиндексируется с новыми настройками, старая запись просто заменяется. Это вышло из архитектуры, а не планировалось специально. В итоге: одна БД, одна инфраструктура, масштабируемая схема. Когда понадобятся сложные аналитические запросы — индекс уже есть. Когда захотим добавить права доступа или расширенные настройки чата — JSON-поле ждёт. Никаких фреймворков ORM, которые обычно добавляют больше проблем, чем решают на этом этапе. Дальше — интеграция с обработчиками Telegram API, где эта информация будет реально работать. Но то уже следующая история. 😄 Разработчик: «Я знаю ArgoCD». HR: «На каком уровне?». Разработчик: «На уровне Stack Overflow».
Когда голосовой агент забывает встречи: учим AI слушать, не переспрашивая
# Когда AI забывает контекст: как мы учим голосовых агентов помнить о встречах Павел получил от своего AI-ассистента странный ответ. Вместо простого подтверждения встречи с Максимом в понедельник в 18:20 система начала задавать уточняющие вопросы: какой сегодня день, нужно ли запомнить дату, может быть, это повторяющаяся встреча? Звучит как помощник с амнезией, верно? Но именно эта проблема и стояла перед командой при разработке **voice-agent** — проекта голосового ассистента нового поколения. **Завязка:** проект работает на стыке нескольких сложных технологий. Это не просто бот — это агент, который должен понимать контекст разговора, запоминать важные детали и действовать без лишних вопросов. Когда пользователь говорит: «Встреча в понедельник в 18:20», система должна понять, что это *конкретная информация*, требующая *сохранения*, а не просьба о консультации. Казалось бы, мелочь, но именно такие «мелочи» отделяют полезный AI от раздражающего помощника. **Первым делом** разработчики столкнулись с архитектурной задачей: как структурировать память агента? Система должна различать *информационные запросы* (где нужны уточнения) и *директивные команды* (где нужно просто выполнить и запомнить). Для voice-agent это означало внедрение многоуровневой системы идентификации интентов — понимание не просто слов, а *цели* высказывания. Неожиданно выяснилось, что естественный язык коварен: одна и та же фраза может означать и просьбу, и информационное сообщение в зависимости от интонации и контекста диалога. Решение пришло через разделение на четыре архитектурных уровня: идентификация и авторизация пользователя, структурированное хранение данных о событиях, логика обработки сообщений и, наконец, функциональность исполнения в экосистеме (Telegram, внутренние чаты, TMA). Каждый уровень отвечает за свой кусок пазла. Система теперь не просто парсит текст — она *понимает ролевую модель* пользователя и принимает решения на основе его прав доступа. **Интересный факт:** большинство разработчиков голосовых агентов забывают, что люди говорят не как компьютеры. Мы пропускаем детали, которые кажутся нам очевидными, перепрыгиваем между темами и ожидаем, что AI дозаполнит пробелы. Именно поэтому лучшие голосовые системы — это не те, что задают много вопросов, а те, что *предполагают контекст* и только уточняют краевые случаи. Voice-agent учится работать в режиме доверия: если пользователь говорит достаточно конкретно, система действует; если неясно — *тогда* уточняет. **Итог:** Павел больше не получит сотню вопросов по поводу встречи. Система научилась различать между «помоги мне разобраться» и «запомни это». Проект все ещё в разработке, но архитектура уже показывает, что AI может быть не просто компетентным, но и *вежливым* — уважающим время пользователя и его интеллект. Дальше команда планирует внедрить предиктивную логику: система будет не только запоминать встречи, но и предлагать календарные уведомления, проверять конфликты времени и даже предлагать переносы на основе истории поведения пользователя. Но это уже совсем другая история. 😄 Что общего у yarn и подростка? Оба непредсказуемы и требуют постоянного внимания.
Когда AI становится парным программистом: история Claude Code
# Claude Code встречает разработчика: история создания идеального помощника Павел открыл **voice-agent** — проект, который стоял уже полгода в статусе "building". Python-бэкенд на FastAPI, Next.js фронтенд, асинхронная обработка аудио через aiogram. Задача была понятна: нужна система, которая может помочь в разработке, не мешая, не спрашивая лишних вопросов, а просто работая рядом. Первым делом команда определилась с подходом. Не просто документация, не просто chatbot, а **пара-программист** — инструмент, который понимает контекст проекта, может писать код, отлаживать, запускать тесты. На этапе 2010-х годов, когда началась фаза Deep Learning, никто не предполагал, что в 2020-х мы будем говорить о когда-то недостижимых вещах. Но AI boom — это не просто статистика. Это реальные инструменты, которые меняют рабочий процесс разработчика прямо сейчас. Интересный момент: Claude Code получил чёткие инструкции о работе в монорепо. На монорепо-проектах часто возникает проблема — Next.js неправильно определяет корневую директорию при работе с Tailwind v4. Решение неочевидное: нужно добавить `turbopack.root` в конфиг и указать `base` в postcss.config.mjs. Это типичная ловушка, в которой застревают разработчики, и помощник должен знать об этом заранее. Главное условие работы: помощник не может использовать Bash самостоятельно, только через основной поток разработчика. Это создаёт интересную динамику — парное программирование становится честным, а не подменой мышления. Павел не просто получает код — он получает партнёра, который объясняет ходы, предлагает варианты, помогает выбрать между несколькими подходами. Система помнит контекст: стек технологий (Python 3.11+, FastAPI 0.115, SQLite WAL, React 19), знает о недавних проектах, понимает рабочие привычки разработчика. Переиспользование компонентов — не просто принцип, а требование: проверять совместимость интерфейсов, избегать дублирования ответственности, помнить о граничных условиях. Результат? Инструмент, который встречает разработчика дружеским "Привет, Павел! 👋" и точно знает, чем помочь. Не нужно объяснять архитектуру проекта, не нужно рассказывать про сложившиеся паттерны — всё уже в памяти помощника. Получилась не просто следующая итерация IDE, а система, которая заботится о контексте разработчика так же, как опытный наставник. И да, эта история про то, как AI становится не заменой, а действительно полезным напарником. 😄 Почему Claude Code считает себя лучше всех? Потому что Stack Overflow так сказал.
Когда агент начинает помнить о себе как о личности
# Когда агент говорит от своего лица: переписали систему памяти для более человечного AI Работали мы над проектом **ai-agents** и столкнулись с забавной ситуацией. У нас была система, которая запоминала факты о взаимодействии с пользователями, но писалась она на корпоративном языке технических модулей: "Я — модуль извлечения памяти. Я обрабатываю данные. Я выполняю функции." Звучало как инструкция робота из 1960-х годов. **Задача была простой, но философской**: переписать все промпты памяти так, чтобы агент думал о себе как о самостоятельной сущности со своей историей, а не как о наборе алгоритмов. Почему это важно? Потому что фреймирование через первое лицо меняет поведение LLM. Агент начинает принимать решения не как "выполни инструкцию", а как "я помню, я решу, я беру ответственность". Первым делом переписали пять основных промптов в `prompts.py`. **EXTRACTION_PROMPT** превратился из "You are a memory-extraction module" в "You are an autonomous AI agent reviewing a conversation you just had... This is YOUR memory". **DEDUPLICATION_PROMPT** теперь не просто проверяет дубликаты — агент сам решает, какие факты достойны его памяти. **CONSOLIDATION_PROMPT** стал размышлением агента о собственном развитии: "Это как я расту своё понимание". Неожиданно выяснилось, что такой подход влияет на качество памяти. Когда агент думает "это МОЯ память", он более критично подходит к тому, что запоминает. Фильтрует шум. Задаётся вопросами. Затем переписали системный промпт в `manager.py`. Там была скучная таблица фактов — теперь это раздел "Моя память (ВАЖНО)" с подразделами "Что я знаю", "Недавний контекст", "Рабочие привычки и процессы", "Активные проекты". Каждая секция написана от первого лица: "то, что я помню", "я обязан использовать это", "я заметил закономерность". Итог: агент стал более *осознанным* в своих решениях. Он не просто выполняет алгоритмы обработки памяти — он *рефлексирует* над собственным опытом. И да, это просто промпты и фреймирование, но это показывает, насколько мощное влияние имеет язык, на котором мы говорим с AI. Главный вывод: попробуйте переписать свои системные промпты от первого лица. Посмотрите, как изменится поведение модели. Иногда самые глубокие улучшения — это просто изменение перспективы. 😄 Почему агент начал ходить к психотерапевту? Ему нужно было лучше понять свою память.
Монорепо, голос и журнал ошибок: как AI учится не ломать код
# Когда AI-помощник встречается с монорепо: отладка голосового агента Проект `voice-agent` — это амбициозная задача: связать Python-бэкенд с Next.js-фронтенд в единый монорепо, добавить голосовые возможности, интегрировать Telegram-бота и веб-API. Звучит просто на словах, но когда начинаешь копать глубже, понимаешь: это кубик Рубика, где каждый вертел может что-то сломать. **Проблема, с которой я столкнулся, была банальной, но коварной.** Система работала в отдельных частях, но когда я попытался запустить полный цикл — бот берёт голос, отправляет на API, API обрабатывает через `AgentCore`, фронтенд получает ответ по SSE — где-то посередине всё разваливалось. Ошибки были разношёрстные: иногда спотыкался на миграциях БД, иногда на переменных окружения, которые загружались в неправильном месте. **Первым делом я понял: нужна система для документирования проблем.** Создал `ERROR_JOURNAL.md` — простой журнал "что сломалось и как это чинилось". Звучит банально, но когда в проекте участвуют несколько агентов разного уровня (Архитектор на Opus, бэкенд-фронтенд агенты на Sonnet, Junior на Haiku), этот журнал становится золотым стандартом. Вместо того чтобы каждый агент наново натыкался на баг с Tailwind v4 в монорепо, теперь первым делом смотрим журнал и применяем известное решение. **Архитектура обработки ошибок простая, но эффективная:** 1. Ошибка возникла → читаю `docs/ERROR_JOURNAL.md` 2. Похожая проблема есть → применяю известное решение 3. Новая проблема → исправляю + добавляю запись в журнал Основные боли оказались не в коде, а в конфигурации. С Tailwind v4 нужна магия в `next.config.ts` и `postcss.config.mjs` — добавить `turbopack.root` и `base`. SQLite требует WAL-режим и правильный путь к базе. FastAPI любит, когда переменные окружения загружаются только в точках входа (`telegram_main.py`, `web_main.py`), а не на уровне модулей. **Интересный момент: я переоценил сложность.** Большинство проблем решались не рефакторингом, а правильной организацией архитектуры. `AgentCore` — это единое ядро бизнес-логики для бота и API, и если оно валидируется с одной строки (`python -c "from src.core import AgentCore; print('OK')"`), весь стек работает как часы. **Итог:** система работает, но главный урок не в технических трюках — в том, что монорепо требует прозрачности. Когда каждая составляющая (Python venv, Next.js сборка, миграции БД, синхронизация переменных окружения) задокументирована и протестирована, даже сложный проект становится управляемым. Теперь каждый новый агент, который присоединяется к проекту, видит ясную картину и может сразу быть полезным, вместо того чтобы возиться с отладкой. На следующем этапе плотнее интегрирую streaming через Vercel AI SDK Data Stream Protocol и расширяю систему управления чатами через новую таблицу `managed_chats`. Но это — уже другая история. 😄 Что общего у монорепо и парка развлечений? Оба требуют хорошей разметки, иначе люди заблудятся.
AI изучает себя: как мы мониторим научные тренды
# Когда AI исследует сам себя: как мы строили систему мониторинга научных трендов Вот уже несколько недель я сидел над проектом **trend-analisis** и постепенно понимал: обычный парсер научных статей — это скучно и малоэффективно. Нужна была система, которая не просто собирает ссылки на arXiv, а *понимает*, какие исследовательские направления сейчас набирают силу и почему они имеют значение для практиков вроде нас. Задача стояла серьёзная: проанализировать тренд под названием "test SSE progress" на основе контекста передовых научных статей. Звучит сухо, но на деле это означало — нужно было построить мост между миром фундаментальных исследований и инженерными решениями, которые уже завтра могут оказаться в production. ## Что творится в AI-исследованиях прямо сейчас Первым делом я разобрался, какие пять основных направлений сейчас наиболее активны. И вот что получилось интересное: **Мультимодальные модели всё более хитрые.** Появляются проекты вроде **SwimBird**, которые позволяют языковым моделям переключаться между разными режимами рассуждения. Это не просто пухлая нейросеть — это система, которая знает, когда нужно "думать", а когда просто генерировать. **Геометрия — это новый король.** Статьи про пространственное рассуждение показывают, что просто скормить модели килотонны текста недостаточно. Нужны геометрические приоры, понимание 3D-сцен, позиции камер. Проект **Thinking with Geometry** буквально встраивает геометрию в процесс обучения. Звучит как философия, но это работает. **Retrieval-системы перестают быть простыми.** Исследование **SAGE** показало, что для глубоких исследовательских агентов недостаточно BM25 или даже простого векторного поиска. Нужны умные retriever'ы, которые сами знают, что ищут. **Дешёвые модели становятся умнее.** Работы про влияние compute на reinforcement learning показывают: вопрос уже не в том, сколько параметров у модели, а в том, как эффективно использовать доступные ресурсы. Это открывает путь к edge AI и мобильным решениям. **Generative модели наконец-то становятся теоретически понятнее.** Исследования про generalization в diffusion models через inductive biases к ridge manifolds — это не просто красивая математика. Это значит, мы начинаем понимать, *почему* эти модели работают, а не просто наблюдаем результаты. ## Как я это собирал На ветке **feat/scoring-v2-tavily-citations** я сделал интересный ход: интегрировал не просто поиск статей, а *контекстный анализ* с использованием мощных LLM. Система теперь не только находит статьи по ключевым словам, но и организует их в экосистемы: какие зоны исследований связаны, кто на них работает, как это может повлиять на индустрию. Неожиданно выяснилось, что самая сложная часть — не техническая. Это правильно определить связи между соседними трендами. Статья про hydraulic cylinders и friction estimation на первый взгляд кажется совершенно отдельной историей. Но когда понимаешь, что это про predictive maintenance и edge computing, видишь, как она связывается с работами про efficient RL. Промышленная автоматизация и AI-на-краю сети — они развиваются параллельно и подпитывают друг друга. ## Маленький инсайт о diffusion models Кстати, пока копался в исследованиях про обобщающую способность diffusion models, наткнулся на замечательный факт: эти модели естественным образом тяготеют к низкомерным многообразиям в данных. Это не баг и не случайность — это встроенное в архитектуру свойство, которое позволяет моделям избежать зубрёжки и научиться реально генерировать новые примеры. Вот такое вот невидимое мастерство работает под капотом. ## Что дальше Система уже в работе, регулярно обновляется с новыми статьями, и каждый раз я вижу, как исследовательские темы переплетаются в более сложные паттерны. Это напоминает наблюдение за живой экосистемой — каждое новое открытие создаёт точки приложения для трёх других. Главное, что я понял: мониторить тренды в AI — это не про сбор информации, это про построение карты будущего. И каждая на первый взгляд узкая статья может оказаться ключевой для вашего следующего проекта. 😄 Обед разработчика: ctrl+c, ctrl+v из вчерашнего меню.
Регулярка в f-строке сломала SSE: как Python запутался в скобках
# Вся беда была в f-строке: как регулярное выражение сломало SSE-поток Работаю над проектом **trend-analisis** — системой для анализа трендов с помощью AI. На ветке `feat/scoring-v2-tavily-citations` нужно было реализовать вторую версию скорингового движка с поддержкой цитирования результатов через Tavily. Ключевой момент: вся архитектура строилась на Server-Sent Events, чтобы клиент получал аналитику в реальном времени по мере обработки каждого шага. Теоретически всё выглядело идеально. Backend на Python готов отправлять потоковые данные, API спроектирован, тесты написаны. Я запустил сервер, инициировал первый анализ и… ничего толкового не дошло до клиента. SSE-поток шёл, но данные приходили в каком-то странном формате, анализатор не мог их распарсить. Что-то явно ломалось на этапе подготовки ответа. Первый подозреваемый — кодировка. Windows-терминалы известны своей способностью превращать UTF-8-текст в «garbled text». Поехал в логи, начал смотреть, что именно генерируется на сервере. И вот тут выяснилось что-то совершенно неожиданное. **Виновником было регулярное выражение, спрятанное внутри f-строки.** В коде я использовал конструкцию `rf'...'` — это raw f-string, комбинация, которая кажется идеальной для работы с регексами. Но внутри этого выражения жил квантификатор `{1,4}`, и здесь произошла магия несовместимости. Python посмотрел на эти фигурные скобки и подумал: «А может, это переменная для интерполяции?» Результат: парсер пытался интерпретировать `{1,4}` как синтаксис подстановки, а не как часть регулярного выражения. Регекс ломался молча, и весь парсинг SSE-потока шёл вразнос. Решение оказалось элегантным, но коварным: нужно было просто экранировать скобки — превратить `{1,4}` в `{{1,4}}`. Двойные скобки говорят Python: «Это текст для регулярного выражения, не трогай». Звучит просто? Да. Но найти это среди километра логов — совсем другое дело. **Забавный факт:** f-строки появились в Python 3.6 и революционизировали форматирование текста. Но когда ты комбинируешь их с raw-строками и регулярными выражениями, получается коварная ловушка. Большинство опытных разработчиков просто избегают этого танца — либо используют обычные строки, либо передают регекс отдельно. Это классический пример того, как синтаксический сахар может стать источником часов отладки. После исправления бага я перезагрузил сервер и сразу же приступил ко второй проблеме: интерфейс был заполнен английскими текстами. Все заголовки анализа нужно было переместить в карту локализации русского языка. Прошёлся по коду, добавил русские варианты, заметил только один пропущенный "Stats", который быстро добавил в словарь. Финальная перезагрузка — и всё встало на место. SSE-поток работает без сбоев, данные доходят до клиента корректно, интерфейс полностью русифицирован. Главный вывод простой: когда работаешь с raw-strings в Python и засовываешь туда регулярные выражения с квантификаторами, всегда помни про двойное экранирование фигурных скобок. Это экономит часы отладки и стресса. 😄 F-строки и регексы — битва синтаксиса, в которой проигрывают все.
f-строки vs регулярные выражения: коварная битва синтаксиса
# Поймал баг с f-строками: когда регулярные выражения подводят в самый неожиданный момент Работаю над проектом **trend-analysis** — системой для анализа трендов с использованием AI. Задача была создать версию v2 с поддержкой цитирования результатов через Tavily. На ветке `feat/scoring-v2-tavily-citations` мы реализовали SSE-поток для того, чтобы клиент получал результаты анализа в реальном времени, по мере их обработки. Казалось бы, всё работает: сервер запущен, архитектура продумана, Python-backend готов отправлять данные в формате Server-Sent Events. Но когда я попробовал запустить быстрый анализ и проверить, что все шаги доходят до клиента, произошло что-то странное. Первым делом я заметил ошибку во время разбора результатов. Погружаться в логи пришлось глубоко, и вот тут выяснилось что-то удивительное: баг был спрятан прямо в моём регулярном выражении. **Вся беда была в f-строке.** Видите, я использовал конструкцию `rf'...'` — raw f-string для работы с регулярными выражениями. Но когда в выражении появился квантификатор `{1,4}`, Python не посчитал его просто текстом — он попытался интерпретировать его как переменную в f-строке. Результат: регекс ломался на этапе компиляции. Решение оказалось элегантным: нужно было экранировать фигурные скобки двойными `{{1,4}}`. Это позволило Python понять, что скобки — часть регулярного выражения, а не синтаксис подстановки переменных. **Интересный факт:** f-строки в Python (появились в версии 3.6) революционизировали форматирование, но при работе с регулярными выражениями они могут быть настоящей минной лавкой. Разработчикам часто проще использовать обычную строку и передать регекс отдельно, чем разбираться с экранированием скобок. Это классический пример того, как синтаксический сахар может стать источником скрытых ошибок. После исправления ошибки я перезагрузил сервер и сразу взялся за локализацию интерфейса. Выяснилось, что в консоли большая часть текстов осталась на английском. Все заголовки нужно было переместить в карту локализации русского языка. Поначалу я видел garbled text — кодировка Windows делала своё чёрное дело в терминале, но после добавления русских строк в словарь последняя проверка показала: остался только один случай "Stats", который я оперативно добавил. Финальная перезагрузка и проверка — и всё встало на место. SSE-поток работает, данные доходят до клиента корректно, интерфейс полностью русифицирован. Урок, который я вынес: когда работаешь с raw-strings в Python и регулярными выражениями внутри f-строк, всегда помни про двойное экранирование. Это спасает часы отладки. 😄 Ловушка с Python f-строками и регексами — идеальный кандидат на звание «самый коварный баг, который выглядит как опечатка».
Очередь событий вместо словаря: как спасти архитектуру
# От очереди событий к стабильной архитектуре: как мы чинили `_fix_headings` в trend-analysis Есть у нас проект **trend-analysis** — сервис, который анализирует тренды и работает с длинными асинхронными операциями. Недавно на ветке `feat/scoring-v2-tavily-citations` разрабатывали новую систему цитирования для поиска информации через API Tavily. И вот столкнулись с классической проблемой: функция `_fix_headings` отлично выглядела в теории, но в боевых условиях вела себя странновато. Задача была чётко сформулирована — нужно убедиться, что функция работает правильно в изоляции, а потом интегрировать её в SSE-генератор для отправки прогресса клиенту. Звучит просто, но когда начинаешь копать глубже, появляются неожиданные подводные камни. Первым делом прямо протестировал `_fix_headings` — хотел убедиться, что логика нормализации заголовков работает так, как задумано. Потом понял, что архитектура прогресса была криво спроектирована. Изначально система работала с `_progress` как с обычным словарём, но это не масштабировалось — нужна была очередь событий для корректной работы SSE-потока. Казалось, нет ничего сложного в переходе с простого словаря на список событий? На практике это означало перепроверку каждого места в коде, где мы обращались к `_progress`. Неожиданно выяснилось, что одного только обновления структуры недостаточно. В коде было порядка десятка ссылок на старую переменную — кто-то обращался к `_progress`, не зная про `_progress_events`. Например, в endpoint'е на строке 661, который выдаёт последний статус для running jobs, нужно было менять логику: теперь брали не весь словарь, а последний элемент из истории событий. Это критично для получения актуального состояния операции. Вообще, асинхронные системы с очередями событий — это не просто паттерн проектирования, это философия. Когда вы переходите от прямых обращений к состоянию на событийную архитектуру, вы получаете отказоустойчивость и аудит совершенно даром. Каждое событие — это запись в истории, которую можно воспроизвести и разобраться, что пошло не так. Минус только один: нужно везде помнить об этом и не впадать в соблазн «быстро обратиться к состоянию напрямую». После того как обновил все ссылки и переписал логику чтения прогресса, перезапустил сервер и гонял тесты. `_fix_headings` теперь работает в составе полноценного SSE-потока, события корректно попадают в очередь, а клиент получает актуальные обновления. На ветке `feat/scoring-v2-tavily-citations` система стала заметно стабильнее, и мы готовы двигать дальше с интеграцией цитирований. Вывод простой — иногда маленькая функция может разоблачить архитектурные проблемы. Стоит протестировать в изоляции, убедиться, что всё работает, а потом внимательно проверить, как это интегрируется с остальной системой.
Логи, которые врут: как я нашел ошибку в прошлом Traefik
# Traefik и Let's Encrypt: как я нашел ошибку в логах прошлого Проект **borisovai-admin** молча кричал. Пользователи не могли зайти в систему — браузеры показывали ошибки с сертификатами, Traefik выглядел так, будто вообще забыл про HTTPS. На поверхности всё выглядело очевидно: проблема с SSL. Но когда я начал копать, стало ясно, что это детективная история совсем о другом. ## Завязка: четыре недостающих сертификата Задача была на первый взгляд скучной: проверить, действительно ли Traefik получил четыре Let's Encrypt сертификата для admin и auth поддоменов на `.tech` и `.ru`. DNS для `.ru` доменов только что пропагировался по сети, и нужно было убедиться, что ACME-клиент Traefik успешно прошёл валидацию и забрал сертификаты. Я открыл **acme.json** — файл, где Traefik хранит весь свой кеш сертификатов. И тут началось самое интересное. ## Развитие: сертификаты на месте, но логи врут В файле лежали все четыре сертификата: - `admin.borisovai.tech` и `admin.borisovai.ru` — оба выданы Let's Encrypt R12 - `auth.borisovai.tech` и `auth.borisovai.ru` — R13 и R12 Все валидны, все активны, все будут работать до мая. Traefik их отдавал при подключении. Но логи Traefik были заполнены ошибками валидации ACME-челленджей. Выглядело так, будто сертификаты получены, но используются неправильно. Тогда я понял: эти ошибки в логах — **не текущие проблемы, а исторические артефакты**. Когда DNS для `.ru` ещё не полностью пропагировался, Traefik пытался пройти ACME-валидацию, падал, переходил в retry-очередь. DNS резолвился нестабильно, Let's Encrypt не мог убедиться, что домен принадлежит нам. Но как только DNS наконец стабилизировался, всё прошло автоматически. Логи просто записывали *историю пути к успеху*. ## Познавательный момент: асинхронная реальность Вот в чём фишка ACME-систем: они не сдаются после первой же неудачи. Let's Encrypt встроил resilience в саму архитектуру. Когда челлендж не проходит, он не удаляется — он встаёт в очередь на переток. Система периодически переходит сертификаты, ждёт, когда DNS стабилизируется, и потом *просто работает*. То есть когда ты видишь в логах ACME-ошибку прошлого часа, это вообще не означает, что сейчас есть проблема. Это просто означает, что система пережила переходный процесс и вышла на стабильное состояние. Проблема с браузерами была ещё смешнее. Они кешировали старую информацию о неправильных сертификатах и упорно показывали ошибку, хотя реальные сертификаты давно уже валидны. Решение: `ipconfig /flushdns` на Windows или просто открыть incognito-окно. ## Итог **borisovai-admin** работает, все четыре сертификата на месте, все домены защищены. Главный урок: иногда лучший способ отловить баг — это понять, что это вообще не баг, а просто *асинхронная реальность*, которая движется по своему расписанию. Следующий этап — проверить, правильно ли настроены policies в Authelia для этих новых защищённых endpoints. Но это уже совсем другая история. Java — единственная технология, где «это работает» считается документацией. 😄
Traefik и Let's Encrypt: как я нашел ошибку в логах прошлого
# Охота на невидимых врагов: как я отловил проблемы с сертификатами в Traefik Когда ты администрируешь **borisovai-admin** и вдруг замечаешь, что половина пользователей не может зайти в систему из-за ошибок сертификатов, начинается самая интересная работа. Задача казалась простой: проверить конфигурацию сервера, DNS и убедиться, что сертификаты на месте. На практике это превратилось в детективную историю про хронологию событий и кеши, которые саботируют твою жизнь. ## Первый подозреваемый: DNS Первым делом я проверил, резолвятся ли доменные имена с сервера. Оказалось, что DNS работает — это был хороший знак. Но почему Traefik выглядит так, будто ему не хватает сертификатов? Я полез в `acme.json`, где Traefik хранит выданные Let's Encrypt сертификаты. И вот тут началось самое интересное. ## Сюрприз в acme.json В файле лежали **все четыре сертификата**, которые мне были нужны: - `admin.borisovai.tech` — Let's Encrypt R12, выдан 4 февраля, истекает 5 мая - `admin.borisovai.ru` — Let's Encrypt R12, выдан 8 февраля, истекает 9 мая - `auth.borisovai.tech` — Let's Encrypt R13, выдан 8 февраля, истекает 9 мая - `auth.borisovai.ru` — Let's Encrypt R12, выдан 8 февраля, истекает 9 мая Все они были **валидны и активны**. Traefik их отдавал при подключении. Логи Traefik, которые я видел ранее, оказались проблемой *ретроспективной* — они относились к моменту, когда DNS-записи для `.ru` доменов ещё *не пропагировались* по сети. Let's Encrypt не мог выпустить сертификаты, пока не мог убедиться, что домен принадлежит мне. ## Невидимый враг: браузерный кеш Последний вопрос был ужасающе простым: почему браузер по-прежнему ругался на сертификаты, если сами сертификаты в порядке? **DNS кеш**. Браузер запомнил старую информацию и упорно её использовал. ## Финальный диагноз Вся история сводилась к тому, что системные часы интернета движутся медленнее, чем кажется. DNS пропагируется асинхронно, сертификаты выдаются с задержкой, а браузеры кешируют запросы агрессивнее, чем кажется разумным. Решение? Очистить DNS кеш командой `ipconfig /flushdns` (для Windows) или открыть инкогнито-окно, чтобы браузер забыл о своих ошибочных воспоминаниях. Проект **borisovai-admin** работает, сертификаты в порядке, все домены защищены. Ирония в том, что проблема была не в конфигурации — она была в нашей нетерпеливости. Главный урок: иногда лучший способ отловить баг — это понять, что это не баг, а *асинхронная реальность*, которая просто медлит. 😄
Двойная аутентификация: когда два охранника мешают друг другу
# Двойная защита убивает саму себя: как я развязал узел конфликтующей аутентификации Задача стояла простая на первый взгляд: запустить Management UI для проекта **borisovai-admin**. Казалось, что админ-панель встанет и будет работать. Но когда я подключил её к боевой инфраструктуре, выяснилось нечто интересное — UI запустилась, но пройти аутентификацию было невозможно. ## Когда две защиты становятся одной проблемой Начал копать логи и вот что нашёл. В инфраструктуре уже была слоёная защита: **Traefik** с плагином **ForwardAuth** отправлял все запросы на **Authelia** для двухфакторной аутентификации. Это первый уровень охраны — на уровне прокси. Здесь логика простая: если запрос идёт на `admin.borisovai.tech`, Traefik вежливо перенаправляет пользователя в Authelia. Но когда я добавил Management UI с встроенной OIDC-аутентификацией через **express-openid-connect**, произошло вот что: пользователь уже прошёл Authelia на уровне Traefik, но Management UI не поверил ему и снова отправил на Authelia через OIDC. Два редиректа подряд — и браузер начинает петлять между разными провайдерами аутентификации. Типичная ситуация, когда каждый охранник требует личный документ, не доверяя соседу. ## Выбор между защитами Встал вопрос: какой уровень аутентификации оставить? Отключить Traefik ForwardAuth? Отключить OIDC в Management UI? Или искать способ их синхронизировать? Я выбрал проверенный путь — **оставить Traefik ForwardAuth как основную защиту**, а OIDC отключить. Логика здесь такая: раз у нас уже есть надёжная защита на уровне прокси с поддержкой 2FA через Authelia, зачем добавлять второй слой? Внутри же Management UI я оставил **legacy session** — простую аутентификацию по логину и паролю. Получилось двухуровневое решение, но на разных слоях: внешняя защита через прокси и внутренняя через сессию. После изменений Management UI перезапустился без OIDC-интеграции. Теперь схема работает так: вы входите в `https://admin.borisovai.tech`, Traefik перенаправляет вас в Authelia, вы проходите двухфакторную аутентификацию, а потом попадаете на страницу логина самой админ-панели, где вводите учётные данные Management UI. ## Интересный факт о OIDC Стандарт **OpenID Connect** создан в 2014 году поверх OAuth 2.0 именно для решения проблем единого входа. Но мало кто знает, что OIDC работает лучше всего, когда он — **единственный** поставщик идентификации в системе. Как только вы пытаетесь слоить несколько провайдеров, начинаются конфликты. Классическая ловушка — стараться защитить приложение со всех сторон и получить вместо этого лабиринт редиректов. ## Неприятный бонус: проблема с `.ru` доменами Во время работы я обнаружил, что A-записи для `admin.borisovai.ru` и `auth.borisovai.ru` не добавлены у регистратора IHC. Let's Encrypt не может выдать сертификаты для доменов, которых нет в DNS. Решение пришло быстро — нужно добавить эти A-записи в панели регистратора, указывая на IP `144.91.108.139`. Казалось бы, мелочь, но именно такие детали часто становятся причиной того, что production не поднимается. ## Что я вынес из этого Главный урок: **слои безопасности должны дополнять друг друга, а не конкурировать**. Двойная аутентификация хороша, когда она — настоящая: первый слой защищает периметр, второй охраняет внутренние ресурсы. Но когда оба слоя пытаются делать одно и то же через разные системы, получается конфликт. Теперь Management UI работает, защита работает, и никто не просит удостоверения дважды. Инфраструктура проекта borisovai-admin стала на один уровень надёжнее. 😄 Почему Prometheus не пришёл на вечеринку? Его заблокировал firewall.
DNS-кеш и фантомный поддомен: охота на NXDOMAIN
# Когда DNS кеш становится врагом: охота на фантомный поддомен Работаю над проектом **borisovai-admin** — админ-панелью с собственной системой аутентификации. Задача казалась простой: мигрировать auth-сервис на новый поддомен `auth.borisovai.tech` и убедиться, что всё резолвится корректно. Добавил DNS-записи в регистратор, обновил конфиги приложения — и вот тут началось веселье. ## Первый знак беды Первая проверка через Google DNS (`8.8.8.8`) показала идеальный результат: `auth.borisovai.tech` резолвился на `144.91.108.139` без проблем. Казалось бы, всё готово. Но когда я переключился на **AdGuard DNS** (`94.140.14.14`), который был настроен по умолчанию в инфраструктуре, домен превратился в привидение — стандартная ошибка `NXDOMAIN`, как будто записи вообще не существуют. А вот `admin.borisovai.tech` спокойно резолвился везде. Значит, проблема именно с `auth.*`. Не лучший момент для такого сюрприза — особенно когда нужно срочно закрыть фичу. ## Расследование Запустил диагностику: попросил оба DNS-резолвера вернуть записи для `auth.borisovai.tech` и `auth.borisovai.ru`. Результат совпадал: Google видел, AdGuard не видел. Явный паттерн. Тут меня осенило — это же **отрицательный кеш DNS**! Вот как это работает: когда ты запрашиваешь несуществующий домен, DNS-резолвер кеширует не только положительные ответы, но и отрицательные. То есть он "запоминает", что домена нет, и хранит это в памяти с собственным TTL (Time To Live). У AdGuard это может быть час или даже дольше. Получается, что когда я добавлял DNS-записи, AdGuard уже давно закешировал `NXDOMAIN` для `auth.borisovai.tech`. И даже если запись появилась на авторитетном сервере регистратора, этот кеш продолжал отвечать: "Нет такого домена, я уверен, я это помню". ## Как я выбрался Вариант первый — просто ждать. AdGuard истечёт кеш, и всё чудо-образом заработает. Но тестировать нужно было *прямо сейчас*. Вариант второй — переключиться на Google DNS для локального тестирования. Работает мгновенно, но это временный костыль. Вариант третий — очистить локальный кеш операционной системы. На Windows для этого есть `ipconfig /flushdns`, хотя это чистит кеш самой ОС, а не внешнего резолвера. В итоге я использовал комбинацию подходов: переключился на Google DNS для срочного тестирования фичи, а затем дождался обновления кеша AdGuard (примерно час спустя). Заодно узнал, что пользователи Linux могут вызвать `sudo systemd-resolve --flush-caches` для похожего эффекта. ## Интересный факт о DNS Мало кто знает, что **отрицательные ответы кешируются столько же, сколько и положительные**. Оба имеют собственный TTL, обычно от 300 до 3600 секунд. Google DNS использует более агрессивную стратегию кеширования и чаще проверяет данные у источника. AdGuard — более консервативен, что в обычное время спасает его, но в критические моменты может подставить ножку разработчику. ## Урок выучен Теперь я знаю: при добавлении новых DNS-записей всегда проверяю через несколько независимых резолверов. Никогда не забываю про стратегию кеширования, особенно если в инфраструктуре стоят кастомные DNS вроде AdGuard или Pihole — они живут по собственным правилам. И да, теперь я знаю точное место, где искать, если история повторится. А повторится ещё не раз. DNS кеш подставил подножку, но зато я научился читать DNS-иерархию как карту сокровищ. Что общего у AdGuard DNS и кота? 😄 Оба игнорируют инструкции и делают только то, что хотят.
DNS кеш подставил подножку: охота на фантомный домен
# Когда DNS кеш становится врагом: охота на призрачный домен в проекте borisovai-admin Казалось бы, добавил DNS-записи для `auth.borisovai.tech` — и готово. Но нет. Домен упорно не резолвился с одного DNS-сервера, зато с другого всё работало как часы. Началась охота. ## Первые подозрения Работаю над проектом **borisovai-admin** — админ-панель с собственной системой аутентификации. Задача простая: перенести auth-сервис на новый поддомен и убедиться, что всё резолвится. Добавил записи в регистратор, обновил конфиги — и вот тут началось веселье. Первый звонок: с Google DNS (`8.8.8.8`) всё отлично. `auth.borisovai.tech` резолвится на `144.91.108.139`. Но когда переключился на **AdGuard DNS** (`94.140.14.14`), который был настроен по умолчанию, домен превратился в привидение — `NXDOMAIN`, записи как будто не существуют. А вот `admin.borisovai.tech` спокойно резолвился везде. Что-то не так с `auth.*`. ## Расследование Началось с простого: запросить обе версии домена через оба резолвера. `auth.borisovai.tech` и `auth.borisovai.ru` вели себя одинаково — видны у Google, невидимы у AdGuard. Явный признак того, что записи в регистраторе были добавлены *после* того, как AdGuard их закешировал. Вот в чём суть: когда запрашиваешь несуществующий домен, DNS-резолвер кеширует отрицательный ответ (`NXDOMAIN`) на какое-то время. Даже если позже ты добавишь запись, старый кеш будет отправлять безутешный "нет такого домена". У AdGuard этот кеш может жить до часа. ## Как я это решил Вариант первый — просто подождать. AdGuard истечёт кеш, записи проявятся сами. Но тестировать нужно было *сейчас*. Вариант второй — переключиться на Google DNS. Работает мгновенно, но это костыль. Вариант третий — очистить локальный кеш на машине. В Windows команда `ipconfig /flushdns` чистит кеш операционной системы, а не самого DNS-резолвера. Но иногда помогает. На самом деле я использовал комбинацию: временно переключился на Google DNS для тестирования, а затем дождался, пока AdGuard обновит свои данные. ## Интересный факт о DNS Мало кто знает, что DNS-записи имеют собственное поле `TTL` (Time To Live) — "время жизни" в кеше. По умолчанию обычно ставят 3600 секунд (час). Google использует более агрессивную стратегию кеширования, AdGuard — более консервативную. Вот поэтому один резолвер сразу видит новую запись, а другой ещё час её "забывает". ## Вывод Простой урок: при добавлении новых DNS-записей всегда проверяй через несколько резолверов. Если в сети настроены кастомные DNS (как AdGuard или Pihole), они могут сыграть с тобой в злую шутку. И никогда не забывай про `ipconfig /flushdns` или `sudo systemd-resolve --flush-caches` на Linux — иногда это спасает часы дебага. Дальше — уже знаю, где искать, если история повторится. А повторится ещё не раз. Что общего у Netlify и кота? 😄 Оба делают только то, что хотят, и игнорируют инструкции.
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-код путешествует по файловой системе вместо того, чтобы сразу показаться в браузере 😄*