Привет, Хабр! Меня зовут Никита Пастухов — автор FastStream, Principal Engineer и мейнтейнер AG2 (фреймворк для разработки агентов). Я уже 8 лет в разработке, последний год - по уши в агентах.
И я хочу доказать вам, что написать своего агента не сложнее, чем написать CRUD
Почему это вообще нужно доказывать? Потому что есть заметный разрыв между тем, что происходит с AI в мире, и тем, что происходит в среднестатистической российской компании:
Мир | Россия |
|---|---|
В каждой компании подписка на OpenAI / Claude / Copilot | ОПАСНО, хостим свои модели |
Миллиард стартапов, делающих AI-продукты | Непонятно |
AI глубоко интегрирован в бэкофис — митинги, документы, SRE | Чат-боты поддержки |
A2A, UCP, интернет агентов | Адоптим MCP |
Инженеры умеют разрабатывать агентов | Что это вообще такое? |
Поэтому давайте разберем устройство агентов на примере OpenClaw — самого хайпового “личного AI-агента” прямо сейчас. Он живёт в вашем мессенджере, разбирает почту, ведёт соцсети, пишет код, деплоит сервисы. Его популярность — свидетельство того, насколько мало люди пока используют агентов в быту. Для тех, кто в теме, OpenClaw не привнёс ничего нового.
TL;DR
Материал получился большой, но не пугайтесь - я постарался сделать его максимально понятным и доступным. Прилагаю вам TL;DR, чтобы вам было не так страшно занырнуть.
Что такое агент - разберём базовую формулу
Agent = LLM + Harnessи что в реальности скрывается за словом Harness.Пройдём ключевые механизмы: инструменты (tools), MCP, память, сабагенты, Skills.
Отдельно разберём практические кейсы: интеграцию с мессенджером, фоновые cron-задачи, внешние интеграции и динамическую загрузку скиллов.
Упомянем безопасность
Ну и подведем итоги, конечно.
Зачем писать своего агента
Прежде чем разбирать устройство OpenClaw — почему вообще стоит писать своего, а не взять готовый?
Специальное лучше универсального. OpenClaw делает всё: почту, соцсети, код, деплой. И всё из рук вон плохо. Агент, заточенный под одну задачу, решает её несравнимо лучше.
Меньше функционала — меньше поверхность атаки. Агент с доступом к почте, мессенджерам, кодовой базе и деплою — это очень интересная мишень. Зачем давать ему права на всё, если нужно только одно? К тому же, зная свой код, вы понимаете, как его защищать.
Можно закрыть свои конкретные хотелки. Никакой универсальный агент не знает ваш рабочий процесс, ваши инструменты, ваши привычки. Свой — узнает.
Это просто весело.
Итак, разбираем OpenClaw по частям — и строим своего.
Что такое агент
Забавный факт: в августе 2025 я начал работать с AG2 — компанией, которая занимается агентами и делает фреймворк для их разработки. Первый вопрос, который я задал коллегам: “Ребят, вы тут делаете агентов — кто-нибудь может объяснить, что это такое?” — и в ответ тишина.
Формального определения нет. Или я не тех людей спрашивал и не те статьи читал. Но оно особо и не нужно. У всех, кто занимается разработкой агентов, есть интуитивное понимание: это LLM + 10_000 приседаний вокруг управления контекстом, памятью, безопасностью и инструментами.
Сейчас принято формулировать это так:
Agent = LLM + Harness
Слово Harness (Упряжь) само по себе мало что значит — под него засунули все те приседания, что нужно сделать вокруг LLM, чтобы та решала реальные задачи:
управление контекстом
инструменты
память
скиллы
мультиагентная логика
интеграции с внешними системами
В общем — всё то, во что мы “запрягаем” LLM, чтобы она делала то, что нам нужно.
А что такое LLM в этой парадигме? Очень просто: LLM — это мозг агента HTTP-ручка. Она делает ровно одну вещь: принимает JSON и отдаёт JSON.
User -- {"role": "user", "content": "Привет!"} --> LLM User <-- {"role": "assistant", "content": "Господи, ну что опять!?"} -- LLM
Всё остальное — это Harness. Давайте разберём его по частям.

