Я собрал отдел из шести AI-джунов и вот что из этого вышло
Вначале была мысль
Я технический писатель и моя работа — извлекать смысл из хаоса и превращать этот смысл в документацию, которую действительно читают. Под «хаосом» я подразумеваю множество записей созвонов, постановок от аналитиков, Jira-тикетов.
Дело в том, что между «хаосом» и «документацией, которую читают» есть прослойка в виде «механической» работы: например, несколько раз переслушать запись созвона, чтобы не пропустить ключевые нюансы, вручную пройтись по пачке тикетов, чтобы собрать релиз-ноты или побуквенно сверить текст со стайлгайдом.
Такая рутинная «механика» съедает время, нужное для смысловой работы, и, осознав это, я подумал: «А что, если такую рутину делегировать агентам?» (коллегам, увы, невозможно — я все-таки один техписатель в отделе)
В этой статье я расскажу, как придумал и сделал своих агентов, что получилось хорошо, а что — нет, и почему это делегирование, а не замена «кожаного» специалиста.
От разрозненных агентов к одному мегаинструменту
К единой системе агентов я пришел не сразу и сначала у меня появились отдельные наработки в виде нескольких агентов.
Каждый из них жил сам по себе, хранился и настраивался отдельно, запускались они тоже в отдельных командных строках. Со временем меня начало бесить, что приходится запускать несколько командных строк и переключаться между ними, особенно когда задач много.
И я-таки додумался объединить всех агентов в одном месте и весьма вовремя наткнулся на документацию OpenClaw — это стало отправной точкой для сборки своего «отдела AI-джунов».
Почему OpenClaw?
Здесь было бы логичным спросить меня: «Дима, а почему ты выбрал OpenClaw, а не Hermes? Почему не делал с помощью LangChain/LangGraph?»
Я не проводил сравнительный анализ фреймворков. На одном из мероприятий я услышал, что OpenClaw — это open-source-шлюз между LLM и мессенджерами, которому можно настраивать skills. Это звучало именно так, как мне было нужно, поэтому я решил попробовать.
Если хотите почитать о нем подробнее, оставлю здесь ссылку на документацию. А если коротко: OpenClaw — это агент, который принимает запросы пользователя в мессенджере, использует инструкции и skills, умеет вызывать внешние инструменты и жить как долгоживущий процесс.
Связка OpenClaw с Telegram была очевидной: в этом мессенджере я провожу много времени и хотелось, чтобы все агенты жили в одном чатике.
Общая схема: Docker, оркестратор и шесть агентов
Решение крутится в Docker. Его я выбрал, чтобы изолировать OpenClaw от файловой системы и глобального окружения, а не сидеть и гадать, не решит ли он что-то «оптимизировать» без спроса.
Кодовую часть я собирал в связке с Claude Code: я писал спецификацию, задавал архитектуру, тестировал поведение и принимал решения, а Claude Code занимался кодом агентов и правил ошибки.
В моем супер-агенте OpenClaw играет роль оркестратора, а шесть специализированных агентов работают рядом с ним как отдельные FastAPI-приложения.
Вот они, слева направо, как говорится:
- agent-spec2doc — превращает постановки от аналитиков в черновик документации;
- agent-figma — генерирует черновик руководства пользователя на основе Figma-макетов;
- agent-release-notes — собирает release notes по репозиторию или Jira-тикетам;
- agent-api-docs — делает API-документацию из OpenAPI-спеки;
- agent-reviewer — ревьюит тексты;
- agent-transcribe — делает расшифровку видео или аудио.
Эта архитектура появилась в первом коммите проекта — 6ff8379 scaffold super-agent with 6 FastAPI services and OpenClaw config. Там же появился docker-compose.yml:
services:
agent-spec2doc:
build: ./agent-spec2doc
ports:
- "8001:8001"
env_file: .env
networks:
- agent-network
restart: unless-stopped
agent-figma:
build: ./agent-figma
ports:
- "8002:8002"
env_file: .env
networks:
- agent-network
restart: unless-stopped
agent-transcribe:
build: ./agent-transcribe
ports:
- "8003:8003"
env_file: .env
networks:
- agent-network
restart: unless-stopped
agent-release-notes:
build: ./agent-release-notes
ports:
- "8004:8004"
env_file: .env
networks:
- agent-network
restart: unless-stopped
agent-api-docs:
build: ./agent-api-docs
ports:
- "8005:8005"
env_file: .env
networks:
- agent-network
restart: unless-stopped
agent-reviewer:
build: ./agent-reviewer
ports:
- "8006:8006"
env_file: .env
networks:
- agent-network
restart: unless-stopped
openclaw:
image: ghcr.io/openclaw/openclaw:latest
volumes:
- ./openclaw:/home/node/.openclaw
env_file: .env
networks:
- agent-network
restart: unless-stopped
depends_on:
- agent-spec2doc
- agent-figma
- agent-transcribe
- agent-release-notes
- agent-api-docs
- agent-reviewerОтмечу, что перечисленные агенты являются агентами по роли, но технически это обычные HTTP-сервисы, а не внутренние субагенты OpenClaw. Об этом пришлось прямо упомянуть в инструкциях оркестратору: вызывать их через curl, использовать имена из docker compose, не ходить через localhost в контейнере и не запускать дополнительных сессий.
Архитектура агентов держится не только на docker compose, но и на четко установленных границах роли оркестратора. Если не объяснить модели, что она только перенаправляет запросы, она начинает искать «творческие способы помочь» (об этом я расскажу далее).
Еще одна немаловажная часть работы — это инструкции. В OpenClaw поведение оркестратора и отдельных сценариев задается обычными Markdown-файлами: AGENTS.md, SOUL.md, USER.md, а также файлами skills вроде workspace/skills/release-notes/SKILL.md или workspace/skills/spec2doc/SKILL.md.
В моем случае эти инструкции можно разделить на два уровня.
1. Общие правила оркестратора. Они лежат в AGENTS.md и говорят, как классифицировать входящее сообщение, какие есть агенты, по каким URL их вызывать, что делать с ошибками, чего нельзя показывать пользователю и т.д.
2. Skills под конкретные задачи. Например, agent-release-notes знает, что Jira-ссылки нужно передавать только в /generate-jira, agent-transcribe — что конвертация видео в текст может занимать несколько минут, а agent-spec2doc — что нельзя писать документацию самостоятельно вместо агента.
Промптинг здесь похож не на литературное «будь полезным», а на инструкцию диспетчеру:
- классифицируй запрос; - выбери одного агента; - вызови его через HTTP; - дождись результата; - верни результат как есть; - не переписывай, не суммаризируй, не запускай второй процесс.
Вышеперечисленные файлы стали не менее важной частью системы, чем программный код агентов. Код обрабатывает данные, а инструкции удерживают оркестратор в нужной роли.
agent-release-notes: разбираем в деталях
agent-release-notes оказался самым полезным агентом из шести, т.к. работает безотказно и предсказуемо, а также экономит время.
Агент работает в двух режимах.
1. История коммитов. Агент получает ссылку на репозиторий и промежуток, за который нужно собрать релиз-ноты. Затем он забирает коммиты, нормализует их и формирует release notes/changelog.
2. Jira-задачи. Агент получает пачку ссылок на задачи, идет в Jira API, забирает summary, description, тип задачи, статус, компоненты и fix version и собирает текст release notes/changelog.
Сделай release notes по задачам: https://jira.example.com/browse/PROJ-1201 https://jira.example.com/browse/PROJ-1198 https://jira.example.com/browse/PROJ-1187 ИЛИ Сделай release notes по этому репозиторию [ссылка на репозиторий] за период с ДД.ММ.ГГГГ по ДД.ММ.ГГГГ.
На выходе получается подробный release notes с описанием новых фич, улучшений и багфиксов:
В первом варианте GitHub-запрос был низкоуровневым: owner, repo, since, branch. Оркестратор должен был сам разобрать сообщение пользователя и разложить его по полям. Позже, в коммите 9a55ac4 feat(agents): refine orchestrator workflows, я приблизил контракт к реальному пользовательскому вводу: API агента стал принимать repository, date_from, date_to, а разбор URL и дат переехал в код.
Первая версия агента могла возвращать сразу и release notes, и changelog (причем на английском), даже если я просил что-то одно, поэтому пришлось внести фикс в схему: в ней появился output_type со значениями release_notes или changelog. Теперь формат выбирает не настроение модели, а параметр запроса.
С Jira была отдельная история. Чтобы агент не передавал гигантский HTML и не забивал контекст, чтение тикетов вынесено в код: агент ходит в /rest/api/3/issue/{KEY} и забирает только нужные поля.
resp = requests.get(
f"{base_url}/rest/api/3/issue/{key}",
auth=self._auth,
params={
"fields": "summary,issuetype,status,description,labels,components,fixVersions"
},
timeout=REQUEST_TIMEOUT,
)Кроме того, в инструкции оркестратора это закреплено как правило: Jira-ссылки нельзя открывать через браузер или web-fetch, а передавать в release-notes их нужно только через /generate-jira.
- For Jira issue URLs, never use `web_fetch`, `web_search`, browser tools, or direct page scraping. - Jira URLs must be passed only to `agent-release-notes` via `/generate-jira`.
Вывод: LLM хорошо упаковывает смысл, но плохо подходит для всей механики вокруг задачи. URL, даты, форматы, API-ошибки и HTML-страницы лучше отдавать обычному коду.
agent-figma и agent-transcribe: ожидание не совпало с реальностью
Конечно, не все было идеально (ну, а как иначе...) и два следующих агента показали, что если что-то работает локально, то не факт, что заработает в Docker.
agent-figma: осторожно, двери закрываются Docker закрывается
Этот агент переродился из агента, которого я представлял в этом году в своем докладе на Techwriter Days 3. Тот агент работал так: получал ссылку на Figma-макет, шел в Figma REST API, получал структуру слоев и генерировал черновик руководства пользователя.
Я попробовал перенести его в Docker и тут начался сущий кошмар. Как можно увидеть из истории проекта, в коммите 380cd3f fix(figma): handle API access limitations with fallback: в сообщении прямо указано, что Figma API из Docker блокировался CloudFront с ошибкой 403.
Я попробовал сделать клиент аккуратнее: сначала запрос без токена для публичных файлов, потом повтор с токеном, отдельная обработка 401, 403, 404 и 429. После многих бесплодных попыток я плюнул и сделал вывод: «Не работает Figma API из контейнера? Да и Huyndai с ним!».
В итоге пользовательский путь стал таким: агент получает PNG/JPEG-вариант макета, а агент по нему составляет черновик. Да, топорно и не так элегантно, зато отпадает зависимость от CloudFront и лимитов Figma REST API (а то, честно говоря, раздражало ловить ошибку 429 после пятой генерации).
Дополнительно в skill для Figma я вынес правило, что если агенту пришла ссылка figma.com/..., то нужно попросить скриншот, а не вызывать agent-figma.
Если пользователь прислал ссылку `figma.com/...`, не вызывай `agent-figma`. Ответь: `Figma-ссылки сейчас не разбираю напрямую. Пришли скриншот нужного экрана или фрейма — я составлю user guide по изображению.`
Проще говоря, лучше устойчивый сценарий с небольшими «телодвижениями», чем красивый, но нерабочий.
agent-transcribe: размеры, форматы и болтливый оркестратор
В случае с этим агентом все уперлось в более приземленные ограничения.
Первое — размер. Telegram API не пропускает большие файлы и никаким промптом это не починить. Второе — формат. В моем сценарии MOV-файлы не проходили, поэтому приходилось переконвертировать видео в поддерживаемый формат.
Позже в коммите d5f2602 feat(agents): add media link transcription для больших файлов появился обходной путь: я добавил эндпоинт /transcribe/url. За счет этого можно было отправлять публичную ссылку на медиафайл, а агент ее сам скачивает, конвертирует, распознает в текст, обрабатывает и выдает пересказ видео.
В коде также появилась нормализация Google Drive-ссылок и проверка размера не только по content-length, но и по фактически скачанным байтам. Это как раз та механика, которую лучше держать в коде, а не объяснять модели словами.
Еще один прикол выкинул оркестратор. Пока agent-transcribe работал над медиафайлом, OpenClaw начинал присылать в Telegram промежуточные статусы Sifting..., Process: fast-shell, Process: young-forest. Выглядело как лишний шум и заставляло психовать от обилия сообщений.
Вылечился этот «недуг» изменением конфигурации OpenClaw и инструкций оркестратора. Я отключил потоковые статусы и уведомления о завершении команд, а в инструкциях отдельно прописал, что при распознавании нужно ждать до 900 секунд, не запускать повторный запрос, не говорить, что процесс не завершился, пока агент сам не вернет ошибку:
- Таймаут для `/transcribe` и `/transcribe/url` — не меньше 900 секунд. - Если `exec` вернул активную process-сессию, продолжай ждать эту же сессию. - Не запускай повторный запрос, пока первый еще выполняется. - Не называй процесс упавшим, пока команда или агент реально не вернули ошибку.
Еще немного про поехавший оркестратор
С agent-spec2doc тоже произошла забавная история. Первое время агент иногда возвращал пустой ответ по непонятным причинам. Оркестратор видел, что задача вроде бы не выполнена, писал в чат что-то в духе «агент не отвечает, сделаю работу за него» и начинал генерировать документацию самостоятельно.
Как уже и говорилось выше, оркестратор должен оркестрировать, а агенты — выполнять запросы. Если оркестратор начнет подхватывать чужую работу, то выйдет черт-те что.
Поэтому я внес фикс 5676d00 fix(agents): harden documentation workflows. До него agent-spec2doc мог вернуть пустую строку как будто это нормальный результат:
return response.choices[0].message.content or ""
После фикса пустой ответ стал ошибкой:
content = (response.choices[0].message.content or "").strip()
if not content:
raise GenerationError("LLM вернул пустой черновик документации")Одновременно в инструкциях появился прямой запрет на составление черновика вместо agent-spec2doc:
- Service `result` must be returned exactly as provided: no prefixes, no commentary, no bullet conversion, no summarizing. - If a service returns `result`, send exactly that `result` to the user without rewriting, evaluating, or adding commentary. - If a service returns `error`, reply in Russian with a short "service error" message and include the error text. - If `result` is empty and `error` is empty, reply in Russian that the service returned an empty result and ask the user to repeat the request or send the source material again.
Позже это правило было перенесено на всех агентов: если агент вернул result, оркестратор должен вернуть именно result, без префиксов, комментариев, пересказа и «улучшений». А если процесс еще выполняется — ждать, а не отправлять пользователю промежуточные статусы.
Короче говоря, промптинг в некоторых случаях — по-прежнему наше все. Оркестратору мало сказать «перенаправляй запросы», нужно ж еще прописать, чего не стоит делать, когда агент молчит или возвращает пустой ответ.
Еще два агента: api-docs и reviewer
Про agent-spec2doc и agent-figma я уже рассказал выше, поэтому здесь коротко остановлюсь на двух оставшихся агентах, которые закрывают более точечные задачи.
agent-spec2doc я создавал для ситуаций, когда вместо нормальной документации на руках есть только OpenAPI-спецификация в YAML или JSON. Формально это уже «документация», но на практике читать такую спеку как пользовательский материал неудобно: много служебных полей, схем, параметров, кодов ответа, но мало нормального объяснения.
Агент принимает OpenAPI-файл, разбирает эндпоинты и возвращает более человекочитаемый черновик: назначение метода, параметры запроса, тело, ответы, ошибки и примеры. Это не заменяет полноценную API-документацию, но хорошо закрывает первый проход: вместо пустого листа уже есть структура, которую можно проверить, уточнить и привести к стилю проекта.
agent-reviewer — это «проверятор», который сверяет текст со стайлгайдом и возвращает список замечаний.
Сценарий такой: сначала агенту нужно скормить стайлгайд, потом ему можно отправлять текст, а он возвращает замечания и рекомендации по исправлению.
Собственно, вот как это выглядит:
Причем стайлгайд не нужно передавать при каждом ревью, он загружается один раз и хранится в памяти.
Этот ревьюер хорош для первого прохода по формальным правилам. Но финальное решение все равно остается за автором (то бишь мной): иногда стайлгайд нужно применить строго, а иногда осознанно отступить от него ради смысла или читаемости.
Делегирование и замена. В чем разница?
Дабы упредить возможные упреки вроде «Из-за тебя скоро нас, техписателей, заменят на ИИ-шницу», подчеркну: это не замена, а делегирование. И вот в чем разница:
Замена — это когда ИИ делает все работы по документации, от сбора знаний до финального результата, а специалист превращается в «технического читателя»: видит, что выдала машина, и все. В итоге смысл утерян, ответственность размыта и ничего хорошего из этого не будет.
Делегирование — это когда механическую работу делает ИИ, а смысловую — специалист. Агент разбирает артефакты, готовит черновики и делает за несколько минут то, что вручную заняло бы полчаса-час. Специалист получает черновик, проверяет галлюцинации, принимает финальное решение и несет ответственность за результат целиком.
Ключевое слово здесь — «ответственность», которую целиком и полностью несу я. Именно поэтому все, что выдает агент — это всегда черновик, а не финальный документ.
Бэклог: что планирую доделать
Сейчас проект находится в состоянии «работает — не трожь» «работает, но есть куда расти». И вот в какие стороны планируется расти:
- реализовать хранение контекста предыдущих задач, чтобы заново не объяснять одно и то же (дополнительно подключал внешний OpenClaw-скилл self-improving-agent, но пока что не увидел от него существенной пользы);
- довести до ума сценарий, когда бот долго молчит во время работы и пишет только финальный результат;
- прикрутить версионирование промптов, чтобы откатываться к рабочим версиям промптов, если вдруг что-то ломается;
- встроить автоматическую проверку результата другой LLM перед тем, как черновик попадет ко мне;
- унифицировать промпты по агентам и оркестратору, т.к. сейчас часть из них на русском, часть — на английском;
- возможно, вынесу все на сервер для доступа 24/7, но вопрос пока открытый, т.к. с одной стороны, хочется автономности, а с другой, вроде бы и нет смысла платить за сервер, если агентом пользуюсь в основном в рабочее время.
Надеюсь, статья вам понравилась. Если у вас есть опыт с похожими решениями или идеи по любому из этих пунктов — пишите в комментарии или личные сообщения.