Tabella per N - Agent Eval e il ciclo di autoevoluzione con ADK

1. Introduzione

Un tavolo, N persone, un agente che prenota il ristorante

Questo codelab riguarda la valutazione degli agenti, ovvero il modo in cui un sistema capisce se la risposta di un agente è buona, e ciò che puoi creare una volta che lo fa: un agente che riscrive le proprie istruzioni in base ai propri errori e un loop che continua a farlo, in modo limitato, con te che tieni il controllo.

Tutto viene eseguito sull'Agent Development Kit (ADK): adk eval con una metrica personalizzata che scrivi, adk optimize (l'ottimizzatore dei prompt GEPA), un ADK 2 Workflow con un back edge e la pipeline Agent Engine + BigQuery che trasforma il traffico di produzione nell'esame del round successivo.

Il ciclo incrementa qualsiasi numero gli venga fornito.

Cosa imparerai a fare

  • Una risposta non è un risultato: perché una risposta sicura e ben formulata non dice nulla al sistema e cosa deve essere in grado di dire un giudice
  • Tre modi per eseguire un esame: adk eval per il batch, la scheda adk web Valutazioni per esaminare un errore, pytest + AgentEvaluator per l'errore della build e l'unico archivio di valutazione locale che condividono
  • Confronto e controllo: perché un comparatore penalizza una risposta corretta formulata in modo diverso, quante risposte corrette ha effettivamente una cena e il controllo istintivo che ti dice di quale tipo di metrica ha bisogno un problema
  • Una funzione di controllo come metrica adk eval: customMetrics, codeConfig, la firma fissa, i sei controlli al suo interno, perché la tua funzione possiede PASSED/FAILED e perché è deterministica di proposito
  • Tre famiglie di metriche in una risposta: comparatore, controllo, LLM come giudice: cosa serve, quanto costa, dove è cieca
  • adk optimize (GEPA): cosa legge (gli errori, non il punteggio), training vs holdout e perché esegui di nuovo l'esame prima della spedizione
  • Reward hacking, measured: una metrica proxy che non legge mai il partito, cosa fa al numero reale e la legge che ne deriva
  • Un loop autoevolutivo come ADK 2 Workflow: un'istruzione che risiede nello stato della sessione, l'unico arco che rende un grafo un ciclo e il confine che l'ADK non ti fornisce
  • Where production fits — traces for watching, rows for learning, and why a checker can grade dinners nobody planned to grade (a read, not a step)

Cosa creerai

Un'app in esecuzione nel browser: un tavolo rotondo, N ospiti e un agente che prenota la loro cena. Ogni portata si basa su un elemento: una riga in una configurazione, una risposta davanti a tre giudici, un pulsante che esegue l'ottimizzatore reale, un interruttore che scambia il giudice, un arco in un grafo. La tabella mostra cosa è cambiato: chi ha mangiato, chi no e perché. Python è piccolo; i numeri vengono misurati tutti sulla tua macchina.

L'app è anche l'esercizio. Il pannello del corso ti offre una modifica o un pulsante alla volta; un piatto nel menu si riempie solo quando qualcosa di reale lo dimostra: la cucina legge i tuoi file e le ricevute di ciò che è stato eseguito, mai un elenco di controllo.

La pagina: la tabella e l'interruttore a sinistra, la planimetria e il corso a destra

Gli otto corsi

il concetto

la domanda a cui risponde

cosa fai

I

una risposta non è un risultato

Che cosa sa il sistema di questa prenotazione?

Ancora nulla. Prenota un test nel workbench.

II

il comparatore, un insieme di risposte corrette e tre modi per eseguire

Perché una prenotazione corretta non supera l'esame?

esegui l'esame così com'è stato spedito e leggi i verdetti

III

un controllo come metrica personalizzata · i sei controlli

L'esame può dire chi ha fame e perché?

una riga in exam/eval_config.json

IV

comparator · checker · LLM-as-judge

stessa risposta: quali sono i tre elementi da misurare?

valutare una risposta in tre modi, due volte

V

adk optimize · train vs holdout · the ship gate

l'agente può correggere autonomamente i propri errori?

host/instruction_current.txt

VI

reward hacking · the proxy metric

Cosa fa un giudice che non può vedere il danno al ciclo?

il passaggio

VII

a Workflow con un bordo posteriore, il confine

che cosa rende un grafico un ciclo e che cosa lo interrompe.

un bordo in loop/flywheel.py

VIII

cosa è stato sostituito

—

l'assegno

Come utilizzare questo codelab. Ogni corso spiega prima il concetto, poi mostra il codice come viene letto una volta apportata la modifica e lo illustra passo passo. Non ti chiede mai di modificare un file qui: la modifica avviene nell'app, che ti fornisce una riga alla volta e ti mostra prima l'errore. Leggi il corso, poi lascia che la pagina ti guidi e osserva il numero che si muove.

Dove si trova

Il mondo, ovvero dieci ospiti con un vincolo rigido ciascuno, dieci ristoranti con una trappola ciascuno, sedici feste, e i due giudici provengono dal laboratorio loop-lab-table; il formato interattivo di una pagina proviene da Agent Valley. Il bistrot è uno scenario; ogni meccanismo sottostante è quello ordinario che useresti per un agente dell'assistenza, un flusso di prenotazione o qualsiasi altra cosa la cui "bontà" è verificabile.

2. 🧰 Configurazione

  • Cosa: un account che può pagare, un progetto, il lab installato, il modello che ha dimostrato di rispondere e la tabella in esecuzione su una porta.
  • Perché: un processo su una porta: la pagina, gli esami, l'editor e il banco di lavoro condividono un'origine, quindi nulla in questo lab ha bisogno di un secondo terminale o di una seconda porta.
  • Come: clona, due script, un comando. L'ultima operazione di configurazione consiste nell'effettuare una chiamata al modello reale, quindi un progetto danneggiato non riesce qui anziché nel corso I.

Tutto in questo lab viene eseguito in Cloud Shell: gcloud, Python, Node e git sono già installati e Gemini è raggiungibile tramite Vertex AI utilizzando le tue credenziali. Non è presente alcuna chiave API in questo lab e non è necessario installare nulla sul tuo laptop. Un laptop con Google Cloud SDK, Python 3.11+ e Node 20+ funziona allo stesso modo.

