almdev Технический блог

Задача на четырёх субагентов, из которых нужны были три
Параллельность экономит контекст и тратит время на согласование.

Восьмой заход лаборатории: декомпозиция задачи между субагентами, контракт на стыке, изоляция контекста как главная выгода и три признака, по которым параллельность проигрывает одному проходу.

Подкатегория: Лаборатория

Чтение
5 мин
Технологии / версии
Symfony · Coding agent · PHP · Лаборатория
Серия
Лаборатория · часть 8 из 9
18 авг 2026 · 5 мин · AI
Координатор распределяет задачу четырём изолированным агентам
Лаборатория 08/2026

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

Восьмой заход серии. Проект прежний: Symfony 8, модули Core, Tasks, Docs, 96 тысяч строк.

Задача

Экспорт данных организации. Полная выгрузка задач, страниц базы знаний, вложений и журнала активности в архив, который забирает администратор. Запускается в фоне, письмом приходит ссылка.

Задача заметно больше всего, что было в серии до этого: миграция, три выгрузчика по модулям, упаковка архива, обработчик сообщения, хранилище, интерфейс, уведомление. Оценка глазами — около 900 строк.

Почему не одним проходом

Первый заход был одним агентом на всю задачу. Он справился, и результат меня не устроил по одной конкретной причине.

К моменту, когда он писал упаковку архива, в его контексте лежали три выгрузчика, схема базы, шаблоны писем и половина модуля Core. Качество последних кусков заметно упало: в обработчике сообщения появились те же ошибки, которые он аккуратно обошёл в первом выгрузчике — не проверил организацию, не почистил временный файл.

Это не про размер окна. Это про то, что чем больше в контексте деталей, тем хуже удерживаются правила, заданные в начале.

Разделение по границам, а не по объёму

Первая попытка делить была неправильной: разрезал по количеству работы, примерно поровну на четверых. Получил четыре куска, каждый из которых лез в чужую зону.

Правильная граница — та, где стык описывается одним интерфейсом.

Агент 1 — Схема и контракт
  миграция таблицы export_request, сущность, репозиторий,
  интерфейс ModuleExporter

Агент 2 — Выгрузчики модулей
  реализации ModuleExporter для Tasks, Docs, Core
  зависит только от интерфейса

Агент 3 — Обработчик и упаковка
  обработчик сообщения, сборка архива, запись в хранилище
  зависит только от интерфейса

Агент 4 — Интерфейс и уведомление
  кнопка, страница статуса, письмо со ссылкой
  зависит только от сущности

Четыре зоны, три из которых знают ровно один общий контракт:

interface ModuleExporter
{
    public function moduleKey(): string;

    /** @return iterable<ExportEntry> */
    public function export(Organization $organization): iterable;
}

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

Контракт на стыке

Интерфейса недостаточно, к нему нужны решения, о которых иначе каждый договорится сам с собой.

ОБЩИЕ РЕШЕНИЯ ДЛЯ ВСЕХ ЗОН

- Выгрузчик возвращает генератор. Не массив: организация может дать
  200 тысяч задач.
- Путь в архиве: {moduleKey}/{entity}.jsonl, по одному объекту на строку.
- Даты в ISO 8601 с зоной UTC.
- Идентификаторы — строки UUID по RFC 9562, не объекты.
- JSONL возвращается как поток строк внутри ExportEntry.
- Вложения не читаются в память: ExportEntry содержит путь в хранилище,
  упаковкой занимается зона 3.
- Ошибка одного модуля не отменяет весь экспорт: пишем в отчёт и продолжаем.

Семь пунктов, каждый — из вопроса, который иначе решается по-разному в разных зонах. Формат даты кажется мелочью ровно до момента, когда три выгрузчика написали три формата.

Отдельно про генератор. Без явного требования два агента из трёх вернули бы массив, и экспорт большой организации падал бы по памяти — причём только на большой организации, то есть в проде.

Изоляция контекста — вот ради чего это

Ускорение оказалось не главным. Главное — то, что каждый агент видит маленький кусок мира.

Агенту с выгрузчиками не передаются упаковка архива, письма и интерфейс. В его контексте — три сущности, контракт и правила формата. Но малый контекст сам по себе не ограничивает доступ к репозиторию: чтобы он действительно не правил обработчик, я задаю разрешённые каталоги или выдаю отдельный worktree с чёткой областью изменений.

Практическое следствие: качество последнего написанного файла не отличается от качества первого. В едином проходе отличалось заметно.

Второе следствие: дифф каждой зоны читается отдельно и целиком. 900 строк одним куском я читаю час и невнимательно, четыре диффа по 200 строк — сорок минут и внимательно.

Что пошло не так

Три вещи, честно.

Дублирование. Зоны 2 и 3 обе написали помощник для безопасного имени файла в архиве. Почти одинаковый, в разных местах. Уборка после слияния — десять минут, но найти это можно только глазами.

Разное понимание одного слова. «Отчёт об ошибках» в зоне 2 стал массивом строк, в зоне 3 — объектом с уровнями. На стыке не сошлось, пришлось переделывать зону 3.

Это ровно тот класс проблем, ради которого нужен контракт, и он показывает, что мой контракт был неполным: слово «отчёт» в нём было, а его форма — нет.

Порядок. Зона 4 делала интерфейс по сущности, которую в это время дописывала зона 1. Два поля переименовались, интерфейс сломался. Пришлось ждать.

Отсюда правило: зона, от которой зависят другие, идёт до них, а не параллельно с ними. Настоящая параллельность возможна только между зонами-листьями.

Когда параллельность проигрывает

Три признака, при которых делить не надо.

Задача небольшая и с одним владельцем. В моём замере граница оказалась около 300 строк: накладные расходы на контракт и сборку съели выигрыш. Это не универсальный порог. Задача на подписку из третьего захода в разделённом виде заняла больше времени, чем одним проходом.

Границы неизвестны. Если непонятно, где стык, декомпозиция становится угадыванием, а угаданный неправильно стык дороже любого выигрыша.

Задача исследовательская. Когда нужно понять, как лучше сделать, разделение мешает: каждая зона исследует своё и никто не видит картину.

Четвёртый агент в этой задаче, кстати, был лишним. Интерфейс и письмо — это 140 строк, которые спокойно делаются в одной зоне с обработчиком. Значит, из четырёх исходных исполнителей здесь оправданы три: один готовит схему и контракт, после него два независимых исполнителя делают выгрузчики и остальную сборку. Я разделил последнюю часть из симметрии, и симметрия стоила лишнего контракта.

Замер

СпособВремяДиффОшибок на ревьюМоё время
Один проход1 ч 10 мин880 строк91 ч
Четыре зоны55 мин910 строк440 мин

Ускорение по работе агентов — минут пятнадцать, в пределах шума. Реальная разница в числе ошибок и в моём времени на ревью.

Из девяти ошибок единого прохода четыре были в последней трети работы — там, где контекст переполнен. В разделённом варианте такой закономерности нет вовсе.

Что дальше

Во всех заходах серии я был в цикле: ставил задачу, читал план, принимал результат. В следующем выхожу из цикла целиком — агент должен сам довести работу до готового PR.

Серия

Лаборатория

#symfony #coding-agent #php #lab #subagents