ADK 2 Orchestration: Графовые, совместные и динамические рабочие процессы

1. Обзор

Главная тема ADK 2 — три шаблона оркестровки . В этом практическом занятии мы обучаем всем трем, создавая одно приложение — « Тренер марафонского забега» — поэтапно, шаг за шагом, по одному рабочему уровню. Каждый уровень отвечает на один вопрос, добавляет одну идею и работает самостоятельно.

Что вы узнаете

  • Графическое представление рабочих процессов (Основа 1) — когда вы можете нарисовать схему потока до поступления входных данных .
  • Совместная работа агентов (второй столп) — когда вы знаете команду , но запрос выбирает подмножество — и все три режима совместной работы ( chat / task / single_turn ), каждый из которых работает в режиме реального времени.
  • Динамические рабочие процессы (Столп 3) — когда форма самой работы зависит от входных данных.
  • Как сделать выбор — дерево решений, основанное на одном вопросе, и как формируются эти закономерности.

Сквозная линия

Известная структура → известная команда / подмножество переменных → неизвестная форма → выбрать правильную

Ваш план обучения

Что вы построите

Одно приложение — « Тренер для марафонского забега» — создавалось по одному уровню за раз. Каждый уровень представляет собой простой модуль Python, который запускается из терминала; к уровню L5 все компоненты ниже уже готовы к использованию.

Схема построена на основе работающего кода : каждая сплошная линия была считана из Workflow.graph.edges . Это первый урок — части, которые можно нарисовать заранее, являются именно Столпом 1, а части, которые нельзя, объясняют существование Столпов 2 и 3.

Всё приложение целиком — и то, что график не может показать.

Что вам понадобится

  • Для работы с Colab требуется учетная запись Google — локальная настройка не требуется .
  • Примерно 50 минут (два уровня L4 самые длинные — заложите их в свой бюджет).
  • Достичь модели Gemini можно двумя способами. Выберите свой путь — выполните один этап настройки и пропустите другой:

🎓 Мастер-класс

🏠 На вынос

ВОЗ

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

Все остальные, включая участников семинара, после его завершения.

Вам нужно

Ссылка для подтверждения заявки и учетная запись Google, позволяющая создавать облачные проекты.

Бесплатный ключ API для AI Studio

Работает на

Vertex AI, в рамках проекта, оплачиваемого за счет вашего зачета за участие в семинаре.

Google AI Studio

Расходы

Покрывается кредитом

Бесплатный уровень

Шаг настройки

Настройка мастерской (следующий шаг)

Подготовка дома (шаг, следующий за первым)

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

Два способа следить за происходящим

Каждый из описанных ниже шагов соответствует одной ячейке в блокноте Colab и одной папке в репозитории GitHub . Выберите один из вариантов:

  • ▶ Colab (рекомендуется): Откройте блокнот → запускайте ячейки сверху вниз.
  • 💻 Локально: git clone репозиторий с помощью git, ./setup_venv.sh , затем запустите каждый уровень как модуль ( python -m ... ) или просмотрите их все с помощью ./run.sh ( adk web ).

2. Настройка мастерской · Получите свой кредит и переключитесь на Vertex AI

На семинаре вам будет предоставлен кредит Google Cloud . Вы его активируете, создадите проект, оплачиваемый с его помощью, и укажете в блокноте Vertex AI вместо AI Studio. После активации все действия выполняются в одной ячейке.

1. Получите свой кредит (~1 мин)

  1. Откройте ссылку на заявку, которую вам предоставил преподаватель. Она выглядит примерно так: https://me.developers.google.com/benefits/claim/your-workshop-name .
  2. Войдите в систему и пройдите все необходимые действия на странице, чтобы принять кредит.
  3. Запомните, какой аккаунт Google вы использовали. Все последующие шаги необходимо выполнять от имени этого же аккаунта.

2. Откройте ноутбук и установите ADK 2 (~1 мин).

Нажмите «Открыть в Colab» ▶ , затем запустите первую ячейку с кодом . Она зафиксирует точную версию ADK 2, на которой была проверена эта практическая работа, и выведет ✓ installed .

3. Запустите ячейку «Настройка мастерской» (~3 мин).

Это ячейка с названием 🎓 Путь A · Мастерская . Запустите её, и Colab запросит авторизацию — выберите ту же учетную запись Google, с помощью которой вы только что получили кредит , и разрешите доступ.

Программа выполняет четыре действия: создает проект под названием adk-2-tutorial-XXXX на вашем счету, включает в него API искусственного интеллекта Vertex , устанавливает четыре переменные среды, которые считывает каждая последующая ячейка, а затем выполняет тестовый вызов к Vertex и ждет ответа — таким образом, настройка либо завершается, либо сообщает вам причину, а не завершается с ошибкой позже внутри уровня.

Ожидаемый результат — важна последняя строка:

Signed in as: you@example.com
...
Successfully created GCP project 'adk-2-tutorial-4817'.
Successfully linked 'adk-2-tutorial-4817' to billing account '01ABCD-...'.
   waiting for Vertex AI to come up on the new project... (10s)
   waiting for Vertex AI to come up on the new project... (20s)

 Vertex AI on adk-2-tutorial-4817 · us-central1 · gemini-2.5-flash  answered a test call

4. Пропустите шаг «Настройка устройства дома».

Не запускайте ячейку с ключом AI Studio — это переключит блокнот обратно на AI Studio и отменит только что сделанное. (Ячейка предотвращает это и откажется запускаться, но проще всего просто пропустить её.) Отсюда сразу переходите к ячейке «Общие строительные блоки» .

