Orchestration ADK 2 : graphiques, workflows collaboratifs et dynamiques

1. Présentation

Le titre de l'ADK 2 est trois schémas d'orchestration. Cet atelier de programmation vous apprendra à les utiliser en créant une application, un coach pour le jour du marathon, une étape exécutable à la fois. Chaque niveau répond à une seule question, ajoute une idée et s'exécute de manière autonome.

Points abordés

  • Workflows graphiques (pilier 1) : lorsque vous pouvez dessiner le flux avant l'arrivée de l'entrée.
  • Agents collaboratifs (pilier 2) : lorsque vous connaissez l'équipe, mais que la demande sélectionne le sous-ensemble, et les trois modes de collaboration (chat / task / single_turn), chacun s'exécute en direct.
  • Workflows dynamiques (pilier 3) : lorsque la forme du travail lui-même dépend de l'entrée.
  • Comment choisir : un arbre de décision à une seule question et la façon dont les modèles se composent.

Le fil conducteur

Structure connue → Sous-ensemble connu d'équipes / de variables → Forme inconnue → Choisir la bonne

Votre feuille de route d'apprentissage

Ce que vous allez faire

Une application, Marathon Race Day Coach, a été assemblée un niveau exécutable à la fois. Chaque niveau est un module Python simple que vous exécutez depuis le terminal. Au niveau 5, les éléments ci-dessous vous appartiennent.

L'image est tirée du code en cours d'exécution : chaque ligne pleine a été lue à partir de Workflow.graph.edges. C'est la première leçon : les parties qui peuvent être dessinées à l'avance correspondent exactement au pilier 1, et les parties qui ne le peuvent pas expliquent l'existence des piliers 2 et 3.

L'ensemble de l'application et ce qu'un graphique ne peut pas vous montrer

Prérequis

  • Un compte Google (pour Colab) : aucune configuration locale n'est requise.
  • Environ 50 minutes (les deux niveaux 4 sont les plus longs, prévoyez-le).
  • L'une des deux façons d'accéder à un modèle Gemini. Choisissez votre méthode : vous exécutez une étape de configuration et ignorez l'autre :

🎓 Atelier

🏠 À emporter

Participants

Vous participez à un atelier en direct et l'instructeur vous a fourni un lien pour demander un crédit.

Toutes les autres personnes, y compris les participants à l'atelier, après l'atelier

Ce dont vous avez besoin

Le lien de revendication et un compte Google pouvant créer un projet Cloud

Une clé API AI Studio sans frais

Fonctionne sur

Vertex AI, dans un projet facturé sur votre crédit d'atelier

Google AI Studio

Coût

Couvert par l'avoir

Niveau sans frais

Étape de configuration

Configuration de l'atelier (étape suivante)

Configuration à domicile (étape suivante)

Tout ce qui suit le prologue est identique dans les deux cas. La voie ne fait que déterminer le point de terminaison du modèle auquel le notebook s'adresse.

Deux façons de suivre

Chaque étape ci-dessous correspond à une cellule du notebook Colab et à un dossier du dépôt GitHub. Choisissez l'une des options suivantes :

  • ▶ Colab (recommandé)  : ouvrez le notebook et exécutez les cellules de haut en bas.
  • 💻 Local  : git clone le dépôt, ./setup_venv.sh, puis exécutez chaque niveau en tant que module (python -m ...) ou parcourez-les tous avec ./run.sh (adk web).

2. Configuration de l'atelier : revendiquer votre crédit et passer à Vertex AI

Lors de l'atelier, vous recevez un crédit Google Cloud. Vous allez le revendiquer, créer un projet facturé sur celui-ci et pointer le notebook vers Vertex AI au lieu d'AI Studio. Une cellule effectue tout après la revendication.

1 · Demandez votre avoir (environ 1 min)

  1. Ouvrez le lien de revendication partagé par votre enseignant. Il se présente comme suit : https://me.developers.google.com/benefits/claim/your-workshop-name.
  2. Connectez-vous et suivez les instructions sur la page pour accepter le crédit.
  3. Notez le compte Google que vous avez utilisé. Chaque étape ci-dessous doit être exécutée avec ce même compte.

2 · Ouvrez le notebook et installez ADK 2 (environ 1 min)

Cliquez sur Ouvrir dans Colab ▶, puis exécutez la première cellule de code. Il épingle la version exacte d'ADK 2 sur laquelle cet atelier de programmation a été validé et affiche ✓ installed.

3) Exécutez la cellule "Workshop setup" (Configuration de l'atelier) (~3 min)

Il s'agit de la cellule intitulée 🎓 Parcours A · Atelier. Exécutez-le. Colab vous demandera de l'autoriser. Choisissez le même compte Google que celui avec lequel vous avez demandé le crédit et autorisez l'accès.

Il effectue quatre actions : il crée un projet appelé adk-2-tutorial-XXXX sur votre compte, active l'API Vertex AI sur ce projet, définit les quatre variables d'environnement que chaque cellule ultérieure lit, puis effectue un appel de test à Vertex et attend qu'il réponde. La configuration se termine donc ou vous indique pourquoi elle ne fonctionne pas, au lieu d'échouer plus tard dans un niveau.

Résultat attendu : la dernière ligne est celle qui compte :

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 · Passer l'étape "Configuration à domicile"

N'exécutez pas la cellule de clé AI Studio, car cela rétablirait AI Studio pour le notebook et annulerait ce que vous venez de faire. (La cellule se protège contre cela et refusera de s'exécuter, mais la méthode la plus propre consiste simplement à la passer.) Accédez directement à la cellule Shared building blocks (Blocs de construction partagés).

