Orchestrazione ADK 2: grafici, flussi di lavoro collaborativi e dinamici

1. Panoramica

Il titolo dell'ADK 2 è Tre pattern di orchestrazione. Questo codelab insegna tutti e tre gli aspetti creando un'app, un allenatore per il giorno della maratona, un'unità eseguibile alla volta. Ogni livello risponde a una singola domanda, aggiunge un'idea e viene eseguito in modo indipendente.

Obiettivi didattici

  • Flussi di lavoro del grafico (pilastro 1): quando puoi disegnare il flusso prima dell'arrivo dell'input.
  • Agenti collaborativi (pilastro 2): quando conosci il team, ma la richiesta seleziona il sottoinsieme e tutte e tre le modalità di collaborazione (chat / task / single_turn), ciascuna eseguita in tempo reale.
  • Workflow dinamici (pilastro 3): quando la forma del lavoro stesso dipende dall'input.
  • Come scegliere: un albero decisionale a una domanda e come si compongono i pattern.

Il filo conduttore

struttura nota → team / sottoinsieme di variabili noto → forma sconosciuta → scegli quella giusta

La tua roadmap di apprendimento

Cosa creerai

Un'app, Marathon Race Day Coach, ha assemblato un livello giocabile alla volta. Ogni livello è un semplice modulo Python che esegui dal terminale; al livello 5, i componenti riportati di seguito sono tutti tuoi.

L'immagine è tratta dal codice in esecuzione: ogni linea continua è stata letta da Workflow.graph.edges. La prima lezione è che le parti che possono essere disegnate in anticipo sono esattamente il Pilastro 1, mentre le parti che non possono esserlo sono il motivo per cui esistono i Pilastri 2 e 3.

L'intera app e ciò che un grafico non può mostrarti

Che cosa ti serve

  • Un Account Google (per Colab) senza necessità di configurazione locale.
  • Circa 50 minuti (i due livelli L4 sono quelli più lunghi, quindi pianificali).
  • Uno dei due modi per accedere a un modello Gemini. Scegli la tua strada: esegui un passaggio di configurazione e salta l'altro:

🎓 Workshop

🏠 Da portare a casa

Chi

Ti trovi in un workshop dal vivo e l'insegnante ti ha fornito un link per richiedere un credito.

Tutti gli altri, inclusi i partecipanti al workshop, in seguito

Cosa ti serve

Il link di rivendicazione e un Account Google in grado di creare un progetto Cloud

Una chiave API AI Studio senza costi

Funziona su

Vertex AI, in un progetto fatturato al tuo credito del workshop

Google AI Studio

Costo

Coperto dal credito

Livello senza costi

Passaggio di configurazione

Configurazione del workshop (passaggio successivo)

Configurazione da fare a casa (il passaggio successivo)

Tutto ciò che segue il prologo è identico in entrambi i casi: la corsia decide solo con quale endpoint del modello comunica il notebook.

Due modi per seguire

Ogni passaggio riportato di seguito corrisponde a una cella nel notebook di Colab e a una cartella nel repository GitHub. Scegli una delle seguenti opzioni:

  • ▶ Colab (consigliato): apri il notebook → esegui le celle dall'alto verso il basso.
  • 💻 Locale: git clone il repository, ./setup_venv.sh, quindi esegui ogni livello come modulo (python -m ...) o sfogliali tutti con ./run.sh (adk web).

2. Configurazione del workshop · Richiedi il credito e passa a Vertex AI

Durante il workshop ti viene fornito un credito Google Cloud. Lo rivendicherai, creerai un progetto fatturato e indirizzerai il notebook a Vertex AI anziché ad AI Studio. Una cella esegue tutto ciò che segue la richiesta.

1. Richiedi il credito (~1 min)

  1. Apri il link per il riscatto condiviso dal tuo insegnante. Sembra https://me.developers.google.com/benefits/claim/your-workshop-name.
  2. Accedi e segui la pagina per accettare il credito.
  3. Prendi nota dell'Account Google che hai utilizzato. Ogni passaggio riportato di seguito deve essere eseguito con lo stesso account.

2. Apri il notebook e installa ADK 2 (~1 minuto)

Fai clic su Apri in Colab ▶, quindi esegui la prima cella di codice. Blocca la versione esatta di ADK 2 su cui è stato verificato questo codelab e stampa ✓ installed.

3. Esegui la cella "Workshop setup" (~3 min)

Si tratta della cella intitolata 🎓 Percorso A · Workshop. Esegui il comando e Colab ti chiederà l'autorizzazione. Scegli lo stesso Account Google con cui hai appena richiesto il credito e consenti l'accesso.

Svolge quattro operazioni: crea un progetto denominato adk-2-tutorial-XXXX sul tuo credito, abilita l'API Vertex AI, imposta le quattro variabili di ambiente che ogni cella successiva legge e poi effettua una chiamata di test a Vertex e attende la risposta, in modo che la configurazione termini di funzionare o ti dica il motivo, anziché non riuscire in un secondo momento all'interno di un livello.

Output previsto: l'ultima riga è quella che conta:

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. Salta il passaggio "Configurazione da fare a casa"

