|
Professor Seleznov
|
Привет, Хабр! Меня зовут Никита Пастухов — автор FastStream, Principal Engineer и мейнтейнер AG2 (фреймворк для разработки агентов). Я уже 8 лет в разработке, последний год - по уши в агентах. И я хочу доказать вам, что написать своего агента не сложнее, чем написать CRUD Почему это вообще нужно доказывать? Потому что есть заметный разрыв между тем, что происходит с AI в мире, и тем, что происходит в среднестатистической российской компании:
| Мир |
Россия |
| В каждой компании подписка на OpenAI / Claude / Copilot |
ОПАСНО, хостим свои модели |
| Миллиард стартапов, делающих AI-продукты |
Непонятно |
| AI глубоко интегрирован в бэкофис — митинги, документы, SRE |
Чат-боты поддержки |
| A2A, UCP, интернет агентов |
Адоптим MCP |
| Инженеры умеют разрабатывать агентов |
Что это вообще такое? |
Поэтому давайте разберем устройство агентов на примере OpenClaw — самого хайпового “личного AI-агента” прямо сейчас. Он живёт в вашем мессенджере, разбирает почту, ведёт соцсети, пишет код, деплоит сервисы. Его популярность — свидетельство того, насколько мало люди пока используют агентов в быту. Для тех, кто в теме, OpenClaw не привнёс ничего нового. - TL;DR Материал получился большой, но не пугайтесь - я постарался сделать его максимально понятным и доступным. Прилагаю вам TL;DR, чтобы вам было не так страшно занырнуть.
Ну и подведем итоги, конечно. - Зачем писать своего агента Прежде чем разбирать устройство 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 может вызывать несколько инструментов одновременно — один запрос способен стартовать несколько параллельных подзадач. Мощно, но осторожно: токены сгорят быстро.

Когда заспавнил 100 сабагентов Есть два паттерна для работы с результатами:
- Сабагент сам приносит результат по готовности
- Сабагент отдаёт 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), только направленные на внешние системы. И изобретать велосипед не нужно — каталогов готовых решений достаточно:
- Интеграции с мессенджером
 Основная проблема при интеграции агента с любым 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-схемой, которые модель вызывает сама |
| Память |
Обычные инструменты для хранения и поиска долгосрочной информации |
| Сабагенты |
Те же инструменты, только вызывают другого агента |
| Скиллы |
SKILL.md + пара скриптов, инжектятся в контекст по запросу через инструмент |
| Интеграции |
Готовые инструменты / MCP на сотни сервисов |
| Мессенджер |
Обычный Telegram бот, вы это умеете |
| Фоновые задачи |
Cron, который дёргает агента по расписанию |
Всё это — один паттерн. Вы отправляете JSON в LLM, получаете JSON обратно, выполняете команду, кладёте результат в контекст. Повторяете. Вот и весь Harness. Разработать агента — не сложнее, чем написать CRUD. Единственная разница: вместо базы данных — LLM, вместо REST-ручек — инструменты.-Помните таблицу в начале? Надеюсь, теперь агенты не кажутся вам черным ящиком. Они перестают быть магией ровно в тот момент, когда вы смотрите на них изнутри. Так что вам не нужен OpenClaw. Теперь вы можете написать агента под свои задачи.-Все примеры кода в этой статье написаны на AG2 Beta — это полностью новая версия фреймворка, который я развиваю прямо сейчас. Мы хотим использовать ее в качестве основной при переходе к 1.0. Если хотите поучаствовать в OpenSource-разработке — мы ищем пользователей, контрибуторов и фидбек — приходите. А в моём Telegram-канале я пишу об агентах, OpenSource, разработке и остальном, что мне интересно. Ссылки
-Источник
|