5 · Exécutez la cellule "Shared building blocks" (Composants de base partagés).

Exécutez-le une fois. Il définit les schémas Pydantic et les scénarios de marathon prédéfinis que chaque niveau à partir du niveau 2 réutilise. Vous verrez ✓ schemas + scenarios ready.

Après l'atelier

Votre crédit et le projet qu'il a créé ne sont pas éternels. Pour continuer à exécuter ces niveaux sans frais une fois l'atelier terminé, exécutez plutôt l'étape Configuration à emporter. Vous obtiendrez une clé AI Studio sans frais, sans projet Cloud ni facturation. Seule cette cellule change.

Pour libérer de l'espace plus tôt, ouvrez la console Cloud, sélectionnez adk-2-tutorial-XXXX, puis supprimez-le. Aucune autre ressource payante n'est créée dans cet atelier de programmation.

3. Configuration à domicile : clé API AI Studio

Tout ce qui se trouve sur ce chemin d'accès s'exécute sur une clé API Google AI Studio sans frais. Vous n'avez pas besoin de projet Google Cloud, de facturation ni d'installation locale. Cette étape prend environ trois minutes.

1) Ouvrez le notebook.

Cliquez sur Ouvrir dans Colab ▶. Vous serez redirigé vers le notebook, qui contient une introduction en Markdown, puis une cellule exécutable par niveau. Vous exécutez les cellules de haut en bas. Chacune imprime son propre résultat juste en dessous.

2 · Installer ADK 2 (environ 1 min)

Exécutez la première cellule de code. Il épingle la version exacte sur laquelle cet atelier de programmation a été validé :

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

Attendez la fin de l'opération. Le message ✓ installed s'affichera. (L'installation prend environ 30 à 60 secondes la première fois, puis elle est mise en cache.)

3 · Obtenez votre clé API Gemini depuis AI Studio (environ 1 min)

  1. Ouvrez aistudio.google.com/app/apikey dans un nouvel onglet du navigateur.
  2. Connectez-vous à l'aide de votre compte Google.
  3. Cliquez sur Créer une clé API (en haut à droite).
  4. Sélectionnez un projet Google existant ou laissez-le en créer un.
  5. Copiez la clé, qui commence par AIza... et comporte environ 40 caractères.

4 · Ajoutez votre clé à Colab (~1 min)

Option A : Secrets Colab (recommandée, la clé reste masquée)

  1. Cliquez sur l'icône en forme de clé 🔑 dans la barre latérale de gauche de Colab.
  2. Cliquez sur + Ajouter un secret.
  3. Définissez Nom sur GOOGLE_API_KEY.
  4. Collez votre clé dans Valeur.
  5. Activez l'option Accès au notebook.

Option B : coller lorsque vous y êtes invité (rapide) : ignorez le secret. Lorsque vous exécutez la cellule suivante, une invite masquée 🔑 Enter your Google AI Studio API key: s'affiche. Collez le secret et appuyez sur Entrée.

5 : Exécuter la cellule clé

Il lit le secret (ou revient à l'invite de collage), puis pointe ADK vers AI Studio (et non 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.")

Résultat attendu : ✅ API key set — using Google AI Studio.

6 Exécutez la cellule "Shared building blocks" (Composants de base partagés).

Exécutez la cellule Shared building blocks (Composants de base partagés) une fois. Il définit les schémas Pydantic et les scénarios de marathon prédéfinis que chaque niveau à partir du niveau 2 réutilise. Vous verrez ✓ schemas + scenarios ready.

Vous avez terminé la configuration ! 🎽 Avant de passer à L0, la version que tout le monde crée en premier, faisons un petit détour.

4. Prologue : pourquoi ne pas utiliser une seule requête volumineuse ?

⚡ Avant de l'exécuter, concentrez-vous sur UNE chose à observer : d'où proviennent tous les nombres spécifiques ? C'est tout ce qu'il y a à faire. Tout le reste est superflu.

Avant d'utiliser le ladder, exécutez l'élément qu'il remplace : un agent dont l'invite promet tout (récupérer la météo, analyser le parcours, lire le journal d'entraînement, définir l'itinéraire en fonction des conditions, générer le plan).

Ce que vous verrez : une stratégie spécifique, bien structurée et qui inspire confiance, mais dont les chiffres sont inventés. Lors d'un test en direct, il a commencé par "J'ai récupéré les données météo d'aujourd'hui" et a indiqué 11 °C, un vent de 14,5 km/h et une analyse d'un journal d'entraînement qu'il n'avait jamais vu. Il n'y a pas d'API météo ici, pas de données de cours, pas de journal : un appel de modèle opaque fabrique ses entrées ou les couvre de manière inutile.

Il s'agit de la maladie, qui présente quatre symptômes à mentionner :

  1. Vous ne pouvez pas lui faire confiance : les données sont inventées, mais de manière fluide.
  2. Vous ne pouvez pas le tester : le routage de l'étape 4 se trouve dans la prose. Il n'y a pas de if pour les tests unitaires.
  3. Vous ne pouvez pas échanger une étape : il n'y a pas de joint où une véritable API météo pourrait se brancher.
  4. Vous payez tout, à chaque fois : cinq étapes, un appel géant, aucune mise en cache d'une partie déterministe.

Gardez cette sensation. Les neuf niveaux suivants suppriment ces étapes de l'invite, une par une : les fonctions fetch (L1–L2a), une instruction if routes (L2b), les spécialistes divisent le travail (L3a–L3b) et le code délimite la forme (L4a–L4b).

Le coach de méga-requêtes : confiant, sans rien derrière le graphique

💻 Local  : python -m shared.prologue

5. L0 · Votre premier agent ADK 2

Feuille de route : vous êtes ici (L0)

⚡ En bref : un agent est un modèle + une instruction + des outils qu'il peut appeler. Un Runner l'exécute. Tout ce qui se trouve au-delà de ce niveau n'est qu'un plus grand nombre d'agents, disposés de manière plus esthétique.

La question : pouvez-vous obtenir une réponse d'un modèle et lui faire utiliser du code réel lorsque l'arithmétique est importante ?

Une idée en trois parties :

  • Agent : l'élément qui raisonne (un modèle Gemini + une instruction).
  • Runner : élément qui exécute un agent dans une session et diffuse des événements.
  • un outil : une simple fonction Python (pace_splits) que le modèle décide d'appeler. L'ADK lit la signature et la docstring, puis transmet une déclaration au modèle. Aucune écriture de schéma n'est nécessaire.

Après le prologue, voici la première réparation : un LLM qui fait des calculs de rythme dans sa tête se trompera volontiers. pace_splits est du Python déterministe, donc les nombres de la réponse sont calculés, et non improvisés.

▶ Colab : exécutez la cellule L0 · 📁 GitHub : L0_first_agent/ · 💻 Local : python -m L0_first_agent.agent

Flux 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

🔍 Les repères  : Agent(...) · tools=[pace_splits] · Runner(...). Dans la sortie, la ligne 🔧 correspond à la décision du modèle d'appeler votre code en cours de réponse.

Ce que vous verrez :

   🔧 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...

Les lignes 🔧 sont la leçon : au milieu de la réponse, le modèle a choisi d'appeler votre fonction, et le 8:00/mile exact de sa réponse provient de votre code, et non des statistiques sur les jetons.

❓ Vous vous demandez peut-être si le modèle appelle toujours l'outil. Non, il décide pour chaque question. Posez une question sans aucun chiffre et les lignes 🔧 disparaissent (c'est exactement ce que vous devez faire dans Playground).

👀 Lisez pace_splits (une fonction simple) et la ligne tools=[pace_splits]. · ▶ Exécutez-le. · ✏️ Modification : posez une question générale (sans temps cible). Vous remarquerez que les lignes 🔧 ont disparu : le modèle décide quand il est utile d'appeler un outil. Réécrivez ensuite instruction et réexécutez-le. L'instruction correspond au reste du programme.

6. L1 : Votre premier workflow

Feuille de route : vous êtes ici : L1

⚡ En bref : une fonction simple et un agent LLM sont le même type de nœud. Travail prévisible : fonction (0 LLM, déterministe) ; raisonnement : agent.

La question : comment mélanger du code brut et un LLM dans un même flux, sans payer pour un appel de modèle sur les parties qui ne sont que du code ?

L'idée principale : dans un Workflow, une fonction Python simple et un agent LLM ne sont que des nœuds dans la même liste edges.

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

▶ Colab : exécutez la cellule L1 · 📁 GitHub : L1_graph_basics/ · 💻 Local : python -m L1_graph_basics.workflow

Flux L1

Le nœud de fonction affiche les données qu'il a produites (sans appel de modèle), puis l'agent donne des conseils qui font référence à la température et au vent réels qu'il a reçus :

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)])

