Много сервисов, один конвейер: контракты, порядок выкладки и фабрика агентов
Одна фича часто задевает несколько сервисов и репозиториев. Разберём, как разделить работу между агентами, не сломать соседей и как выглядит почти полностью автоматический конвейер разработки.
После урока вы сможете
- найти все сервисы, которые затронет изменение контракта;
- менять API без простоя по схеме expand/contract;
- объяснить паттерн «оркестратор и исполнители» и зачем каждому исполнителю своя копия кода;
- понять, что такое Dark Factory и где в ней обязательно нужен человек.
Прораб и бригады на стройке
Строят дом. Прораб не кладёт кирпичи сам. Он делит работу между бригадами: электрики, сантехники, отделочники. Каждая работает на своём участке по общему чертежу. В конце — приёмка.
От простого к сложному
Одна фича — несколько репозиториев
В микросервисах фича «промокоды» задевает заказы, оплату, сайт и письма. Это четыре репозитория, четыре набора правил и тестов. Один агент в одном контексте всё это не удержит: окно переполнится, а правки в одном сервисе сломают другой.
Поэтому работу делят. Один агент-оркестратор планирует и проверяет. Несколько агентов-исполнителей работают каждый в своём репозитории. Связывает их контракт — договорённость, какие данные один сервис отдаёт другому.
discount_minor — скидку в копейках». Исполнитель в orders его добавляет. Исполнитель в billing его читает. Им не нужно знать код друг друга, только контракт.Карта сервисов, expand/contract и изоляция
Общий контекст. Оркестратору нужна карта: какие сервисы есть, кто чей потребитель, где лежат контракты, кто владелец. Это обычный файл в отдельном репозитории или в корне рабочего пространства.
Expand/contract (расширить, потом сузить). Сервисы выкатываются в разное время. Если переименовать поле за один шаг, потребители упадут. Поэтому так:
- Expand: отдаём и старое поле, и новое.
- Migrate: потребители по одному переходят на новое.
- Contract: когда старое никто не читает — удаляем его.
Оркестратор и исполнители. Оркестратор делит задачу на подзадачи, раздаёт параллельно (это называют fan-out), ждёт всех и собирает результат (fan-in). Потом запускает проверки и, если что-то упало, возвращает подзадачу исполнителю с текстом ошибки.
Изоляция. Каждому исполнителю — своя копия кода: отдельный git worktree или песочница. Иначе два агента правят один файл и затирают работу друг друга.
Контрактные тесты. Проверяют, что ответ сервиса совпадает с тем, чего ждут потребители. Они ловят несовместимость до выкладки, а не на проде.
Оркестратор с параллельными исполнителями — TS, Python, PHP
runWorker — ваш агент-исполнитель: запуск CLI агента в нужной папке или вызов API с инструментами. Здесь важна схема: план, параллельный запуск, проверка, повтор.
interface Subtask { repo: string; goal: string; contract: string }
interface Result { repo: string; ok: boolean; log: string }
async function orchestrate(feature: string): Promise {
const plan: Subtask[] = await planSubtasks(feature); // оркестратор делит задачу
// fan-out: все исполнители работают параллельно, каждый в своём worktree
let results = await Promise.all(plan.map(t => runWorker(t)));
// проверка контрактов и повтор с текстом ошибки, не больше 2 раз
for (let attempt = 0; attempt < 2; attempt++) {
const failed = await contractTests(results);
if (failed.length === 0) break;
const retried = await Promise.all(
failed.map(f => runWorker({ ...plan.find(p => p.repo === f.repo)!, goal: `Исправь: ${f.log}` })),
);
results = results.map(r => retried.find(x => x.repo === r.repo) ?? r);
}
return results; // fan-in: связанные PR
}
import asyncio
async def orchestrate(feature: str) -> list[dict]:
plan = await plan_subtasks(feature) # [{"repo": "orders", "goal": ...}, ...]
# fan-out: исполнители работают одновременно
results = await asyncio.gather(*(run_worker(t) for t in plan))
for _ in range(2): # не больше двух повторов
failed = await contract_tests(results)
if not failed:
break
retried = await asyncio.gather(*(
run_worker({**t, "goal": f"Исправь: {f['log']}"})
for f in failed for t in plan if t["repo"] == f["repo"]
))
by_repo = {r["repo"]: r for r in retried}
results = [by_repo.get(r["repo"], r) for r in results]
return results # fan-in
<?php
// Идея на Symfony Messenger (псевдокод): каждая подзадача — сообщение в очередь,
// отдельный воркер на репозиторий. Оркестратор ждёт все ответы и запускает проверки.
final class Orchestrator
{
public function __construct(private MessageBusInterface $bus) {}
public function start(Feature $feature): void
{
foreach ($this->planner->split($feature) as $subtask) {
// fan-out: воркеры разбирают очередь параллельно
$this->bus->dispatch(new RunWorker($feature->id, $subtask->repo, $subtask->goal));
}
}
// вызывается, когда пришёл результат последнего исполнителя (fan-in)
public function onAllDone(string $featureId): void
{
$failed = $this->contracts->check($featureId);
foreach ($failed as $f) {
$this->bus->dispatch(new RunWorker($featureId, $f->repo, 'Исправь: ' . $f->log));
}
}
}
# Изоляция: у каждого исполнителя своя рабочая папка и ветка git -C orders worktree add ../wt/orders-promo -b feat/promo git -C billing worktree add ../wt/billing-promo -b feat/promo
Контракт события в JSON Schema — пример
Контракт лежит в общем репозитории. Его читают и исполнители, и контрактные тесты. Новое поле добавлено как необязательное — это шаг expand.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "OrderPaid",
"type": "object",
"required": ["order_id", "amount_minor", "currency", "paid_at"],
"properties": {
"order_id": { "type": "string" },
"amount_minor": { "type": "integer", "description": "Сумма в копейках" },
"currency": { "type": "string", "enum": ["BYN", "RUB", "USD"] },
"paid_at": { "type": "string", "format": "date-time" },
"promo_code": { "type": ["string", "null"], "description": "Новое поле, пока необязательное" }
},
"additionalProperties": false
}
Dark Factory: конвейер без человека в цикле
Dark Factory (тёмная фабрика) — максимально автоматический конвейер. Задача, спека, реализация, проверки и готовый к поставке PR — без человека на каждом шаге.
Людям остаётся три вещи:
- Постановка. Что делать и зачем. Хорошая задача и спека — половина результата.
- Ворота высокого риска. Деньги, персональные данные, безопасность, необратимые миграции. Здесь решение принимает человек.
- Разбор отказов. Каждая ошибка, которая прошла конвейер, превращается в новый тест, правило или задачу в eval-набор.
Чем меньше людей в цикле, тем сильнее должны быть автоматические проверки. Dark Factory опирается на всё, что было в курсе: правила и контекст (уроки 2, 3), спецификации (урок 5), генераторы (урок 6), контроль ошибок (урок 7) и eval-наборы (урок 8). Без них это не фабрика, а генератор незаметных поломок.
Как понять, что фабрика работает — метрики
- Пропускная способность: сколько задач в сутки проходит конвейер до готового PR.
- Сбежавшие дефекты: сколько ошибок нашли уже после выкладки. Главная метрика качества.
- Доля переделок: сколько задач ворота вернули на доработку. Высокая доля — плохие спеки или слабые исполнители.
- Часы людей: сколько времени уходит на ворота и разбор отказов.
- Стоимость задачи: токены и время машин.
Всё это можно покрутить в конструкторе фабрики ниже.
Сервисы, исполнители и фабрика
Кого затронет изменение контракта
интерактив 1 из 4Стрелка идёт от потребителя к поставщику: «frontend → api-gateway» значит «frontend вызывает api-gateway». Выберите изменение.
Здесь появится план изменения.
Оркестратор и исполнители: «добавить промокоды»
интерактив 2 из 4Конструктор Dark Factory
интерактив 3 из 4Включите ворота проверки и запустите 200 задач. Модель упрощённая: у каждых ворот своя доля пойманных ошибок и своя цена во времени. Цифры иллюстративные.
Порядок релизов при переименовании поля
интерактив 4 из 4Сервисы выкатываются по очереди. Расставьте релизы так, чтобы ни в какой момент никто не упал. Потом нажмите «Проверить».
Песочницы с автопроверкой
Код выполняется в браузере. Ctrl+Enter — запуск. Ваш код сохраняется.
Песочница 1. Кого затронет изменение
JSГраф задан как «потребитель → список поставщиков». Найдите всех, кто зависит от изменённого сервиса напрямую или через цепочку. В графе бывают циклы — код не должен зациклиться.
Песочница 2. Порядок выкладки сервисов
JSПоставщик выкатывается раньше потребителя. Верните порядок выкладки. Если из готовых к выкладке можно взять несколько — берите первый по алфавиту. Нашли цикл — бросьте ошибку со словом «цикл».
Проверьте себя
Нужно переименовать поле в API, которым пользуются три сервиса. Какой порядок правильный?
Сервисы выкатываются в разное время. Expand/contract гарантирует, что в каждый момент любая версия потребителя найдёт нужное поле.
Зачем каждому агенту-исполнителю своя рабочая копия (worktree или песочница)?
Два агента в одной папке правят одни и те же файлы и ломают сборку друг другу. Изоляция делает параллельную работу безопасной.
Что ловят контрактные тесты?
Каждый сервис по отдельности может быть зелёным, а вместе — не работать. Контрактные тесты проверяют именно стык.
Что остаётся людям в Dark Factory?
Конвейер делает рутину. Люди решают, что делать, отвечают за рискованные изменения и превращают каждую ошибку в новую проверку.
Задание на своём проекте — 1 час
- Нарисуйте карту своих сервисов или модулей в формате «потребитель → поставщики». Запишите её как объект из песочницы 1.
- Возьмите последнюю фичу, которая задела несколько сервисов. Посчитайте затронутые сервисы функцией из песочницы. Совпало с тем, что было на самом деле?
- Распишите для неё план expand/contract и порядок релизов.
- Отметьте шаги, где вы поставили бы человека, если бы весь путь делал конвейер агентов.
Отметьте, что усвоили
- Понимаю, зачем делить фичу на подзадачи по репозиториям.
- Могу объяснить роли оркестратора и исполнителей, fan-out и fan-in.
- Знаю, зачем исполнителям отдельные worktree или песочницы.
- Могу расписать expand/contract для переименования поля.
- Понимаю, что проверяют контрактные тесты.
- Могу объяснить Dark Factory и что в ней остаётся людям.
- Решил песочницы с графом зависимостей и порядком выкладки.
Что почитать и где попробовать
Материалы на русском отмечены зелёной меткой. Все ссылки проверены 18 октября 2026 года.
Почитать
- За всё ответишь! Consumer Driven Contracts глазами разработчикакакие проблемы решают контракты между сервисами, пример на Pact, ограничения. рус.
- Эволюционный дизайн баз данныхперевод Фаулера и Садаладжа: миграции и «фаза перехода», на которой держится expand/contract. рус., перевод
- Parallel Changeкороткая заметка про expand → migrate → contract. англ.
- How we built our multi-agent research systemAnthropic о схеме оркестратор-исполнители в реальной системе. англ.
- Что такое Dark Factory AI Agent?«тёмная фабрика»: конвейер от спецификации до кода почти без человека, где уместен. рус., перевод
Песочницы по теме
- DB Fiddleна PostgreSQL руками пройти expand/contract: добавить колонку, перенести данные, удалить старую.
- AsyncAPI Studioописать контракт событий между сервисами (очереди, топики).
- Mermaid Live Editorнарисовать диаграмму последовательности «оркестратор → исполнители → проверка».
Запустить код из урока
- TypeScript PlaygroundJS и TS из примеров урока: вставить и запустить кнопкой Run.
- 3v4l.orgPHP-примеры урока на любой версии PHP.
- JupyterLitePython-примеры урока прямо в браузере, без установки.