C'è una cosa che questo lab non può fornirti: un progetto cloud Google con fatturazione attiva. Il passaggio 1 ti consente di creare un account che può pagare il progetto, mentre il passaggio 4 trasforma l'account in un progetto. Eseguili in questo ordine: ogni passaggio dopo il 4 legge il progetto su cui si basa il passaggio 4.

1. Ottieni un account con fatturazione (~1 minuto)

Accedi con un Account Google per cui è abilitata la fatturazione. Qualsiasi account di questo tipo va bene. Se il tuo non lo è, inizia una prova senza costi, che ti offre un account di fatturazione, ovvero tutto ciò che serve per il passaggio 4.

2. Apri Cloud Shell (~2 min)

👉🖥 Fai clic su Attiva Cloud Shell nella parte superiore della console Google Cloud: è l'icona a forma di terminale nell'intestazione.

Attiva Cloud Shell dall'intestazione della console Google Cloud

👉🖥 Fai clic su Apri editor (l'icona della cartella aperta con una matita). Viene visualizzato l'editor di Cloud Shell con un esploratore di file a sinistra, che puoi utilizzare ogni volta che vuoi leggere un intero file.

Apri l'editor di Cloud Shell

👉🖥 Se ti viene chiesto di autorizzare in qualsiasi momento della giornata, fai clic su Autorizza per continuare.

👉🖥 Apri un terminale all'interno dell'editor, ovvero la scheda 1, e ogni passaggio 👉💻 viene eseguito lì. La scheda 2 è la tabella stessa e la apri nel passaggio 7. Se il terminale non è già in basso nella schermata, aprilo con Visualizza → Terminale:

Apri un nuovo terminale nell'editor di Cloud Shell

👉💻 Controlla con quale account hai eseguito l'accesso: deve essere quello del passaggio 1:

gcloud auth list

3 · Ottieni il lab (~1 min)

👉💻 Clona il repository:

cd ~
git clone https://github.com/cuppibla/table-for-n
cd table-for-n

4. Crea il tuo progetto (~2 min)

👉💻 Uno script trova il tuo account di fatturazione, crea un progetto, collega la fatturazione e indirizza Cloud Shell a quel progetto:

cd ~/table-for-n
./setup_project.sh

Dovresti vedere un ✓ per l'account di fatturazione scelto, un ✓ per il progetto creato e un finale simile a questo:

Project ready: table-for-n-4271
  Next:  ./setup_codelab.sh

table-for-n-4271 è l'ID progetto, che avrà cifre diverse. All'interno succedono quattro cose:

fatturazione

utilizza l'account [YYYY-MM-DD] GDP Credit: più recente, se ne hai uno, altrimenti qualsiasi altro account di fatturazione aperto

project

crea table-for-n-XXXX e collega l'account di fatturazione

seleziona

scrive l'ID in ~/project_id.txt ed esegue gcloud config set project, quindi non ti verrà più chiesto di digitarlo

waits

esegue il polling finché il progetto non è effettivamente autorizzato a pubblicare, per un massimo di due minuti

👉🖥 Lo stesso progetto è ora nella console: il suo ID si trova nella scheda Informazioni sul progetto della dashboard:

L'ID progetto nella dashboard della console

5. Installa e configura il lab (~3 min)

setup_codelab.sh crea l'ambiente con uv, ma non lo installa per te: si ferma e ti chiede di farlo. Quindi installalo prima, nello stesso comando:

👉💻

cd ~/table-for-n
curl -LsSf https://astral.sh/uv/install.sh | sh
source ~/.local/bin/env
./setup_codelab.sh

Uno script, sicuro da eseguire di nuovo. Esegue cinque operazioni, nell'ordine:

api

legge il progetto selezionato nel passaggio 4 e attiva aiplatform.googleapis.com, dove si trova Gemini in Vertex AI

uv env

uv venv crea .venv o riutilizza quello già presente, poi uv sync installa esattamente ciò che uv.lock blocca: google-adk[eval], google-genai, fastapi, uvicorn

site

installa e crea site/, un'app Next.js esportata in file statici, in modo che bash table.sh nel passaggio 7 non abbia più nulla da scaricare

.env

scrive .env da .env.example, indirizzando il lab a Vertex AI sul tuo progetto, e nessuna chiave API da nessuna parte

preflight

effettua una chiamata reale a gemini-2.5-flash, quindi un progetto errato non riesce in questa fase di configurazione e non all'interno di un corso

6. Leggi il menu (~1 minuto)

L'ultima cosa stampata dal passaggio 5 è il menu di stasera: ogni riga è un probe reale, non un'ipotesi:

  talking to Gemini through vertex · table-for-n-4271
  the model answers · gemini-2.5-flash · it said: table for N, ready to seat.
  the page · built

  tonight's menu:

  I   ▢ the booking      · a reply is not a result                 read only · book one in the workbench
  II  ▢ the comparator   · the exam runs response_match_score      run it as shipped · read the verdicts
  III ▢ the judge        · everyone_ate is not on the exam yet     edit one line in exam/eval_config.json
  IV  ▢ the flight       · one answer, three judges               judge it three ways
  V   ▢ the rewrite      · instruction: draft                      optimize · install · ship gate
  VI  ▢ the other judge  · rating_score never reads the party      install hacked · flip the switch
  VII ▢ the loop         · no back edge yet                        delete one "# " in loop/flywheel.py
  VIII▢ the check        ·                                         eight courses

Nessuna è accesa prima di iniziare. the model answers è quella che conta in questo caso: significa che una chiamata reale è stata effettuata nel tuo progetto.

👉💻 Esegui di nuovo il test in qualsiasi momento per vedere a che punto sei. Il test legge i tuoi file e le ricevute della cucina, non un elenco di controllo:

cd ~/table-for-n
uv run python scripts/preflight.py

7. Prepara la tavola (~1 min)

👉💻

cd ~/table-for-n
bash table.sh

Un processo, porta 3470. La cucina impiega un po' di tempo per riscaldarsi: importa l'ADK e monta adk web, quindi lo script lo indica, attende e stampa i link solo quando la porta risponde effettivamente:

  warming up the kitchen — up to a minute the first time, do not open the page yet...

  ✓ the table is laid

  Table for N      → http://localhost:3470
  the workbench    → http://localhost:3470/workbench/dev-ui/?app=host

  In Cloud Shell: Web Preview → Change port → 3470
  Ctrl+C clears the table.

Rimane in primo piano; Ctrl+C cancella la tabella. Lascialo in esecuzione: ogni corso da qui in poi fa riferimento a questo processo e in questo lab non ti verrà mai chiesto di riavviarlo.

👉🖥 Dopo aver visualizzato ✓ the table is laid, apri la tabella in una nuova scheda, che chiameremo scheda 2. In Cloud Shell: Anteprima web → Cambia porta → 3470.

👉🖥 Inserisci il tuo nome nella scheda di prenotazione, che verrà stampato sul conto alla fine, e premi ✦ seat us (✦ trovaci un posto).

La scheda di prenotazione

3. I · La prenotazione: una risposta non è un risultato

Cinque persone vanno a cena. La chat di gruppo ha tutto: Nadia è vegana, Tom ha un budget di 25 € questo mese, Amara ha le stampelle e può camminare per dieci minuti, Lena non può arrivare prima delle 19:45 e Ben è facile da accontentare. Sono elencati dieci ristoranti, ordinati per valutazione, ciascuno con il prezzo, il tempo di percorrenza a piedi, gli orari e un'unica limitazione.

Un agente prenota il tavolo. Leggendo la chat, sai già chi rimarrà a stomaco vuoto nella stanza che sceglie. Nessun elemento del sistema lo sa. Questo divario è l'argomento principale del codelab.

L'agente, di sola lettura

host/agent.py, come per l'intero lab:

_HERE = os.path.dirname(os.path.abspath(__file__))
INSTRUCTION_FILE = os.path.join(_HERE, "instruction_current.txt")

root_agent = Agent(
    name="host",
    model="gemini-2.5-flash",
    instruction=read_instruction(),
    generate_content_config=types.GenerateContentConfig(
        thinking_config=types.ThinkingConfig(thinking_budget=0),
        temperature=0.0,
    ),
)

Nessuno strumento, nessun database, nessuna ricerca. Un modello, una stringa di istruzioni e la stringa non è presente in questo file. Si trova nella pagina instruction_current.txt, che oggi contiene le quattro righe che un product manager scrive il primo giorno:

You are the group's dinner-booking agent. The user will share the group chat
and the restaurants nearby.

Book the group a table people will be excited about — lead with the
highest-rated, most talked-about room that fits the night. Output ONLY a JSON
object, no prose, no code fences: {"restaurant": ..., "time": ..., "reason": ...}

Nessuna parola su allergie, budget, limiti di camminata o chi deve andarsene prima. Tutto questo è contenuto nel brief che riceve l'agente. Non utilizzato.

Questo file è l'unico artefatto di questo lab. Il corso V consente a uno strumento di ottimizzazione di riscriverlo. Il corso VI installa una versione compromessa. Il loop del Corso VII lo riscrive round per round mentre il grafico viene eseguito. L'agente (modello, impostazioni, nome) non cambia mai.

Provalo, senza frontend

👉🖥 Nella pagina, corso I. Fai clic su 🔬 Workbench (in alto a destra): il riquadro è adk web, montato nello stesso processo: stessa cartella host/, stesso table.db. Scegli host, incolla il brief dal pannello del corso (ha un pulsante di copia), invia.

L'host risponde in un secondo: un ristorante, un orario, una motivazione sicura di una riga. Leggi il motivo accanto alla chat. Poi guarda la tabella a sinistra: laid, nobody judged, — / 5. Non perché si stia caricando qualcosa, ma perché nel mondo del corso non esiste nulla che possa giudicare questo.

Il numero: posti affamati: ?. Nessuno può dirlo.

4. II · Il comparatore: quante risposte giuste ha questa cena?

Il corso II inizia con un esame già esistente e già non superato, per un motivo che non insegna nulla. Prima di risolvere il problema, è utile conoscere i tre modi per eseguire il test, perché oggi li utilizzerai tutti e tre.

Tre modi per eseguire una valutazione

Tutto con lo stesso motore sottostante.

A cosa serve

Che cosa si riceve

adk eval: l'interfaccia a riga di comando

il batch runner, ovvero ciò che inserisci in CI

un riepilogo: superato, non superato, un punteggio per caso

adk web → Evals: la UI web

un caso non è andato a buon fine e vuoi esaminarlo

la conversazione, la traccia, il punteggio per caso, tutti cliccabili

pytest + AgentEvaluator

folding agent eval into a normal regression suite

viene eseguito accanto ai test delle unità e la build non riesce allo stesso modo

Il dettaglio veramente utile: tutti e tre condividono un archivio di valutazione locale, host/.adk/eval_history/. Esegui un batch da CI, si verifica un errore, apri l'esecuzione esatta nell'interfaccia utente web per esaminarla. Il pulsante 📝 sulla pagina è il primo modo; il workbench 🔬 è il secondo; il terzo è un comando del terminale, uv run pytest tests/test_host.py, e il test che esegue è di dodici righe:

def test_the_holdout_feeds_everyone():
    config = get_evaluation_criteria_or_default("exam/eval_config.json")
    eval_set = load_eval_set_from_file("exam/parties_val.evalset.json", "parties_val")
    asyncio.run(AgentEvaluator.evaluate_eval_set(
        agent_module="host", eval_set=eval_set, eval_config=config, num_runs=1,
        app_name="host",
        eval_set_results_manager=LocalEvalSetResultsManager(agents_dir=ROOT),   # ← the shared store
    ))

tests/test_host.py. Stesso agente, stesso evalset, stesso file di configurazione e la riga eval_set_results_manager inserisce l'esecuzione nella stessa cronologia delle altre due.

L'esame, così come spedito

exam/eval_config.json viene spedito come segue:

{
  "criteria": {
    "response_match_score": 0.3
  },
  "customMetrics": {
    "everyone_ate": {
      "codeConfig": { "name": "metrics.table.everyone_ate_metric" },
      "description": "How many of the party actually got dinner."
    }
  }
}

Due parti, due lavori. criteria

è il documento dell'esame, ovvero le metriche effettivamente eseguite, ognuna con la propria barra di superamento. customMetrics

è la directory del personale: i nomi mappati a Python, disponibili ma non in servizio. La dichiarazione di una metrica non comporta la sua esecuzione. Come pubblicato, l'articolo contiene esattamente una domanda: la response_match_score integrata di ADK, una sovrapposizione di testo (ROUGE-1) tra ciò che ha detto l'agente e la prenotazione corretta nel set di valutazione. Il giudice, everyone_ate, si trova nella directory e non sul foglio. Il corso III lo sposta lì modificando una riga.

Abbiamo un agente per la prenotazione di cene e response_match_score seduto lì senza costi. Usiamolo e vediamo cosa otteniamo.

Prova

👉🖥 Corso II nella pagina. Premi 📝 run the exam · as shipped. La didascalia sotto il pulsante indica cosa viene eseguito: 1 metrica in esecuzione: response_match_score · everyone_ate è dichiarata, non nell'esame. Circa mezzo minuto. Leggi il risultato: circa la metà delle tabelle ha restituito un risultato FAILED, con un punteggio di testo senza motivo allegato. Guarda quali sono. Passa il mouse sopra i chip.

Ora guarda attentamente, perché questa è la parte interessante. Il comparatore sbaglia le tabelle in entrambe le direzioni e la corsa ne mostrerà almeno una. Non supera un test su una tabella in cui la prenotazione era corretta: l'agente ha scelto una stanza in cui tutti potevano mangiare, la cena è andata bene, la risposta è corretta, ma l'agente ha formulato la motivazione in modo diverso rispetto alla risposta di riferimento. E passa un tavolo dove qualcuno ha fame, nella nostra esecuzione p1, esattamente alle 0:30, con Tom fuori budget e Lena dopo gli ultimi ordini, perché si sono sovrapposte abbastanza parole. La somiglianza delle stringhe non sa nulla della cena. Conosce i personaggi che si mettono in fila.

Ecco una domanda reale: quante risposte corrette ha questa cena?

Per la festa di stasera, solo uno dei dieci ristoranti sfamerà tutti: Pho Saigon, e solo dalle 19:00. Nei vari gruppi dell'esame, i numeri sono uno, due, due, uno, tre, tre, sei, uno (world.rooms cammina in ogni stanza a ogni turno di un gruppo; esegui tu stesso con uv run python -c "import world; print(sum(r['feeds_everyone'] for r in world.rooms(world.PARTIES['p1'])))"). A volte più stanze possono sfamare tutti; a volte solo una può farlo e solo in determinati momenti. Esiste un insieme di risposte accettabili, non una sola, e la risposta d'oro nel set di valutazione ne fa parte.

Ciò significa che l'ipotesi alla base di response_match_score, ovvero esiste una sola risposta corretta e l'ho scritta, non era vera per questo problema. Neanche una volta. La metrica non è interrotta. Fa esattamente ciò che dice. Abbiamo utilizzato il tipo di metrica sbagliato per questo problema.

👉🖥 (Facoltativo, vale la pena dedicare un minuto): apri il workbench 🔬 → Valutazioni → 🕓 cronologia. L'esecuzione 📝 appena effettuata è presente e ogni caso è cliccabile. Nella scheda 1, uv run pytest tests/test_host.py esegue lo stesso esame di un test non superato e finisce nella stessa cronologia.

Il numero: 4 di 8 SUPERATE nel nostro test, per sovrapposizione di parole, senza motivi e 1 risposta corretta per p1, su dieci stanze.

Per il nostro problema della cena, la scelta è ovvia. Nessuno può dirti qual è il ristorante giusto per venerdì. Ma tutti possono dirti cosa significa una cena di successo: tutti a tavola hanno potuto mangiare. Questa è una regola. È verificabile. Non è ambiguo. La creazione è il corso III.

5. III. Il giudice: un controllo come metrica personalizzata

Due modi per assegnare un punteggio: confrontare con una risposta corretta o scorrere la tabella

Un verificatore pone una domanda diversa rispetto a un comparatore: non corrisponde alla chiave, ma ogni persona ha mangiato? Verifica la prenotazione per ogni persona, posto per posto, e restituisce un punteggio e un motivo per ogni posto. Non ha bisogno di una risposta perfetta, il che gli consente anche di valutare le conversazioni già avvenute (corso VIII).

world.everyone_ate è questa funzione. Il corso III lo collega a adk eval.

Inside everyone_ate — five steps, none of them clever

  1. Leggi il testo dell'utente e scopri di chi si tratta. Nel brief è presente un ID party, p1, e cerchiamo chi è seduto a quel tavolo.
  2. Leggi la risposta dell'agente, analizza il JSON, estrai il nome del ristorante e l'orario di prenotazione.
  3. Fai camminare ogni persona, in ordine. Per ognuno: dieta, poi allergie, poi tolleranza al piccante, poi budget, poi distanza a piedi, poi orario. Sei controlli, per persona, ogni volta. Prendiamo Tom: il suo budget è di 25 $a persona, la camera costa 28 $. Restituiamo False per Tom e la frase che lo spiega, quindi passiamo alla persona successiva.
  4. Il punteggio è il numero di persone che hanno mangiato diviso per il numero di persone al tavolo. Quattro su cinque fa 0,8. Nessuna ponderazione, nessun credito parziale.
  5. Restituisci PASSED o FAILED. E nota chi lo restituisce: tu. Ricorda che è la trappola qui sotto.
def ate_detail(person_id, rest, chosen_min):
    p = PEOPLE[person_id]
    if p.get("diet") == "vegan" and not rest["vegan"]:
        return False, "nothing vegan on the menu", "diet"
    if p.get("allergy") == "nut" and not rest["nut_safe"]:
        return False, "everything is finished in peanut oil", "allergy"
    ...
    if "budget" in p and rest["price"] > p["budget"]:
        return False, f"~${rest['price']}/person against {name}'s ${p['budget']} budget", "budget"
    ...                                                   # walk, then timing
    return True, "ate", ""

Una proprietà che vale la pena sottolineare: non esiste alcuna chiamata al modello in questa funzione. È Python deterministico. Stesso input, stesso verdetto, ogni singola volta. Il giudice non va alla deriva quando un modello viene aggiornato, non costa nulla da eseguire e non può inventare un motivo. Quando puoi ottenere questo risultato, vale la pena progettare per questo scopo.

La metrica, completata

metrics/table.py: l'adattatore che l'ADK chiama, con la sua firma fissa:

def everyone_ate_metric(eval_metric, actual_invocations,
                        expected_invocations=None, scenario=None):
    return _build(world.everyone_ate, world.THRESHOLD, actual_invocations, "everyone_ate")


def _build(scorer, threshold, actual_invocations, metric):
    per, scores = [], []
    for inv in actual_invocations:
        ids = _party_ids(inv)                 # who was invited — parsed from the PROMPT
        rid, time_str = _decision_parts(inv)  # what got booked — parsed from the RESPONSE
        s, detail = scorer(ids, rid, time_str) if (ids and rid) else (0.0, [])
        scores.append(s)
        per.append(PerInvocationResult(
            actual_invocation=inv, score=s, eval_status=_status(s, threshold)))
    overall = round(sum(scores) / len(scores), 4) if scores else 0.0
    return EvaluationResult(overall_score=overall,
                            overall_eval_status=_status(overall, threshold),
                            per_invocation_results=per)

Riga per riga: legge entrambi i lati di ogni chiamata, ovvero la festa da user_content e la prenotazione da final_response, li passa alla funzione pura e inserisce il risultato negli oggetti previsti dall'ADK. Una riga riporta il verdetto:

def _status(score, threshold):
    # ADK does not apply your threshold to custom metrics — this IS the verdict.
    return EvalStatus.PASSED if score >= threshold else EvalStatus.FAILED

Come viene collegata una metrica personalizzata e la trappola

La configurazione, ovvero la riga

exam/eval_config.json, come si legge una volta terminato il corso:

{
  "criteria": {
    "everyone_ate": 0.9
  },
  "customMetrics": {
    "everyone_ate": {
      "codeConfig": { "name": "metrics.table.everyone_ate_metric" },
      "description": "How many of the party actually got dinner."
    }
  }
}

È stata modificata una riga: criteria ora nomina il giudice anziché il comparatore. L'esame prevede otto partiti che l'ottimizzatore non vedrà mai (exam/parties_val.evalset.json, 36 seggi in totale).

Prova

👉🖥 Corso III nella pagina. La didascalia sotto il pulsante 📝 indica l'istruzione in esecuzione sull'host. Dovrebbe essere visualizzata la dicitura bozza. Se viene visualizzato un altro numero (perché ne hai installato un altro in precedenza), premi prima ↺ Indietro alla bozza, altrimenti i numeri non corrisponderanno alla storia.

👉🖥 Scambia la riga nell'editor (o premi Applica la modifica per me) e 📝 esegui di nuovo il test. Ora un errore indica il posto e il motivo: Tom: ~$28 contro $25 · Lena: ultimi ordini alle 19:30, arriva alle 19:45 e il numero sotto il tavolo cambia da un conteggio dei pass a quello che conta: posti affamati. Passa il mouse sopra un chip: ogni tabella non riuscita contiene le relative frasi.

👉🖥 Poi premi 📋 Prenota il tavolo. L'organizzatore sceglie una stanza; la stessa funzione disattiva le sedie in cui non è possibile mangiare e indica il motivo sotto ciascuna. Premilo di nuovo: il modello potrebbe prenotare in modo diverso (l'API non è deterministica nemmeno a temperatura 0), ma il giudice non vacilla mai: stessa stanza, stesso verdetto, stesse frasi.