Non eseguire la cella della chiave AI Studio, altrimenti il notebook tornerà ad AI Studio e annullerà le modifiche appena apportate. La cella è protetta da questo problema e si rifiuterà di essere eseguita, ma la mossa più ordinata è semplicemente quella di saltarla. Vai direttamente da qui alla cella Componenti condivisi.

5. Esegui la cella "Componenti di base condivisi"

Esegui una volta. Definisce gli schemi Pydantic e gli scenari di maratona predefiniti che ogni livello da L2 in poi riutilizza. Vedrai ✓ schemas + scenarios ready.

Dopo il workshop

Il credito e il progetto che ha creato non dureranno per sempre. Per continuare a eseguire questi livelli senza costi al termine del workshop, esegui il passaggio Configurazione da portare a casa, che prevede una chiave AI Studio senza costi, nessun progetto Cloud e nessuna fatturazione. Quella cella è l'unica cosa che cambia.

Per liberare spazio prima, apri la console Cloud, seleziona adk-2-tutorial-XXXX ed eliminalo. Nient'altro in questo codelab crea risorse fatturabili.

3. Configurazione da casa · Chiave API AI Studio

Tutto ciò che si trova in questo percorso viene eseguito con una chiave API Google AI Studio senza costi: nessun progetto cloud Google, nessuna fatturazione, nessuna installazione locale. L'intero passaggio richiede circa 3 minuti.

1. Apri il notebook

Fai clic su Apri in Colab ▶. Si aprirà il notebook, con un'introduzione in markdown e una cella eseguibile per livello. Le celle vengono eseguite dall'alto verso il basso e ognuna stampa il proprio output subito sotto.

2. Installa ADK 2 (~1 minuto)

Esegui la prima cella di codice. Blocca la versione esatta su cui è stato verificato questo codelab:

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

Attendi il completamento dell'operazione. Vedrai ✓ installed. L'installazione richiede circa 30-60 secondi la prima volta; in seguito viene memorizzata nella cache.

3. Ottieni la chiave API Gemini da AI Studio (~1 minuto)

  1. Apri aistudio.google.com/app/apikey in una nuova scheda del browser.
  2. Esegui l'accesso con il tuo Account Google.
  3. Fai clic su Crea chiave API (in alto a destra).
  4. Scegli un progetto Google esistente o creane uno.
  5. Copia la chiave, inizia con AIza... e contiene circa 40 caratteri.

4. Aggiungi la chiave a Colab (~1 minuto)

Opzione A: Colab Secrets (consigliata; la chiave rimane nascosta):

  1. Fai clic sull'icona della chiave 🔑 nella barra laterale sinistra di Colab.
  2. Fai clic su + Aggiungi nuovo segreto.
  3. Imposta Nome esattamente su GOOGLE_API_KEY.
  4. Incolla la chiave in Valore.
  5. Imposta l'opzione Accesso al notebook su ON.

Opzione B: incolla quando richiesto (rapido). Salta il segreto. Quando esegui la cella successiva, viene visualizzato un prompt nascosto 🔑 Enter your Google AI Studio API key:. Incolla e premi Invio.

5. Esegui la cella della chiave

Legge il segreto (o torna al prompt di incolla), quindi indirizza ADK a AI Studio (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.")

Output previsto: ✅ API key set — using Google AI Studio.

6. Esegui la cella "Componenti di base condivisi"

Esegui la cella Componenti di base condivisi una volta. Definisce gli schemi Pydantic e gli scenari di maratona predefiniti che ogni livello da L2 in poi riutilizza. Vedrai ✓ schemas + scenarios ready.

La configurazione è completata. 🎽 Una piccola deviazione prima di L0, la versione che tutti creano per prima.

4. Prologo · Perché non un unico prompt?

⚡ Prima di eseguire il test, concentrati su un aspetto specifico da osservare: da dove proviene ogni numero specifico? Questo è l'intero esercizio, tutto il resto è decorazione.

Prima della scaletta, esegui l'attività che la scaletta sostituisce: un agente il cui prompt promette tutto: recupera le informazioni meteo, analizza il percorso, leggi il registro di allenamento, calcola il percorso in base alle condizioni e genera il piano.

Cosa vedrai:una strategia sicura, specifica e ben formattata... i cui numeri sono inventati. In un test dal vivo si è aperto con "Ho recuperato le metriche meteo di oggi" e ha riportato 11 °C, un vento di 14 km/h e un'analisi di un registro di allenamento che non aveva mai visto. Non esiste un'API meteo, nessun dato sul percorso, nessun log: una chiamata di modello opaco inventa i suoi input o li rende inutili.

Questa è la patologia e ha quattro sintomi che vale la pena menzionare:

  1. Non puoi fidarti: i dati sono inventati, in modo fluente.
  2. Non puoi testarlo: il routing del passaggio 4 si trova all'interno del testo; non esiste un if per il test unitario.
  3. Non puoi scambiare un passaggio: non c'è un punto di connessione per un'API meteo reale.
  4. Paghi tutto, ogni volta: cinque passaggi, una chiamata gigante, nessuna memorizzazione nella cache di una parte deterministica.

Mantieni questa sensazione. I nove livelli successivi rimuovono i passaggi dal prompt, uno alla volta: recupero delle funzioni (L1-L2a), un'istruzione if instrada (L2b), gli specialisti dividono il lavoro (L3a-L3b) e il codice delimita la forma (L4a-L4b).

Il coach di mega-prompt: sicuro, senza nulla dietro il grafico

💻 Locale: python -m shared.prologue

5. L0 · Il tuo primo agente ADK 2

Roadmap: ti trovi qui: L0

⚡ In breve: un agente è un modello + un'istruzione + strumenti che può chiamare; un Runner lo esegue. Tutto ciò che segue questo livello è costituito da altri agenti, disposti in forme migliori.

La domanda:puoi fare in modo che un modello risponda e utilizzi codice reale quando l'aritmetica è importante?

Un'idea in tre parti:

  • Agent: l'elemento che ragiona (un modello Gemini + un'istruzione).
  • Runner: l'elemento che esegue un agente all'interno di una sessione e trasmette gli eventi in streaming.
  • uno strumento: una semplice funzione Python (pace_splits) che il modello decide di chiamare. ADK legge la firma e la docstring e fornisce al modello una dichiarazione; non è necessario scrivere schemi.

