Files
clickstream-ch-kafka-supers…/docs/research/2026-06-11-subagent-coordinator-experiment.md
T
ddadminandClaude Opus 4.8 899a3f07a1 docs(research): зафиксировано внешнее ревью петли субагентов
- Зачем:
  - закрыть отложенный заход эксперимента: внешнее ревью результата
    автономной петли моделью другой родословной (дизайн-линия, не Кодекс).
- Что:
  - добавлена секция об итогах ревью: прогноз по находке A не оправдался,
    петля её поймала (try→fresh + регрессионный тест в коммите 640050e).
  - зафиксировано, что внешний взгляд добавил сквозные находки (расхождение
    живого/восстановленного путей, форма распределения длины визита).
  - уточнена гипотеза о пользе reviewer-а другой родословной.
- Проверка:
  - git show --stat HEAD; чтение docs/research/2026-06-11-subagent-coordinator-experiment.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 13:17:06 +03:00

432 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Эксперимент с координатором субагентов
Status: Research
Дата: 2026-06-11
## Зачем проводили эксперимент
Проверяли рабочую схему, где верхнеуровневый агент не реализует задачу сам, а
управляет цепочкой субагентов:
- выдаёт одному worker-субагенту один локальный issue;
- требует от worker-а работать через `/goal` и `/tdd`;
- получает отчёт;
- просит того же worker-а сделать саморевью;
- классифицирует находки;
- запускает исправления только по важным пунктам;
- при необходимости подключает отдельного reviewer-субагента;
- сам выполняет финальную проверку, разделяет коммиты и принимает решение о
переходе к следующей задаче.
Цель эксперимента была не только закрыть задачи генератора, но и понять, можно
ли из такого процесса сделать повторяемый агентный навык.
## Контекст
Эксперимент проводился в ветке `feature/data-generator` на последних трёх
срезах переработки steady-stream генератора:
1. `.scratch/feature-data-generator/issues/05-intensity-and-flow-calibration.md`
2. `.scratch/feature-data-generator/issues/06-state-v2-and-restart.md`
3. `.scratch/feature-data-generator/issues/07-service-integration-and-docs.md`
Задачи были зависимыми и меняли близкие места генератора, поэтому параллелить
их было нельзя. Каждая следующая задача должна была стартовать только после
коммита предыдущей.
Итоговые рабочие коммиты:
- `8f1e997 feat(generator): откалиброван поток steady-stream генератора`
- `640050e feat(generator): добавлено состояние версии 2 для рестартов`
- `6c4e0f4 feat(generator): подключена новая модель к steady-stream сервису`
Отдельные docs-коммиты фиксировали ход и выводы эксперимента.
## Начальная гипотеза
Ожидалось, что полезное разделение ролей такое:
- **Координатор** держит порядок, границы задач, проверки, коммиты и решение,
какие замечания действительно надо чинить.
- **Worker-субагент** глубоко погружается в один issue и реализует его через
короткие TDD-срезы.
- **Саморевью worker-а** полезно, потому что у него уже есть подробный контекст
реализации.
- **Отдельный reviewer-субагент** нужен не всегда, а на рискованных переходах
или после больших исправлений.
Верхнеуровневому координатору решили не включать `/goal`: это могло бы сместить
поведение в сторону «закрыть большую цель любой ценой», тогда как задача
координатора — управлять этапами и останавливаться на контрольных точках.
## Словарь ролей
- **Координатор** — верхнеуровневый агент в текущей сессии. Он не пишет основную
реализацию, а управляет порядком работ, проверяет границы, классифицирует
замечания, запускает дополнительные проверки и делает коммиты.
- **Worker** — субагент-исполнитель. Он получает один issue, работает в общем
рабочем дереве, меняет файлы, запускает тесты, но не коммитит.
- **Саморевью worker-а** — отдельная фаза после реализации. Worker временно
меняет роль с исполнителя на проверяющего свою работу, но сначала только
выдаёт находки, не исправляя их.
- **Reviewer** — отдельный субагент для независимой проверки. По умолчанию он
работает только на чтение: анализирует diff, issue, спеки и тесты, но не
редактирует файлы.
- **Находка** — гипотеза о проблеме, а не автоматическое требование правки.
Решение принимает координатор.
Важная техническая деталь: все агенты работают с одним рабочим деревом. Поэтому
координатор обязан регулярно проверять `git status --short` и явно отделять
рабочие изменения задачи от процессных заметок, handoff-файлов и чужих правок.
## Протокол, который сложился в эксперименте
Рабочий цикл на задачу:
1. Проверить чистоту дерева и, если нужно, отделить процессные правки отдельным
docs-коммитом.
2. Запустить нового worker-субагента на один issue.
3. В задании worker-у указать:
- `/goal`-образную цель;
- `/tdd` как методику;
- границы задачи;
- запрет на коммит;
- запрет на откат чужих изменений;
- требование финального отчёта: файлы, тесты, проверки, критерии, риски,
`git status --short`.
4. После реализации запросить у того же worker-а саморевью без правок.
5. Классифицировать находки.
6. Вернуть worker-у только одобренные правки.
7. Запустить локальные проверки координатора.
8. На сложных местах запустить отдельного reviewer-а.
9. Снова классифицировать находки reviewer-а.
10. При больших исправлениях выполнить второй reviewer-круг.
11. Коммит делает координатор.
12. Старый worker закрывается; следующая задача стартует в новом worker-е.
Это описание фиксирует фактический процесс эксперимента. Будущий скилл не обязан
механически повторять каждый шаг, но должен сохранить инварианты ниже.
## Классификация reviewer-находок
Reviewer не является источником истины. Его выводы работают как гипотезы,
которые координатор обязан проверить и классифицировать:
- `чинить до коммита` — реальный дефект, нарушение acceptance criteria или
слабое доказательство ключевого поведения;
- `записать как риск` — важно помнить, но не блокирует текущий коммит;
- `ложная тревога` — reviewer неверно понял код, тест или границы задачи;
- `вне скоупа` — может быть полезно позже, но не относится к текущему issue.
Это оказалось критически важным. Без фильтрации reviewer легко превращается в
источник расползания задачи.
## Наблюдения по задаче 05
Задача 05 была самой дорогой по обратной связи: калибровка статистической
модели требует длинных симуляций и не даёт быстрый «один инвариант — один
результат».
Worker долго работал без промежуточного отчёта. Координатор сначала поставил
мягкий статус-чек, затем прервал агента ради статуса. Это не сломало работу, но
показало: для задач со статистическими тестами стоит заранее закладывать
контрольные точки.
Саморевью worker-а нашло реальные проблемы:
- `docker-compose.yml` оставлял старую интенсивность `GEN_LAMBDA_BASE_PER_MIN=200`;
- тест межсессионной паузы был слишком широким;
- в тестовой конфигурации оставались старые числа;
- README мог быть двусмысленным про `GEN_MIN/MAX_EVENTS_PER_TICK`.
Координаторская проверка добавила отдельную ценность: был найден конфликт между
новым `GEN_LAMBDA_BASE_PER_MIN=30` и старым нижним пределом
`GEN_MIN_EVENTS_PER_TICK=5`. При тике 5 секунд это давало минимум 60 событий в
минуту и ломало критерий интенсивности.
Вывод: саморевью хорошо ловит локальные несостыковки реализации, но координатор
нужен для проверки связей между дефолтами, обычным запуском, документацией и
acceptance criteria.
## Наблюдения по задаче 06
Задача 06 показала, зачем нужен отдельный reviewer-субагент между задачами.
Саморевью worker-а и отдельный reviewer независимо нашли две существенные
проблемы:
- битое state v2 с валидным состоянием ГПСЧ могло пройти `from_dict_safe`, а
затем уронить сервис уже в `restore_state`;
- снимок активных визитов сохранялся полными batch-словарями и на верхних
лимитах получался порядка мегабайт, хотя спека говорила о компактном
состоянии.
Координатор проверил обе гипотезы локально:
- `restore_state` действительно падал на битой вложенной структуре;
- оценка JSON-снимка при 200 активных визитах и популяции 300 дала около 4.7 MB.
Обе находки были классифицированы как `чинить до коммита`.
После исправления компактного состояния понадобился второй reviewer-круг. Он
нашёл новый дефект уже в исправленной версии: формально похожий v2-state с
`population=[]` или строковым `pending_visit_births` проходил первичную
загрузку, но затем оставлял поток без пользователей или ронял следующий тик.
Координатор подтвердил это локальной проверкой и вернул worker-у как
обязательный пункт второго круга. Затем координатор дополнительно нашёл
парный случай: пользователь с `active_click_id`, но без соответствующего
`active_visit`.
Вывод: если исправление по reviewer-находке меняет дизайн, нужен повторный
reviewer-круг. Иначе можно закрыть старый дефект и внести новый рядом.
## Наблюдения по задаче 07
Задача 07 показала другую пользу reviewer-а: проверку силы доказательства, а не
только поиск падений.
Worker добавил сервисный тест с мок-публикацией. Он доказывал, что сервисный
тик публикует связанные сообщения во все четыре топика, но внешний reviewer
заметил: один опубликованный event ещё слабо доказывает уход от старой плоской
модели. Для acceptance criteria было важнее доказать невырожденную модель:
один визит должен дать несколько событий с одним `click_id`.
Координатор классифицировал это как `чинить до коммита`. Тест был усилен:
сервисный контур теперь делает несколько тиков и проверяет несколько событий
одного визита с общим `click_id`, разными `event_id` и согласованными
location/device/geo.
Reviewer также поймал документационные неточности:
- README слишком широко говорил, что переменные из таблицы проброшены через
compose, хотя `KAFKA_BOOTSTRAP_SERVERS` и `GEN_DATA_DIR` должны оставаться
безопасными внутренними значениями контейнера;
- `docs/OPERATIONS.md` отставал от новых параметров и метрик.
Вывод: финальная интеграционная задача требует reviewer-а не только по коду, но
и по доказательности тестов и честности документации.
## Что сработало
- Один worker на один issue хорошо удерживает контекст и границы.
- `/goal`-образное задание worker-у работает как хороший контракт даже тогда,
когда координатор сам не находится в goal-режиме.
- Саморевью того же worker-а полезно, если явно запретить правки на первом
шаге и попросить сначала выдать находки.
- Отдельный reviewer полезен на границах задач и после больших исправлений.
- Координаторская классификация находок обязательна.
- Коммиты должен делать координатор: это последняя точка контроля состава diff.
- Процессные наблюдения лучше фиксировать отдельно от рабочих коммитов.
## Что не стоит автоматизировать слепо
- Не стоит всегда запускать reviewer-а после каждой мелкой правки. Это дорого и
может раздувать задачу.
- Не стоит принимать все reviewer-находки как правду.
- Не стоит давать координатору верхнеуровневый `/goal` на всю цепочку, если от
него требуется управленческая осторожность, а не слепое достижение цели.
- Не стоит смешивать handoff-наблюдения с рабочими коммитами задач.
- Не стоит требовать реальный Kafka-стек для каждого шага, если acceptance
criteria допускают мок-публикацию. Но надо явно фиксировать, что именно не
проверялось.
## Ограничения эксперимента
Выводы нельзя считать универсально доказанными. Эксперимент прошёл в одном
репозитории, на одной ветке и на трёх зависимых задачах одного домена. Это
хороший сигнал для класса задач «последовательные инженерные срезы с тестами,
документацией и явными acceptance criteria», но не доказательство, что процесс
так же хорошо подойдёт для:
- независимых задач, которые можно безопасно параллелить;
- маленьких однофайловых правок;
- задач без тестовой базы;
- продуктовых или исследовательских задач, где итог заранее неясен;
- задач, где субагентам нельзя писать в общее рабочее дерево.
Стоимость процесса тоже заметна. Статистические и state-задачи требуют долгих
прогонов, ожидания worker-а, reviewer-кругов и ручной классификации находок.
Поэтому будущий скилл должен уметь выбирать облегчённый режим, а не всегда
запускать полную схему.
## Инварианты будущего скилла
Если на базе эксперимента делать скилл, в него стоит перенести не конкретные
команды из этого репозитория, а следующие обязательные правила:
- **Один worker — один bounded issue.** Цепочка задач ведётся координатором, а
не одним долгоживущим исполнителем.
- **Reviewer по умолчанию read-only.** Он выдаёт находки, но не правит файлы.
- **Исправления идут только после классификации.** Даже хорошие reviewer-находки
сначала проходят фильтр координатора.
- **Коммит делает координатор.** Это защищает границы задачи и состав diff.
- **Большое исправление требует повторной проверки.** Если правка меняет дизайн
или формат данных, результат самой правки надо ревьюить заново.
- **Рабочее дерево — общий ресурс.** Каждый цикл должен начинаться и
заканчиваться явной проверкой статуса.
- **Документация и тесты являются частью доказательства.** Reviewer должен
проверять не только падения, но и силу тестов, честность README/операционных
документов и соответствие acceptance criteria.
## Эвристики для будущего скилла
Если делать скилл на базе эксперимента, в нём стоит закрепить такие правила:
1. **Начинать с чистого дерева.** Если есть процессные или чужие правки,
отделить их до запуска worker-а.
2. **Давать worker-у один issue.** Не просить закрывать цепочку задач в одном
агенте.
3. **Формулировать task prompt как контракт.** В нём должны быть цель, границы,
методика, запрет на коммит, запрет на откат чужого и формат отчёта.
4. **Требовать саморевью без правок.** Исправления только после решения
координатора.
5. **Классифицировать находки.** Минимальный набор: чинить до коммита, риск,
ложная тревога, вне скоупа.
6. **Запускать отдельного reviewer-а на рискованных точках.** Особенно:
state/serialization, сервисная интеграция, документация, изменения формата
данных, статистические модели.
7. **Повторять reviewer-круг после больших исправлений.** Если исправление
изменило дизайн, оно само нуждается в ревью.
8. **Координатор делает собственный sanity-check.** Не полный реимплемент, а
проверка связей: дефолты, compose, документация, acceptance criteria,
`git status`, тесты.
9. **Коммитить только координатору.** Worker не должен фиксировать изменения.
10. **Закрывать агентов.** Завершённые subagents надо закрывать, чтобы не
держать лишний контекст и лимиты.
## Возможный скелет скилла
1. Прочитать issue и зависимости.
2. Проверить рабочее дерево.
3. Запустить worker.
4. Дождаться отчёта или запросить статус, если работа слишком долго молчит.
5. Запустить self-review того же worker-а.
6. Классифицировать находки.
7. Вернуть worker-у только выбранные правки.
8. Запустить проверки координатора.
9. Если задача рискованная, запустить reviewer-а.
10. Классифицировать reviewer-находки.
11. При больших исправлениях повторить reviewer-круг.
12. Сделать коммит.
13. Обновить исследовательскую заметку или итоговый лог, если эксперимент ещё
идёт.
Этот скелет должен быть параметризуемым. Например, для простой задачи можно
ограничиться worker + саморевью + проверки координатора. Для state/serialization,
интеграции сервиса, документации или статистической модели нужен reviewer.
## Оставшиеся вопросы
- Как заранее понять, когда нужен отдельный reviewer, а когда достаточно
саморевью?
- Стоит ли задавать worker-у обязательные промежуточные отчёты для статистики и
долгих тестов?
- Нужно ли явно ограничивать бюджет worker-а или число reviewer-кругов?
- Как лучше формализовать критерий «большое исправление требует второго
reviewer-а»?
- Должен ли будущий скилл сам создавать research log, или это должен быть
отдельный режим?
## Предварительная рекомендация
Эксперимент стоит считать успешным. Процесс дал не только рабочий результат, но
и поймал дефекты, которые легко могли пройти обычный одиночный поток:
- конфликт дефолтов интенсивности;
- падение на битом state v2;
- слишком большой state v2;
- неполная валидация компактного state;
- слабый интеграционный тест, не доказывавший уход от старой плоской модели.
Для будущего скилла ядро должно быть не «запусти много агентов», а
**координационный цикл с классификацией находок и контролем границ**.
Главная формула процесса:
```text
worker реализует -> worker сам себя ревьюит -> координатор фильтрует ->
worker чинит выбранное -> reviewer проверяет риск -> координатор фильтрует ->
при большом исправлении повторить reviewer -> координатор коммитит
```
## Внешнее ревью другой моделью (отложенный заход, 2026-06-14)
Отложенное ревью результата петли (его наметил handoff
`2026-06-11-generator-time-adr-and-pending-review.md`) выполнено. Ревью провела
**модель другой родословной** — та, что вела дизайн, ADR и спеки, а не линия
Кодекс, писавшая код. Это и есть проверка центральной гипотезы эксперимента:
какой класс дефектов автономная петля систематически не видит без внешнего
взгляда.
Основа проверки: перечитаны ADR-0004/0005 и обе спеки; код прочитан помодульно;
марковская модель путей прогнана симуляцией на 300 тыс. визитов; прогнан весь
набор тестов (113 passed); происхождение обёртки `try→fresh` прослежено по git.
### Прогноз по находке A не оправдался — петля её поймала
Handoff предсказывал, что петля пропустит ссылочную целостность при
`restore_state` («слепые зоны одной линии Кодекс скоррелированы»). На деле:
- висячая ссылка визит→пользователь ловится валидатором формы (`state.py:88`);
- висячая ссылка пользователь→словарь (неизвестный `seed_click_id`) валидатору
формы недоступна, но петля **в коммите задачи 06** (`640050e`) добавила обёртку
`try→fresh` вокруг `restore_state` в сервисе **и** написала регрессионный тест
`test_invalid_restored_v2_state_starts_fresh` (профиль с `missing-click-id`).
Это совпадает с разделом «Наблюдения по задаче 06»: и саморевью worker-а, и
отдельный reviewer независимо нашли «битый v2 проходит `from_dict_safe`, но
роняет `restore_state`».
Вывод: многоролевой цикл (worker + саморевью + отдельный reviewer) **поймал**
ровно тот класс дефекта, который ставился как главный индикатор эксперимента.
Прогноз о скоррелированной слепоте по этому пункту ошибочен.
### Что внешний взгляд всё же добавил
Дефекты, дожившие до внешнего ревью, оказались **не точечными, а сквозными**
их не пинит ни один тест, и каждый по отдельности «проходит»:
- **Расхождение живого и восстановленного путей.** Живой визит берёт
браузер/локацию из *случайной* сид-сессии и `event_id` = `_new_uuid`
(`generation.py`), восстановленный — из сид-сессии *профиля* и `event_id` =
`uuid5` (`runtime.py`). У визита, пережившего рестарт, атрибуты «доезжающих»
событий меняются на середине. Инвариант «один визит — однородный контекст»
нарушается на стыке двух путей, который ни один тест не сводит вместе.
- **Форма распределения длины визита.** Среднее (10.7) и конверсия в
`/confirmation` (27%) на цели, воронка монотонна — это тесты проверяют и
подтверждают. Но *форма* дальше от сида: спайк 16% на длине 2 (форс минимума в
2 события убирает визиты-отказы из одного события, которые сид допускает),
~6.6% визитов срезаются о потолок 30, медиана 8 сидит на нижней границе
тестовой полосы 8–12. Тесты пинят средние и пороги, но не форму распределения.
### Уточнённая гипотеза
Главный индикатор A был *локальным* дефектом — падение в одной точке на битом
входе; того же семейства, что и прочие находки задачи 06 (раздутый снимок,
неполная валидация). Такие вещи дисциплинированный многоролевой цикл ловит
хорошо. А выжили до внешнего ревью свойства **межмодульные** (инвариант поверх
двух путей кода) и **распределительные** (форма, а не среднее) — ровно там, где
слабы и ревью внутри одной линии, и покритериальные тесты.
Отсюда уточнение к открытому вопросу «когда нужен отдельный reviewer»: ценнее
всего reviewer **другой родословной**, и именно там, где (а) свойство тянется
через несколько модулей/путей, либо (б) корректность распределительная (форма),
а не одно утверждение. Для точечных дефектов на входе хватает саморевью и
отдельного reviewer той же линии.
### Побочно подтвердилось
Разделение двух линий сработало: ADR-0005 (модельные часы ×K) остался решением
на бумаге, код петли честно держался реал-тайм-модели, нечаянного переплетения
линий нет. Процессное решение «не мешать брейншторм времени с петлёй 06/07»
выдержало.
## Связанные артефакты
- `.scratch/handoffs/2026-06-11-subagent-coordinator-experiment.md`
исходный одноразовый handoff и рабочий журнал наблюдений.
- `.scratch/feature-data-generator/issues/05-intensity-and-flow-calibration.md`
- `.scratch/feature-data-generator/issues/06-state-v2-and-restart.md`
- `.scratch/feature-data-generator/issues/07-service-integration-and-docs.md`
- `docs/adr/0003-handoffs-in-scratch.md`
- `docs/adr/0002-specs-as-durable-design-docs.md`