5. Запустите ячейку "Общие строительные блоки".

Запустите один раз. Это определит схемы Pydantic + готовые сценарии для марафона, которые используются на каждом уровне, начиная со 2-го. Вы увидите ✓ schemas + scenarios ready .

После мастер-класса

Ваши кредиты и созданный на их основе проект не будут существовать вечно. Чтобы бесплатно проходить эти уровни после завершения мастер-класса, выполните вместо этого шаг настройки «Домашний доступ» — получите бесплатный ключ AI Studio, не нужно создавать облачный проект и платить за него. Меняется только эта одна ячейка.

Чтобы ускорить очистку: откройте консоль Cloud , выберите adk-2-tutorial-XXXX и удалите его. Ничто другое в этом практическом задании не создает оплачиваемых ресурсов.

3. Настройка для домашнего использования · Ключ API AI Studio

Все на этом пути работает с использованием бесплатного ключа API Google AI Studio — никаких проектов Google Cloud, никаких платежей, никакой локальной установки. Весь этот этап занимает около 3 минут.

1. Откройте блокнот.

Нажмите «Открыть в Colab» ▶ . Вы попадете в блокнот — вводная часть в формате Markdown, а затем по одной исполняемой ячейке на каждом уровне. Вы запускаете ячейки сверху вниз; каждая выводит свой собственный результат прямо под ней.

2. Установите ADK 2 (~1 мин)

Запустите первую ячейку кода. Она зафиксирует точную версию, на которой была проверена данная практическая работа:

%pip install -q "google-adk==2.3.0" python-dotenv pydantic nest_asyncio

Дождитесь завершения процесса — вы увидите ✓ installed . (Первая установка занимает примерно 30–60 секунд; после этого данные кэшируются.)

3. Получите свой API-ключ Gemini в AI Studio (~1 мин)

  1. Откройте aistudio.google.com/app/apikey в новой вкладке браузера.
  2. Войдите в систему, используя свою учетную запись Google.
  3. Нажмите «Создать ключ API» (в правом верхнем углу).
  4. Выберите существующий проект Google или позвольте системе создать новый.
  5. Скопируйте ключ — он начинается с AIza... и состоит примерно из 40 символов.

4 · Добавьте свой ключ в Colab (~1 мин)

Вариант А — Секреты Colab (рекомендуется; ключ останется скрытым):

  1. Нажмите на значок клавиши 🔑 в левой боковой панели Colab.
  2. Нажмите + Добавить новый секрет .
  3. Установите имя точно таким же, как GOOGLE_API_KEY .
  4. Вставьте ваш ключ в поле "Значение" .
  5. Включите доступ к блокноту .

Вариант B — вставьте по запросу (быстро): пропустите ввод секрета; при запуске следующей ячейки отобразится скрытый запрос 🔑 Enter your Google AI Studio API key: — вставьте и нажмите Enter.

5 · Запустите ячейку с ключом

Он считывает секретный ключ (или возвращается к подсказке для вставки), а затем направляет ADK на AI Studio (а не на Vertex AI):

import os

# 🏠 TAKE-HOME ONLY — if you ran the Workshop setup cell, skip this one.
if os.environ.get("GOOGLE_GENAI_USE_VERTEXAI") == "True":
    raise SystemExit("✋ You're set up on the workshop path (Vertex AI). Skip this cell.")

# Google AI Studio API key — add GOOGLE_API_KEY in the 🔑 Secrets panel (or paste when prompted).
try:
    from google.colab import userdata
    key = userdata.get("GOOGLE_API_KEY")
except Exception:
    import getpass
    key = getpass.getpass("Enter your Google AI Studio API key: ")

os.environ["GOOGLE_API_KEY"] = "".join(key.split())    # drop any stray whitespace/newlines
os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "False"      # use AI Studio, not Vertex AI
print("✅ API key set — using Google AI Studio.")

Ожидаемый результат: ✅ API key set — using Google AI Studio.

6. Запустите ячейку "Общие строительные блоки".

Запустите ячейку «Общие строительные блоки» один раз. Она определяет схемы Pydantic + готовые сценарии для марафона, которые используются на каждом уровне, начиная со 2-го. Вы увидите ✓ schemas + scenarios ready .

Всё готово! 🎽 Небольшое отступление перед L0 — версией, которую все собирают первой.

4. Пролог · Почему бы не использовать одну большую тему для обсуждения?

⚡ Прежде чем запускать программу, обратите внимание на ОДИН важный момент: откуда берется каждое конкретное число? В этом и заключается вся суть — все остальное лишь украшение.

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

Что вы увидите: уверенную, конкретную, хорошо структурированную стратегию... цифры в которой выдуманы . В одном из реальных запусков она начиналась с сообщения "Я получил сегодняшние погодные данные" и показала 52°F, ветер 9 миль в час и анализ тренировочного журнала, который она никогда не видела. Здесь нет API погоды, нет данных о курсе, нет журнала — один непрозрачный вызов модели либо фальсифицирует свои входные данные, либо делает их бесполезными.

Это болезнь, и у неё есть четыре симптома, которые стоит упомянуть:

  1. Этому нельзя доверять — данные выдуманы, причем совершенно необоснованны.
  2. Проверить это невозможно — маршрутизация на шаге 4 осуществляется в текстовом формате; здесь нет if для модульного тестирования.
  3. Нельзя поменять местами шаги — нет места, куда можно было бы подключить настоящий API для прогноза погоды.
  4. Вы платите за всё, каждый раз — пять шагов, один большой звонок, никакого кэширования, детерминированная часть.