Dopo il prologo, questo è il primo problema: un LLM che esegue calcoli di ritmo a mente sbaglierà volentieri. pace_splits è Python deterministico, quindi i numeri nella risposta sono calcolati, non improvvisati.

Colab: esegui la cella L0 · 📁 GitHub: L0_first_agent/ · 💻 Locale: python -m L0_first_agent.agent

Flusso 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

🔍 I marcatori: Agent(...) · tools=[pace_splits] · Runner(...). Nell'output, la riga 🔧 indica che il modello ha deciso, a metà della risposta, di chiamare il tuo codice.

Cosa vedrai:

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

Le righe 🔧 sono la lezione: a metà della risposta, il modello ha scelto di chiamare la tua funzione e il 8:00/mile esatto nella sua risposta proviene dal tuo codice, non dalle statistiche sui token.

Ti starai chiedendo: il modello chiama sempre lo strumento? No, decide per domanda. Se chiedi qualcosa che non contiene numeri, le linee 🔧 scompaiono (il playground ti chiede di provare proprio questo).

👀 Leggi: pace_splits (una funzione semplice) e la riga tools=[pace_splits]. · ▶ Esegui. · ✏️ Modifica:poni la domanda generale (senza orario obiettivo). Nota che le linee 🔧 scompaiono: il modello decide quando vale la pena chiamare uno strumento. Poi riscrivi instruction ed esegui di nuovo: l'istruzione è il resto del programma.

6. L1 · Il tuo primo workflow

Roadmap - you are here: L1

⚡ In breve: una funzione semplice e un agente LLM sono lo stesso tipo di nodo. Lavoro prevedibile → funzione (0 LLM, deterministica); ragionamento → agente.

La domanda:come si combinano codice semplice e un LLM in un unico flusso, senza pagare una chiamata al modello per le parti che sono solo codice?

L'idea: in un Workflow, una funzione Python semplice e un agente LLM sono entrambi nodi nella stessa lista edges.

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

Colab: esegui la cella L1 · 📁 GitHub: L1_graph_basics/ · 💻 Locale: python -m L1_graph_basics.workflow

Flusso L1

Il nodo della funzione stampa i dati che ha prodotto (nessuna chiamata al modello), poi l'agente fornisce un consiglio che fa riferimento alla temperatura e al vento effettivi che ha ricevuto:

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

🔍 I marcatori: una tupla di bordi, (START, fetch_conditions, advise), con una funzione Python semplice al centro e input_schema= che convalida il trasferimento.

