Меня зовут Никита Пастухов — автор 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— текстовая инструкция, которую загрузим в контекст, когда агент захочет работать с pytest -
scripts/— исполняемые скрипты, правила использования которых описаны в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 — если всё-таки хотите посмотреть оригинал
Автор: Propan671