Сохраните это чувство. Следующие девять уровней последовательно исключают эти шаги из подсказки : выборка функций (L1–L2a), маршрутизация с помощью оператора if (L2b), распределение работы между специалистами (L3a–L3b) и ограничение формы кодом (L4a–L4b).

Супер-тренер по кратким заданиям — уверенный в себе, но ничего не стоящий за графиком.

💻 Локально: python -m shared.prologue

5. L0 · Ваш первый агент ADK 2

Карта маршрута — вы здесь: L0

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

Вопрос: можно ли получить ответ от модели — и при этом использовать реальный код, когда важны арифметические вычисления?

Одна идея — три части:

  • Agent — это то, что рассуждает (модель Близнецов + инструкция).
  • Runner — это инструмент, который запускает агента внутри сессии и передает события потоком.
  • Инструмент — простая функция Python ( pace_splits ), которую модель решает вызвать. ADK считывает сигнатуру и документацию и передает модели объявление; схема не создается.

После пролога следует первая поправка: программа LLM, выполняющая вычисления в уме, с удовольствием допустит ошибку — pace_splits — это детерминированный Python, поэтому числа в ответе вычисляются, а не импровизируются .

Colab: запуск ячейки L0 · 📁 GitHub: L0_first_agent/ · 💻 Локально: python -m L0_first_agent.agent

Поток L0

def pace_splits(target_finish: str) -> dict:
    """Convert a goal time like '3:30:00' into exact per-mile / per-km paces."""
    ...                                  # deterministic Python — no LLM

pace_coach = Agent(
    name="pace_coach", model=MODEL,
    tools=[pace_splits],                 # the model may call it; ADK reads the signature
    instruction="You are a friendly, concise marathon coach. ... If the runner "
                "mentions a goal time, call pace_splits — never do arithmetic yourself.",
)
runner = Runner(node=pace_coach, session_service=InMemorySessionService(), auto_create_session=True)
async for event in runner.run_async(user_id="u1", session_id="s1", new_message=msg):
    ...  # events carry the model's text

🔍 Маркеры: Agent(...) · tools=[pace_splits] · Runner(...) . А в выводе строка 🔧 — это то, как модель, в процессе ответа, решает вызвать ваш код.

Что вы увидите:

   🔧 model called tool  pace_splits({'target_finish': '3:30:00'})
   🔧 tool returned      {'per_mile': '8:00', 'per_km': '4:58', ...}
🧠 Coach: To finish in 3:30:00, you need an average pace of 8:00 per mile...

Строки 🔧 — это урок: в середине ответа модель решила вызвать вашу функцию, и точное значение 8:00/mile в её ответе было получено из вашего кода, а не из статистики токенов.

Возможно, вы задаетесь вопросом: всегда ли модель вызывает инструмент? Нет — она решает это в зависимости от вопроса. Задайте вопрос, в котором нет чисел, и линии 🔧 исчезнут (в интерактивной среде вам предлагается попробовать именно это).

👀 Прочитайте: pace_splits (простая функция) и строку tools=[pace_splits] . · ▶ Запустите . · ✏️ Измените: задайте общий вопрос (без целевого времени) — обратите внимание, что строки 🔧 исчезают: модель решает, когда стоит вызывать инструмент. Затем перепишите instruction и запустите снова — инструкция представляет собой остальную часть программы.

6. L1 · Ваш первый рабочий процесс

План действий — вы здесь: L1

⚡ Краткое содержание: простая функция и агент LLM — это один и тот же тип узла . Предсказуемая работа → функция (0 LLM, детерминированный); рассуждения → агент.

Вопрос: как объединить обычный код и модель LLM в одном рабочем процессе, не платя за вызов модели для тех частей, которые представляют собой просто код?

Основная идея: в Workflow простая функция Python и агент LLM представляют собой просто узлы в одном и том же списке edges .

START ──► fetch_conditions (function, 0 LLM) ──► advise (agent, 1 LLM)

Colab: запуск ячейки L1 · 📁 GitHub: L1_graph_basics/ · 💻 Локально: python -m L1_graph_basics.workflow

Поток L1

Функциональный узел выводит полученные данные (без вызова модели), затем агент дает рекомендации, основанные на фактических данных о температуре и ветре:

def fetch_conditions(node_input):                # function node — 0 LLM
    return Event(output=Conditions(temp_f=78, wind_mph=12, conditions="sunny").model_dump())

advise = Agent(name="advise", model=MODEL, mode="single_turn",
               input_schema=Conditions, instruction="...give pacing + gear advice...")

workflow = Workflow(edges=[(START, fetch_conditions, advise)])

🔍 Маркеры: кортеж ребер — (START, fetch_conditions, advise) — с простой функцией Python посередине и input_schema= , проверяющей передачу данных.

Что нового по сравнению с L0: Workflow(edges=[...]) , START (где поступает ввод), узел функции, возвращающий Event(output=...) , и input_schema=Conditions так что выходные данные функции проверяются на соответствие этой схеме, прежде чем агент их увидит (в виде текста JSON — input_schema проверяет границу, а не передает агенту объект Python).

Возможно, вы задаетесь вопросом: является ли порядок "функция, затем агент" обязательным? Нет — любой порядок, любое сочетание, любое количество. advise выполняется второй только потому, что ей нужны данные из fetch_conditions . Урок заключается в сравнении, а не в последовательности.