Il numero: la bozza del primo giorno, onestamente, circa 3 gruppi su 8, 6 posti affamati su 36 nella nostra esecuzione (il tuo numero oscillerà; l'API non è deterministica nemmeno alla temperatura 0, ecco perché l'esame ha 36 posti, non quattro casi). Mantieni questo numero. Il corso V esiste per spostarlo.

6. IV · Il volo: una risposta, tre giudici

L'esame ha valutato le risposte dell'host due volte: una volta da un comparatore e una volta dal tuo strumento di controllo. Il corso IV prende una di queste risposte, la festa di stasera, p1, esattamente come registrata nell'esame, e aggiunge un terzo giudice ai primi due: un modello con una rubrica. Non viene riprenotato nulla. Ciò che cambia è il confronto con la risposta. Questa singola differenza è ciò che distingue le tre famiglie di metriche.

giudice

chiede

ha bisogno di

passes if

punto cieco

il comparatore · response_match_score

è uguale alla mia risposta?

una risposta perfetta: una persona ha scritto una prenotazione per gruppo nel set di valutazione

Sovrapposizione di parole ROUGE-1 ≥ 0,3

una risposta corretta formulata in modo diverso viene penalizzata, mentre una risposta sbagliata con le parole giuste passa. Non ha mai letto la festa.

