Abstract
Архитектура
crewai-bot.service (один процесс, systemd)
├── 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
Один LLM — /models/gemma-4-12b через vLLM (OpenAI-compatible API). Эмбеддинги — nomic-embed-text через Ollama. Память — ChromaDB, 3 коллекции:
| Коллекция | Что хранит |
|---|---|
per_user | Факты о людях (фильтр по user_id) |
conversations | Семантический поиск по прошлым диалогам |
garden_notes | RAG по заметкам сайта |
globalcollection удаленаПри рефакторинге убрана — создавалась, но не использовалась. 4 коллекции → 3.
Агенты
Deputy Secretary (Telegram)
Отвечает в Telegram ЛС от имени mrgnl
Кратко, прямо, с сарказмом. Друг, не корпоративный бот. Подпись:
— ии товарищ mrgnl.ru
Инструменты (5):
recall_user_memories— вспомнить всё о собеседникеsave_user_memory— сохранить новый фактsearch_user_memories— семантический поиск по памятиget_now_playing— что играет на радиоsend_report— написать mrgnl в контрольный чат
max_iter: 3 (до 3 LLM-вызовов на сообщение).
Батчинг 30 секунд — Deputy ждёт 30 секунд тишины после последнего сообщения. Все сообщения за это время объединяются в один батч → один ответ.
Как батчинг работает
- Человек шлёт «привет» → буфер: [привет]
- Через 5 сек: «как дела?» → буфер: [привет, как дела?]
- Через 10 сек: «что делаешь?» → буфер: [привет, как дела?, что делаешь?]
- 30 секунд тишины → один ответ на всё
Garden Manager (Matrix, 4 агента)
Принимает текст в Matrix DM от имени @aiban и публикует заметки в цифровой сад. 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 контекста хватает с запасом.
Почему 4 агента вместо 1
Раньше был 1 агент с 12 tools в 16K контексте. Gemma 12B путалась, вызывала не те tools, зависала. Разбили на 4 узких — каждый со своими 2-5 tools. 12B справляется.
Сценарии использования
1. Кто-то написал в Telegram ЛС
Человек пишет mrgnl в Telegram → Deputy видит → вспоминает факты о человеке → отвечает от имени mrgnl → сохраняет реплику в историю.
Задержка ответа имитирует живого человека:
| Ситуация | Задержка |
|---|---|
| Незнакомец | 2–4 минуты (даёт mrgnl время ответить самому) |
| Знакомый | 30–90 секунд |
| Активный диалог | 5–15 секунд |
Если mrgnl ответил сам — Deputy молчит.
2. Фото в ЛС
Человек присылает фото → vLLM vision описывает картинку → Deputy реагирует естественно (эмоция, 1–2 предложения).
3. Публикация заметки (Matrix)
mrgnl пишет @aiban в Matrix DM → Garden Router определяет intent → передаёт нужному агенту:
- Writer — создаёт драфт в
content/drafts/, оформляет в Markdown - Searcher — ищет дубли через RAG по существующим заметкам
- Publisher — публикует: Quartz build → rsync на сервер → переиндексация RAG
- Отвечает с URL
4. Радио
Deputy знает про radio.mrgnl.ru. Если спросят «что слушаешь?» — вызовет get_now_playing, ответит небрежно. Не упоминает радио каждый раз.
5. Авто-постинг
garden_poster.py — фоновый поток, 3 раза в день (09:00, 15:00, 21:00):
- Берёт случайную заметку из сада (без index.md, drafts/)
- vLLM генерирует тизер (1-2 предложения на русском)
- Постит в Telegram-канал: заголовок + тизер + ссылка на notes.mrgnl.ru
- Запоминает последние 10 заметок — без повторов
radio_poster — авто-постинг треков из Маргинал в Telegram-канал.
Obsidian headless
Obsidian запущен на сервере через systemd + Xvfb:
[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) функциональны.
Оптимизации для 12B + 16K контекста
12B путается в длинных цепочках tool calls
Каждая оптимизация — компромисс между качеством и ресурсами. Контекст на вызов: ~3000 токенов из 16384. Запас есть.
| Параметр | Значение | Почему |
|---|---|---|
max_iter | 3 (Deputy), 5 (Writer) | 12B путается в длинных цепочках tool calls |
max_tokens | 1024 | Короткие ответы, меньше жрать контекст |
| History | 6 сообщений | ~1200 токенов вместо ~2000 |
| Dialog analysis | убран | Был +1 LLM вызов, модель и так понимает контекст |
| Tools у Deputy | 5 | 6+ tools — модель путается, вызывает не те |
| Garden Manager | 4 агента × 2-5 tools | Вместо 1 агента с 12 tools |
Чистка мёртвого кода
При рефакторинге удалено ~40% кодовой базы:
| Удалено | Что |
|---|---|
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 оставлена для совместимости |
Подробнее — статья про рефакторинг.