👀 Прочитайте: fetch_conditions возвращает данные без вызова модели ; advise имеет input_schema=Conditions . · ▶ Запустите . · ✏️ Измените: установите temp_f=30 в функции и запустите снова — advice изменится, и функция по-прежнему будет иметь 0 вызовов LLM.

7. L2a · Параллельное разветвление + JoinNode (компонент 1a)

Дорожная карта — вы здесь: L2a

⚡ Краткое содержание: распределить задачи параллельно (бесплатно), дождаться завершения всех , объединить в пакет, передать одному агенту полную картину.

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

Форма:

START ──► fetch_weather ──┐
START ──► analyze_course ─┼─► JoinNode ─► strategy (1 agent)
START ──► pull_fitness ───┘   (bundles)

Colab: запуск ячейки L2a · 📁 GitHub: L2a_parallel_join/ · 💻 Локально: python -m L2a_parallel_join.workflow

Поток L2a

🔍 Маркеры: три ребра, каждое из которых начинается с STARTэто точка разветвления — и JoinNode , точка встречи.

  • Три операции выборки являются функциями — они выполняются параллельно , 0 вызовов LLM.
  • JoinNode ожидает получения всех трех данных и объединяет их в один типизированный полезный груз ( BundledRunData ), ключом которого является имя функции.
  • Один из агентов strategy читает пакет документов и создает RaceStrategy .

Что вы увидите: для каждого запроса выводится метка времени started / finished . Все три начинаются с 0,0 с , а разветвление заканчивается на 2,0 с — это самый медленный запрос, а не 4,5 с, которые в сумме дали бы их длительность. Это перекрытие — параллелизм. ( Общее время, выведенное в конце, составляет примерно 8 с, поскольку оно также включает вызов LLM агента стратегии — читайте метки времени запросов для параллельного выполнения, а не для общего времени.)

💡 Пролог: мега-подсказка придумала свою погоду. Здесь температура получается из функции выборки — реальный код, реальный шов. Замените стандартный словарь на настоящий API погоды, и ничего больше не изменится.

Возможно, вам интересно: сколько это стоит?

JoinNode

Нужно ли мне понимать? Одно предложение: он ждет, пока не будет завершена обработка каждой параллельной ветви, упаковывает выходные данные в один словарь, ключом которого является имя вышестоящей функции , и сам ничего не вычисляет. Именно этот словарь позволяет маршрутизатору L2b записывать node_input["fetch_weather"]["temp_f"] .

👀 Чтение: три ребра расходятся от START ; JoinNode объединяет их для одного агента. · ▶ Запустите и прочтите метки времени , а не общее количество. · ✏️ Изменение: сделайте задержку для одной выборки 3.0 — сначала предскажите новое время окончания расхода, а затем проверьте.

8. L2b · Добавить детерминированный маршрутизатор (Pillar 1b)

Дорожная карта — вы здесь: L2b

⚡ Краткое содержание: L2a остаётся без изменений + простой if определяет, какой из агентов будет бежать. Ветвление без запроса к модели.

Вопрос: план должен отличаться в зависимости от погоды (жара или холод). Как реализовать ветвление — не прибегая к помощи модели в принятии решения?

Форма (L2a + маршрутизатор):

... JoinNode ─► route_by_weather ─► hot_strategy
               (if-statement)   ─► normal_strategy
                                ─► cold_strategy

Colab: запуск ячейки L2b — попробуйте run("NORMAL") / run("COLD") · 📁 GitHub: L2b_router/ · 💻 Локально: python -m L2b_router.workflow COLD

Поток L2b

def route_by_weather(node_input):                        # an if-statement, 0 LLM
    temp = node_input["fetch_weather"]["temp_f"]
    route = "HOT" if temp >= 70 else "COLD" if temp <= 40 else "NORMAL"
    return Event(output=node_input, route=route)

(route_by_weather, {"HOT": hot_strategy, "NORMAL": normal_strategy, "COLD": cold_strategy})

🔍 Маркеры: Event(output=..., route=...) — функциональный узел , задающий имя пути, — и dict-edge {"HOT": ..., "NORMAL": ..., "COLD": ...} , который сопоставляет имена с узлами.

Главный вывод — три вида работы, три дома:

  • Предсказуемая работа → функции (3 параллельных операции выборки)
  • Четкое правило → явная маршрутизация ( route_by_weather — это if выражение, а не решение модели).
  • Рассуждение → модель (работает ровно один агент, использующий одну стратегию)

Что вы увидите: temp=78F -> route=HOT , затем структурированная RaceStrategy . Чистая стоимость: 1 вызов LLM.

⚠️ Если вы добавляете четвертую ветвь, добавьте в словарь маршрутов запись DEFAULT_ROUTE . Маршрут, которому словарь не соответствует, не является ошибкой — ветвь просто завершается, и программа завершается с кодом 0 без вывода , что создает запутанный тупик для отладки.

Возможно, вы задаетесь вопросом: значит, L2b — это буквально L2a плюс маршрутизатор? Да, операции получения данных и объединения остаются неизменными, и это по-прежнему ровно один вызов LLM . Что изменилось: «всегда один и тот же агент» стало «один из трех, выбранный на основе данных».

👀 Прочитайте: route_by_weather — маршрутизатор является оператором if , а не агентом. · ▶ Также запустите run("COLD") . · ✏️ Измените: добавьте ветку WINDY с четвертым агентом — и перед этим прочтите предупреждение DEFAULT_ROUTE выше.

