Klepa AI
Установить
Часть 5 · Большие проекты · Урок 10

Много сервисов, один конвейер: контракты, порядок выкладки и фабрика агентов

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

~55 минут 4 интерактива 2 песочницы с проверками Примеры: JS · TS · PHP · Python

После урока вы сможете

  • найти все сервисы, которые затронет изменение контракта;
  • менять API без простоя по схеме expand/contract;
  • объяснить паттерн «оркестратор и исполнители» и зачем каждому исполнителю своя копия кода;
  • понять, что такое Dark Factory и где в ней обязательно нужен человек.
1 Аналогия

Прораб и бригады на стройке

Представьте

Строят дом. Прораб не кладёт кирпичи сам. Он делит работу между бригадами: электрики, сантехники, отделочники. Каждая работает на своём участке по общему чертежу. В конце — приёмка.

Прораб: «Добавить промокоды»
ordersрасчёт скидки
billingсумма к оплате
frontendполе ввода
notificationsтекст письма
Приёмка: ожидание
Прораб= агент-оркестратор: делит задачу и собирает результат
Бригада на своём участке= агент-исполнитель в своём репозитории
Общий чертёж= контракты: OpenAPI, схемы событий
Приёмка= контрактные и сквозные тесты
Цех, где ночью работают одни роботы= Dark Factory: конвейер без человека в цикле
2 Объяснение

От простого к сложному

Уровень 1 · Суть

Одна фича — несколько репозиториев

В микросервисах фича «промокоды» задевает заказы, оплату, сайт и письма. Это четыре репозитория, четыре набора правил и тестов. Один агент в одном контексте всё это не удержит: окно переполнится, а правки в одном сервисе сломают другой.

Поэтому работу делят. Один агент-оркестратор планирует и проверяет. Несколько агентов-исполнителей работают каждый в своём репозитории. Связывает их контракт — договорённость, какие данные один сервис отдаёт другому.

ПримерКонтракт: «orders отдаёт поле discount_minor — скидку в копейках». Исполнитель в orders его добавляет. Исполнитель в billing его читает. Им не нужно знать код друг друга, только контракт.
Уровень 2 · Детали

Карта сервисов, expand/contract и изоляция

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

Expand/contract (расширить, потом сузить). Сервисы выкатываются в разное время. Если переименовать поле за один шаг, потребители упадут. Поэтому так:

  1. Expand: отдаём и старое поле, и новое.
  2. Migrate: потребители по одному переходят на новое.
  3. 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
}
Уровень 3 · Усложнение

Dark Factory: конвейер без человека в цикле

Dark Factory (тёмная фабрика) — максимально автоматический конвейер. Задача, спека, реализация, проверки и готовый к поставке PR — без человека на каждом шаге.

Людям остаётся три вещи:

  • Постановка. Что делать и зачем. Хорошая задача и спека — половина результата.
  • Ворота высокого риска. Деньги, персональные данные, безопасность, необратимые миграции. Здесь решение принимает человек.
  • Разбор отказов. Каждая ошибка, которая прошла конвейер, превращается в новый тест, правило или задачу в eval-набор.

Чем меньше людей в цикле, тем сильнее должны быть автоматические проверки. Dark Factory опирается на всё, что было в курсе: правила и контекст (уроки 2, 3), спецификации (урок 5), генераторы (урок 6), контроль ошибок (урок 7) и eval-наборы (урок 8). Без них это не фабрика, а генератор незаметных поломок.

Граница автоматизацииДаже в самой автоматической фабрике агент не выкатывает на прод изменения денег и данных сам. И у него нет доступа к боевым секретам. Автоматизация готовит результат, ответственность остаётся у человека.
Как понять, что фабрика работает — метрики
  • Пропускная способность: сколько задач в сутки проходит конвейер до готового PR.
  • Сбежавшие дефекты: сколько ошибок нашли уже после выкладки. Главная метрика качества.
  • Доля переделок: сколько задач ворота вернули на доработку. Высокая доля — плохие спеки или слабые исполнители.
  • Часы людей: сколько времени уходит на ворота и разбор отказов.
  • Стоимость задачи: токены и время машин.

Всё это можно покрутить в конструкторе фабрики ниже.

3 Интерактив

Сервисы, исполнители и фабрика

Кого затронет изменение контракта

интерактив 1 из 4

Стрелка идёт от потребителя к поставщику: «frontend → api-gateway» значит «frontend вызывает api-gateway». Выберите изменение.

меняемпрямой потребительчерез цепочку

Здесь появится план изменения.

Оркестратор и исполнители: «добавить промокоды»

интерактив 2 из 4
Оркестратор
ordersждёт
billingждёт
frontendждёт
notificationsждёт
Нажмите «Дальше» или «Авто».

Конструктор Dark Factory

интерактив 3 из 4

Включите ворота проверки и запустите 200 задач. Модель упрощённая: у каждых ворот своя доля пойманных ошибок и своя цена во времени. Цифры иллюстративные.

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

Порядок релизов при переименовании поля

интерактив 4 из 4

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

    4 Практика

    Песочницы с автопроверкой

    Код выполняется в браузере. Ctrl+Enter — запуск. Ваш код сохраняется.

    Песочница 1. Кого затронет изменение

    JS

    Граф задан как «потребитель → список поставщиков». Найдите всех, кто зависит от изменённого сервиса напрямую или через цепочку. В графе бывают циклы — код не должен зациклиться.

    Песочница 2. Порядок выкладки сервисов

    JS

    Поставщик выкатывается раньше потребителя. Верните порядок выкладки. Если из готовых к выкладке можно взять несколько — берите первый по алфавиту. Нашли цикл — бросьте ошибку со словом «цикл».

    Проверьте себя

    Нужно переименовать поле в API, которым пользуются три сервиса. Какой порядок правильный?

    Сервисы выкатываются в разное время. Expand/contract гарантирует, что в каждый момент любая версия потребителя найдёт нужное поле.

    Зачем каждому агенту-исполнителю своя рабочая копия (worktree или песочница)?

    Два агента в одной папке правят одни и те же файлы и ломают сборку друг другу. Изоляция делает параллельную работу безопасной.

    Что ловят контрактные тесты?

    Каждый сервис по отдельности может быть зелёным, а вместе — не работать. Контрактные тесты проверяют именно стык.

    Что остаётся людям в Dark Factory?

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

    Задание на своём проекте — 1 час
    1. Нарисуйте карту своих сервисов или модулей в формате «потребитель → поставщики». Запишите её как объект из песочницы 1.
    2. Возьмите последнюю фичу, которая задела несколько сервисов. Посчитайте затронутые сервисы функцией из песочницы. Совпало с тем, что было на самом деле?
    3. Распишите для неё план expand/contract и порядок релизов.
    4. Отметьте шаги, где вы поставили бы человека, если бы весь путь делал конвейер агентов.
    5 Чек-лист

    Отметьте, что усвоили

    • Понимаю, зачем делить фичу на подзадачи по репозиториям.
    • Могу объяснить роли оркестратора и исполнителей, fan-out и fan-in.
    • Знаю, зачем исполнителям отдельные worktree или песочницы.
    • Могу расписать expand/contract для переименования поля.
    • Понимаю, что проверяют контрактные тесты.
    • Могу объяснить Dark Factory и что в ней остаётся людям.
    • Решил песочницы с графом зависимостей и порядком выкладки.
    Курс пройденВы прошли путь от устройства модели до конвейера агентов. Вернитесь на карту курса: там итоговый проект, где всё собирается вместе.