> Go Батя

llmagent — я написал свою замену OpenClaw на Go

#go #llm #agent #clean-architecture #telegram #openclaw #self-hosting #хакатон

За месяц я собрал llmagent (нафиг креатив!) — свою замену OpenClaw на Go: один статический бинарник вместо сотен файлов node_modules, Telegram и CLI, память, расписания, скиллы, браузер и MCP. Система стоит под systemd и уже делает часть моей повседневной работы. Дальше — зачем я это писал, как устроен демон и какие грабли собрал по пути.

Зачем

Я живу с OpenClaw на домашнем сервере: он сидит в Telegram, помнит, что я ему говорил, стучится по расписанию, умеет лазить в браузер. Штука удобная, но это большая система, которую писали под чужие задачи. Хочется понимать, что именно у меня крутится в демоне, почему он так ответил и что он реально может сделать в системе. И менять поведение под себя, а не под чужой роадмап.

Отдельный плюс — язык. OpenClaw — это Node.js: сотни файлов в node_modules, рантайм, который надо тащить на сервер и кормить памятью. Мой — Go: один статический бинарник на выходе, скопировал — и работает. Быстрее, надёжнее, удобнее в эксплуатации. А с JS мне всегда было в тягость работать.

Так родился llmagent — за август 2026 это ~56k строк Go и 120 тестовых файлов.

Главное решение: не клонировать OpenClaw

Я записал себе в ROADMAP и держал перед глазами:

Do not clone OpenClaw. Keep Clean Architecture on Go: one daemon, Telegram + CLI, memory, hands (exec/fs), cron/heartbeat, skills, optional browser, optional MCP client.

Самое ценное в этом списке — чего там нет. Никакого «всё и сразу»: voice, companion-приложения, Live Canvas, сложная мультиагентность — мимо. Мне нужен был один демон, который делает то, что я делаю руками каждый день: отвечает в Telegram, помнит контекст, умеет работать с файлами и ходить в интернет, выполняет задачи по расписанию. Всё остальное — подключается, только когда в самом деле понадобилось.

Архитектура

Clean Architecture, один бинарник, зависимости строго в одну сторону. Слои разделены так, чтобы domain и application ничего не знали про Telegram, SQLite или HTTP: ядро — это сущности и use cases, а всё внешнее подключается в composition root — internal/wiring. Единственный общий кусок вне слоёв — pkg/llm, тонкий OpenAI-совместимый HTTP-клиент: он нужен и адаптерам, и плагинам, поэтому вынесен отдельно.

Слой Путь Роль
Domain internal/domain Сущности и порты
Application internal/application Use cases: агентский цикл «LLM ↔ tools»
Adapters internal/adapters LLM, каналы, инструменты, память, сессии, конфиг
Plugins internal/plugins In-process плагины (compile-time)
Wiring internal/wiring Composition root
Entrypoint cmd/agent Загрузка конфига и запуск
Shared pkg/llm OpenAI-совместимый HTTP-клиент

Правило одно, и оно железное: application никогда не импортирует adapters. Не «стараемся», а проверяем go-arch-lint при каждом прогоне — я себе не доверяю на слово.

Размеры (строки кода):

  • domain: ~1.4k / 31 файл — маленькое ядро, как и положено
  • application: ~12.3k / 33 файла — весь агентский цикл
  • adapters: ~34k / 162 файла — каналы, браузер, память, MCP, крон
  • wiring ~2.4k, plugins ~1k, pkg ~4.9k, cmd ~0.4k

Итого ~56k строк Go 1.26, SQLite через modernc.org/sqlite — вообще без CGO, что для деплоя на сервер приятно. 120 тестовых файлов. По меркам OpenClaw это скромно, но покрывает всё, что мне нужно — это и есть цена решения «писать под себя».

Как это росло

Роадмап был на 11 фаз (0–10), и я шёл по нему последовательно — каждая фаза заканчивалась рабочим агентом, а не грудой кода.

Фаза 0 — фундамент. Бутстрап Go-модуля с devenv и линтерами, следом — каркас Clean Architecture. devenv для воспроизводимого окружения, go-arch-lint — чтобы архитектурный бардак не накапливался с первого дня.

Фаза 1 — первый канал и память. Telegram через long-poll + SQLite как долговременная память. Демон поднялся, заговорил из Telegram, начал запоминать. С этого момента агент уже был полезен — дальше всё шло итерациями поверх.

Фаза 2 — сессии и веб. Сессии разговоров, веб-инструменты, стриминг ответов — вещь, без которой диалог ощущается как переписка с пейджером. Потом добавился web_search: сначала Brave, позже Perplexity как альтернативный провайдер.

Фаза 3 — руки. Файловые инструменты в workspace и exec. Тут я принял важное решение: exec — не «делай что хочешь», а allowlist git, go, make плюс лимиты на таймаут и размер вывода. Модель получает ровно столько власти, сколько ей положено.

Фаза 4 — автоматизация. Крон-планировщик и задачи из файла. Heartbeat с отдельным промптом и suppress_ok — чтобы агент молчал, когда всё тихо, а не спамил «всё хорошо, работаю».

Фаза 5 — скиллы. Скилл — это просто папка в workspace с SKILL.md, которая инжектится в контекст по ситуации. Без магии, без компиляции — положил markdown, и агент это умеет.

Фаза 6 — эксплуатация. Подкоманда doctor, которая перед запуском проверяет конфиг: сырые секреты в YAML, права на файлы, пустые allowlist каналов. Плюс юниты systemd и graceful shutdown с таймаутом. Без этого этапа всё предыдущее — игрушка, которая умрёт при первом ребуте.