9. L3a · Агенты, работающие в сотрудничестве: один флаг, два мира — Столп 2

Дорожная карта — вы здесь: L3a

⚡ Краткое содержание: одна команда, один флаг. chat передает весь разговор одному специалисту и больше к нему не возвращается; single_turn превращает каждого специалиста в инструмент — параллельное подмножество, автоматический возврат, один синтез.

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

Структура: координатор руководит шестью специалистами (медицина, погода, темп, снаряжение, питание, психологическая подготовка). На этом уровне одна и та же команда запускается дважды — с тем же координатором и теми же шестью специалистами. Единственное отличие — один флажок у суб-агентов. Контраст — это урок.

Colab: запуск ячейки L3a · 📁 GitHub: L3a_collaborative/ · 💻 Локально: python -m L3a_collaborative.concierge --mode chat "What about fueling?"

Поток L3a

🔍 Маркеры: mode="single_turn" на заводе — и на выходе , TRANSFER → (такт 1) против серии строк DISPATCH → с одной меткой времени (такт 2).

Задача 1 · Сначала запустите вариант по умолчанию — и наблюдайте, как задание завершается с ошибкой.

Нет mode= в тексте → субагенты по умолчанию используют chat . Что вы увидите:

TRANSFER  nutrition_specialist   (transfer_to_agent  the only tool chat subagents provide)
Final speaker: nutrition_specialist

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

Это не ошибка — это просто режим чата выполняет свою работу. Разговор принадлежит тому, кто его ведет, пока кто-то явно не переведет его на другую тему. Это подходит для ассистента с открытым концом; не подходит для этапа конвейера обработки запросов.

Beat 2 · Один флаг, два мира

Единственное отличие: mode="single_turn" у каждого специалиста. Тот же вопрос, запускаем снова:

[t= 7.8s] DISPATCH  medical_specialist       same timestamp =
[t= 7.8s] DISPATCH  weather_specialist         one turn, many calls
[t=14.5s]    medical_specialist replied      replies land inside
[t=14.5s]    weather_specialist replied        one short window
🧠 Concierge (synthesized): <one answer>

Теперь ADK внедряет по одному инструменту делегирования для каждого специалиста — названному в честь субагента и описываемому с помощью description= (этот текст координатор читает при выборе подмножества; если его пропустить, маршрутизация будет осуществляться только по именам). Координатор отправляет несколько вызовов за один ход, ADK выполняет их параллельно , каждый автоматически возвращает свой результат, и координатор выполняет синтез.

Вопрос

Специалисты, которые стреляют

«А как насчет заправки?»

только питание

«У меня болит колено на 18-й миле»

только по медицинским показаниям

"Стоит ли мне сегодня участвовать в гонках?"

медицинский + погода + темп

«Мне стоит о чём-нибудь беспокоиться?»

все 6

Почему каждому специалисту предоставляется полный брифинг: каждый субагент single_turn работает в своей собственной изолированной ветви сессии — он не видит разговора или своих коллег. Ничего не происходит в фоновом режиме: координатор должен передавать весь SpecialistInput (вопрос + стратегия + данные от исполнителя) отдельно в каждый параллельный вызов.

💡 В ADK 2 это реализовано напрямую: LLM выбирает подмножество запросов И выполняет их параллельно — это объявляется с помощью sub_agents + mode="single_turn" . В версии 1.x можно было бы собрать ту же структуру, обернув каждого специалиста в AgentTool ; изменение заключается в том, что теперь это объявление, а не инфраструктура. ( ParallelAgent всегда работает со всеми агентами, а transfer_to_agent — последовательно.)

⚠️ Два важных замечания: (1) модель выбирает подмножество, поэтому она менее детерминирована, чем жестко заданный маршрутизатор L2 — точное подмножество может меняться от запуска к запуску. (2) Иногда вы можете увидеть строку Error validating input: ... для одного из специалистов. Это почти никогда не выход специалиста — output_schema заставляет Gemini обеспечивать это на стороне сервера. Это входные данные : координатор должен воспроизводить весь вложенный SpecialistInput дословно для каждого параллельного вызова, и иногда он допускает ошибку. ADK возвращает ошибку как результат работы этого инструмента, координатор восстанавливается, и синтез все равно завершается успешно.

Возможно, вам интересно: это

chat

Делегирование в стиле версии 1.x — по одному агенту за раз? По сути, да: это поведение по умолчанию в версии 1.x, теперь с именем. Разница с single_turn трехмерна: что контролирует координатор (один transfer_to_agent против одного инструмента на специалиста ) · сколько может работать (один, управляющий разговором, против N параллельно) · возвращается ли управление (никогда против автоматически, с результатами). И что касается кода: ветка if mode == в фабрике существует только для того, чтобы можно было построить одну команду для обоих вариантов этого сравнения — в реальном приложении один режим жестко прописывается, и условие if исчезает.

👀 Чтение: фабрика _specialist — параметр mode — это весь уровень. · ▶ Запустите оба ритма. · ✏️ Изменение: спросите "у меня болит колено на 18-й миле"сначала предскажите подмножество , затем проверьте строки DISPATCH.

# The factory's mode parameter is THE variable this level teaches:
def _specialist(name, domain, focus, mode):
    kwargs = {}
    if mode == "single_turn":   # the structured contract only makes sense for a TOOL
        kwargs = dict(mode="single_turn",
                      input_schema=SpecialistInput, output_schema=SpecialistResponse)
    return Agent(name=name, model=MODEL,
                 description=f"Marathon {domain} specialist. Consult for: {focus}.",
                 instruction=..., **kwargs)