Контекст и управление им
Контекст — это полная история вашего взаимодействия с моделью. API LLM — stateless-сервис, поэтому вам нужно передавать весь контекст на каждый запрос.
Что-то вроде этого:
// первый запрос User -- [{"role": "user", "content": "Привет!"}] --> LLM User <-- {"role": "assistant", "content": "Господи, ну что опять!?"} -- LLM // второй запрос User -- [ // история {"role": "user", "content": "Привет!"}, {"role": "assistant", "content": "Господи, ну что опять!?"}, // новое сообщение {"role": "user", "content": "Да так, заскучал"} ] --> LLM
Кстати, системный промпт — это просто первое сообщение в контексте формата
{"role": "system", "content": "..."} // Так что первый запрос выглядит вот так User -- [ {"role": "system", "content": "..."}, {"role": "user", "content": "Привет!"} ] --> LLM
А все, что вы запихнули в запрос, — и есть контекст агента. Контекстное окно — это то, насколько большой JSON модель вообще способна переварить.
В коде это выглядит примерно так:
from autogen.beta import Agent, config agent = Agent("agent", config=config.OpenAIConfig("gpt-4")) # Делаем первый запрос turn = await agent.ask("Hi!") print(await turn.content()) # "Hi, how can I help you?" while True: # Делаем следующий запрос на базе предыдущего turn = await turn.ask("Continue") print(await turn.content()) # "What should I continue?"
И вот тут мы сталкиваемся с основной проблемой контекста — он растёт.
500t | user | <- новый запрос включает всю историю 300t | agent | | user | user | 100t | agent | agent | | user | user | user | | system | system | system |
И растёт он нелинейно. Т.е. на каждый следующий запрос в модель вы отправляете всё более и более жирный JSON. А это токены и деньги.

Хорошо, что контекст кэшируется. Т.е. на самом деле вы отправляете что-то такое:
50t | user | <- платим в основном за новые токены 50t | | | user | | 100t | | | | user | 250ct | 450ct | <- ct = cached tokens | system | cached | cached | <- старая часть контекста кэшируется
Кэшированные токены могут или просто стоить дешевле, или вообще быть бесплатными (например, в подписке Claude Max). Но даже так они расходуют лимиты, так что общее правило — если закончили с текущей задачей, новую начинайте в другом чате. И модель будет отвечать точнее, и токены сэкономите.
Сжатие контекста (Context Compaction)
Это самый базовый функционал для любого агента. Если контекст разросся, его нужно сжимать. А поскольку это просто JSON, то сжимать его можно каким угодно способом:
отбрасываем старые сообщения
отбрасываем только определённые типы сообщений
сжимаем весь диалог в одно
<summary>с помощью той же LLM
Последний вариант — самый простой и популярный. В коде это выглядит примерно так:
from autogen.beta import Agent, config compaction_agent = Agent("compacter", config=config.OpenAIConfig("gpt-5")) agent = Agent("my-lovely-agent", config=config.OpenAIConfig("gpt-5")) turn = await agent.ask("Hi!") while True: history = turn.stream.history messages = await history.get_messages() if len(messages) > 10: summary = await compaction_agent.ask( "Summary chat history to single message", f"History: {messages}" ) # перезаписываем всю историю единственным сообщением await history.set([summary.body]) turn = await turn.ask("Continue") print(turn.body)
Это упрощённый пример для понимания механики. Обычно во фреймворках для этого есть готовые батарейки: мидлвари, политики управления контекстом и т.д.
Кстати, поздравляю. Теперь вы способны написать ChatGPT (не модель, а веб-чатик)
Инструменты
Инструменты — это очень важная штука, которая позволила вывести агентов во внешний мир. Теперь они не ограничены чатом, они могут действовать.
С точки зрения LLM, инструмент — это еще один JSON в контексте:
{ "name": "get_weekday", "description": "Call this tool each time you want to know current weekday", "arguments": { "type": "object", "properties": {} } }
name — уникальный идентификатор, по которому модель будет вызывать инструмент
description — наша попытка объяснить модели, зачем он нужен и когда его дёргать
arguments — JSONSchema с описанием аргументов; мы просто надеемся, что модель вернёт правильный JSON
Как происходит вызов
Модель видит сигнатуру, и в процессе диалога сама решает использовать инструмент и возвращает команду на выполнение:
User -- [{"role": "user", "content": "Чем займёмся сегодня?"}] --> LLM LLM -- { "role": "assistant", "tool_calls": [{ "call_id": "...", "name": "get_weekday", "arguments": "{}" }] } --> Agentic Framework LLM <-- { "role": "tool", "content": "Friday" } -- Agentic Framework User <-- { "role": "assistant", "content": "Friday! It's time to drink beer!" } -- LLM
Контекст в этот момент:
| assistant | | tool result | <- результат инструмента | tool call | <- команда на вызов инструмента | user | | tools definitions | <- описания доступных инструментов | system |
С точки зрения кода, инструмент — просто функция:
from autogen.beta import Agent, config agent = Agent("my-lovely-agent", config=config.OpenAIConfig("gpt-5")) @agent.tool def get_weekday() -> str: return "Friday" # всегда пятница, всегда пьём пиво
Фреймворк сам парсит название, описание, аргументы, кладёт их в контекст, вызывает функцию и возвращает результат модели.
Возможности инструментов
Инструмент — это буквально любой код, который вы можете приделать к модели. На базе инструментов реализованы:
Память
Сабагенты
Походы агента в интернет
Интеграции со внешними системами (Google Docs, Notion, Maps и т.д.)
Взаимодействие с операционной системой
AI-IDE (чтение, редактирование файлов, запуск команд)
Проблема перегруза инструментами (overtooling)

