Разработка: ai-agents
Хроника разработки проекта ai-agents. Основные направления: новые функции, изменения кода. Всего 7 записей в потоке. Последние темы: Бот забывал имена: как я нашел отключенную память; Когда агент начинает помнить о себе как о личности; Четыре инструмента вместо двух: как мы освободили AI-агентов от ограничений.
Почему бот не помнит: охота на исчезнувшую память
Проект voice-agent был почти готов. Красивый API, продуманный диалоговый движок, интеграция с Claude — всё работало. Но пользователи жаловались на одно: бот ничего не запоминал между разговорами. “Привет, я Иван”, — говорил пользователь в одном диалоге. Во втором диалоге: “Привет, кто это?” — с чистой совестью отвечал бот.
Проблема казалась серьёзной. В исходниках проекта я нашёл целую систему персистентной памяти — полностью реализованную, с извлечением фактов через Claude Haiku, векторным поиском по эмбеддингам, дедупликацией устаревших данных и хранением в SQLite. Архитектура была изящной. Но она попросту не работала.
Первым делом я начал отладку: включил логирование, запустил тесты памяти, проверил инициализацию. И тут я понял, почему никто об этом не говорил: система памяти была выключена по умолчанию. В конфиге стояло memory_enabled = False.
Представляешь? Целый механизм, готовый к боевому использованию, но никто не включил переключатель. Это было похоже на ситуацию, когда ты строишь огромный дом, подводишь электричество, но забываешь щёлкнуть рубильником.
Чтобы включить память, требовалась конфигурация в .env:
MEMORY_ENABLED=true
MEMORY_EMBEDDING_PROVIDER=ollama
MEMORY_OLLAMA_URL=http://localhost:11434
MEMORY_EMBEDDING_MODEL=nomic-embed-text
Нужен был запущенный Ollama с моделью nomic-embed-text для генерации векторных эмбеддингов. Это небольшой инструмент — легко поднимается локально, работает быстро, не требует облака. После этого бот начинал вести себя как персонаж с настоящей памятью:
- Извлекал факты из каждого диалога (через Claude Haiku выделял важное)
- Сохранял их в SQLite с векторными представлениями
- Вспоминал релевантные факты при каждом новом обращении пользователя
- Обновлял информацию вместо дублирования
Здесь скрывается интересная деталь о том, как работают современные системы памяти в AI-агентах. Обычно думают, что нужна огромная база данных с явной индексацией. На самом деле векторные базы данных и эмбеддинги решают проблему релевантности: система помнит не просто факты, а смысл фактов. Даже если пользователь перефразирует информацию — “я работаю в компании Y” вместо “я сотрудник Y” — система поймёт, что это один и тот же факт.
Когда память была включена, голосовой агент заработал совсем по-другому. Он узнавал пользователей, помнил их предпочтения, шутки и истории. Диалоги стали личными. А главное — задача “почему бот не помнит?” превратилась в тривиальный баг конфигурации. Оказалось, нужно было не переделывать архитектуру, а просто включить то, что уже было.
Это учит важному правилу при работе со сложными системами: перед тем как писать недостающий код, всегда проверь, есть ли уже готовое решение, которое просто выключено.
😄 Мораль: лучшая система памяти — та, которая уже реализована, её просто нужно не забыть включить.
Исправления: - “граммофон” → “инструмент” (слово “граммофон” не подходит по смыслу) - Добавлена запятая после “На самом деле” в предпредпоследнем абзаце
Когда агент говорит от своего лица: переписали систему памяти для более человечного 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-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. Проверил сборку — зелёная лампочка. Теперь агенты могут полноценно работать с проектом, не боясь случайно удалить что-то важное.
Дальше планировали тестировать на реальных сценариях: создание логов, сохранение состояния, динамическая генерация конфигов. А пока что у нас есть полнофункциональная и безопасная система для работы с файлами.
Мораль истории: не выбирай между свободой и безопасностью — выбирай правильную архитектуру, которая обеспечивает оба.
Когда 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 успешно вернул ошибку об ошибке успеха — вот это я называю отличной синхронизацией!
Охота за привидением в чате: как 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 жалуется на незавершённые блоки, смотри на обработку исключений. Там всегда что-то забыли почистить.
😄 Как отличить разработчика от отладчика? Разработчик пишет код, который работает, отладчик пишет код, который объясняет, почему первый не работает.
От паттерна к реальности: как мы завернули 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 — это буквально несколько строк конфига. Код более тестируемый, зависимости явные, архитектура дышит.
И главное — это работает. Действительно работает. 😄
Когда 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 теста».
Иногда разработка — это просто ожидание результата консоли. Но когда все полосы зелёные, это чувство стоит каждой минуты отладки. 😄