race_concierge = Agent(name="race_concierge", model=MODEL,
                       sub_agents=[...six specialists...],   # NOTE: no `mode` on the coordinator
                       instruction="...DECIDE which specialists are relevant... call them IN PARALLEL... SYNTHESIZE...")

10. L3b · Режим задачи: разговор с финишной линией — Столп 2

Дорожная карта — вы здесь: L3b

⚡ Краткое содержание: промежуточный режим — общение с пользователем до тех пор, пока не будут собраны все поля , затем автоматический возврат с проверенным объектом .

Вопрос: L3a оставил пробел. chat полностью контролирует весь диалог; single_turn вообще не общается с пользователем. Но реальная работа по сбору информации происходит между этими этапами: «общайтесь с пользователем, ПОКА не соберете X — затем вернитесь с проверенным объектом». Что это за режим?

Форма:

race_desk (coordinator)
  └─ gear_fitter (mode="task", output_schema=GearOrder)

Colab: запуск ячейки L3b · 📁 GitHub: L3b_task_desk/ · 💻 Локально: python -m L3b_task_desk.desk

Поток L3b

gear_fitter удерживает задачу открытой — это приостановленная задача, а не зависание.

🔍 Маркеры: mode="task" + output_schema= на одном и том же агенте — и в выходных данных ⏸ вызов pause и finish_task .

Что вы увидите:

━━ TURN 1 ━━  user: 'I need shoes for the marathon.'
  race_desk  delegate: gear_fitter
  gear_fitter: What is your shoe size?
    The run ENDED  but nothing failed. This is a PAUSED task.

━━ TURN 2 ━━  user: 'Size 9, wide.'   (same session  resumes the task)
  gear_fitter  finish_task   (payload validates as GearOrder)
  race_desk: Your order ... in size 9 Wide has been confirmed.

Произошли три вещи, которые не под силу ни одному из режимов L3a:

  1. Выполнение задачи действительно остановилось посреди выполнения — это была приостановленная задача, а не зависание и не сбой. Агент задал уточняющий вопрос и удерживает задачу открытой. (В adk web ответ нужно просто ввести; в скрипте harness он отображается как второе сообщение в той же сессии.)
  2. Следующее сообщение возобновило работу ТОГО ЖЕ агента задач — без переадресации и перераспределения. Сеанс знает, кто ожидал.
  3. finish_task завершила процесс — инструмент ADK, внедренный из- за mode="task" . Агент должен вызвать его для завершения, и его полезная нагрузка должна соответствовать output_schema . Разговор с введенной строкой завершения — затем управление автоматически возвращается координатору, результат прилагается.

Правило выбора режима из одного вопроса

💡 "Нужно ли пользователю общаться с ним — и до КОГДА?" chat = неограниченно · task = пока не будут собраны поля · single_turn = никогда.

Режим

Человек в процессе

Параллельно?

Возвращает родительский элемент

chat (по умолчанию для субагента) — помощник поддержки, вспомогательный пилот с неограниченными возможностями.

полный разговор

нет

вручную (путем передачи)

task — прием заявок, бронирование, устранение неполадок

только уточняющие вопросы

нет

автоматически (через finish_task , с использованием проверенного объекта)

single_turn — классифицировать · извлечь · оценить · сгенерировать

никто

да

автоматический (с результатом)

mode применяется только к субагентам — никогда к координатору. А узлы рабочего процесса по умолчанию используют single_turn (поэтому L1–L2b никогда его не писали), в то время как субагенты по умолчанию используют chat (поэтому L3a пришлось это сделать).

⚠️ Два примечания к версии, прежде чем вы начнете работу над этим: (1) task

Статический узел графа зависит от версии — в версиях 2.0.0b1–2.3.0 (версия, указанная в этом коде) Workflow(...) вызывает исключение при создании; используйте именно то, что делает этот уровень (координатор чата с субагентами задач), или отправляйте вызовы через ctx.run_node . Исправлено в версии 2.5.0. (2) «Агенты задач должны быть конечными агентами» (без собственных субагентов) — это документированное ограничение ADK, но это контракт , а не гарантия во время выполнения: ни 2.3.0, ни 2.5.0 не остановят вас. Не воспринимайте отсутствие ошибки как разрешение.

💡 Подробнее: агент task , встроенный в рабочий процесс графа (версия 2.5.0+), с маршрутизацией, позволяющей зациклить разговор для повторной попытки: сопутствующий репозиторий 22_agent_in_workflow · руководство по полному режиму: docs/agent-modes.md .

Возможно, вам интересно: что это такое?

task

Что мне нужно, чтобы другие два варианта не сработали? Три вещи: автоматический возврат (чат перенаправляет разговор) · набранная финишная линия (полезная нагрузка finish_task должна соответствовать схеме — вы получаете данные, а не стенограмму) · пауза/возобновление (символ ⏸ обозначает приостановленную задачу, ожидающую человека, а не зависание).

👀 Прочитайте: gear_fittermode="task" + output_schema — это весь контракт. · ▶ Запустите его. · ✏️ Измените: run_desk("I need a hydration vest", "2 liters, medium") — уточняющий вопрос адаптируется, а финишная линия остается набранной.

11. L4a · Параллельное разветвление во время выполнения (Pillar 3a)

Карта маршрута — вы здесь: L4a

