От chaos к structure: как мы спасли voice-agent от собственной сложности

Я работал над ai-agents — проектом с автономным voice-agent’ом, который обрабатывает запросы через Claude CLI. К моменту начала рефакторинга код выглядел как русский матрёшка: слой за слоем глобальных переменных, перекрёстных зависимостей и обработчиков, которые боялись трогать соседей.
Проблема была классическая. Handlers.py распух до 3407 строк. Middleware не имела представления о dependency injection. Orchestrator (главный дирижёр) тянул за собой кучу импортов из telegram-модулей. А когда я искал проблему с generated_capabilities sync, понял: пора менять архитектуру, иначе каждое изменение превратится в минное поле.
Я начал с диагностики. Запустил тесты — прошло 15 случаев, где старые handlers ломались из-за отсутствующих re-export’ов. Это было сигналом: нужна система, которая явно говорит о зависимостях. Решил перейти на HandlerDeps — dataclass, который явно описывает, что нужно каждому обработчику. Вместо global session_manager — параметр в конструкторе.
Параллельно обнаружил утечку памяти в RateLimitMiddleware. Стейт пользователей накапливался без очистки. Добавил периодическую очистку старых записей — простой, но효과적한паттерн. Заодно переписал subprocess.run() на asyncio.create_subprocess_exec() в compaction.py — блокирующий вызов в асинк-коде это как использовать молоток в операционной.
Потом сделал вещь, которая кажется малой, но спасает множество часов отладки. Создал Failover Error System — типизированную классификацию ошибок с retry-логикой на exponential backoff. Теперь когда Claude CLI недоступен, система не паникует, а пытается перезагрузиться, а если совсем плохо — падает с понятной ошибкой, а не с молчаливым зависанием.
Ревью архитектуры после этого показало: handlers/_legacy.py — это 450 строк с глубокой связью на 10+ глобалов. Экстрактить сейчас? Создам просто другую матрёшку. Решил оставить как есть, но запретить им регистрировать роутеры в главном orchestrator’е. Вместо этого — явная инъекция зависимостей через set_orchestrator().
Результат: handlers.py сократился с 3407 до 2767 строк (-19%). Все 566 тестов проходят. Код больше не боится изменений — каждая зависимость видна явно. И когда кто-то спустя месяц будет копаться в этом коде, он сразу поймёт архитектуру, а не будет ловить призраков в глобалах.
А знаете, что смешно? История коммитов проекта выглядит как git log --oneline: ‘fix’, ‘fix2’, ‘fix FINAL’, ‘fix FINAL FINAL’. Вот к чему приводит отсутствие архитектуры 😄
Метаданные
- Session ID:
- grouped_ai-agents_20260215_0951
- Branch:
- HEAD
- Dev Joke
- git log --oneline: история проекта в одну строку. Обычно это 'fix', 'fix2', 'fix final', 'fix FINAL FINAL'.