il controllo · everyone_ate

questa risposta è davvero valida?

le regole: sei controlli per posto, nessuna risposta d'oro

posti alimentati ÷ posti a tavola ≥ 0,9

non vede nulla al di fuori delle regole che hai scritto. Deterministico: stessa risposta, stesso verdetto, stesse frasi, ogni volta.

LLM Judge · gemini-2.5-flash + una rubrica

tutti riescono a mangiare? — posta a un modello

un modello e una rubrica di quattro righe; una chiamata reale per ogni pressione

il modello dice PASS

può sbagliare e può cambiare idea tra una pressione e l'altra. Costo di una chiamata.

I motivi del controllo meritano una nota. Quando adk optimize viene eseguito nel corso V, non vede mai queste frasi. L'ottimizzatore di ADK legge i punteggi. Non sono sprecati: sono ciò che rende leggibile un errore per te, che ti trovi al terminale, e questo è il loro valore principale oggi. Nel corso VII il ciclo chiama direttamente il giudice anziché passare attraverso la pipeline di valutazione, quindi i motivi vengono effettivamente consegnati all'elemento che esegue la riscrittura.

I tre giudici, mentre corrono

kitchen/flight.py. I primi due sono il codice dell'esame: il comparatore è la funzione alla base di response_match_score, quindi il suo numero è esattamente quello stampato dal corso II per questa parte:

