Блог
Публикации о процессе разработки, решённых задачах и изученных технологиях
От хаоса к объектам: как переделали API для трендов
# Регистрируем API эндпоинт: как архитектура трендов выросла из хаоса документации Мне нужно было разобраться с проектом **trend-analysis** — системой для отслеживания трендов из GitHub и Hacker News. Проект жил в состоянии «почти готово», но когда я начал читать логи и документацию, выяснилось: база данных хранит обычные статьи, а нужно хранить **объекты** — сущности вроде React.js или ChatGPT, за которыми стоит десятки упоминаний. Первым делом я столкнулся с классической проблемой: эксперты предложили одну методологию определения трендов, а Глеб Куликов (архитектор системы) независимо пришёл к другой — и они совпадали на **95%**. Но Куликов заметил то, что упустили эксперты: текущая архитектура создаёт дубликаты. Одна статья о React — один тренд, вторая статья о React — второй тренд. Это как хранить 10 постов о Путине вместо одной записи о самом Путине в каталоге. Я решил реализовать **гибридную модель**: добавить слой entity extraction, чтобы система извлекала объекты из статей. Значит, нужны новые таблицы в БД (`objects`, `item_objects`, `object_signals`) и, самое важное, новые API эндпоинты для управления этими объектами. **Вот тут начинается интересная часть.** API эндпоинты я размещал в `api/auth/routes.py` — стандартное место в проекте. Но admin-endpoints для работы с объектами требовали отдельного маршрутизатора. Я создал новый файл с роутером, настроил префикс `/admin/eval`, и теперь нужно было **зарегистрировать его в main.py**. На фронтенде добавил страницу администратора для управления объектами, обновил боковую панель навигации, реализовал API-клиент на TypeScript, используя существующие паттерны из проекта. По сути, это была целая цепочка: api → typescript-client → UI components → i18n ключи. **Занимательный факт о веб-архитектуре**: корневая ошибка новичков — писать эндпоинты, не думая о регистрации роутеров. Flask и FastAPI не магическим образом находят ваши функции. Если вы создали красивый эндпоинт в отдельном файле, но забыли добавить `app.include_router()` в main.py — для клиента это будет 404 Not Found. Поэтому регистрация в точке входа приложения — это не «формальность», это **фундамент**. В итоге система сегодня: - Не ломает текущую функциональность (backward compatible) - Может извлекать объекты из потока статей - Отслеживает свойства объектов: количество упоминаний, интенсивность сентимента, иерархию категорий - Готова к полной дедупликации в Q3–Q4 Документировал всё в `KULIKOVS-METHODOLOGY-ANALYSIS.md` — отчёт на 5 фаз имплементации. Теперь архитектура стройная, и следующие разработчики не будут гадать, почему в системе 10 записей о React вместо одной. 😄 Почему Ansible расстался с разработчиком? Слишком много зависимостей в отношениях.
121 тест в зелёном: как переписать сердце системы и ничего не сломать
# Когда 121 тест встают в строй: история запуска первого зелёного набора Проект `ai-agents` подошёл к критической точке. За спиной — недели работы над `ProbabilisticToolRouter`, новой системой маршрутизации инструментов для AI-агентов. На столе — 121 новый тест, которые нужно было запустить в первый раз. И вот, глубоко вдохнув, запускаю весь набор. Ситуация была напряженная. Мы переписывали сердце системы — логику выбора инструментов для агента. Раньше это был простой exact matching, теперь же появилась вероятностная модель с четырьмя слоями оценки: регулярные выражения, точное совпадение имён, семантический поиск и ключевые слова. Каждый слой мог конфликтовать с другим, каждый мог сломаться. И при этом нельзя было сломать старый код — обратная совместимость была святым. Первый запуск ударил болезненно: **120 пройдено, 1 упал**. Виноват был тест `test_threshold_filters_low_scores`. Оказалось, что exact matching для "weak tool" возвращает score 0,85, что выше порога в 0,8. Сначала я испугался — неужели роутер работает неправильно? Но нет, это было *корректное поведение*. Тест ловил старую логику, которую мы переделали. Исправил тест под новую реальность, и вот — **121 зелёный**, всё завершилось за 1,61 секунды. Но главное — проверить, что мы ничего не сломали. Запустил старые тесты. **15 пройдено за 0,76 секунды**. Все зелёные. Это было облегчение. Интересный момент здесь в том, как мы решали задачу покрытия. Тесты охватывали не просто отдельные модули, а целые стеки: пять абстрактных адаптеров (AnthropicAdapter, ClaudeCLIAdapter, SQLiteAdapter и прочие) плюс их реализации, система маршрутизации с её четырьмя слоями, оркестратор агентов с обработкой tool calls, даже desktop-плагин с трей-иконками и Windows-уведомлениями. Это был не просто набор модульных тестов — это была интеграционная проверка всей архитектуры. **А знаете интересный факт?** Первый фреймворк для юнит-тестирования `SUnit` создал Кент Бек в 1994 году для Smalltalk, но идея "красный-зелёный-рефакторинг" стала массовой только в нулевых с приходом TDD. Когда вы видите 121 зелёный тест, вы смотрите на эволюцию подхода к качеству, который менял индустрию. После этого запуска система стала более уверенной в себе. Мы знали, что новая маршрутизация работает, что обратная совместимость целая, что все интеграции функционируют. Это дало зелёный свет для дальнейших оптимизаций и рефакторинга кода. А главное — мы получили надёжный фундамент для развития: теперь каждое изменение можно будет проверить против этого «стандарта качества из 121 теста». Иногда разработка — это просто ожидание результата консоли. Но когда все полосы зелёные, это чувство стоит каждой минуты отладки. 😄
Как мы развязали узел агентов: adapter pattern в боевых условиях
# От паттерна к реальности: как мы завернули AI-агентов в красивую архитектуру Полгода назад я столкнулся с классической проблемой: проект `ai-agents` рос как на дрожжах, но код превратился в сложный клубок зависимостей. LLM-адаптеры, работа с БД, поиск, интеграции с платформами — всё смешалось в одном месте. Добавить новый источник данных или переключиться на другую модель LLM стало настоящим квестом. Решение было очевидным: **adapter pattern** и **dependency injection**. Но дьявол, как всегда, сидит в деталях. Первым делом я создал иерархию абстрактных адаптеров. `LLMAdapter` с методами `chat()`, `chat_stream()` и управлением жизненным циклом, `DatabaseAdapter` для универсального доступа к данным, `VectorStoreAdapter`, `SearchAdapter`, `PlatformAdapter` — каждый отвечает за свой слой. Звучит скучно? Но когда ты реализуешь эти интерфейсы конкретно — начинает быть интересно. Я написал **AnthropicAdapter** с полной поддержкой streaming и tool_use через AsyncAnthropic SDK. Параллельно сделал **ClaudeCLIAdapter** — суперсредство, позволяющее использовать Claude через CLI без затрат на API (пока это experimental). Для работы с данными подключил **aiosqlite** с WAL mode — асинхронность плюс надёжность. **SearxNGAdapter** с встроенным failover между инстансами. **TelegramPlatformAdapter** на базе aiogram. Всё это управляется через **Factory** — просто конфиг меняешь, и готово. Но главная фишка — это **AgentOrchestrator**. Это сердце системы, которое управляет полным chat-with-tools циклом через адаптеры, не зная о деталях их реализации. Dependency injection через конструктор означает, что тестировать проще простого: подай mock'и — и программа думает, что работает с реальными сервисами. Вторая часть истории — **ProbabilisticToolRouter**. Когда у агента сто инструментов, нужно понимать, какой из них нужен на самом деле. Я построил систему с четырьмя слоями scoring: regex-совпадения (вес 0,95), точное имя (0,85), семантический поиск (0,0–1,0), ключевые слова (0,3–0,7). Результат — ранжированный список кандидатов, который автоматически инжектится в system prompt. Никаких случайных вызовов функций. А потом я подумал: почему бы не сделать это ещё и десктопным приложением? **AgentTray** с цветовыми индикаторами (зелёный — работает, жёлтый — обрабатывает, красный — ошибка). **AgentGUI** на pywebview, переиспользующий FastAPI UI. **WindowsNotifier** для уведомлений прямо в систему. И всё это — тоже адаптеры, интегрированные в ту же архитектуру. **Интересный факт**: паттерн adapter родился в 1994 году в книге «Gang of Four», но в эру микросервисов и облачных приложений он переживает второе рождение. Его главная суперсила — не столько в самом коде, сколько в психологии: когда интерфейсы чётко определены, разработчики начинают *думать* о границах компонентов. Это спасает от копипасты и циклических зависимостей. По итогам: 20 новых файлов, полностью переработанная `config/settings.py`, обновленные requirements. Система теперь масштабируется: добавить нового LLM-провайдера или переключиться на PostgreSQL — это буквально несколько строк конфига. Код более тестируемый, зависимости явные, архитектура дышит. И главное — это работает. Действительно работает. 😄
Волшебный токен GitLab: от поиска до первого скопирования
# Как я чуть не сломал CI/CD, ища волшебный токен В проекте **borisovai-admin** встала задача: нужно проверять статус GitLab pipeline прямо из CI/CD, чтобы убедиться, что деплой прошёл успешно. Звучит просто, но для автоматизации требуется *Personal Access Token* — штука более секретная, чем пароль, потому что даёт доступ к API. Первым делом я попытался вспомнить, где в GitLab хранятся эти токены. Инстинкт подсказал: где-то в настройках профиля. Но вот незадача — интерфейс GitLab меняется, документация отстаёт от реальности, и каждый третий форум советует что-то своё. Начал искать по URL-адресам, как детектив, собирающий пазл. Выяснилось, что нужно открыть ровно вот этот URL: `https://gitlab.dev.borisovai.ru/-/user_settings/personal_access_tokens`. Не Settings, не API, не Profile — именно этот путь. Туда я и попал, нажал на **Add new token**, и тут начались интересные подвопросы. **Правило первое:** токену нужно дать имя, которое потом разберёшься. Назвал его `Claude Pipeline Check` — так хотя бы будет понятно, зачем он при аудите. **Правило второе:** scope. Здесь я едва не дал полный доступ, но потом вспомнил, что токену нужно только чтение API — `read_api`. Ни write, ни delete. Безопасность прежде всего. После создания токен показывается ровно один раз. Это не шутка. Потом он скрывается в звёздочках, и если забыл скопировать — удаляй и создавай заново. Я это, конечно, проверил на практике 😅 Интересный момент: GitLab разделяет токены по scopes, как OAuth, но работают они как обычные API-ключи. Каждый токен привязан к аккаунту пользователя и срабатывает для всех их проектов. Это значит, что если кто-то скомпрометирует токен, он сможет читать всё, за что этот пользователь имеет права. Поэтому в боевых системах их хранят в **secret** переменных CI/CD, а не в коде. **Что дальше?** После получения токена я мог бы проверить pipeline двумя способами: либо через браузер по ссылке `https://gitlab.dev.borisovai.ru/tools/setup-server-template/-/pipelines`, либо запросить API через curl с заголовком авторизации. Для **borisovai-admin** выбрали первый вариант — простой и понятный. Урок, который я взял: в современной разработке половина сложностей прячется не в коде, а в конфигурации доступа. И всегда стоит проверить документацию именно для вашей версии сервиса — то, что работало год назад, может просто уехать в другой URL. --- Что сказал GitLab, когда разработчик забыл скопировать токен? «Вот тебе урок — я показываю его только один раз!» 😄
API ключи как головная боль: как мы организовали chaos в trend-analisis
# Когда API ключей больше, чем смысла их хранить: как мы организовали регистрацию в trend-analisis Проект **trend-analisis** рос быстрее, чем мы себе представляли. Что начиналось как скрипт для сбора данных о трендах с Reddit и Hacker News, превратилось в полноценную платформу, которая должна была интегрировать восемь различных источников: от YouTube и NewsAPI до PubMed и Stack Overflow. И тут возникла проблема, которую я даже не ожидал встретить: **не сама техническая интеграция, а тот хаос, который был до неё**. Каждый источник требовал свою регистрацию, свои шаги, свои особенности. Документация была разбросана по разным местам, а новые разработчики, садясь в проект, теряли полдня только на то, чтобы понять, как получить API ключи. Я помню, как смотрел на список источников и видел эти **[9.0, 8.0, 9.0, 7.0, 8.0, 6.0, 7.0, 7.0]** — оценки влияния каждого источника на общее качество трендов. Среднее значение выходило **7.6 балла**. Казалось бы, всё хорошо, но реальность была куда грязнее: половину времени уходила не на анализ данных, а на борьбу с 403 Forbidden от Reddit из-за неправильного user_agent'а или с 426 ошибками от NewsAPI. Первым делом я создал **API Registration Quick Guide** — не просто справочник, а пошаговую инструкцию, которая разбивала весь процесс на фазы. Фаза 1: Essential (Reddit, NewsAPI, Stack Overflow) — это то, что нужно для MVP. Фаза 2: Video & Community (YouTube, Product Hunt, Dev.to) — дополнение. Фаза 3: Search & Research — когда уже есть пользователи. Фаза 4: Premium — это потом, после того как мы подтвердили бизнес-модель. Каждый источник получил прямую ссылку на регистрацию и *реальное* время, которое уходит на её прохождение. Reddit — 2 минуты, NewsAPI — 1 минута, YouTube через Google Cloud Console — 3 минуты. Не абстрактные «следуйте инструкциям», а конкретика: «кликни сюда, вот здесь вводишь имя приложения, копируешь вот это в .env». Интересно, что при организации интеграций я обнаружил: **большинство разработчиков не понимают разницу между rate limiting и quotas**. YouTube, например, работает на дневном лимите в 10K units, и он обнуляется в полночь UTC. Это не ошибка API — это by design. Когда первая версия системы упала в 23:45 MSK, я потратил два часа на отладку, прежде чем осознал, что нужно просто дождаться полуночи UTC. Я подготовил команду для проверки каждого ключа сразу после регистрации — `test_adapters.py` запускает краткий тест на каждом источнике. Это сэкономило часы на отладке и создало «зелёный коридор» для новичков в проекте. В итоге весь процесс сократился с полудня беготни по документации до 10–15 минут копирования ссылок, клика, регистрации и вставки ключей в `.env`. Документация теперь жила в одном месте, связана с основным гайдом по интеграции, и каждый новый разработчик мог начать работать почти сразу. **Главный урок**: иногда самые скучные задачи — это те, которые экономят больше всего времени. Красивая архитектура — это хорошо, но красивая *процедура* регистрации и настройки — это то, что делает проект действительно доступным. 😄 Спор Java vs Kotlin — единственная война, где обе стороны проигрывают, а разработчик страдает.
Привидение в истории сообщений: как tool_use без tool_result сломал бота
# Охота за привидением в чате: как `tool_use` без `tool_result` сломал бот Проект AI Agents — это система голосовых агентов с телеграм-интеграцией. Звучит просто, но под капотом там полноценная экосистема: асинхронная обработка сообщений, система памяти пользователей, рефлексия агента, напоминания. И вот однажды бот просто перестал запускаться. Сначала казалось, что это типичная проблема с конфигурацией. Но логи рассказывали более странную историю. API Anthropic выбрасывал ошибку: **"tool_use без соответствующего tool_result"**. Как будто кто-то забыл закрыть скобку, но на уровне сессии. Начал копать. Оказалось, что в `handlers.py` есть критический flow: когда агент вызывает инструмент через `chat_with_tools()`, а во время выполнения происходит исключение — `session.messages` остаётся в "повреждённом" состоянии. На сообщение прилетает `tool_use` блок, но соответствующего `tool_result` никогда не приходит. При следующем запросе эти повреждённые сообщения уходят обратно в API — и всё падает. Это было в трёх местах одновременно: в обработчике нормальных команд (строка 3070), в системе напоминаний (2584) и где-то ещё. Классический паттерн копируй-вставь с одинаковым багом. **Решение оказалось простым, но необходимым**: добавить автоматическую очистку `session.messages` в обработчик исключений. Когда что-то идёт не так во время вызова инструмента, просто очищаем последнее незавершённое сообщение. Вот и вся магия. Пока чинил это, нашёл ещё несколько интересных проблем. Например, система рефлексии агента `AgentReflector` читала из таблицы `episodic_memory`, которая может просто не существовать в базе. Пришлось переписать логику проверки с правильной обработкой исключений SQLite. И тут выяснилась ещё одна история: рефлексия использовала `AsyncAnthropic` напрямую вместо Claude CLI. Это означало, что каждый раз при рефлексии расходовались API credits. Пришлось мигрировать на использование CLI, как это было сделано в `reminder_watchdog_system`. Теперь агент может размышлять о своей работе совершенно бесплатно. Отдельное приключение ждало команду `/insights`. Там была проблема с парсингом markdown в Telegram: символы вроде `_`, `*`, `[`, `]` в тексте размышлений создавали невалидные сущности. Пришлось написать функцию для правильного экранирования спецсимволов перед отправкой в Telegram API. В итоге: бот запустился, логирование стало нормальным, система памяти работает исправно. Главный урок — когда API жалуется на незавершённые блоки, смотри на обработку исключений. Там всегда что-то забыли почистить. 😄 Как отличить разработчика от отладчика? Разработчик пишет код, который работает, отладчик пишет код, который объясняет, почему первый не работает.
Монорепо как зеркало: когда Python и JS живут в одном доме
# Монорепо как зеркало: Python + Next.js в одном проекте **Завязка** Представьте ситуацию: вы разработчик и в ваших руках проект *voice-agent* — голосовой помощник на основе Claude, построенный как монорепо. С одной стороны Python-backend (FastAPI, aiogram для Telegram), с другой — Next.js фронтенд (React 19, TypeScript 5.7) для Telegram Mini App. Звучит здорово, но вот в чём подвох: когда в одном репозитории живут две экосистемы с разными правилами игры, управлять ими становится искусством. **Развитие** Первой проблемой, которая выпрыгнула из неоткуда, была **забывчивость переменных окружения**. В Python проект использует `pydantic-settings` для конфигурации, но выяснилось, что эта библиотека не экспортирует значения автоматически в `os.environ`. Результат? Модульный код, читавший переменные прямо из окружения, падал с загадочными ошибками. Пришлось документировать эту ловушку в ERROR_JOURNAL.md — живом архиве подводных камней проекта, где уже скопилось десять таких «моментов истины». Далее встал вопрос архитектуры. Backend требовал **координатора** — центрального паттерна, который бы оркестрировал взаимодействие между агентами и фронтенд-запросами. На бумаге это выглядело идеально, но в коде его не было. Это создавало технический долг, который блокировал Phase 2 разработки. Пришлось вводить **phase-gate валидацию** — автоматическую проверку, которая гарантирует, что прежде чем переходить к следующей фазе, все артефакты предыдущей действительно на месте. В процессе появилась и проблема с **миграциями базы данных**. SQLite с WAL-режимом требовал аккуратности: после того как junior-агент создавал файл миграции, она не всегда применялась к самой БД. Пришлось вводить обязательный чек: запуск `migrate.py`, проверка таблиц через прямой SQL-запрос, документирование статуса. Без этого можно часами отлавливать фантомные ошибки импорта. **Познавательный блок** Интересный факт: монорепо — это не просто удобство, это *культурный артефакт* команды разработки. Google использует одно гигантское хранилище для всего кода (более миллиарда строк!), потому что это упрощает синхронизацию и рефакторинг. Но цена высока: нужны инструменты (Bazel), дисциплина и чёткие протоколы. Для нашего voice-agent это значит: не просто писать код, а писать его так, чтобы Python-part и Next.js-part *доверяли друг другу*. **Итог** В итоге сложилась простая истина: монорепо работает только если есть **система проверок**. ERROR_JOURNAL.md превратился не просто в логирование ошибок, а в живой артефакт культуры команды. Phase-gate валидация стала гарантией, что при параллельной работе нескольких агентов архитектура не съезжает в стороны. А обязательная проверка миграций — это не занудство, а спасение от трёх часов ночного отлавливания, почему таблица не там, где ей быть. Главный урок: в монорепо важна не столько архитектура, сколько **честность системы**. Чем раньше вы перейдёте от надежды на память к автоматическим проверкам, тем спокойнее спать будете. Почему Python и Java не могут дружить? У них разные dependency trees 😄
Umami Analytics: как я сделал админ-панель data-driven
# Самостоятельная аналитика: как я превратил borisovai-admin в data-driven продукт Несколько месяцев назад передо мной встала типичная для любого владельца проекта проблема: я совершенно не видел, кто и как использует мою админ-панель **borisovai-admin**. Google Analytics казался избыточным (и страшным с точки зрения приватности), а простой счётчик посещений — примитивным. Нужно было что-то лёгкое, приватное и полностью под своим контролем. Выбор пал на **Umami Analytics** — открытую веб-аналитику, которая уважает приватность пользователей, не использует cookies и полностью GDPR-compliant. Главное же — её можно развернуть самостоятельно, прямо в своей инфраструктуре. ## Четыре этапа внедрения **Первый шаг — упростить развёртывание.** Стандартная Umami требует двух контейнеров (приложение + PostgreSQL), но для небольшого проекта это избыточно. Я нашёл fork **maxime-j/umami-sqlite**, который использует SQLite — файловую БД в одном контейнере. Экономия памяти была существенной: вместо ~300 MB получил ~100 MB. Затем написал скрипт **install-umami.sh** из семи шагов, который может быть запущен много раз без побочных эффектов (идемпотентный — именно это было важно для автоматизации). **Второй этап — автоматизировать через CI/CD.** Создал два job'а в пайплайне: один автоматически ставит Docker (если его нет), второй — развёртывает саму Umami. Добавил health check, чтобы пайплайн не переходил к следующему шагу, пока контейнер не будет готов. Инкрементальный деплой через **deploy-umami.sh** позволяет обновлять конфигурацию без перезагрузки приложения. **Третий этап — дать пользователям интерфейс.** Создал страницу **analytics.html**, где каждый новый сервис может получить код для интеграции отслеживания. Плюс добавил API endpoint `GET /api/analytics/status` для проверки, всё ли работает. Async-скрипт Umami весит всего ~2 KB и не блокирует рендеринг страницы — вот это я ценю. **Четвёртый этап — документировать.** Написал **AGENT_ANALYTICS.md** с инструкциями для будущих разработчиков, обновил главный **CLAUDE.md** таблицей всех сервисов. ## Что интересного я узнал Оказывается, боль большинства разработчиков с традиционной аналитикой — это не функциональность, а приватность. Umami решает это элегантно: скрипт отправляет только агрегированные данные (сессии, страницы, источники трафика) без ID пользователей и истории кликов. А главное — нет необходимости в **consent banner**, который все равно раздражает пользователей. Порт **3001** внутри контейнера пробросил через **Traefik** на HTTPS-домены `analytics.borisovai.ru` и `analytics.borisovai.tech`. Вообще, это я оценил: такая простота развёртывания чуть ли не впервые в моём опыте с self-hosted решениями. Встроенная авторизация в самой Umami (не потребовался дополнительный Authelia) — и это экономия на инфраструктуре. Один лайфхак: чтобы скрипт аналитики не блокировался AdBlock, назвал его `stats` вместо стандартного `umami` — простой способ обойти базовые фильтры. ## Итог Теперь **borisovai-admin** наконец-то видит себя со стороны. Я получил данные о том, какие страницы реально используют люди, откуда они приходят и сколько времени длятся сессии. Всё это — на своём сервере, без третьих лиц и без чувства вины перед пользователями. Следующий шаг — подключить аналитику ко всем остальным сервисам проекта. Это уже не задача месяца, а скорее вопрос пары часов на каждый сервис. Учимся: иногда лучший инструмент — это не самый популярный, а самый честный. 😄
Молчаливый API: когда успех — это просто пустота
# Когда API молчит: охота на призрак в системе обработки команд Это была обычная воскресенье в проекте **ai-agents**. Пользователь Coriollon отправил простую команду через Telegram: "Создавай". Три слова. Невинные на вид. Но система ответила молчанием — и началась охота на баг, которая заняла почти семь минут и три попытки переподключения. ## Что мы видим в логах Сначала всё выглядит нормально. Запрос приходит в 12:23:58. Система маршрутизирует его на **Claude API** с моделью Sonnet. Промпт имеет 5344 символа — немалый объём контекста. Первый запрос уходит в API и... здесь начинается интересное. API отвечает за 26 секунд. Кажется, успешно: `is_error: False`, `num_turns: 2`, даже token usage выглядит логичным. Но вот `result: ''` — пустой результат. Система ловит эту аномалию и логирует `cli_empty_response`. Мой первый инстинкт: "Может, сетевой глюк?" Система делает то же самое — ждёт 5 секунд и повторяет запрос. Вторая попытка в 12:24:31. История повторяется: успех по метрикам, но снова пустой ответ. ## Третий раз — не удача К третьей попытке я уже понял, что это не случайный сетевой перебой. Система работает корректно, API возвращает `success: true`, токены учитываются (даже видны попадания в кэш: `cache_read_input_tokens: 47520`). Но результат, ради которого всё затевалось, так и не приходит. Вот здесь кроется классическая ловушка в работе с LLM API: **успешный HTTP-ответ не гарантирует наличие полезной нагрузки**. API может успешно обработать запрос, но вернуть пустое поле `result` — это может означать, что модель вернула только служебные данные (вроде использованных токенов) без фактического содержимого. Финальная попытка заканчивается в 12:25:26. Три запроса, три молчания, общее время ожидания — почти семь минут. Система логирует финальную ошибку: `message_handler_error: CLI returned empty response`. ## Чему это учит Когда вы работаете с внешними API, особенно с такими мощными, как Claude, недостаточно проверять только HTTP-статус. Нужно валидировать **содержимое ответа**. В данном случае система сделала ровно это — поймала пустой результат и попыталась восстановиться через retry-логику с экспоненциальной задержкой (5, 10 секунд). Но вот что интересно: кэшированные токены (видны в каждом логе) говорят, что контекст был успешно закэширован. Это означает, что на второй и третий запрос система платила дешевле — 0.047 и 0.037 USD вместо 0.081 на первый запрос. Автоматическое кэширование контекста в Claude API — это фишка, которая спасает в ситуациях вроде этой. Корень проблемы остался в логах как загадка: был ли это timeout на стороне модели, недопонимание в структуре запроса или что-то ещё — сказать сложно. Но система сработала как надо: зафиксировала проблему, задокументировала все попытки, сохранила данные сессии для постмортема. Lesson learned: в системах обработки команд от пользователей нужна не только retry-логика, но и мониторинг пустых ответов. И да, Telegram-боты любят такие фокусы. 😄 **API успешно вернул ошибку об ошибке успеха — вот это я называю отличной синхронизацией!**
Когда unit-тесты лгут: боевые испытания Telegram-бота
# Telegram-бот на боевых испытаниях: когда unit-тесты не подстраховывают Проект **bot-social-publisher** начинался просто. Полнофункциональный Telegram-бот с памятью, командами, интеграциями. Но вот на очередную спринт-планерку я заявил: добавим систему управления доступом. Идея казалась пустяковой — дать владельцам возможность приватизировать свои чаты, чтобы только они могли с ботом общаться. Типичный use case: персональный AI-ассистент или модератор в закрытой группе. Теория была прекрасна. Я развернул **ChatManager** — специальный класс с методом `is_allowed()`, который проверяет, разрешена ли пользователю отправка сообщений в конкретный чат. Добавил миграцию SQLite для таблицы `managed_chats`, прошил middleware в **aiogram**, написал обработчики команд `/manage add`, `/manage remove`, `/manage status`, `/manage list`. Unit-тесты прошли с зелёным светом — `pytest` даже не чихнул. Документация пока отложена, но это же детали! Потом наступил момент истины. Запустил бота локально через `python telegram_main.py`, переключился в личный чат и отправил первую `/manage add`. Бот записал ID чата, переключился в режим приватности. Нормально! Попробовал отправить обычное сообщение — ответ пришёл. Открыл чат со своего второго аккаунта, отправил то же самое — тишина. Бот ничего не ответил. Перфект, middleware работает. Но не всё было так гладко. Первая проблема вылезла при быстрых командах подряд. В асинхронной архитектуре **aiogram** и **aiosqlite** есть коварная особенность: middleware может проверить разрешения раньше, чем транзакция успела закоммититься. Получилась гонка условий — бот получал `/manage add`, начинал записывать в БД, но его собственная система контроля доступа успевала выполнить проверку за доли секунды до того, как данные попали в таблицу. Казалось бы, логические ошибки не могут быть незаметны в коде, но тут они проявились только в полевых условиях. Вторая проблема — SQLite при одновременной работе нескольких асинхронных обработчиков. Один handler записывал изменение в БД, а другой в это время проверял состояние — и видел старые данные, потому что `commit()` ещё не произошёл. Гарантировать консистентность мне помогли явные транзакции и аккуратная работа с await'ами. Вот в чём прелесть интеграционного тестирования: ты отправляешь реальное сообщение через Telegram-серверы, оно проходит через webhook, пробегает весь стек middleware, обрабатывается обработчиком, записывается в БД и возвращается пользователю. Unit-тесты проверяют логику функции. Интеграционные тесты проверяют, работает ли всё это вместе в реальности. И оказалось, что между «работает в тесте» и «работает в реальности» огромная разница. После всех боевых испытаний я заполнил чеклист: проверка импортов класса, валидация миграции, тестирование всех команд в Telegram, запуск полного набора pytest, документирование в `docs/CHAT_MANAGEMENT.md` с примерами и описанием архитектуры. Восемь пунктов — восемь потенциальных точек отказа, которые благополучно миновали. Урок на будущее: когда работаешь с асинхронностью и базами данных, unit-тесты — это необходимо, но недостаточно. Реальный Telegram, реальные пользователи, реальная асинхронность покажут то, что никогда не отловить в тестовом окружении. 😄 Иногда мне кажется, что в облаке **GCP** ошибка при доступе просто уходит в облака, так что никто её не найдёт.
Четыре инструмента вместо двух: как мы освободили AI-агентов от ограничений
# От изоляции к открытости: как мы расширили доступ к файловой системе в AI-агентах Работал я над проектом **ai-agents** — платформой для создания интеллектуальных помощников на Python. И вот в какой-то момент нас настигла настоящая боль роста: агенты работали в тесной клетке ограничений. ## Проблема: узкие границы доступа Представь ситуацию. У нас была виртуальная файловая система — и звучит круто, пока не начнёшь с ней работать. Агенты могли читать только три папки: `plugins/`, `data/`, `config/`. Писать вообще не могли. Нужно было создать конфиг? Нет. Сохранить результат работы? Нет. Отредактировать существующий файл? Снова нет. Это было словно программировать с одной рукой, привязанной за спину. Функциональность `file_read` и `directory_list` — хорошо, но недостаточно. Проект рос, требования расширялись, а система стояла на месте. ## Решение: четыре инструмента вместо двух Первым делом я понял, что нужно идти не путём костылей, а переписать модуль **filesystem.py** целиком. Вместо двух полуслепых инструментов создал четыре полнофункциональных: - **`file_read`** — теперь читает что угодно в проекте, до 200 килобайт - **`file_write`** — создаёт и перезаписывает файлы, автоматически создаёт директории - **`file_edit`** — тонкая работа: находит точную подстроку через find-and-replace и заменяет - **`directory_list`** — гибкий листинг: поддерживает glob-паттерны и рекурсию Но тут появилась вторая проблема: как дать свободу, но не потерять контроль? ## Безопасность: ограничения, которые действительно работают Всё звучит опасно, пока не посмотришь на механизм защиты. Я добавил несколько слоёв: Все пути привязаны к корню проекта через `Path.cwd()`. Выбраться наружу невозможно — система просто не позволит обратиться к файлам выше по дереву директорий. Плюс чёрный список: система блокирует доступ к `.env`, ключам, секретам, паролям — ко всему, что может быть опасно. А для дополнительной уверенности я добавил проверку path traversal через `resolve()` и `relative_to()`. Получилась архитектура, где агент может свободно работать внутри своей песочницы, но не может ей повредить. ## Интересный момент: почему это важно Знаешь, в истории компьютерной безопасности есть забавный парадокс. Чем больше ты запрещаешь, тем больше люди ищут обходные пути. А чем правильнее ты даёшь разрешения — с умными ограничениями — тем спокойнее всем. Unix-философия в действии: дай инструменту ровно столько мощи, сколько нужно, но убедись, что он не сможет что-то сломать. ## Итого Переписал модуль, обновил константы в **constants.py**, экспортировал новые классы в **\_\_init\_\_.py**, подключил всё в **core.py** и **handlers.py**. Проверил сборку — зелёная лампочка. Теперь агенты могут полноценно работать с проектом, не боясь случайно удалить что-то важное. Дальше планировали тестировать на реальных сценариях: создание логов, сохранение состояния, динамическая генерация конфигов. А пока что у нас есть полнофункциональная и безопасная система для работы с файлами. Мораль истории: не выбирай между свободой и безопасностью — выбирай правильную архитектуру, которая обеспечивает оба.
ChatManager: как научить бота помнить, где ему работать
# Как научить AI-бота помнить свои чаты: история ChatManager Задача стояла простая на словах, но коварная на деле. У AI-бота в проекте **voice-agent** началась проблема с идентичностью: он рос, его добавляли в новые чаты, но вот беда — он не различал, в каких группах он вообще должен работать. Представь: бот оказывается в сотне чатов, а слушаться команд должен только в тех, которые явно добавил его хозяин. Без этого механизма вся система на мине. **Первым делом разобрали задачу по кирпичикам.** Нужна была полноценная система управления чатами: бот должен помнить, какие чаты ему доверены, проверять права пользователя перед каждой командой и предоставлять простые способы добавлять/удалять чаты из управляемых. Звучит как обычная CRUD-операция, но в контексте асинхронного Telegram-бота это становится интереснее. Решение разделили на пять логических контрольных точек. Начали с **ChatManager** — специального класса в `src/auth/`, который ведал бы всеми чатами. Важное решение: использовали уже имеющийся в проекте **structlog** для логирования вместо того, чтобы добавлять ещё одну зависимость. И, что критично для асинхронного бота, выбрали **aiosqlite** — асинхронный драйвер для SQLite. Почему это важно? Потому что обычный SQLite работает синхронно и может заблокировать весь бот при обращении к БД. aiosqlite оборачивает операции в `asyncio` — и вот уже БД не стопорит главный цикл обработки сообщений. **Дальше пошли миграции и middleware.** Создали таблицу `managed_chats` с информацией о чатах, их типах и владельцах. Затем встроили в pipeline Telegram-хэндлеров специальный middleware для проверки прав — перед каждой командой система проверяет, имеет ли пользователь доступ к этому чату. Если нет — молчит или вежливо отказывает. Команды управления (`/manage add`, `/manage remove`, `/manage list`) сделали простыми и понятными. Напишешь в личку боту `/manage add` — и чат добавляется в управляемые. Никакого магического угадывания, всё явно. **Интересный момент про асинхронные БД.** Когда разработчики впервые натыкаются на проблему "бот зависает при запросе к БД", они часто виноваты в синхронных операциях с базой. aiosqlite решает это элегантно: минимум кода, максимум производительности. Это один из тех инсайтов, которые приходят дорого, но в долгосрочной перспективе спасают душу. **В итоге получилось мощно.** Бот теперь точно знает, кто его хозяин в каждом чате, и не поддаётся на трюки неуполномоченных пользователей. Архитектура масштабируется — можно добавить роли, разные уровни доступа, историю изменений. Всё работает асинхронно и не тормозит. Дальше очередь интеграционных тестов и production. 😄 **Совет дня:** всегда создавай миграции БД отдельно от логики — так проще откатывать и тестировать. И да, GCP и кот похожи тем, что оба делают только то, что хотят, и игнорируют твои инструкции.
ChatManager: как AI-боту дать контроль над своими чатами
# От хаоса к порядку: как мы научили AI-бота управлять собственными чатами Столкнулись с интересной проблемой в проекте **voice-agent** — нашему AI-боту нужно было получить контроль над тем, в каких чатах он работает. Представь: бот может оказаться в сотнях групп, но обслуживать он должен только те, которые явно добавил владелец. И вот здесь начинается магия архитектуры. ## Задача была классической, но хитрой Нужно было реализовать систему управления чатами — что-то вроде белого списка. Бот должен был: - Помнить, какие чаты он курирует - Проверять права пользователя перед каждой командой - Добавлять/удалять чаты через понятные команды - Хранить всё это в надежной базе данных Звучит просто? На деле это требовало продумать архитектуру с нуля: где брать данные, как валидировать команды, как не потерять информацию при перезагрузке. ## Как мы это делали Первым делом создали класс **ChatManager** — специалист по управлению чатами. Он бы жил в `src/auth/` и работал с SQLite через **aiosqlite** (асинхронный драйвер, чтобы не блокировать главный цикл обработки сообщений). Важный момент: использовали уже имеющийся в проекте **structlog** для логирования — не захотелось добавлять ещё одну зависимость в requirements.txt. Затем создали миграцию БД — новую таблицу `managed_chats` с полями для типа чата (приватный, группа, супергруппа, канал), ID владельца и временной метки. Ничего сложного, но по-человечески: индексы на часто используемых полях, CHECK-констрейнты для валидности типов. Дальше идет классический паттерн — **middleware для проверки прав**. Перед каждой командой система проверяет: а имеет ли этот пользователь доступ к этому чату? Если нет — бот скромно молчит или вежливо отказывает. Это файл `src/telegram/middleware/permission_check.py`, который встраивается в pipeline обработки сообщений. И, конечно, **handlers** — набор команд `/manage add`, `/manage remove`, `/manage list`. Пользователь пишет в личку боту `/manage add`, и чат добавляется в управляемые. Просто и понятно. ## Маленький инсайт про асинхронные БД Знаешь, почему **aiosqlite** так хороша? SQLite по умолчанию работает синхронно, но в асинхронном приложении это становится узким местом. aiosqlite оборачивает операции в `asyncio`, и вот уже БД не блокирует весь бот. Минимум кода, максимум производительности. Многие разработчики об этом не думают, пока не столкнутся с тем, что бот «зависает» при запросе к БД. ## Итог: от плана к тестам Весь процесс разбили на логические шаги с контрольными точками — каждый шаг можно было проверить отдельно. После создания ChatManager идёт миграция БД, потом интеграция в основной бот, затем handlers для команд управления, и наконец — unit-тесты в pytest. Результат: бот теперь знает, кто его хозяин в каждом чате, и не слушает команды от неуполномоченных людей. Архитектура масштабируется — можно добавить роли, разные уровни доступа, историю изменений. А главное — всё работает асинхронно и не тормозит. Дальше план — запустить интеграционные тесты и загнать в production. Но это уже другая история. 😄 **Совет дня:** всегда создавай миграции БД отдельно от логики — так проще откатывать и тестировать.
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 и подростка? Оба непредсказуемы и требуют постоянного внимания.
Когда API успешен, но ответ пуст: охота на невидимого врага
# Когда AI молчит: охота на призрак пустого ответа В одном из проектов случилось странное — система обращалась к API, получала ответ, но внутри него... ничего. Как в доме с открытыми дверями, но все комнаты пусты. Задача была простая: разберись, почему сообщение от пользователя *Coriollon* через Telegram генерирует пустой результат, хотя API клиента уверенно докладывает об успехе. История началась 9 февраля в 12:23 с обычной команды. Пользователь отправил в бот сообщение «Создавай», и система маршрутизировала запрос в CLI с моделью Sonnet — всё как надо. Промпт собрали, отправили на API. Система была настроена с максимум тремя повторными попытками при ошибках. Логично, правда? Первый запрос обработался за 26 с лишним секунд. API вернул успех. Но в поле `result` зияла пустота. Не ошибка, не исключение — просто пустая строка. Система поняла: что-то не так, нужно пробовать ещё. Через 5 секунд — вторая попытка. Снова успех на бумаге, снова пустой ответ. Третий раз был поспешен: через 10 секунд ещё один запрос, и снова тишина. Что интересно — в логах были видны все признаки нормальной работы. Модель обработала 5000+ символов промпта, израсходовала токены, потратила API-бюджет. Кэш работал прекрасно — вторая и третья попытки переиспользовали 47000+ закэшированных токенов. Но конечный продукт — результат для пользователя — остался фантомом. Здесь скрывается коварная особенность асинхронных систем: успешный HTTP-статус и валидный JSON в ответе ещё не гарантируют, что внутри есть полезная нагрузка. API может спокойно ответить 200 OK, но с пустым полем результата. Механизм повторных попыток поймал проблему, но не смог её решить — повторял одно и то же три раза, как сломанный проигрыватель. На четвёртый раз система сдалась и выбросила ошибку: *«CLI returned empty response»*. Урок был ценный: валидация ответа от внешних сервисов должна быть двухуровневой. Первый уровень — проверяем HTTP-статус и структуру JSON. Второй уровень, важнее — проверяем, что в ответе есть актуальные данные. Просто наличие полей недостаточно; нужна проверка *содержания*. В нашем случае пустое значение в `result` должно было сработать как маячок уже на первой попытке, а не ждать третьей. Кэширование в таких ситуациях работает против нас — оно закрепляет проблему. Если первый запрос вернул пусто, и мы кэшировали эту пустоту, второй и третий запросы будут питаться из одного источника, перечитывая одну и ту же ошибку. Лекарство простое, но необычное: кэшировать нужно не все ответы подряд, а только те, которые прошли валидацию содержимого. Итог: система заработала, но теперь с более умным механизмом выявления невидимых ошибок. Повторные попытки стали умнее — они теперь различают, когда нужно переопробовать запрос, а когда отклонить ответ как невалидный. Пользователь Coriollon теперь получает либо результат, либо честную ошибку, но уже не это мучительное молчание. 😄 **Node.js — единственная технология, где «это работает» считается документацией.**
Боевой тест Telegram-бота: когда теория встречается с реальностью
# Проверяем Telegram-бота в боевых условиях: тестируем управление доступом Когда создаёшь бота для Telegram, одно дело — писать тесты в PyTest, и совсем другое — убедиться, что он работает с реальными аккаунтами и обрабатывает команды так, как задумано. Вот я и пришёл к моменту, когда нужно было залезть в грязь и провести самый важный тест: запустить бота и написать ему сообщение из Telegram. ## Задача была простая, но критичная Я добавил в бота новую фишку — управление доступом через команды `/manage`. Идея: в групповом чате владелец может сделать его приватным (`/manage add`), и тогда бот будет отвечать только ему. Затем команда `/manage remove` открывает доступ для всех обратно. Плюс ещё `/recall` и `/remember` для сохранения данных в памяти чата. Звучит просто, но нужно убедиться, что: - бот действительно игнорирует сообщения посторонних в приватном режиме; - владелец всегда может управлять ботом; - после отключения приватности всё работает как раньше. ## Как я это проверял Сначала поднял бота локально: ``` python telegram_main.py ``` Затем начались «полевые испытания»: 1. **Первый скрин-тест** — написал боту `/manage add` из своего аккаунта. Бот должен был записать ID чата в таблицу БД `managed_chats` и включить режим приватности. Отправил обычное сообщение — бот ответил. ✅ 2. **Второй аккаунт** — попросил друга отправить то же сообщение из другого Telegram. Бот молчал, хотя обычно отвечает на всё. Middleware `permission_check.py` срабатывал корректно и блокировал обработку. ✅ 3. **Финальный тест** — написал `/manage remove` и снова попросил друга отправить сообщение. На этот раз бот ответил. Приватность снята, всё доступно. ✅ ## Что оказалось неочевидным Интеграционное тестирование в Telegram выявило одну тонкость: когда ты работаешь с асинхронным обработчиком команд в aiogram, timing-зависимые проверки могут создавать гонки условий. У меня был момент, когда команда `/manage add` срабатывала, но middleware проверял доступ *до* того, как запись попала в БД. Пришлось добавить небольшой await после insert'а, чтобы гарантировать консистентность. Ещё обнаружил: если ты работаешь с SQLite и одновременно несколько обработчиков пишут в таблицу, нужен явный `commit()` или использовать контекстный менеджер транзакций. Иначе другой процесс не увидит изменения до commit'а. ## Что дальше? После успешных интеграционных тестов я задокументировал всё в README.md — добавил секцию про управление доступом с примерами команд. Создал отдельный файл `docs/CHAT_MANAGEMENT.md` с полной архитектурой ChatManager, схемой БД и API reference для всех методов класса. Теперь у меня есть надёжная система для создания приватных чатов с ботом. Это можно использовать для ассистентов, которые работают с конфиденциальными данными, или для модераторов в больших группах. Главный вывод: прежде чем гордиться unit-тестами, обязательно проверь свой код в реальной среде. Иногда именно там появляются неожиданные проблемы, которые не поймать никаким PyTest. 😄 Что общего у Telegram API и инструкций по использованию телефона? И то, и другое люди игнорируют в пользу метода проб и ошибок.
Монорепо, голос и журнал ошибок: как 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`. Но это — уже другая история. 😄 Что общего у монорепо и парка развлечений? Оба требуют хорошей разметки, иначе люди заблудятся.
Одна БД для всех: как мы добавили чаты без архитектурного хаоса
# Одна база на всех: как мы добавили управление чатами без архитектурного хаоса Когда проект растёт, растут и его аппетиты. В нашем Telegram-боте на основе Python уже была отличная инфраструктура — `UserManager` для управления пользователями, собственная SQLite база в `data/agent.db`, асинхронные запросы через `aiosqlite`. Но вот беда: чат-менеджер ещё не появился. А он нам был нужен. Стояла вот какая задача: нужно отслеживать, какие чаты управляет бот, кто их владелец, какой это тип чата (приватный, группа, супергруппа, канал). При этом не создавать отдельную базу данных — это же кошмар для девопса — а переиспользовать существующую инфраструктуру. **Первым делом** заглянул в текущую архитектуру. Увидел, что всё уже завязано на одной БД, один конфиг, одна логика подключения. Идеально. Значит, нужна просто одна новая таблица — `managed_chats`. Задумал её как простую структуру: `chat_id` как первичный ключ, `owner_id` для связи с пользователем, `chat_type` с проверкой типов через `CHECK`, поле `title` для названия и JSON-колонка `settings` на будущее. Обычно на этом месте разработчик бы создал абстрактный `ChatRepository` с двадцатью методами и паттерном `Builder`. Я же решил сделать проще — скопировать философию `UserManager` и создать классический `ChatManager`. Три-четыре асинхронных метода: добавить чат, проверить, управляется ли он, получить владельца. Всё на `aiosqlite`, как и везде в проекте. **Неожиданно выяснилось**, что индексы — это не украшение. Когда начну искать чаты по владельцу, индекс на `owner_id` будет спасением. SQLite не любит полные скены таблиц, если можно обойтись поиском по индексу. Интересный момент: SQLite часто недооценивают в стартапах, думают, что это игрушка. На самом деле она справляется с миллионами записей, если её правильно использовать. Индексы, `PRAGMA` для оптимизации, подготовленные statements — и у вас есть боевая база данных. Многие проекты потом переходят на PostgreSQL только потому, что привыкли к MySQL, а не из реальной нужды. В итоге получилась чистая архитектура: одна БД, одна точка подключения, новая таблица без какого-либо дублирования логики. `ChatManager` живёт рядом с `UserManager`, используют одни и те же библиотеки и утилиты. Когда понадобятся сложные запросы — индекс уже есть. Когда захотим добавить настройки чата — JSON-поле ждёт. И никаких лишних микросервисов. Следующий шаг — интегрировать это в обработчики событий Telegram API. Но это уже другая история. 😄 Почему база данных никогда не посещает вечеринки? Её постоянно блокирует другой клиент!