🔍 Les marqueurs : un tuple de bordure — (START, fetch_conditions, advise) — avec une fonction Python nue au milieu, et input_schema= validant le transfert.

Nouveautés par rapport à L0  : Workflow(edges=[...]), START (où l'entrée est saisie), un nœud de fonction renvoyant Event(output=...) et input_schema=Conditions afin que la sortie de la fonction soit validée par rapport à ce schéma avant que l'agent ne la voie (en tant que texte JSON : input_schema valide la limite, il ne fournit pas à l'agent un objet Python).

❓ Vous vous demandez peut-être si l'ordre "fonction-puis-agent" est obligatoire. Non. Vous pouvez choisir l'ordre, le mix et le nombre de votre choix. advise s'exécute en deuxième position, car il a besoin des données de fetch_conditions. La leçon est la pairie, pas la séquence.

👀 À lire : fetch_conditions renvoie des données sans appel de modèle, tandis que advise contient input_schema=Conditions. · ▶ Exécutez-le. · ✏️ Modification : définissez temp_f=30 dans la fonction et réexécutez-la. Le conseil est inversé et la fonction coûte toujours 0 appels LLM.

7. L2a · Distribution ramifiée parallèle + JoinNode (Pillar 1a)

Feuille de route : vous êtes ici : L2a

⚡ En bref : distribuez les tâches en parallèle (sans frais), attendez que toutes soient terminées, regroupez-les et donnez l'ensemble à un agent.

La question : vous pouvez dessiner le flux avant l'arrivée de l'entrée. Commencez par le squelette : collectez les données en parallèle, regroupez-les et transmettez-les à un agent.

Forme :

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

▶ Colab : exécutez la cellule L2a · 📁 GitHub : L2a_parallel_join/ · 💻 Local : python -m L2a_parallel_join.workflow

Flux L2a

🔍 Les marqueurs : trois bords qui commencent tous à START (c'est ça, la distribution ramifiée) et JoinNode, le point de rencontre.

  • Les trois récupérations sont des fonctions qui s'exécutent en parallèle, sans aucun appel de LLM.
  • JoinNode attend les trois et les regroupe dans une charge utile typée (BundledRunData), indexée par nom de fonction.
  • Un agent strategy lit le bundle et écrit un RaceStrategy.

Ce que vous verrez : chaque récupération affiche un code temporel started / finished. Les trois commencent à 0,0 s et la distribution ramifiée se termine à 2,0 s, soit la récupération la plus lente, et non à 4,5 s, qui correspond à la somme de leurs durées. Ce chevauchement correspond au parallélisme. (Le temps écoulé total affiché à la fin est d'environ 8 secondes, car il contient également l'appel LLM de l'agent stratégique. Lisez les codes temporels de récupération pour l'affirmation parallèle, et non le total.)

💡 Rappel du prologue : le méga-prompt a inventé la météo. Ici, la température provient d'une fonction de récupération : code réel, couture réelle. Remplacez le dict prédéfini par une véritable API météo, sans rien changer d'autre.

❓ Vous vous demandez peut-être : combien

JoinNode

dois-je comprendre ? En une phrase : elle attend que chaque branche parallèle atterrisse, regroupe les sorties dans un dictionnaire dont les clés sont les noms des fonctions en amont et ne calcule rien elle-même. C'est précisément ce dictionnaire qui permet au routeur de L2b d'écrire node_input["fetch_weather"]["temp_f"].

👀 Lecture : trois arêtes partent de START ; JoinNode les regroupe pour un agent. · ▶ Exécutez-le et lisez les codes temporels, pas le total. · ✏️ Modification : effectuez une récupération de la veille 3.0. Prédisez d'abord la nouvelle heure de fin de la distribution ramifiée, puis vérifiez-la.

8. L2b · Ajouter le routeur déterministe (pilier 1b)

Feuille de route : L2b

⚡ En bref : L2a non modifié + un if simple déterminent quel agent s'exécute. Créer des branches sans demander au modèle.

Question : le plan doit être différent selon qu'il fait chaud ou froid. Comment créer une branche sans demander au modèle de décider ?

Forme (L2a + un routeur) :

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

▶ Colab : exécutez la cellule L2b – essayez run("NORMAL") / run("COLD") · 📁 GitHub : L2b_router/ · 💻 Local : python -m L2b_router.workflow COLD

Flux 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})

🔍 Les marqueurs  : Event(output=..., route=...), un nœud de fonction nommant le chemin d'accès, et l'arête de dictionnaire {"HOT": ..., "NORMAL": ..., "COLD": ...} qui mappe les noms aux nœuds.

En résumé : trois types de travail, trois maisons

  • Travail prévisible → fonctions (les trois récupérations parallèles)
  • Règle claire → Routage explicite (route_by_weather est une instruction if, et non une décision du modèle)
  • Raisonnement → le modèle (exactement un agent de stratégie s'exécute)

Ce que vous verrez  : temp=78F -> route=HOT, puis un RaceStrategy structuré. Coût net : un appel LLM.

⚠️ Si vous ajoutez une quatrième branche,ajoutez également une entrée DEFAULT_ROUTE au route-dict. Une route qui ne correspond pas au dictionnaire n'est pas une erreur. La branche se termine simplement et le programme se ferme avec 0 sans sortie, ce qui est une impasse déroutante à déboguer.

❓ Vous vous demandez peut-être : L2b est-il littéralement L2a plus un routeur ? Oui, les récupérations et la jointure sont intactes, et il s'agit toujours exactement d'un appel LLM. Modification : "toujours le même agent" est devenu "l'un des trois, choisi par les données".

👀 Lire  : route_by_weather — le routeur est une instruction if, pas un agent. · ▶ Exécutez run("COLD") également. · ✏️ Modification : ajoutez une branche WINDY avec un quatrième agent et lisez l'avertissement DEFAULT_ROUTE ci-dessus avant de le faire.

9. L3a · Collaborative agents: one flag, two worlds — Pillar 2

Feuille de route : L3a

⚡ En bref : une seule équipe, un seul drapeau. chat transfère l'intégralité de la conversation à un spécialiste et ne revient jamais ; single_turn transforme chaque spécialiste en outil : sous-ensemble parallèle, retour automatique, une synthèse.

La question : vous connaissez l'équipe, mais la demande décide des membres qui doivent répondre. Comment laisser un LLM choisir le sous-ensemble et les exécuter simultanément ?

La forme : un coordinateur pour six spécialistes (médecine, météo, allure, équipement, nutrition, mental). Dans ce niveau, la même équipe est utilisée deux fois (même requête de coordination, mêmes six spécialistes). La seule différence est un indicateur sur les sous-agents. Le contraste est la leçon.

▶ Colab : exécutez la cellule L3a · 📁 GitHub : L3a_collaborative/ · 💻 Local : python -m L3a_collaborative.concierge --mode chat "What about fueling?"

Flux L3a

🔍 Les repères  : mode="single_turn" dans l'usine et dans la sortie, TRANSFER → (temps 1) par rapport à une série de DISPATCH → lignes partageant un même code temporel (temps 2).

Beat 1 : Exécutez d'abord la valeur par défaut et regardez le job échouer

Aucun mode= écrit → les sous-agents sont définis par défaut sur chat. Nouveautés :

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

Le coordinateur ne dispose d'aucun outil de délégation. Les sous-agents de chat ne lui donnent que transfer_to_agent, un transfert séquentiel de l'intégralité de la conversation à un seul spécialiste. Ce spécialiste répond directement à l'utilisateur, et l'exécution se termine là. Aucune expédition parallèle. Aucun retour. Aucune synthèse. Posez la question générale et la situation empire : six spécialistes, un transfert.

Il ne s'agit pas d'un bug, mais du mode Chat qui fait son travail. La conversation appartient à la personne qui la détient, jusqu'à ce qu'elle soit explicitement transférée. Bonne réponse pour un assistant à réponse ouverte, mais mauvaise réponse pour une étape de pipeline.

Beat 2 : un drapeau, deux mondes

La seule différence : mode="single_turn" pour chaque spécialiste. Même question, exécution à nouveau :

[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>

L'ADK injecte désormais un outil de délégation par spécialiste, nommé d'après le sous-agent et décrit par son description= (le coordinateur lit ce texte lorsqu'il choisit le sous-ensemble ; ignorez-le et basez-vous uniquement sur les noms). Le coordinateur émet plusieurs appels en un tour, l'ADK les exécute en parallèle, chacun renvoyant automatiquement son résultat, et le coordinateur synthétise.

Question

Spécialistes qui tirent

"Et l'alimentation ?"

nutrition uniquement

"Mon genou me fait mal au 29e kilomètre"

médical uniquement

"Dois-je courir aujourd'hui ?"

médical + météo + rythme

"Y a-t-il quelque chose qui devrait m'inquiéter ?"

les six

Pourquoi chaque spécialiste reçoit-il l'intégralité du brief ? Chaque sous-agent single_turn s'exécute dans sa propre branche de session isolée. Il ne peut pas voir la conversation ni ses pairs. Rien n'est ambiant : le coordinateur doit transmettre l'intégralité de SpecialistInput (question + stratégie + données du programmeur) séparément à chaque appel parallèle.

💡 Dans ADK 2, cette fonctionnalité est directement intégrée : un LLM sélectionne un sous-ensemble par requête ET l'exécute en parallèle, déclaré via sub_agents + mode="single_turn". Dans la version 1.x, vous pouviez assembler la même forme en encapsulant chaque spécialiste dans AgentTool. La différence est qu'il s'agit désormais d'une déclaration plutôt que d'un raccordement. (ParallelAgent est toujours-tout et transfer_to_agent est sériel.)

⚠️ Deux mises en garde honnêtes : (1) le modèle choisit le sous-ensemble, il est donc moins déterministe que le routeur codé en dur de L2. Le sous-ensemble exact peut varier d'une exécution à l'autre. (2) Il arrive qu'une ligne Error validating input: ... s'affiche pour un spécialiste. Il s'agit presque toujours de la sortie du spécialiste : output_schema permet à Gemini d'appliquer cette règle côté serveur. Il s'agit de l'entrée : le coordinateur doit reproduire l'intégralité de l'SpecialistInput imbriqué à l'identique pour chaque appel parallèle, et il arrive qu'il en oublie un. L'ADK renvoie l'erreur en tant que résultat de cet outil, le coordinateur récupère et la synthèse est toujours effectuée.

❓ Vous vous demandez peut-être : est-ce que

chat

just 1.x-style delegation — one agent at a time? Oui, en gros : il s'agit du comportement par défaut de la version 1.x, mais avec un nom. L'écart par rapport à single_turn est tridimensionnel : ce que le coordinateur détient (un transfer_to_agent par rapport à un outil par spécialiste), le nombre de personnes pouvant travailler (une, propriétaire de la conversation, par rapport à N en parallèle) et le retour du contrôle (jamais par rapport à automatiquement, avec des résultats). À propos du code : la branche if mode == de l'usine n'existe que pour qu'une équipe puisse être créée de deux manières pour ce contraste. Une application réelle code en dur un mode et if disparaît.

👀 Lire : l'usine _specialist – le paramètre mode correspond à l'ensemble du niveau. · ▶ Exécutez les deux rythmes. · ✏️ Modification : demande "mon genou me fait mal au 29e kilomètre" — prédisez d'abord le sous-ensemble, puis vérifiez les lignes 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 · Mode tâche : une conversation avec une ligne d'arrivée — Pilier 2

Feuille de route : vous êtes ici : L3b

⚡ En bref : le mode intermédiaire consiste à parler à l'utilisateur jusqu'à ce que les champs soient collectés, puis à revenir automatiquement avec un objet validé.

La question : L3a a laissé un espace vide. chat est propriétaire de l'intégralité de la conversation, tandis que single_turn ne s'adresse jamais à l'utilisateur. Mais le véritable travail d'ingestion se situe entre les deux : "parlez à l'utilisateur JUSQU'À ce que vous ayez collecté X, puis revenez avec un objet validé". Quel mode est-ce ?

Forme :

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

▶ Colab : exécutez la cellule L3b · 📁 GitHub : L3b_task_desk/ · 💻 Local : python -m L3b_task_desk.desk

Flux L3b

gear_fitter holding the task open — a paused task, not a hang

🔍 Les repères  : mode="task" + output_schema= sur le même agent, et dans la sortie, l'appel ⏸ Pause et finish_task.

Ce que vous verrez :

━━ 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.

Trois choses se sont produites que le mode L3a ne peut pas faire :

  1. L'exécution s'est réellement arrêtée en cours de tâche : il s'agit d'une tâche mise en veille, et non d'un blocage ou d'un échec. L'agent a posé sa question de clarification et a laissé la tâche ouverte. (Dans adk web, il vous suffit de saisir la réponse. Le script de test la saisit en tant que deuxième message dans la même session.)
  2. Le message suivant a repris le MÊME agent de tâches : aucun réacheminement ni aucune nouvelle délégation. La session sait qui attendait.
  3. finish_task a mis fin à l'opération : un outil ADK injecté parce que mode="task". L'agent doit l'appeler pour terminer, et sa charge utile doit être validée par rapport à output_schema. Une conversation avec une ligne d'arrivée saisie au clavier : le contrôle revient automatiquement au coordinateur, et le résultat est joint.

La règle d'une question pour choisir un mode

💡 "L'utilisateur doit-il lui parler, et jusqu'à QUAND ?" chat = indéfiniment ; task = jusqu'à ce que les champs soient collectés ; single_turn = jamais.

Mode

Human-in-the-loop (avec intervention humaine)

Parallèle ?

Retour au parent

chat (sous-agent par défaut) : assistant d'assistance, copilote à réponse libre

conversation complète

non

manuelle (par transfert)

task : accueil, réservation, dépannage

questions de clarification uniquement

non

automatique (via finish_task, avec un objet validé)

single_turn : classer, extraire, évaluer, générer

aucun

oui

automatique (avec son résultat) ;

mode ne s'applique qu'aux sous-agents, jamais au coordinateur. Les nœuds de workflow sont définis par défaut sur single_turn (c'est pourquoi les niveaux 1 à 2b ne l'ont jamais écrit), tandis que les sous-agents sont définis par défaut sur chat (c'est pourquoi le niveau 3a a dû le faire).

⚠️ Deux notes sur les versions avant de commencer : (1) task

en tant que nœud de graphique statique dépend de la version : sur 2.0.0b1–2.3.0 (version épinglée de cet atelier de programmation), Workflow(...) se déclenche lors de la construction. Utilisez exactement ce que fait ce niveau (un coordinateur de chat avec des sous-agents de tâches) ou distribuez-le via ctx.run_node. Supprimé dans la version 2.5.0. (2) L'erreur "Task agents must be leaf agents" (les agents de tâches doivent être des agents feuilles) est une limite ADK documentée, mais un contrat, et non une protection d'exécution : ni la version 2.3.0 ni la version 2.5.0 ne vous arrêteront. Ne considérez pas l'absence d'erreur comme une autorisation.

💡 En savoir plus : agent task intégré à un workflow de graphe (forme 2.5.0+), avec un routage qui peut relancer la conversation pour une nouvelle tentative : dépôt associé 22_agent_in_workflow · guide du mode complet : docs/agent-modes.md.

❓ Vous vous demandez peut-être : que signifie

task

m'acheter ce que les deux autres ne peuvent pas ? Trois choses : retour automatique (la conversation est interrompue) ; ligne d'arrivée saisie (la charge utile de finish_task doit être validée par rapport au schéma : vous obtenez des données, pas une transcription) ; pause/reprise (⏸ est une tâche en attente d'une intervention humaine, pas un blocage).

👀 À lire  : gear_fitter – mode="task" + output_schema correspond à l'intégralité du contrat. · ▶ Exécutez-le. · ✏️ Modifier  : run_desk("I need a hydration vest", "2 liters, medium") la question de clarification s'adapte, la ligne d'arrivée reste saisie.

11. L4a : distribution ramifiée parallèle dimensionnée au moment de l'exécution (axe 3a)

Feuille de route : L4a

⚡ En bref : le squelette comporte toujours trois étapes statiques. L'étape dynamique se cache à l'intérieur de celle du milieu, où la largeur est déterminée par les données au moment de l'exécution.

⚠️ Attention : il s'agit de l'étape la plus difficile. Le niveau précédent comportait 44 lignes, celui-ci environ 120 (trois agents et deux nœuds de workflow, sans aucune marge). Prévoyez environ 15 minutes et appuyez-vous sur la ligne "Lire/Exécuter/Modifier" à la fin : vous n'avez pas besoin de comprendre chaque ligne du premier coup.

La question : la forme de l'œuvre dépend de l'entrée. Vous ne pouvez pas dessiner le graphique à l'avance. Commencez par la largeur de l'exécution : laissez le LLM décider du nombre de sous-questions.

Forme (un niveau de profondeur) :

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

Une question ouverte est décomposée en N sous-questions (N est choisi par le LLM au moment de l'exécution, entre 3 et 7). Chaque sous-question fait l'objet d'une recherche en parallèle, puis est synthétisée en un seul briefing.

▶ Colab : exécutez la cellule L4a · 📁 GitHub : L4a_flat_research/ · 💻 Local : python -m L4a_flat_research.deep_research

Flux L4a

🔍 Les repères : il n'y en a pas

dynamic=True

switch. "Dynamique" est une façon d'écrire, pas une configuration. Deux marqueurs, et seulement deux : @node(parallel_worker=True) (prend une liste de taille d'exécution, exécute un nœud de calcul par élément) et ctx.run_node(...) (planifie directement les nœuds de code). Si vous voyez l'une ou l'autre de ces options, vous êtes en mode dynamique.

Ce que vous verrez : le décomposeur imprime par exemple cinq sous-questions, il effectue des recherches en parallèle, puis il fournit un briefing synthétisé. Le nombre diffère à chaque exécution, ce que le graphique fixe ne pouvait pas faire.

Deux indicateurs sur le nœud de calcul sont à connaître :

  • rerun_on_resume=True est obligatoire sur tout nœud qui appelle ctx.run_node. Sans cela, ADK génère une ValueError. Lors de la reprise, il doit réexécuter le nœud de répartition pour reconstruire les enfants qu'il a générés, car ceux-ci ne figurent pas dans le graphique statique.
  • retry_config= bounds how this FAILS. Un nœud de calcul parallèle annule tous les nœuds de calcul frères et relance l'opération instantanément en cas d'échec d'un nœud de calcul enfant. Ainsi, sans nouvelle tentative, un seul code d'erreur 429 transitoire annule l'ensemble de l'exécution, y compris tous les appels déjà payés. La tentative de relance se produit au niveau du nœud interne par élément. Chaque branche effectue donc une nouvelle tentative de manière indépendante.

❓ Vous vous demandez peut-être : comment l'ADK "sait-il" que c'est dynamique ? Elle n'en a pas besoin, car rien n'est déclaré nulle part. Le décomposeur produit une liste au moment de l'exécution. Le nœud de calcul parallèle s'adapte à ce qui arrive. Le dynamisme est une propriété du flux de données que vous avez écrit, et non un mode que vous avez activé.

👀 Lire : les deux indicateurs sur research_topic : parallel_worker et rerun_on_resume. · ▶ Exécutez-le. · ✏️ Modifier : remplacez la question ouverte par la vôtre. N change, car la saisie a déterminé la largeur.

12. L4b · Ajouter la génération récursive (Pillar 3b)

Feuille de route : L4b

⚡ En bref : la récursivité est écrite, pas donnée. Le nœud de calcul s'appelle lui-même via ctx.run_node, un Python ordinaire. Le frein doit donc également être écrit. Il s'agit de la valeur MAX_DEPTH.

La question : parfois, un résultat de recherche fait apparaître un sous-thème spécifique qui mérite une étude à part entière. Comment faire en sorte qu'une branche génère plus de travail en parallèle tout en le gardant limité ?

Forme (désormais récursive) :

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

▶ Colab : exécutez la cellule L4b · 📁 GitHub : L4b_recursion/ · 💻 Local : python -m L4b_recursion.deep_research

Flux L4b

@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})

🔍 Les marqueurs : ctx.run_node(research_topic, ...) à l'intérieur de research_topic lui-même : l'auto-référence is est la récursion, et la garde depth < MAX_DEPTH se trouve une ligne au-dessus.

Ce que vous verrez : des nœuds de recherche affichant spawning N deeper (récursion en direct), puis une forme d'arborescence d'exécution (par exemple, 5 top-level + 10 recursive children). L'arborescence diffère à chaque exécution.

⚠️ Avant de passer à la vitesse supérieure : le plafond augmente rapidement. MAX_DEPTH=3 passe du pire scénario d'environ 30 appels à environ 93. À la toute fin d'une exécution, vous pouvez voir une ligne de journal cancelling N leftover tasks : cela signifie que l'ADK décompose son groupe de tâches parallèles une fois le résultat terminé. Il s'agit d'un message inoffensif. Selon votre configuration de journalisation, il est possible que vous ne le voyiez jamais.

❓ Vous vous demandez peut-être si le remarketing dynamique n'est pas récursif par défaut. Non. L4a est entièrement dynamique et ne comporte aucune récursion. Dynamic ne vous donne que le flux de contrôle Python ordinaire ; L4b choisit d'écrire la récursivité avec. Et comme vous avez écrit la récursion, vous devez écrire sa limite. C'est là que "laissez le LLM façonner le travail, gardez les limites dans le code" cesse d'être un slogan.

👀 Lu : la protection : if finding.needs_deeper and depth < MAX_DEPTH. · ▶ Exécutez-le. · ✏️ Modification : définissez MAX_DEPTH = 1 et réexécutez. L'arborescence est aplatie (et l'exécution devient moins coûteuse). La limite est la VÔTRE, dans le code.

13. L5 · Quel modèle utiliser ?

Feuille de route : vous êtes ici (niveau 5)

⚡ En bref : un axe décide de tout, qui choisit la prochaine étape : le graphique que vous avez dessiné, le LLM ou votre code.

Vous avez créé les trois. Le modèle qui les rend utiles est le suivant : faire correspondre le modèle à la forme de votre problème.

L'axe : qui décide de ce qui sera exécuté ensuite ?

Pilier

Qui décide de ce qui sera diffusé ensuite ?

Intégré

1 · Graphique

le graphique que vous avez dessiné ;

L2a / L2b

2 · Collaboration

le LLM

L3a / L3b

3 · Dynamique

votre code Python, au moment de l'exécution.

L4a / L4b

Étape 0 : Avez-vous vraiment besoin d'un graphique ?

ADK fournit des agents de workflow prédéfinis : SequentialAgent, ParallelAgent, LoopAgent. Pour une chaîne d'agents simple, il s'agit de la réponse correcte la moins chère et il n'y a pas de graphique à assembler. Contactez-les lorsque vous avez besoin d'un routage explicite (routeur L2b), d'une jointure (JoinNode L2a) ou de nœuds qui ne sont pas des agents (une fonction simple, sans appel LLM). C'est généralement la dernière option qui est la cause.

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 · quel modèle

Comparaison honnête entre 1.x et 2

Il ne s'agit pas de dire que la version 2.0 peut faire des choses que la version 1.x ne pouvait pas faire. La version 1.x pouvait tout faire. La version 2.0 attribue à chaque forme un emplacement plus direct. Ainsi, le flux de contrôle connu quitte l'invite et devient une structure que vous pouvez voir et tester.

Schéma

Coût de la version 1.x

Accueil de l'ADK 2

Graphes

4 appels LLM dans la compilation courante ; routage masqué dans un prompt

Nœuds de fonction et d'agent en tant que pairs → 1 appel, routeur d'instruction if

Collaboratif

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

une équipe déclarée : sub_agents + mode="single_turn"

Dynamique

La récursivité vous fait sortir du framework

parallel_worker + ctx.run_node récursif dans le framework

L'ensemble de l'application et ce qu'un graphique ne peut pas vous montrer

Vous avez maintenant créé chaque élément ci-dessous. Workflow expose sa structure à graph.edges. Cette image est donc générée à partir du code plutôt que dessinée à la main. Ce que l'introspection trouve est le résumé de cet atelier :

Pilier

Contenu de graph.edges

Pourquoi

1. Graphique (L2b)

10 arêtes, itinéraires et tout le reste

vous l'avez dessiné avant l'arrivée de toute entrée.

2 · Collaboratif (L3a)

0 bordure : uniquement sub_agents + mode

Le LLM sélectionne le sous-ensemble par requête.

3) Dynamique (L4a/L4b)

3 arêtes (identiques dans les deux)

la récursivité est écrite en Python, et non câblée dans le graphique.

Cette dernière ligne est la preuve de la réponse à la question L4b : L4a et L4b ont le même graphique, et une seule d'entre elles est récursive.

Ce que vous pouvez créer

Chaque motif que vous venez d'exécuter est une forme de produit réelle :

Vous avez pratiqué

Dans la nature,

Lieu de départ

Graphique + routeur (L2a/L2b)

Pipelines de documents, étapes ETL avec LLM, chaînes d'examen/d'approbation, harnais d'évaluation

L2b de ce dépôt

Coordinateur + équipe single_turn (L3a)

un copilote d'assistance avec des équipes spécialisées, des bureaux de triage et un examen multi-lentilles.

Démo marathon mode 2

Agents task (L3b)

Formulaires d'inscription, parcours de réservation, onboarding, KYC : tout ce qui consiste à "collecter des données, puis agir"

22_agent_in_workflow

Largeur/profondeur dynamique (L4a/L4b)

agents de recherche, générateurs de rapports, balayages d'audit sur des entrées de taille inconnue.

Mode 3 : démo marathon

Ils composent

Les trois modèles ne s'excluent pas mutuellement. Un nœud de graphique peut appeler un coordinateur collaboratif, et un spécialiste peut lancer un workflow dynamique. Choisissez le bon modèle pour chaque partie du problème. C'est ainsi que vous éviterez de transformer chaque système d'agent en un seul prompt géant.

L'ensemble de l'application et ce qu'un graphique ne peut pas vous montrer

💡 Essayez-le sur votre propre workflow : le script qui a généré ce graphique est scripts/graph_dump.py. Pointez-le sur n'importe quel Workflow et il imprimera les bords réels, ce qui vous donnera un schéma structurel sans frais de tout ce que vous construisez.

14. Félicitations

Neuf agents, un bâton, une fin ordonnée

Vous avez créé un coach pour le jour de la course et, au passage, les trois modèles d'orchestration d'ADK 2.

Ce que vous avez appris

  • Prologue : le méga-prompt qui a inventé sa propre météo, ou pourquoi la structure existe.
  • L0–L1 : Agent, Runner, un outil réel que le modèle choisit d'appeler, et votre premier Workflow (nœuds de fonction + nœuds d'agent en tant que pairs).
  • L2a / L2b : workflows de graphes : distribution ramifiée parallèle + JoinNode, puis routage déterministe : un appel LLM.
  • L3a : agents collaboratifs. La même équipe s'exécute dans chat (échoué) puis dans single_turn (sous-ensemble parallèle + synthèse) : un indicateur, deux mondes.
  • L3b : mode task : une question de clarification mise en pause, une reprise scriptée, finish_task renvoyant un objet validé.
  • L4a / L4b : workflows dynamiques : largeur d'exécution (distribution ramifiée), puis profondeur d'exécution (récursion) avec des limites dans le code.
  • L5 : l'arbre de décision et la façon dont les modèles se composent.

Lignes à conserver

Les fonctions préparent le contexte. Les arêtes définissent le workflow. Le routeur choisit le chemin. Le modèle rédige la réponse.

Laissez le LLM façonner le travail, mais gardez les limites dans le code.

Faites correspondre le modèle à la forme de votre problème.

Étapes suivantes

  • Exécutez l'application complète à partir de laquelle ces niveaux ont été extraits : Marathon Race Day Coach, une compilation FastAPI + SSE avec une interface utilisateur de navigateur affichant les trois modes en direct : github.com/cuppibla/adk-2-marathon-demo.
  • Élargissez vos connaissances : adk-workflows-compared : les 23 exemples de workflow ADK 2 officiels, chacun avec un port 1.x et des conseils d'utilisation. Commencez par docs/three-pillars.md, puis par les éléments que cet atelier de programmation a ignorés : 07_loop, 17_request_input et 22_agent_in_workflow.
  • Portez votre propre problème : quelles parties sont de structure connue (L2), d'équipe connue (L3a/L3b) et de forme inconnue (L4) ?
  • Explorez le code : github.com/cuppibla/adk2-tutorial.
  • Vous avez suivi l'atelier ? Votre crédit et le projet qu'il a créé ne sont pas éternels. Pour continuer à exécuter ces niveaux sans frais, suivez plutôt l'étape Configuration à emporter : une clé AI Studio sans frais, sans projet Cloud ni facturation. Le seul changement consiste à inverser ces deux cellules.