⚡ Вкратце: каркас по-прежнему состоит из трех статических шагов — динамические элементы скрываются внутри среднего шага, ширина которого определяется данными во время выполнения.

⚠️ Внимание: это самая крутая ступенька лестницы. Предыдущий уровень состоял из 44 строк; этот — около 120 — три агента и два узла рабочего процесса, и ничего лишнего. Заложите примерно 15 минут и сосредоточьтесь на строке «Чтение/Запуск/Изменение» в конце: вам не нужно считывать каждую строку с первого раза.

Вопрос: форма работы зависит от входных данных. Граф нельзя построить заранее. Начните с ширины во время выполнения: пусть LLM определит количество подвопросов.

Форма (один уровень в глубину):

START ─► decompose ─► research_topic (parallel_worker) ─► synthesize
                                 
                             └──┴──┴─ (flat: no children yet)

Открытый вопрос разбивается на N подвопросов — N выбирается LLM во время выполнения (3–7) — каждый из которых исследуется параллельно , а затем синтезируется в один краткий обзор.

Colab: запуск ячейки L4a · 📁 GitHub: L4a_flat_research/ · 💻 Локально: python -m L4a_flat_research.deep_research

Поток L4a

🔍 Маркеры — их нет

dynamic=True

switch. Dynamic — это способ написания кода , а не конфигурация. Всего два маркера: @node(parallel_worker=True) (принимает список размером с время выполнения, запускает один рабочий процесс на каждый элемент) и ctx.run_node(...) (код, напрямую планирующий работу узлов). Если вы видите хотя бы один из них, значит, вы используете динамический режим.

Что вы увидите: декомпозитор выводит, например, 5 подвопросов, они проводят исследование параллельно, а затем синтезированный отчет. Количество отличается при каждом запуске — фиксированный график этого сделать не мог.

Two flags on the worker worth understanding:

  • rerun_on_resume=True is mandatory on any node that calls ctx.run_node — ADK raises a ValueError without it. On resume it must re-execute the dispatching node to rebuild the children it spawned, since those aren't in the static graph.
  • retry_config= bounds how this FAILS. A parallel worker cancels every sibling and re-raises the instant one child fails — so without a retry, a single transient 429 discards the whole run, including every call already paid for. The retry lands on the inner per-item node, so each branch retries independently.

You might be wondering: where does ADK "know" this is dynamic? It doesn't need to — nothing is declared anywhere. The decomposer produces a list at runtime; the parallel worker sizes itself to whatever arrives. The dynamism is a property of the data flow you wrote, not a mode you switched on.

👀 Read: the two flags on research_topicparallel_worker and rerun_on_resume . · ▶ Run it. · ✏️ Change: swap in your own open question — N changes because the input decided the width.

12. L4b · Add recursive spawning (Pillar 3b)

Roadmap — you are here: L4b

⚡ TL;DR: recursion is written, not given — the worker calls itself through ctx.run_node , ordinary Python — so the brake must be written too. That's MAX_DEPTH .

The question: one research finding sometimes surfaces a narrow sub-topic worth its own investigation. How do you let a branch spawn more parallel work — and keep it bounded?

The shape (now recursive):

START ─► decompose ─► research_topic (parallel_worker, recursive) ─► synthesize
                                 
                                 └─ research(q3) ─► maybe spawn children
                               └─── research(q2) ─► maybe spawn children
                             └────── research(q1) ─► maybe spawn children

Colab: run the L4b cell · 📁 GitHub: L4b_recursion/ · 💻 Local: python -m L4b_recursion.deep_research

L4b flow

@node(parallel_worker=True, rerun_on_resume=True)
async def research_topic(ctx, node_input):
    finding = coerce(await ctx.run_node(research_agent, node_input=...), ResearchFinding)
    if finding.needs_deeper and finding.deeper_questions and depth < MAX_DEPTH:   # boundary in CODE
        children = await ctx.run_node(research_topic, node_input=deeper)          # recursive fan-out
    yield Event(output={..., "children": children})

🔍 The markers: ctx.run_node(research_topic, ...) inside research_topic itself — self-reference is the recursion — and the guard depth < MAX_DEPTH one line above it.

What you'll see: research nodes printing spawning N deeper — recursion happening live — then a runtime tree shape (eg 5 top-level + 10 recursive children ). The tree differs on every run.

⚠️ Before you raise the knob: the ceiling grows fast — MAX_DEPTH=3 takes the worst case from ~30 calls to ~93. And at the very end of a run you may see a cancelling N leftover tasks log line: that's ADK tearing down its parallel task group after the result is already complete. Harmless — and depending on your logging config you may never see it.

You might be wondering: isn't dynamic recursive by default? No — L4a is fully dynamic with zero recursion. Dynamic only hands you ordinary Python control flow; L4b chooses to write recursion with it. And because you wrote the recursion, you must write its boundary — this is where "let the LLM shape the work, keep the boundaries in code" stops being a slogan.

👀 Read: the guard: if finding.needs_deeper and depth < MAX_DEPTH . · ▶ Run it. · ✏️ Change: set MAX_DEPTH = 1 and re-run — the tree flattens (and the run gets cheaper). The boundary is YOURS, in code.

13. L5 · Which pattern should you use?

Roadmap — you are here: L5

⚡ TL;DR: one axis decides everything — who picks the next step: the graph you drew, the LLM, or your code.

You've built all three. This is the model that makes them useful: match the pattern to the shape of your problem.

The axis: who decides what runs next?

Столб

Who decides what runs next

Встроенный