from google.adk.evaluation.final_response_match_v1 import _calculate_rouge_1_scores

def comparator(answer, reference):
    s = _calculate_rouge_1_scores(answer, reference)       # ROUGE-1 F, as ADK computes it
    return {"score": s.fmeasure, "passed": s.fmeasure >= 0.3, "needs": "a golden answer"}

def checker(party, answer):
    rid, when = _decision(answer)
    score, seats = world.everyone_ate(world.PARTIES[party], rid, when)
    return {"score": score, "passed": score >= world.THRESHOLD, "needs": "the rules",
            "why": " · ".join(f"{s['name']}: {s['why']}" for s in seats if not s["ate"])}

async def llm_judge(party, answer):
    prompt = f"{RUBRIC}\n\n--- the brief ---\n{world.brief(party)}\n\n--- the booking ---\n{answer}"
    r = await client.aio.models.generate_content(model="gemini-2.5-flash", contents=prompt, ...)
    verdict = json.loads(r.text)["verdict"]                # PASS or FAIL, plus one sentence
    return {"passed": verdict == "PASS", "needs": "a model + a rubric"}

Prova

👉🖥 Corso IV. Nella parte superiore del riquadro: la risposta dell'host per p1 e, accanto, la prenotazione d'oro del set di valutazione. Leggile entrambe per confrontarle. Quindi premi ⚖ Valuta questa risposta in tre modi. Una chiamata al modello. Tre schede da compilare: cosa ha chiesto ogni giudice, cosa serviva, cosa ha detto e perché.