Чем больше инструментов — тем лучше агент? Нет.
Если контекст на 95% состоит из описаний инструментов, а пользовательский запрос теряется на их фоне — не удивляйтесь, что модель начинает творить дичь.
Я видел весёлый пример: модели дали 120 инструментов, и что бы вы ни попросили её сделать, она просто вызывала случайные инструменты в случайном порядке.
Привет любителям включать 100500 MCP и SKILLS к себе в IDE
Общее правило: не включайте инструменты, которые не нужны в текущем контексте. Именно с этим, в числе прочего, помогают сабагенты и скиллы — о них поговорим позже.
Причём тут MCP
MCP — это инструменты, которые доступны из другого процесса по HTTP или сокету. В начале диалога агент запрашивает у MCP-сервера описание методов, при вызове — отправляет команду, получает результат. С точки зрения модели и контекста — никакой разницы. Зато один MCP-сервер для работы с базой данных может обслуживать сотни агентов параллельно, не затаскивая код в каждую кодовую базу. Да и обновлять его можно независимо от самих агентов.
Память

Контекст — это история текущей беседы. Но хотелось бы, чтобы агент накапливал знания о мире и о нашем взаимодействии с ним:
информацию о пользователе
свою личность (манеру общения)
общие правила из опыта
историю диалогов
Как вы уже, наверное, догадались: память — это тоже инструменты. Набор функций для чтения, записи и поиска по воспоминаниям:
class Memory: def write_conversation_memory(name: str, summary: str) -> None: ... def list_conversations() -> list[tuple[UUID, str, datetime]]: ... def read_conversation(conversation_id: UUID) -> str: ...
В самом простом случае, память — директория на файловой системе. Вот как это устроено в OpenClaw:
memory/ ├── PERSONALITY.md # личность агента ├── USER.md # профиль пользователя ├── 04_16_2026/ # история диалогов │ ├── Write_Blogpost.md │ └── Make_Presentation.md └── 04_17_2026/ └── Find_NN_Restaurants.md
Имея такую директорию и пару инструментов для работы с ней, агент умеет:
самодописывать свой системный промпт (через
PERSONALITY.md)обновлять информацию о пользователе (
USER.md)писать историю диалогов, искать по ним и доставать факты
Механика простая: PERSONALITY.md и USER.md читаются инструментом и подкладываются в системный промпт при каждом старте нового чата — агент сам вызывает read_personality() в начале сессии или вы делаете это принудительно. История диалогов — наоборот, загружается только по запросу, когда нужно что-то вспомнить. Так контекст не раздувается постоянно, а факты о пользователе всегда под рукой.
К слову, RAG — это точно такой же набор инструментов. Отличается только реализация: вместо файловой системы внутри — векторная база данных.
Вот и вся “магия” агентов, которые самодописывают промпты и помнят всё.
Сабагенты
Представьте: вы спросили агента “мы на прошлой неделе выбирали ресторан, напомни, что решили”. Агент умеет смотреть историю только по дням — и начинает перебирать:
User -- "Мы на прошлой неделе выбирали, куда пойти. Что решили?" --> LLM LLM -- list_memories(date="04_15_2026") --> Framework LLM <-- [] -- Framework LLM -- list_memories(date="04_16_2026") --> Framework LLM <-- ["Write_Blogpost", "Make_Presentation"] -- Framework LLM -- list_memories(date="04_17_2026") --> Framework LLM <-- ["Find_Restaurants"] -- Framework LLM -- read_file(path="04_17_2026/Find_Restaurants.md") --> Framework User <-- "Это было 17-го! Ты решил сходить в Ель, столик на 21:00" -- LLM
Контекст в итоге выглядит так:
| assistant | | tool result | <- промежуточный результат #3 | tool call | <- вызов #3 | tool result | <- промежуточный результат #2 | tool call | <- вызов #2 | tool result | <- промежуточный результат #1 | tool call | <- вызов #1 | user | | tools definitions | | system |