Novità rispetto al livello 0: Workflow(edges=[...]), START (dove vengono inseriti gli input), un nodo di funzione che restituisce Event(output=...) e input_schema=Conditions, in modo che l'output della funzione venga convalidato in base a questo schema prima che l'agente lo veda (come testo JSON: input_schema convalida il limite, non fornisce all'agente un oggetto Python).

Ti starai chiedendo: la sequenza funzione-agente è obbligatoria? No, qualsiasi ordine, qualsiasi mix, qualsiasi conteggio. advise viene eseguito per secondo solo perché ha bisogno dei dati di fetch_conditions. La lezione è la nobiltà, non la sequenza.

👀 Leggi: fetch_conditions restituisce dati senza chiamata al modello; advise ha input_schema=Conditions. · ▶ Esegui. · ✏️ Modifica:imposta temp_f=30 nella funzione ed esegui di nuovo. Il consiglio viene invertito e la funzione costa comunque 0 chiamate LLM.

7. L2a · Parallel fan-out + JoinNode (Pillar 1a)

Roadmap - you are here: L2a

⚡ TL;DR: distribuisci in parallelo (senza costi), attendi tutti, raggruppa, fornisci a un agente il quadro completo.

La domanda:puoi disegnare il flusso prima che arrivi l'input. Inizia con la struttura: raccogli i dati in parallelo, raggruppali e consegnali a un agente.

La forma:

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

Colab: esegui la cella L2a · 📁 GitHub: L2a_parallel_join/ · 💻 Locale: python -m L2a_parallel_join.workflow

Flusso L2a

🔍 I marcatori:tre bordi che iniziano tutti da START, ovvero la ventola, e JoinNode, il punto di incontro.

  • I tre recuperi sono funzioni: vengono eseguiti in parallelo, 0 chiamate LLM.
  • JoinNode attende tutti e tre e li raggruppa in un unico payload digitato (BundledRunData), con chiave in base al nome della funzione.
  • Un agente strategy legge il pacchetto e scrive una RaceStrategy.

Cosa vedrai: ogni recupero stampa un timestamp started / finished. Tutti e tre iniziano a 0,0 secondi e il fan-out termina a 2,0 secondi, ovvero il recupero più lento, non a 4,5 secondi, la somma delle loro durate. Questa sovrapposizione è il parallelismo. Il tempo totale di esecuzione total stampato alla fine è di circa 8 secondi perché contiene anche la chiamata LLM dell'agente di strategia. Leggi i timestamp di recupero per l'attestazione parallela, non il totale.

💡 Callback del prologo: il mega-prompt ha inventato il suo tempo. Qui la temperatura viene estratta da una funzione di recupero: codice reale, cucitura reale. Scambia la risposta predefinita con un'API meteo effettiva e non cambia nient'altro.

Forse ti starai chiedendo: quanto

JoinNode

devo capire? In una frase: attende che ogni ramo parallelo termini, comprime gli output in un dizionario con chiave il nome della funzione upstream e non calcola nulla. Questo dizionario è il motivo per cui il router di L2b può scrivere node_input["fetch_weather"]["temp_f"].

👀 Leggi:tre bordi si aprono a ventaglio da START; JoinNode li raggruppa per un agente. · ▶ Esegui il test e leggi i timestamp, non il totale. · ✏️ Modifica:esegui un recupero della sospensione 3.0. Prevedi prima il nuovo orario di fine del fan-out, poi verifica.

8. L2b · Aggiungere il router deterministico (pilastro 1b)

Roadmap: ti trovi qui: L2b

⚡ TL;DR: L2a invariato + un semplice if decide quale agente viene eseguito. Diramazione, senza chiedere al modello.

La domanda:il piano deve essere diverso per il clima caldo e freddo. Come si fa a creare ramificazioni senza chiedere al modello di decidere?

La forma (L2a + un router):

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

Colab: esegui la cella L2b. Prova run("NORMAL") / run("COLD"). · 📁 GitHub: L2b_router/ · 💻 Locale: python -m L2b_router.workflow COLD

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

🔍 I marcatori: Event(output=..., route=...): un nodo di funzione che assegna un nome al percorso e il dict-edge {"HOT": ..., "NORMAL": ..., "COLD": ...} che mappa i nomi ai nodi.

Il risultato: tre tipi di lavoro, tre case:

  • Predictable work → functions (i tre recuperi paralleli)
  • Una regola chiara → routing esplicito (route_by_weather è un'istruzione if, non una decisione del modello)
  • Ragionamento → il modello (viene eseguito esattamente un agente di strategia)

Cosa vedrai: temp=78F -> route=HOT, poi un RaceStrategy strutturato. Costo netto: 1 chiamata LLM.

⚠️ Se aggiungi un quarto ramo,fornisci anche una voce DEFAULT_ROUTE per route-dict. Una route che non corrisponde al dizionario non è un errore: il ramo termina semplicemente e il programma esce con 0 senza output, il che è un vicolo cieco difficile da eseguire il debug.

Ti starai chiedendo: quindi L2b è letteralmente L2a più un router? Sì, i recuperi e l'unione rimangono invariati e si tratta ancora esattamente di una chiamata LLM. Cosa è cambiato: "sempre lo stesso agente" è diventato "uno dei tre, scelto in base ai dati".

👀 Leggi : route_by_weather il router è un'if, non un agente. · ▶ Esegui run("COLD"). · ✏️ Modifica: aggiungi un ramo WINDY con un quarto agente e leggi l'avviso DEFAULT_ROUTE riportato sopra prima di procedere.

9. L3a · Agenti collaborativi: un'unica segnalazione, due mondi - Pilastro 2

Roadmap - you are here: L3a

⚡ TL;DR: stessa squadra, una sola bandiera. chat passa l'intera conversazione a un solo specialista e non torna più indietro; single_turn trasforma ogni specialista in uno strumento: sottoinsieme parallelo, ritorno automatico, una sintesi.

La domanda:conosci il team, ma la richiesta decide quali membri devono rispondere. Come fai a lasciare che un LLM scelga il sottoinsieme e a eseguirli contemporaneamente?

La forma:un coordinatore di sei specialisti (medici, meteo, ritmo, attrezzatura, nutrizione, mentale). In questo livello, lo stesso team viene eseguito due volte: stesso prompt del coordinatore, stessi sei specialisti. L'unica differenza è un flag sugli agenti secondari. Il contrasto è la lezione.

Colab: esegui la cella L3a · 📁 GitHub: L3a_collaborative/ · 💻 Locale: python -m L3a_collaborative.concierge --mode chat "What about fueling?"

L3a flow

🔍 I marcatori : mode="single_turn" in fabbrica e nell'output, TRANSFER → (battuta 1) rispetto a una sequenza di DISPATCH → righe che condividono un timestamp (battuta 2).

Beat 1: esegui il primo test predefinito e osserva l'errore del job

Nessun mode= scritto → i subagenti utilizzano per impostazione predefinita chat. Cosa vedrai:

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

Il coordinatore non ha strumenti di delega: i subagenti della chat gli forniscono solo transfer_to_agent, un trasferimento seriale dell'intera conversazione a un solo esperto. L'esperto risponde direttamente all'utente e la corsa termina qui. Nessuna spedizione parallela. Nessun reso. Nessuna sintesi. Poni la domanda generica e la situazione peggiora: sei specialisti, un trasferimento.

Non si tratta di un bug, ma della modalità Chat che fa il suo lavoro. La conversazione appartiene a chi la detiene, finché qualcuno non la trasferisce esplicitamente. Corretto per un assistente open-ended, errato per un passaggio della pipeline.

Momento 2: una bandiera, due mondi

L'unica differenza: mode="single_turn" per ogni specialista. Stessa domanda, esegui di nuovo:

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

Ora ADK inserisce uno strumento di delega per specialista, denominato in base al subagente e descritto dal relativo description= (questo testo è quello che legge il coordinatore quando sceglie il sottoinsieme; se lo salti, il routing viene eseguito solo in base ai nomi). Il coordinatore emette diverse chiamate in un turno, ADK le esegue in parallelo, ognuna restituisce automaticamente il proprio risultato e il coordinatore esegue la sintesi.

Domanda

Specialisti che sparano

"E il rifornimento?"

solo nutrizione

"Mi fa male il ginocchio al miglio 18"

solo medico

"Devo correre oggi?"

medico + meteo + andatura

"C'è qualcosa di cui dovrei preoccuparmi?"

tutte e 6

Perché ogni specialista riceve l'intero briefing:ogni single_turn subagente viene eseguito nel proprio ramo di sessione isolato, quindi non può vedere la conversazione o i suoi colleghi. Niente è ambientale: il coordinatore deve inoltrare l'intero SpecialistInput (domanda + strategia + dati del runner) separatamente in ogni chiamata parallela.

💡 Dove ADK 2 fornisce una casa diretta: un LLM sceglie un sottoinsieme per richiesta E lo esegue in parallelo, dichiarato tramite sub_agents + mode="single_turn". In 1.x, potevi assemblare la stessa forma racchiudendo ogni specialista in AgentTool; la differenza è che ora si tratta di una dichiarazione anziché di un'infrastruttura. ParallelAgent è sempre tutto e transfer_to_agent è seriale.

⚠️ Due avvertenze oneste: (1) il modello sceglie il sottoinsieme, quindi è meno deterministico del router hardcoded di L2. Il sottoinsieme esatto può variare da un'esecuzione all'altra. (2) Di tanto in tanto vedrai una riga Error validating input: ... per un solo specialista. Quasi mai l'output dello specialista, output_schema fa in modo che Gemini lo applichi lato server. È l'input: il coordinatore deve riprodurre l'intero SpecialistInput nidificato verbatim per ogni chiamata parallela e a volte ne sbaglia una. ADK restituisce l'errore come risultato dello strumento, il coordinatore viene ripristinato e la sintesi viene comunque eseguita.

Ti starai chiedendo: è

chat

Solo delega in stile 1.x, ovvero un solo agente alla volta? Sì, in sostanza: è il comportamento predefinito della versione 1.x, ora con un nome. Il divario rispetto a single_turn è tridimensionale: cosa detiene il coordinatore (un transfer_to_agent rispetto a uno strumento per specialista), quanti possono lavorare (uno, proprietario della conversazione, rispetto a N in parallelo), se il controllo viene restituito (mai rispetto a automaticamente, con i risultati). Per quanto riguarda il codice, il ramo if mode == della fabbrica esiste solo per consentire a un team di creare in entrambi i modi per questo contrasto:un'app reale codifica in modo permanente una modalità e if scompare.

👀 Leggi: la _specialist fabbrica: il parametro mode è l'intero livello. · ▶ Esegui entrambi i ritmi. · ✏️ Modifica: chiedi "mi fa male il ginocchio al miglio 18". Prevedi prima il sottoinsieme, poi controlla le righe 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 · Task mode: a conversation with a finish line — Pillar 2

Roadmap - you are here: L3b

⚡ In breve: la modalità intermedia: parla con l'utente finché i campi non vengono raccolti, poi torna automaticamente con un oggetto con convalida.

La domanda: L3a ha lasciato un vuoto. chat è il proprietario dell'intera conversazione; single_turn non parla mai con l'utente. Ma il vero lavoro di acquisizione si trova nel mezzo: "parla con l'utente FINCHÉ non hai raccolto X, poi torna con un oggetto convalidato". Quale modalità?

La forma:

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

Colab: esegui la cella L3b · 📁 GitHub: L3b_task_desk/ · 💻 Locale: python -m L3b_task_desk.desk

Flusso L3b

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

🔍 I marcatori: mode="task" + output_schema= sullo stesso agente e nell'output, la pausa ⏸ e la chiamata finish_task.

Cosa vedrai:

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

Si sono verificate tre cose che la modalità L3a non può fare:

  1. L'esecuzione si è interrotta a metà dell'attività: un'attività in pausa, non un blocco e non un errore. L'agente ha posto la sua domanda di chiarimento e ha lasciato aperta l'attività. In adk web ti basterà digitare la risposta; lo script la considera come un secondo messaggio nella stessa sessione.
  2. Il messaggio successivo ha ripreso lo STESSO agente di attività: nessun reindirizzamento, nessuna riassegnazione. La sessione sa chi era in attesa.
  3. finish_task ha terminato: uno strumento inserito da ADK perché di mode="task". L'agente deve chiamarlo per completare l'operazione e il relativo payload deve essere convalidato in base a output_schema. Una conversazione con un traguardo digitato, poi il controllo torna automaticamente al coordinatore e il risultato viene allegato.

La regola di una domanda per scegliere una modalità

💡 "L'utente deve interagire con l'AI e fino a QUANDO?" chat = indefinitamente · task = fino alla raccolta dei campi · single_turn = mai.

Modalità

Human-in-the-loop

Parallelismo?

Torna al genitore

chat (subagente predefinito): assistente di supporto, copilota open-ended

conversazione completa

no

manuale (tramite trasferimento)

task: inserimento, prenotazione, risoluzione dei problemi

solo domande di chiarimento

no

automatico (tramite finish_task, con un oggetto convalidato)

single_turn — classifica · estrai · giudica · genera

nessuno

yes

automatico (con il relativo risultato)

mode va solo sugli agenti secondari, mai sul coordinatore. I nodi del flusso di lavoro sono impostati per impostazione predefinita su single_turn (motivo per cui i livelli 1-2b non lo hanno mai scritto), mentre gli agenti secondari sono impostati per impostazione predefinita su chat (motivo per cui il livello 3a ha dovuto farlo).

⚠️ Due note sulla versione prima di procedere: (1) task

come nodo del grafico statico dipende dalla versione: nelle versioni 2.0.0b1-2.3.0 (il pin di questo codelab), Workflow(...) viene generato durante la costruzione; utilizza esattamente ciò che fa questo livello (un coordinatore di chat con subagenti di attività) o invia tramite ctx.run_node. Aggiornato nella versione 2.5.0. (2) "Gli agenti di attività devono essere agenti foglia" (non devono avere subagenti) è una limitazione documentata di ADK, ma un contratto, non una protezione di runtime: né la versione 2.3.0 né la 2.5.0 ti impediranno di farlo. Non interpretare l'assenza di un errore come autorizzazione.

💡 Scopri di più:un agente task incorporato in un flusso di lavoro grafico (forma 2.5.0+), con routing che può riportare la conversazione indietro per un nuovo tentativo: repository complementare 22_agent_in_workflow · guida alla modalità completa: docs/agent-modes.md.

Ti starai chiedendo: che cosa fa

task

buy me that the other two can't? Tre cose: ritorno automatico (la chat porta via la conversazione) · un traguardo digitato (il payload di finish_task deve essere convalidato in base allo schema: ricevi i dati, non una trascrizione) · pausa/ripristino (il simbolo ⏸ indica un'attività in attesa di un intervento umano, non un blocco).

👀 Leggi: gear_fittermode="task" + output_schema è l'intero contratto. · ▶ Esegui. · ✏️ Modifica: run_desk("I need a hydration vest", "2 liters, medium") la domanda chiarificatrice si adatta, il traguardo rimane digitato.

11. L4a · Fan-out parallelo dimensionato in base al runtime (pilastro 3a)

Roadmap - you are here: L4a

⚡ In breve: la struttura è ancora composta da tre passaggi statici. La parte dinamica si nasconde all'interno di quella centrale, dove la larghezza viene decisa dai dati in fase di runtime.

⚠️ Attenzione: questo è il gradino più ripido della scala. Il livello precedente era di 44 righe, questo è di circa 120: tre agenti e due nodi del flusso di lavoro, e nessuno di questi è padding. Dedica circa 15 minuti e concentrati sulla riga Leggi/Esegui/Modifica alla fine: non è necessario memorizzare ogni riga al primo passaggio.

La domanda:la forma dell'opera dipende dall'input. Non puoi disegnare il grafico in anticipo. Inizia con la larghezza del runtime: lascia che l'LLM decida quante domande secondarie.

La forma (un livello di profondità):

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

Una domanda aperta viene decomposta in N sotto-domande. N viene scelto dall'LLM in fase di runtime (3-7). Ogni sotto-domanda viene analizzata in parallelo e poi sintetizzata in un briefing.

Colab: esegui la cella L4a · 📁 GitHub: L4a_flat_research/ · 💻 Locale: python -m L4a_flat_research.deep_research

Flusso L4a

🔍 I marcatori: non esiste

dynamic=True

. Dynamic è un modo di scrivere, non una configurazione. Due marcatori e solo due: @node(parallel_worker=True) (accetta un elenco dimensionato in base al runtime, esegue un worker per elemento) e ctx.run_node(...) (pianifica direttamente i nodi di pianificazione del codice). Se vedi uno dei due, significa che stai utilizzando la funzionalità dinamica.

Cosa vedrai: il decompositore stampa, ad esempio, 5 domande secondarie, che vengono ricercate in parallelo, poi un briefing sintetizzato. Il numero varia a ogni esecuzione, cosa che il grafico fisso non poteva fare.

Due flag sul worker che vale la pena comprendere:

  • rerun_on_resume=True è obbligatorio su qualsiasi nodo che chiama ctx.run_node. L'ADK genera un ValueError senza. Al ripristino, deve eseguire nuovamente il nodo di distribuzione per ricreare i figli generati, poiché non si trovano nel grafico statico.
  • retry_config= bounds how this FAILS. Un worker parallelo annulla ogni fratello e ripropone l'immediato fallimento di un figlio, quindi senza un nuovo tentativo, un singolo errore 429 transitorio scarta l'intera esecuzione, inclusa ogni chiamata già pagata. Il nuovo tentativo viene eseguito sul nodo interno per elemento, quindi ogni ramo viene ritentato in modo indipendente.

Ti starai chiedendo: in che modo ADK "sa" che si tratta di un elemento dinamico? Non è necessario, in quanto non viene dichiarato nulla. Il decompositore produce un elenco in fase di runtime; il worker parallelo si dimensiona in base a ciò che arriva. Il dinamismo è una proprietà del flusso di dati che hai scritto, non una modalità che hai attivato.

👀 Leggi i due flag su research_topic:parallel_worker e rerun_on_resume. · ▶ Esegui. · ✏️ Modifica:sostituisci la domanda aperta con una tua. N modifiche perché l'input ha deciso la larghezza.

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

Roadmap - you are here: L4b

⚡ TL;DR: la ricorsione è scritta, non data: il worker chiama se stesso tramite ctx.run_node, Python normale, quindi anche l'interruzione deve essere scritta. Corrisponde a MAX_DEPTH.

La domanda:a volte, un risultato di ricerca mette in evidenza un argomento secondario ristretto che merita un'indagine a parte. Come fai a consentire a un ramo di generare più lavoro parallelo e a mantenerlo limitato?

La forma (ora ricorsiva):

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

Colab: esegui la cella L4b · 📁 GitHub: L4b_recursion/ · 💻 Locale: python -m L4b_recursion.deep_research

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

🔍 I marcatori: ctx.run_node(research_topic, ...) all'interno di research_topic stesso, l'auto-riferimento è la ricorsione e la guardia depth < MAX_DEPTH una riga sopra.

Cosa vedrai:nodi di ricerca che stampano spawning N deeper, ricorsione in tempo reale, quindi una forma ad albero di runtime (ad es. 5 top-level + 10 recursive children). L'albero è diverso a ogni esecuzione.

⚠️ Prima di aumentare il valore: il limite massimo cresce rapidamente. MAX_DEPTH=3 passa dal caso peggiore di circa 30 chiamate a circa 93. Alla fine di un'esecuzione potresti visualizzare una riga di log cancelling N leftover tasks: ADK sta smantellando il gruppo di attività parallele dopo che il risultato è già stato completato. Innocuo e, a seconda della configurazione della registrazione, potresti non vederlo mai.

Ti starai chiedendo: il ricorsivo dinamico non è predefinito? No, L4a è completamente dinamico con ricorsione zero. Dynamic ti offre solo il flusso di controllo Python ordinario; L4b sceglie di scrivere la ricorsione con esso. E poiché tu hai scritto la ricorsione, tu devi scrivere il suo limite, ovvero il punto in cui lo slogan "lascia che sia il modello linguistico di grandi dimensioni a dare forma al lavoro, mantieni i limiti nel codice" smette di essere uno slogan.

👀 Leggi la guardia: if finding.needs_deeper and depth < MAX_DEPTH. · ▶ Esegui. · ✏️ Modifica:imposta MAX_DEPTH = 1 ed esegui di nuovo. L'albero viene compresso (e l'esecuzione diventa più economica). Il limite è TUO, nel codice.