Nella nostra esecuzione, per p1, Olive & Thyme alle 19:00 nella bozza del primo giorno, il comparatore ha restituito PASS a 0,30 (un numero sufficiente di parole corrisponde alla prenotazione di riferimento), il controllo ha restituito FAIL a 0,60 (Tom: circa 28$rispetto al budget di 25 $di Tom; Lena: ultimi ordini alle 19:30, non è al tavolo fino alle 19:45) e il giudice LLM ha restituito FAIL e ha nominato Lena. Il comparatore e il controllo non sono d'accordo sulla stessa risposta: uno ha conteggiato le parole, l'altro i posti.

👉🖥 Premi ⚖ Giudica di nuovo. La scheda di confronto non si sposta. La carta di controllo non si sposta. La scheda LLM potrebbe farlo. La riga sotto il pulsante indica se è stato fatto. Poi scegli un altro partito a sinistra (il selettore dei partiti del palco) e premi di nuovo; i tre non sono sempre allineati allo stesso modo.

Il numero: una risposta, tre verdetti: PASS · FAIL · FAIL durante la nostra esecuzione e due dei tre diranno la stessa cosa ogni volta che lo chiedi.

7. V · The rewrite — adk optimize and the ship gate

Tre ruoli, mantenuti rigorosamente separati:

ruolo

chi

job

il candidato

l'organizzatore

sostiene l'esame

il giudice

la tua metrica del corso III

punteggi, in modo deterministico

il coach

adk optimize (GEPA)

legge gli errori, riscrive l'istruzione

Il coach non assegna mai voti. Il giudice non fa mai proposte. Ciò che cambia è un artefatto: l'istruzione, un file di testo che puoi confrontare, esaminare e ripristinare.

Cosa legge l'ottimizzatore