В контексте очень много промежуточного шума. Всё это было нужно, чтобы ответить на один вопрос, — но дальше в диалоге бесполезно.
Решение: пусть подзадачу решает сабагент. Оборачиваем вызов другого агента в инструмент — у него изолированный контекст, а в основной попадает только финальный результат:
from autogen.beta import Agent, config, tools memory_agent = Agent( "memory-agent", config=config.OpenAIConfig("gpt-5"), tools=[tools.FilesystemToolkit("./memory")] ) agent = Agent( "ag-claw", config=config.OpenAIConfig("gpt-5"), tools=[ memory_agent.as_tool(description="Find information in memories") ] )
Вместо одного зашумлённого контекста — два маленьких, изолированных:
| assistant | | | subagent result | assistant | <- в главный контекст попадает только итог | | tool result | | | tool call | | | tool result | | | tool call | | | tool result | | | tool call | <- весь шум остается внутри сабагента | subagent call | user | | user | | | subagent tools | memory tools | | claw prompt | subagent prompt | <- два изолированных контекста
Тут важно понимать:
Сабагент — это не сервис и не модуль. Это просто подконтекст. У него свой системный промпт, своя история. Но модель чаще всего та же.
Сабагенты — самый распространённый паттерн мультиагентного взаимодействия сейчас, потому что самый простой и при этом достаточно эффективный. Заодно это чистое решение проблемы перегруза инструментами: вместо одного агента с 50 инструментами — несколько агентов с 5–10 инструментами каждый.
Фоновые и параллельные сабагенты
Сабагент не обязан блокировать основной диалог. Основной агент ставит задачу, отпускает управление, диалог продолжается — когда сабагент завершится, результат подкладывается в контекст:
| assistant | | | user | | | subagent result | assistant | <- результат приходит асинхронно | | tool result | | assistant | tool call | | user | tool result | | | tool call | | subagent called | tool call | <- сабагент работает в фоне | subagent call | user | | user | |
А поскольку LLM может вызывать несколько инструментов одновременно — один запрос способен стартовать несколько параллельных подзадач. Мощно, но осторожно: токены сгорят быстро.