Фаза 7 — VK. Зачем второй канал, если уже есть Telegram? Потому что Telegram в РФ периодически уходит в блокировки и белые списки — и вне дома агент становился недоступен. А VK живёт, пока есть хоть какой-то интернет. Подключил VK через Bots Long Poll API — и не без боли: long-poll умел ронять процесс, подробности в «Грабли».

Фаза 8 — браузер. Chromedp поверх CDP: навигация, снимки страниц, клики, заполнение форм. Позже подъехал KeePassXC: автозаполнение и сохранение логинов, чтобы пароли вообще не попадали в контекст модели.

Фаза 9 — MCP. Клиент для MCP-серверов: stdio, Streamable HTTP, legacy SSE. Инструменты приходят как mcp_<server>_<tool> и для агента ничем не отличаются от встроенных: те же схемы параметров, те же проверки прав. Одна из самых полезных фаз: подключил — и у агента сразу куча готовых инструментов из экосистемы.

Фаза 10 — полировка. Квоты токенов на трёх уровнях (агент/канал/биндинг) — чтобы один болтливый канал не сжёг бюджет всего агента; addressbook с именованными контактами; spaces для изоляции workspace и памяти; /delegate для вложенных разовых запусков, когда результат возвращается в диалог, а собеседник не меняется; /agent — переключение собеседника.

Фичи мимо фаз

Не всё влезло в фазы — многое добавлялось по ходу, между делом. Из того, что реально пригодилось:

  • Агент видит картинки и PDF. Медиа из Telegram прогоняется через vision-модель (в примере конфига — qwen-vl в локальной Ollama, у меня — deepseek-v4-flash-vision-exp). Прислал скриншот или документ — агент сам его прочитает, не нужно пересказывать текстом.
  • «Печатает…» в Telegram. Пока агент думает и дёргает инструменты, в чате висит индикатор набора текста. Мелочь, но диалог перестаёт быть «отправил — и тишина».
  • Хот-релоад конфига. Конфиг меняется на лету, без перезапуска демона: инструменты применяются прямо в процессе. Подкрутил лимит — агент подхватил.
  • Агент знает свой бюджет. Учёт токенов и стоимости по каждому вызову LLM, плюс логируется заполненность контекстного окна. Видно, когда сессию пора чистить и сколько денег уже ушло в разговор.
  • Каталог моделей и переключение на лету. Модели описываются в конфиге (быстрая, reasoning, локальная vision), и сессию можно переключить на другую прямо в середине диалога.
  • Настоящие Go-плагины. Помимо markdown-скиллов есть и код: плагины саморегистрируются через go generate, получают структурированные события каналов и умеют подмешивать свои заметки в ход агента. Первый был банальный uuid — но механизм уже на месте.
  • config_schema как инструмент. Агент может сам посмотреть схему собственного конфига (YAML-каталог с кэшем) — без этого самонастройка была бы вслепую.

Мелочи, которые запомнились

  • /session compact — ручное сжатие контекста, когда чувствуешь, что агент «забыл начало разговора». Автоматика — это хорошо, но ручка должна быть.
  • CLI-ответы не должны падать в лог — звучит смешно, но без этого логи превращаются в кашу из диалогов.
  • config.yaml.lock — flock-файл, который нечаянно попадал в репозиторий, пока я не добавил его в ignore. Мелочь, а глаз мозолила.
  • doctor ловит конфликты мультиагентности — например, когда два агента делят workspace, но у каждого своя память. Такие вещи на глаз не видно, а он находит за секунду.
  • Telegram из России — отдельная тема. Поддержка api_base_url (локальный Bot API) и proxy_url — это не «фича для галочки», это необходимость.

Грабли

VK long-poll умел ронять процесс. Три захода на одну и ту же проблему: сначала ретраи таймаутов, потом фильтрация только транзиентных ошибок, и лишь на третий раз — общий механизм переподключения для VK и Telegram. Урок простой: если канал периодически отваливается, чинить надо цикл подключения, а не конкретный сбой.

Два systemd-юнита из-за KeePassXC. Системный сервис работает от пользователя llmagent и не видит сокет десктопа, а user-сервис — видит. Пришлось держать оба: один для прода, второй для браузерных логинов. Костыль, но рабочий — иначе пароли либо недоступны, либо попадают в контекст модели.

SSRF в браузере. Chromedp ходит туда, куда его попросили, а «попросили» может означать и localhost, и внутреннюю сеть. Добавил фильтр: loopback и приватные подсети — в блок. Браузер агента — это его руки, и у рук должны быть границы.

Проверено в бою

Первый серьёзный боевой сценарий — регистрация на хакатон от Codenrock. Браузерный тул из фазы 8 пришлось допиливать под реальный сайт: подключить KeePassXC и написать скриптовый скилл на Playwright поверх CDP. Полдня ушло, но своего добился — агент сам заполнил форму и зарегистрировал меня.

Теперь это работает как надо: прилетит в почту анонс следующего хакатона — агент сам заметит и скажет, а мне останется написать «зарегистрируй меня». И все аналогичные сценарии — формы, авторизации, заявки — проходят быстрее и легче.

Остальные механизмы из разделов выше — VK-резерв, хот-релоад конфига — в проде работают ровно так, как описано, без сюрпризов.

Итог

Собрать свою замену OpenClaw — реально за месяц, если честно ограничить скоуп. Не «повторить все фичи», а выписать свои сценарии и сделать ровно их, но на нормальной архитектуре, чтобы докручивать годами. А после такого проекта перестаёшь относиться к агентам как к чёрному ящику: каналы, память, инструменты, крон — теперь это просто код, который я сам написал, сам чиню — и который уже начал делать мою работу.