1 · Graph

the graph you drew

L2a / L2b

2 · Collaborative

the LLM

L3a / L3b

3 · Dynamic

your Python code, at runtime

L4a / L4b

Step 0: do you even need a graph?

ADK ships prebuilt workflow agentsSequentialAgent , ParallelAgent , LoopAgent . For a plain chain of agents, those are the cheapest correct answer and there's no graph to assemble. Reach past them when you need explicit routing (L2b's router), a join (L2a's JoinNode ), or nodes that aren't agents (a plain function, zero LLM calls) — that last one is usually the reason.

Would a prebuilt SequentialAgent / ParallelAgent / LoopAgent do?

├─ YES ──────────────────────────────► use it; stop here

└─ NO  I need routing, a join, or non-agent nodes
   
   Can you draw the workflow before the input arrives?
   
   ├─ YES ───────────────────────────► Pillar 1 · Graph workflow    (L2a/L2b)
   
   └─ NO
      ├─ Known team, request picks the subset? ─► Pillar 2 · Collaborative  (L3a/L3b)
      └─ Does the shape depend on the input?  ──► Pillar 3 · Dynamic        (L4a/L4b)

L5 · which pattern

The honest 1.x-vs-2 framing

This is not "2.0 can do things 1.x couldn't" — 1.x could build all of it. The shift is that 2.0 gives each shape a more direct home , so known control flow leaves the prompt and becomes structure you can see and test.

Шаблон

The 1.x cost

The ADK 2 home

График

4 LLM calls in the common build; routing hidden in a prompt

function + agent nodes as peers → 1 call, if -statement router

Совместный

buildable via AgentTool plumbing; ParallelAgent always-all, transfer_to_agent serial

a declared team: sub_agents + mode="single_turn"

Динамический

recursion drops you out of the framework

parallel_worker + recursive ctx.run_node inside the framework

The whole app, and what a graph can't show you

You have now built every piece below. Workflow exposes its structure at graph.edges , so this picture is generated from the code rather than drawn by hand — and what the introspection finds is the summary of this lab:

Столб

What graph.edges contains

Почему

1 · Graph (L2b)

10 edges , routes and all

you drew it before any input arrived

2 · Collaborative (L3a)

0 edges — only sub_agents + mode

the LLM picks the subset per request

3 · Dynamic (L4a/L4b)

3 edges — identical in both

the recursion is written in Python, not wired in the graph

That last row is the proof for the question L4b answers: L4a and L4b have the same graph, and only one of them recurses.

What you can build now

Each pattern you just ran is a real product shape:

You practiced

In the wild, that's

Начните с

Graph + router (L2a/L2b)

document pipelines, ETL-with-LLM-steps, review/approval chains, eval harnesses

this repo's L2b

Coordinator + single_turn team (L3a)

a support copilot with specialist teams, triage desks, multi-lens review

marathon demo mode 2

task agents (L3b)

intake forms, booking flows, onboarding, KYC — any "collect then act"

22_agent_in_workflow

Dynamic width/depth (L4a/L4b)

research agents, report generators, audit sweeps over unknown-sized inputs

marathon demo mode 3

They compose

The three patterns are not mutually exclusive . A graph node can call a collaborative coordinator; a specialist can launch a dynamic workflow. Choose the right pattern per part of the problem — that's how you avoid turning every agent system into one giant prompt.

The whole app — and what a graph can’t show you

💡 Try it on your own workflow: the script that drew this is scripts/graph_dump.py . Point it at any Workflow and it will print the real edges — a free structural diagram of anything you build.

14. Congratulations

Nine agents, one baton, an orderly finish

You built a Marathon Race Day Coach and, along the way, all three of ADK 2's orchestration patterns.

What you learned

  • Prologue — the mega-prompt that invented its own weather : why structure exists at all.
  • L0–L1Agent , Runner , a real tool the model chooses to call, and your first Workflow (function nodes + agent nodes as peers).
  • L2a / L2b — graph workflows: parallel fan-out + JoinNode , then deterministic routing — one LLM call.
  • L3a — collaborative agents: the same team run in chat (stranded) then single_turn (parallel subset + synthesis) — one flag, two worlds.
  • L3btask mode: a paused clarifying question, a scripted resume, finish_task returning a validated object.
  • L4a / L4b — dynamic workflows: runtime width (fan-out), then runtime depth (recursion) with boundaries in code.
  • L5 — the decision tree, and how the patterns compose.

Lines worth keeping

Functions prepare the context. Edges define the workflow. The router chooses the path. The model writes the answer.

Let the LLM shape the work, but keep the boundaries in code.

Match the pattern to the shape of your problem.

Следующие шаги

  • Run the full app these levels were distilled from — the Marathon Race Day Coach , a FastAPI + SSE build with a browser UI showing all three modes live: github.com/cuppibla/adk-2-marathon-demo .
  • Go wider: adk-workflows-compared — all 23 official ADK 2 workflow samples, each with a 1.x port and when-to-use guidance. Start with docs/three-pillars.md , then the things this codelab skipped: 07_loop , 17_request_input , 22_agent_in_workflow .
  • Port your own problem: which parts are known-structure (L2), known-team (L3a/L3b), unknown-shape (L4)?
  • Explore the code: github.com/cuppibla/adk2-tutorial .
  • Came through the workshop? Your credit — and the project it created — won't last forever. To keep re-running these levels for free, do the Take-home setup step instead: a free AI Studio key, no Cloud project, no billing. Swapping that one cell is the only change.