13. L5 · Which pattern should you use?

Roadmap: ti trovi qui: L5

⚡ TL;DR: un asse decide tutto: chi sceglie il passaggio successivo: il grafico che hai disegnato, l'LLM o il tuo codice.

Hai costruito tutti e tre. Questo è il modello che li rende utili: corrispondono al pattern della forma del tuo problema.

L'asse: chi decide cosa verrà eseguito successivamente?

Pilastro

Chi decide cosa viene riprodotto dopo

Integrato

1 · Grafico

il grafico che hai disegnato

L2a / L2b

2 · Collaborative

l'LLM

L3a / L3b

3 · Dinamico

il codice Python, al runtime

L4a / L4b

Passaggio 0: hai davvero bisogno di un grafico?

ADK include agenti del flusso di lavoro predefiniti: SequentialAgent, ParallelAgent, LoopAgent. Per una semplice catena di agenti, queste sono le risposte corrette più economiche e non c'è alcun grafico da assemblare. Superali quando hai bisogno di un routing esplicito (router L2b), di un'unione (JoinNode di L2a) o di nodi che non sono agenti (una semplice funzione, zero chiamate LLM). Quest'ultima è di solito la ragione.

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

Il confronto onesto tra le versioni 1.x e 2

Non si tratta di "la versione 2.0 può fare cose che la 1.x non poteva fare", perché la versione 1.x poteva fare tutto. La novità è che 2.0 assegna a ogni forma una posizione più diretta, quindi il flusso di controllo noto lascia il prompt e diventa una struttura che puoi vedere e testare.

