От одного агента с 12 tools к четырём узким — и всё заработало
Gemma 12B с 16K контекстом не справлялась с 12 инструментами одновременно. Разбили на 4 агента через CrewAI — каждый со своими 2-3 tools. Попутно почистили 40% мёртвого кода, запустили Obsidian headless и настроили авто-постинг заметок в Telegram-канал.
Проблема
CrewAI Bot (Deputy Secretary + Garden Manager) работал, но:
- Garden Manager — один агент с 12 tools в 16K контексте. Gemma 12B путалась, вызывала не те tools, зависала
- 40% кода мёртво —
_voice.py,garden_manageragent,create_garden_crew(), 9 SQLite таблиц, ~30 CRUD-функций, ChromaDBglobalcollection - Obsidian CLI не работал — Obsidian не запущен на сервере, все garden tools возвращали ошибку
- Deputy отвечал на каждое сообщение отдельно — батчинг 2 секунды, человек не успевал дописать
- Дублирование — 2 функции publish, 2 функции search, 2 функции
_build_and_deploy, 2 системы global_memory
Решение
4-агентный Garden Crew
Вместо 1 агента с 12 tools — 4 узких через CrewAI sequential process:
| Агент | Tools | max_iter | Задача |
|---|---|---|---|
| Garden Router | 0 | 1 | Читает сообщение, определяет intent |
| Garden Searcher | 3 | 3 | search, list, show |
| Garden Writer | 5 | 5 | create_draft, add_to_draft, show_draft, edit_note, cancel_draft |
| Garden Publisher | 3 | 3 | publish_draft, delete_note, update_links |
Router → специалист. Каждый агент видит только свои tools — 16K контекста хватает с запасом.
Почему не direct vLLM
Прямой tool-calling через OpenAI API быстрее, но CrewAI даёт оркестрацию. Router определяет intent, передаёт нужному агенту. Скорость не критична — Matrix DM не требует мгновенного ответа. Зато 12B справляется с малым контекстом.
Obsidian headless через systemd
[Service]
Type=simple
Environment=DISPLAY=:99
ExecStart=/home/orca/.local/share/obsidian/app/obsidian --no-sandbox /home/orca/digital-garden
Restart=alwaysXvfb на display :99, Obsidian без GUI, CLI socket работает. Все garden_tools (search, list, show, create, edit, publish) функциональны.
Батчинг Deputy: 30 секунд
_BATCH_WAIT = 30.0
Deputy ждёт 30 секунд тишины после последнего сообщения. Все сообщения за это время объединяются в один батч → один ответ. После ответа — ещё 30 секунд ожидания перед следующим.
Сценарий
- Человек шлёт “привет” → буфер: [привет]
- Через 5 сек: “как дела?” → буфер: [привет, как дела?]
- Через 10 сек: “что делаешь?” → буфер: [привет, как дела?, что делаешь?]
- 30 секунд тишины → один ответ на всё
Авто-постинг заметок в канал
garden_poster.py — фоновый поток, 3 раза в день (09:00, 15:00, 21:00):
- Берёт случайную заметку из сада (без index.md, drafts/)
- vLLM генерирует тизер (1-2 предложения на русском)
- Постит в Telegram-канал: заголовок + тизер + ссылка на notes.mrgnl.ru
- Запоминает последние 10 заметок — без повторов
Чистка мёртвого кода
| Удалено | Что |
|---|---|
agents/_voice.py | Весь файл — voice listener никогда не активировался |
garden_manager agent | 12 tools, мёртвый — заменён на Garden Crew |
create_garden_crew() | Мёртвая фабрика |
ChromaDB global collection | Создавалась, не использовалась |
| 26 CRUD-функций | state.py — определены, не вызывались |
| 7 SQLite таблиц | schema оставлена для совместимости |
Архитектура после рефакторинга
crewai-bot.service (один процесс)
├── Telegram Deputy Secretary (CrewAI, 1 агент)
│ ├── Батчинг 30 сек — один ответ на все сообщения
│ ├── ChromaDB: per_user, conversations, garden_notes
│ ├── radio_poster — авто-постинг радио в канал
│ └── garden_poster — авто-постинг заметок 3x/день
├── Matrix Garden Manager (CrewAI, 4 агента)
│ ├── Router → Searcher/Writer/Publisher
│ └── garden_pipeline — drafts/ → classify → format → link → publish
└── Obsidian headless (systemd, Xvfb :99)
└── obsidian-cli socket → garden_tools