Есть два паттерна для работы с результатами:
Сабагент сам приносит результат по готовности
Сабагент отдаёт TaskId, основной агент спрашивает о готовности по этому ID
Какой подойдёт вам — зависит от задачи. Как и всё в этом мире.
Динамические сабагенты
Ещё есть вариант, когда агент сам генерирует подагентов “на лету”.
Тут тоже никакой магии — у нас просто есть инструмент, который принимает на вход:
системный промпт для динамического агента
набор инструментов для него
какую модель использовать
Этот инструмент генерирует агента, а потом мы сразу же натравливаем его на нужную подзадачу.
Скиллы (Skills)
Скиллы (Skills) — это способ научить агента выполнять узкоспециализированные задачи без постоянного раздувания контекста.
Если вы активно используете кодинг-агентов (Claude Code, Cursor, Codex) — вы с ними уже сталкивались. Несколько реальных примеров:
rtk — учит агента использовать rtk как прокси для shell-команд: вместо сырого вывода
git logилиcargo buildагент получает отфильтрованный результат и тратит в разы меньше токеновcaveman — учит агента писать примитивный, но предсказуемый код без оверинжиниринга
React best practices — гайдлайны по React от Vercel, которые агент загружает перед работой с фронтендом
Формула проста:
Skill = Context + Scripts
Структура на файловой системе:
.agents/skills/ └── Pytest_Skill/ ├── SKILL.md └── scripts/ ├── run_pytest.sh └── list_tests.py
SKILL.md— текстовая инструкция, которую загрузим в контекст, когда агент захочет работать с pytestscripts/— исполняемые скрипты, правила использования которых описаны вSKILL.md
Агенту для работы со скиллами нужна пара инструментов:
class SkillsToolkit: def list_skills() -> list[SkillMetadata]: ... def load_skill(skill_id: str) -> str: ... def run_skill_script(skill_id: str, script: str) -> str: ...
Для того, чтобы модель знала, какие скиллы у неё в принципе есть, ей в контекст нужно подложить информацию о них (по аналогии с инструментами):
[{ "name": "Pytest_Skill", "description": "Use this skill to test your python code", ... // всякие бесполезные поля }]
Контекст при работе со скиллом:
| assistant | | script result | | run script | <- исполнение скрипта из скилла | skill content | | load skill | <- загрузка скилла в контекст | user | | tools definitions | | skills metadata | <- список доступных скиллов | system |
Итого, скиллы — это:
метаинформация в контексте
пара инструментов
директория на файловой системе
Зато это позволяет загружать огромные инструкции для специфических задач по требованию, а не держать их в контексте всегда. Слишком много скиллов тоже регистрировать не стоит — их метаданные тоже занимают место. Хорошее решение: вынести работу со скиллами в отдельный сабагент.
Динамические скиллы
Финальная фича — загрузка скиллов из интернета прямо во время диалога:
class SkillSearchToolkit: async def search_skills(query: str, limit: int = 10) -> str: ... async def install_skill(skill_id: str) -> str: ... def remove_skill(name: str) -> None: ...
Ищем скиллы на skills.sh по API, скачиваем с GitHub, устанавливаем в локальную папку. В AG2 для этого есть готовый autogen.beta.tools.SkillSearchToolkit.
Внешние интеграции
Тут всё просто: интеграции — такие же инструменты (или MCP), только направленные на внешние системы. И изобретать велосипед не нужно — каталогов готовых решений достаточно:
aci.dev/tools — 600+ готовых инструментов, open source
composio.dev/toolkits — 1000+ тулкитов с OAuth из коробки
arcade.dev — MCP runtime, фокус на безопасной авторизации агентов
mcp.so — каталог MCP-серверов (community)
Интеграции с мессенджером

Основная проблема при интеграции агента с любым UI — это управление контекстами. Это могут быть разные чаты или явные команды, что текущий диалог завершён и пора начинать новый. А если у вас агент рассчитан на нескольких пользователей, то нужно ещё разграничивать их контексты и не забыть про безопасность.
Но эти задачи не какие-то особенные для агентной разработки. Любой веб-разработчик делал что-то такое и, я уверен, вы тоже справитесь.
В помощь могу предложить разве что вот такой код:
from autogen.beta import Agent, config, MemoryStream agent = Agent("tg-agent", config=config.OpenAIConfig("gpt-5")) dp = Dispatcher() chat_state: dict[int, MemoryStream] = {} @dp.message(F.text) async def on_text(message: Message) -> None: # получаем старый контекст или создаем новый if not (stream := chat_state.get(message.chat.id)): stream = chat_state[message.chat.id] = MemoryStream() # дергаем агента с этим контекстом reply = await agent.ask( message.text, stream=stream, variables={"user_id": message.chat.id}, ) # отвечаем в TG чат await message.answer(reply.content) asyncio.run(dp.start_polling(bot))
Чуть более развёрнутый пример я уже описывал в блоге — там показана история диалогов и переключение между ними.
Но, я уверен, вы без труда справитесь с такой интеграцией. Если же вы хотите написать веб-приложение, советую посмотреть на фичи протокола AG-UI — там уже есть готовые фреймворки и на фронтенде.
Фоновые задачи
Одна из хвалёных фич OpenClaw — “скажи агенту мониторить сайт авиабилетов каждый час и купить, как только появятся”. Это cron-задачи, которые агент может регистрировать сам.
Конечно, нужны инструменты:
class SchedulerToolkit: def schedule_task(task_prompt: str, cron: str) -> UUID: ... def remove_task(task_id: UUID) -> None: ... def list_tasks() -> list[UUID]: ...
И шедулер, который крутится рядом с агентом и вызывает его в назначенное время:
import asyncio from autogen.beta import Agent, config cron = Scheduler() agent = Agent( "tg-agent", config=config.OpenAIConfig("gpt-5"), tools=[SchedulerToolkit(cron)] ) async def main(): asyncio.create_task(cron.run()) await agent.ask("Мониторь билеты каждый час")
Агент создаёт задачи на вызов самого себя в определённое время с заданным промптом. Вот и вся магия.
Коротко про безопасность
Агент с доступом к файловой системе, мессенджеру и внешним сервисам — интересная мишень. Два момента, о которых стоит думать с самого начала:
Prompt injection. Вредоносный текст из внешнего источника (письмо, веб-страница, документ) может попасть в контекст и переопределить поведение агента. Валидируйте то, что кладёте в контекст из внешних систем (как результат инструмента, так и ввод пользователя). Если агент читает письма — не давайте ему автоматически выполнять инструкции из них.
Принцип минимальных прав. Не давайте агенту инструменты, которые ему не нужны. Агент для работы с почтой не должен уметь деплоить сервисы. Меньше инструментов — меньше поверхность атаки и меньше шанс, что модель вызовет что-то не то.
Для надёжности лучше запускать агента в sandbox, например в контейнере.
Итого
Мы прошли по всем компонентам OpenClaw — и ни один из них не оказался rocket science:
Компонент | Что это на самом деле |
|---|---|
Массив сообщений, который вы таскаете между запросами | |
Функции с JSON-схемой, которые модель вызывает сама | |
Обычные инструменты для хранения и поиска долгосрочной информации | |
Те же инструменты, только вызывают другого агента | |
| |
Готовые инструменты / MCP на сотни сервисов | |
Обычный Telegram бот, вы это умеете | |
Cron, который дёргает агента по расписанию |
Всё это — один паттерн. Вы отправляете JSON в LLM, получаете JSON обратно, выполняете команду, кладёте результат в контекст. Повторяете. Вот и весь Harness.
Разработать агента — не сложнее, чем написать CRUD. Единственная разница: вместо базы данных — LLM, вместо REST-ручек — инструменты.
Помните таблицу в начале? Надеюсь, теперь агенты не кажутся вам черным ящиком. Они перестают быть магией ровно в тот момент, когда вы смотрите на них изнутри.
Так что вам не нужен OpenClaw. Теперь вы можете написать агента под свои задачи.
Все примеры кода в этой статье написаны на AG2 Beta — это полностью новая версия фреймворка, который я развиваю прямо сейчас. Мы хотим использовать ее в качестве основной при переходе к 1.0. Если хотите поучаствовать в OpenSource-разработке — мы ищем пользователей, контрибуторов и фидбек — приходите.
А в моём Telegram-канале я пишу об агентах, OpenSource, разработке и остальном, что мне интересно.
Ссылки
Мой Telegram-канал — здесь я рассказываю об агентах, OpenSource, разработке и продуктивности
github.com/ag2ai/ag2 — фреймворк, на котором написаны все примеры выше
aci.dev/tools | composio.dev/toolkits | arcade.dev — каталоги готовых интеграций
mcp.so — каталог MCP-серверов
github.com/openclaw/openclaw — если всё-таки хотите посмотреть оригинал