Pattern

Costo della versione 1.x

La home page di ADK 2

Grafico

4 chiamate LLM nella build comune; routing nascosto in un prompt

funzione + nodi agente come peer → 1 chiamata, router di istruzioni if

Collaborativo

applicabile a strati tramite AgentTool; ParallelAgent sempre tutto, transfer_to_agent seriale

una squadra dichiarata: sub_agents + mode="single_turn"

Dinamico

la ricorsione ti fa uscire dal framework

parallel_worker + ctx.run_node ricorsivo all'interno del framework

L'intera app e ciò che un grafico non può mostrare

Ora hai creato tutti i pezzi riportati di seguito. Workflow espone la sua struttura all'indirizzo graph.edges, quindi questa immagine è generata dal codice anziché disegnata a mano. Inoltre, ciò che l'introspezione trova è il riepilogo di questo lab:

Pilastro

Cosa contiene graph.edges

Perché

1 · Grafico (L2b)

10 archi, percorsi e tutto il resto

l'hai disegnato prima che arrivasse qualsiasi input

2 · Collaborative (L3a)

0 bordi: solo sub_agents + mode

l'LLM sceglie il sottoinsieme per richiesta

3. Dinamico (L4a/L4b)

3 lati, identici in entrambi

la ricorsione è scritta in Python, non cablata nel grafico