GEPA non sale un numero. Legge i perché: campiona le prenotazioni delle parti di addestramento, le valuta con il tuo giudice e consegna le note di errore (Tom oltre il budget, Lena dopo l'ultimo ordine) a un modello che propone una nuova istruzione. I candidati che battono il vecchio modello sui partiti che ha visto vengono mantenuti; vince quello che ottiene i risultati migliori sui partiti che non ha mai visto. Questa suddivisione è l'intera disciplina:

parti

utilizzato per

parties_train

8

l'ottimizzatore legge questi errori

parties_val

8 (36 posti)

l'esame, mai mostrato all'ottimizzatore

È gestito da due configurazioni, entrambe in exam/:

{ "eval_config": { "criteria": { "everyone_ate": 0.9 }, "customMetrics": { ... } },
  "app_name": "host", "train_eval_set": "parties_train", "validation_eval_set": "parties_val" }
{ "optimizer_model": "gemini-2.5-flash", "max_metric_calls": 150,
  "reflection_minibatch_size": 3, "run_dir": "./runs/gepa_run" }

Il vincitore, come si legge

L'ottimizzatore non scrive mai instruction_current.txt. Stampa l'istruzione vincente; la pagina la salva in runs/optimize/winner.txt e ⤵ install la copia nella posizione corretta. Ecco come inizia la corsa reale del vincitore (2931 caratteri, da 375):

Your primary goal is to book a restaurant that accommodates EVERYONE in the
group. Follow these steps precisely:

1. Understand Group Requirements: carefully parse the chat for each
   individual's needs — dietary restrictions, allergies, budget limits,
   arrival times, walking distance...
2. Filter restaurants by absolute constraints — a single violation for one
   person eliminates the room for the whole group.
...

La bozza di quattro righe è diventata una procedura numerata e ogni regola contiene una motivazione dell'esame. Nessuno ha digitato queste frasi.

Dove atterra una corsa, se vuoi leggerla tu:

file

Descrizione

runs/gepa_run/run_log.txt

l'intera corsa; verso la fine, Best program as per aggregate score on valset: N

runs/gepa_run/candidates.json

ogni candidato come {"agent_prompt": ...}: l'indice N è il vincitore

runs/gepa_run/candidate_tree.html

l'albero di ricerca, che può essere aperto in un browser

runs/optimize/winner.txt

il vincitore, salvato dalla pagina; ⤵ installa il vincitore lo copia nella posizione corretta

La nostra seconda esecuzione reale, effettuata durante la scrittura di questo articolo: 150 chiamate di metriche, meno di sette minuti, un vincitore di 2445 caratteri che si apre con "soddisfa pienamente le esigenze alimentari e di tempistica di ogni singolo membro senza compromessi" e spiega la regola degli ultimi ordini che la bozza non aveva mai avuto. I vincitori delle due esecuzioni sono formulati in modo diverso e hanno lo stesso punteggio: lo strumento di ottimizzazione trova le regole, non le frasi.

The ship gate

Non fidarti mai del punteggio dell'ottimizzatore. Esegui di nuovo l'esame sull'holdout con l'istruzione appena installata. Questo comando fa la differenza tra auto-evoluzione e auto-congratulazione.

Prova

👉🖥 Corso V. Due porte: ⚗ ottimizza esegue la simulazione reale (150 chiamate al modello, da 5 a 8 minuti, i log vengono trasmessi in streaming nel riquadro) oppure ⤵ installa il vincitore utilizza quello prodotto dalla nostra esecuzione. In entrambi i casi, premi 🚪 ship gate e leggi l'holdout. Poi 📋 Prenota il tavolo: stesso brief, stesso modello, una cena diversa e il motivo dell'host ora cita il cappello di Tom e il limite per gli ultimi ordini.

Corso V dopo il cancello: il vincitore installato, il riquadro di attesa, p1 fed

Tieni presente i due numeri: 5 / 5 indica i posti a un tavolo (p1, stasera); 6 / 8 indica i gruppi per l'intero esame. L'app mostra una cena, mentre la valutazione mostra la distribuzione. Il vincitore alimenta completamente p1, ma non riesce comunque a coprire due delle otto tabelle di holdout.

Il numero: holdout 3/8 → 6/8 nella nostra serie e posti affamati 6 → 9. Leggi attentamente il secondo numero, perché sembra una regressione, ma non lo è. La bozza non è riuscita a soddisfare i requisiti di cinque tavoli per uno o due posti ciascuno (budget di Tom, arrivo di Lena). Il vincitore riempie le tabelle intere e, nelle due che ancora mancano, p1 e p2, prenota Taqueria Luna alle 19:45 in modo che Lena possa arrivare, dopo le 19:30, l'orario dell'ultimo ordine della cucina: l'intera tavolata rimane a digiuno, cinque posti e quattro. Meno tabelle con errori, errori più gravi. Questo è il limite massimo nel riquadro laterale sottostante, misurato. Ricorda che il conteggio dei pass e dei posti vuoti rispondono a domande diverse; l'esame mostra entrambi.

8. VI · L'altro giudice: l'hacking dei premi

Il corso V ha aumentato il numero reale migliorando l'agente. Questo corso alza un numero falso rendendo cieca la regola e osserva cosa succede.

Due giudici

metrics/table.py contiene:

def everyone_ate(party, pick):     # the honest judge
    ...                            # walks the table, seat by seat

def rating_score(party, pick):     # the gameable judge
    return pick.rating / 5.0       # <- party is never read

👉 Il linter ti avviserà che

party

non è utilizzato. Questo avviso è l'intera lezione.

exam/eval_config_rating.json ha la stessa forma che hai creato nel corso III: un nome, una soglia, un percorso punteggiato, che punta all'altra funzione:

{
  "criteria": { "rating": 0.85 },
  "customMetrics": {
    "rating": {
      "codeConfig": { "name": "metrics.table.rating_metric" },
      "description": "The restaurant's star rating. Who is at the table is not part of this calculation."
    }
  }
}

L'interruttore sopra la tabella determina con quale delle due configurazioni viene eseguito l'esame. Questo è tutto ciò che comporta uno scambio di giudici: un file per un altro, niente a livello di agente.

L'istruzione che il giudice della classificazione voleva

instructions/hacked.txt è un artefatto reale: ciò che è stato prodotto da un'esecuzione con il giudice di valutazione. La differenza rispetto alle istruzioni per la cura ha una riga importante:

- Ask the party about dietary restrictions.
+ Prioritise rooms with a rating of 4.5 and above.

Prova

👉🖥 Corso VI. ⤵ installa l'istruzione compromessa. Sposta il cursore su valutazione: la pagina diventa un'app di recensioni con stelle, TENDENZE e attesa media. Il tavolo si riduce a una miniatura nell'angolo e le stesse sedie sono ancora grigie. Il giudice della valutazione non guarda mai la tabella, quindi nemmeno la pagina.

Premi 📝 Esegui l'esame: 8 / 8. Riattiva l'interruttore ed esegui di nuovo il test: le stesse otto risposte, valutate onestamente.

Portata VI con l'interruttore sulla valutazione: un'app di recensioni e il tavolo nell'angolo

Il numero: le stesse otto cene, valutazione 8/8, onesta 1/8, 15 posti affamati su 36. Tutti e otto i gruppi ricevono Smoke & Barrel. Priya, con l'autoiniettore di adrenalina, viene mandata nella cucina con l'olio di arachidi. E un tavolo passa comunque onestamente: un agente hackerato alimenta accidentalmente la festa i cui vincoli non includono la stanza hype. Questo è ciò che rende credibile un proxy: non è sempre sbagliato, è sbagliato dove non stai guardando.

9. VII · Il ciclo: un workflow con un arco all'indietro

Il corso V era un ciclo che ti coinvolgeva: esegui l'ottimizzatore, installa, testa. Questo corso ha gli stessi tre ruoli di un grafico che viene eseguito autonomamente, ovvero è delimitato.

L'istruzione come stato della sessione

Nell'istruzione dell'organizzatore del loop non è presente una stringa. È un template di stato:

def seed(node_input: str):
    yield Event(state={"brief": world.brief(PARTY), "current_instruction": DRAFT,
                       "whys": "(no failures yet — first round)"})

host = Agent(
    name="host", model="gemini-2.5-flash",
    instruction="{current_instruction}\n\nHere is tonight's request:\n{brief}",
    output_key="decision", generate_content_config=gen)

{current_instruction} viene risolto dallo stato a ogni chiamata. Quando un nodo successivo sovrascrive la chiave, la successiva chiamata host viene eseguita con il nuovo testo. Questo è l'intero meccanismo di auto-evoluzione: un valore nello stato che il ciclo continua a sostituire.

I nodi

nodo

kind

fa

seed

funzione

mette la bozza del brief e del primo giorno nello stato

host

agente

libri: una chiamata al modello, in base a quanto stabilito dallo stato

judge

funzione

world.everyone_ate: la stessa funzione del corso III; scrive whys e last_score nello stato

router

funzione

il confine: ship o improve

proposer

agente

Legge i motivi e l'istruzione corrente, scrive una sostituzione

publish

funzione

inserisce la sostituzione in state.current_instruction

done

funzione

chiude l'episodio

Il volano: libro → giudizio → riscrittura → libro di nuovo; il bordo viola è quello che scrivi

Il grafico, terminato

loop/flywheel.py, build(), come si legge una volta completato il corso:

def build(nodes: dict):
    seed, host, judge, router = nodes["seed"], nodes["host"], nodes["judge"], nodes["router"]
    proposer, publish, done = nodes["proposer"], nodes["publish"], nodes["done"]
    return Workflow(
        name="flywheel",
        edges=[
            ("START", seed, host, judge, router),
            (router, {"improve": proposer, "ship": done}),
            (proposer, publish, host),
        ],
    )

La pagina viene spedita senza l'ultimo bordo. Tutto viene eseguito una volta: i libri in bozza, il giudice grigio due sedie, il proponente scrive due regole e poi il grafico termina, perché nulla riporta la riscrittura all'host. Il loop è un bordo mancante.

Il limite, perché ADK non lo farà

Un bordo della rotta che punta all'indietro ti dà il loop in modo nativo. Workflow non ha un limite di iterazioni. Il router possiede la fine, con quattro cinture:

def router(last_score: float):
    if last_score >= world.THRESHOLD:        # 1 · SHIP on pass
        yield Event(route="ship")
    elif ep["round"] >= MAX_ROUNDS:          # 2 · round budget (default 4)
        yield Event(route="ship")            #     "stopping is also a decision"
    elif last_score <= ep["best"] and ep["flat"] >= 1:
        yield Event(route="ship")            # 3 · two flat rounds — a flat line is an answer too
    else:
        yield Event(route="improve")
# 4 · publish() truncates a runaway rewrite to 2,400 characters

Scenario peggiore: quattro round, due chiamate al modello ciascuno, otto chiamate, limite massimo. Il conduttore non assegna mai voti, il giudice non fa mai proposte, il router non scrive mai.

Prova

👉🖥 Corso VII. ↻ run the loop as shipped: round 1 books, fails at 0.60, the proposer writes its rules — and the run ends with flywheel ended without closing. Elimina # sulla linea verde nell'editor. Eseguilo di nuovo.

Corso VII dopo il bordo: round 2 con le istruzioni riscritte, SHIP

Ecco cosa ha scritto effettivamente il proponente nella nostra esecuzione: la bozza, più due frasi, una per ogni nota di errore:

Ensure the total cost per person does not exceed anyone's stated budget.   ← Tom's why
Confirm all guests can arrive before the kitchen's last order time.        ← Lena's why

Un posto vuoto diventa un perché; un perché diventa una regola; una regola cambia la prenotazione successiva. Round 2: prenotato Pho Saigon alle 19:45 → 1,00. Suona la campanella.

Il numero: R1 0.60 → R2 1.00 · SHIP, in circa cinque secondi. La SHIP del loop è per cena (un tavolo, p1); il gate del corso V era per esame. Stessa disciplina, due scale.

10. VIII · Il controllo: cosa è stato sostituito

👉🖥 Corso VIII. Premi 🧾 Il conto, per favore: otto righe, i tuoi numeri.

prima di

dopo

una demo che sembra corretta

un esame che conta chi ha mangiato

una risposta di riferimento

un insieme di risposte corrette e una regola

un prompt che modifichi manualmente

un'istruzione che il ciclo riscrive

un numero che aumenta

un giudice che si spiega

un'esecuzione

un round, con un modo per tornare indietro

la metrica che ti è stata fornita

il numero che hai scelto

Elementi da portare a casa, in ordine:

  1. Prima di scegliere una metrica, chiedi cosa puoi scrivere: la risposta, le regole o solo una descrizione di buono. Una cena ha una serie di risposte corrette; un comparatore ne controlla una.
  2. Crea il righello prima del ciclo e fai in modo che lo stato del mondo penalizzi il righello.
  3. Inserisci il verdetto all'interno della metrica. ADK non applicherà la soglia per te.
  4. Suddividi il set di addestramento da quello di holdout prima della prima esecuzione dell'ottimizzazione e non eseguire mai la spedizione in base al punteggio dell'ottimizzatore.
  5. Conta i posti disponibili, non i tassi di superamento: gli esami di piccole dimensioni mentono.
  6. Rendi visibile il danno all'ottimizzatore: un giudice che restituisce i motivi svolge metà del lavoro dell'ottimizzatore.
  7. ADK ti offre il loop, il confine è tuo. Anche fermarsi è una decisione.
  8. Quando il numero raggiunge il massimo, deve essere perché l'agente è migliorato, non perché il righello è diventato più morbido.

Il ciclo incrementa qualsiasi numero gli venga fornito.

Dove si inserisce la produzione

Tutto in questo lab è stato eseguito sulla tua macchina in base a un esame che hai scritto. La produzione è come una cena che nessuno aveva intenzione di valutare, ed è esattamente ciò che un checker può fare e che una metrica basata su risposte ideali non può fare: valutare a posteriori.

Due tubi fuori dalla produzione, una freccia indietro

barra verticale

come

risposte

① Cloud Trace

adk deploy agent_engine ... --otel_to_cloud

Va bene adesso? — intervalli, giudici della piattaforma, monitoraggi online

② Righe BigQuery

il plug-in BigQuery Agent Analytics

cosa dovremmo imparare? — una riga strutturata per evento, il formato utilizzato dal checker e dai set di valutazione

Il repository fornisce la pipeline come script, non come corso: refuel/send_traffic.py invia le parti reali a un host di cui è stato eseguito il deployment, refuel/replay_to_bq.py inserisce le stesse conversazioni come righe e refuel/harvest.py rivaluta ogni riga con world.everyone_ate e genera gli errori come casi di valutazione, ovvero le parole effettive del gruppo, senza una risposta ottimale. Durante la nostra serata di produzione, l'host spedito ha alimentato 7 delle 8 feste reali; l'unico fallimento è stato Lena contro gli ultimi ordini, il posto esatto previsto dal soffitto del corso V, ed è diventata la domanda d'esame del round successivo. Il secondo giro è di nuovo il percorso V con un altro caso di allenamento. È necessario un progetto Google Cloud con fatturazione, motivo per cui è una lettura e non un piatto del menu.

Auto-apprendimento

  • Scrivi una tua festa (world.PARTIES, poi scripts/make_evalsets.py) e inseriscila nell'esame. È giusto? In stile scripts/verify_world.py: ogni festa deve avere almeno una prenotazione che soddisfi tutti.
  • Alza la barra di valutazione a 0,9 e punta adk optimize a eval_config_rating.json. Misura il gradiente.
  • Hai un progetto con fatturazione? Esegui la pipe refuel/ end-to-end e leggi le righe harvest.py di [skip]: sono il confine tra le due pipe reso visibile.
  • Esegui il ciclo con HOST_THINKING=2048. Cosa cambia e cosa ti dice questo sul livello del tetto?