L'ultima riga è la prova della risposta alla domanda L4b: L4a e L4b hanno lo stesso grafico e solo una delle due è ricorsiva.

Cosa puoi creare ora

Ogni pattern che hai appena eseguito è una forma reale del prodotto:

Hai fatto pratica

In natura,

Inizia da

Grafico + router (L2a/L2b)

pipeline di documenti, passaggi ETL con LLM, catene di revisione/approvazione, strumenti di valutazione

L2b di questo repository

Coordinatore + team single_turn (L3a)

un copilota di assistenza con team specializzati, banchi di triage e revisione multi-obiettivo

Modalità demo maratona 2

Agenti task (L3b)

moduli di acquisizione, flussi di prenotazione, onboarding, KYC: qualsiasi "raccogli e agisci"

22_agent_in_workflow

Larghezza/profondità dinamica (L4a/L4b)

agenti di ricerca, generatori di report, controlli di audit su input di dimensioni sconosciute

Modalità demo maratona 3

Compongono

I tre pattern non si escludono a vicenda. Un nodo del grafico può chiamare un coordinatore della collaborazione; uno specialista può avviare un workflow dinamico. Scegli il pattern giusto per ogni parte del problema. In questo modo eviterai di trasformare ogni sistema di agenti in un unico prompt gigante.

L'intera app e ciò che un grafico non può mostrarti

💡 Prova nel tuo workflow:lo script che ha generato questo disegno è scripts/graph_dump.py. Puntalo su qualsiasi Workflow e stamperà i bordi reali, un diagramma strutturale senza costi di qualsiasi cosa tu costruisca.

14. Complimenti

Nove agenti, un testimone, un finale ordinato

Hai creato un Coach per il giorno della maratona e, lungo il percorso, tutti e tre i pattern di orchestrazione di ADK 2.

Che cosa hai imparato

  • Prologo: il mega-prompt che ha inventato il proprio tempo atmosferico: perché esiste una struttura.
  • L0-L1: Agent, Runner, un vero e proprio strumento che il modello sceglie di chiamare e il tuo primo Workflow (nodi funzione + nodi agente come peer).
  • L2a / L2b: flussi di lavoro del grafico: fan-out parallelo + JoinNode, quindi routing deterministico, una chiamata LLM.
  • L3a: agenti collaborativi: lo stesso team viene eseguito in chat (isolato) e poi in single_turn (sottoinsieme parallelo + sintesi) - un flag, due mondi.
  • L3b: modalità task: una domanda di chiarimento in pausa, una ripresa con script, finish_task che restituisce un oggetto convalidato.
  • L4a / L4b: flussi di lavoro dinamici: larghezza di runtime (fan-out), poi profondità di runtime (ricorsione) con limiti nel codice.
  • L5: l'albero decisionale e la composizione dei pattern.

Frasi da conservare

Le funzioni preparano il contesto. I bordi definiscono il workflow. Il router sceglie il percorso. Il modello scrive la risposta.

Lascia che l'LLM modelli il lavoro, ma mantieni i limiti nel codice.

Abbina il pattern alla forma del problema.

Passaggi successivi

  • Esegui l'app completa da cui sono stati estrapolati questi livelli: Marathon Race Day Coach, una build FastAPI + SSE con un'interfaccia utente del browser che mostra tutte e tre le modalità in tempo reale: github.com/cuppibla/adk-2-marathon-demo.
  • Vai oltre: adk-workflows-compared: tutti i 23 esempi di workflow ADK 2 ufficiali, ognuno con una porta 1.x e indicazioni su quando utilizzarli. Inizia con docs/three-pillars.md, poi passa agli argomenti che questo codelab ha saltato: 07_loop, 17_request_input e 22_agent_in_workflow.
  • Porta il tuo problema: quali parti hanno una struttura nota (L2), un team noto (L3a/L3b) e una forma sconosciuta (L4)?
  • Esplora il codice: github.com/cuppibla/adk2-tutorial.
  • È passato dal laboratorio? Il credito e il progetto che ha creato non dureranno per sempre. Per continuare a eseguire di nuovo questi livelli senza costi, segui invece il passaggio Configurazione da fare a casa: una chiave AI Studio senza costi, nessun progetto Cloud, nessuna fatturazione. Lo scambio di una cella è l'unica modifica.