درس 2 — Fondations : agents, workflows et état

État partagé et output_key

5 دقائق قراءة

هذا المحتوى غير مترجم بعد. تُعرض النسخة الأصلية.

État partagé et output_key

L'état, un tableau blanc commun

Une session ADK regroupe l'historique d'une conversation (les événements) et un état : un dictionnaire clé → valeur que tous les agents d'une même invocation peuvent lire et modifier. Dans un workflow, c'est le moyen normal de passer un résultat d'une étape à la suivante.

ADK ne modifie jamais l'état « en douce » : chaque écriture voyage dans un événement, sous event.actions.state_delta, et le SessionService l'applique quand l'événement est enregistré. C'est ce qui rend l'état traçable dans adk web.

sequenceDiagram
  participant C as "chercheur_finances"
  participant S as "État de session"
  participant A as "analyste"
  C->>S: "state_delta : donnees_finances (callback)"
  C->>S: "state_delta : notes_finances (output_key)"
  A->>S: "lit {notes_finances} dans son instruction"
  A->>S: "state_delta : analyse (output_key)"

Trois façons d'écrire

MoyenOùExemple du lab
output_key="cle"sur un LlmAgent : sa réponse finale (texte) est copiée dans state["cle"]notes_registre, analyse, rapport, critique
tool_context.state["cle"] = …dans un outil ou un callback d'outildonnees_finances, statut_relecture
EventActions(state_delta={…})dans un BaseAgent personnaliséscore_risque
files/agent.py (extraits)
analyste = LlmAgent(
    name="analyste", model=model, output_key="analyse", **CALLBACKS,
    instruction=("… Registre : {notes_registre}\nFinances : {notes_finances}\n"
                 "Réputation : {notes_reputation}\n"
                 "Score calculé (il fait foi, ne le modifie pas) : {score_risque}\n" + CONSIGNE_DONNEES),
)
redacteur = LlmAgent(
    name="redacteur", model=model, output_key="rapport", **CALLBACKS,
    instruction=("… Retours de la relecture précédente (vide au premier passage) : {critique?}\n …"),
)

Lire : les gabarits {cle} et {cle?}

Avant chaque appel au modèle, ADK remplace {cle} dans l'instruction par str(state["cle"]). Si la clé n'existe pas, il lève une KeyError. Avec {cle?}, une clé absente est remplacée par une chaîne vide : c'est indispensable pour {critique?}, qui n'existe pas encore au premier passage du rédacteur.

Le rédacteur utilise {critique} sans point d'interrogation. Que se passe-t-il au premier tour de boucle ?

Portée des clés

Le préfixe d'une clé fixe sa durée de vie : sans préfixe, la session ; user: toutes les sessions d'un utilisateur ; app: toute l'application ; temp: l'invocation en cours seulement (jamais persisté). Une due diligence est un dossier ponctuel : toutes nos clés sont sans préfixe.

Le piège du parallèle : une clé par écrivain

Les trois chercheurs tournent en même temps. S'ils ajoutaient tous leur résultat à une même liste state["journal"], chacun lirait la liste, ajouterait son élément et réécrirait la liste : la dernière écriture gagnerait et les autres seraient perdues. C'est une condition de course (race condition).

La parade : une clé distincte par agent parallèle (notes_registre, notes_finances, notes_reputation), puis une étape suivante qui les rassemble. C'est exactement ce que fait l'analyste.

Pourquoi chaque chercheur a-t-il son propre output_key ?

Exercice : ajouter un quatrième chercheur

Ajoutez un chercheur chercheur_conformite qui appelle un outil consulter_conformite(fournisseur) (à écrire, il peut renvoyer {"rgpd": "clauses signées"}), et faites lire son résultat par l'analyste.

Voir la solution
files/agent.py (ajouts)
def consulter_conformite(fournisseur: str) -> dict:
    """Renvoie l'état de conformité contractuelle (RGPD, sous-traitance) d'un fournisseur."""
    return {"fournisseur": fournisseur, "rgpd": "clauses signées"}

# dans construire_pipeline :
_chercheur("chercheur_conformite", consulter_conformite, "notes_conformite", model),
# et dans l'instruction de l'analyste :
"Conformité : {notes_conformite}\n"

Une nouvelle clé notes_conformite, jamais partagée : aucun risque de collision. Le callback journaliser_outil rangera aussi la donnée brute dans donnees_conformite (il dérive la clé du nom de l'outil).

Résultat obtenu

Exécution réelle du pipeline le 2026-10-15 avec le faux modèle des tests (aucun appel réseau), en affichant pour chaque événement son auteur, sa branche et les clés de son state_delta :

chercheur_registre recherche.chercheur_registre ['donnees_registre']
chercheur_finances recherche.chercheur_finances ['donnees_finances']
chercheur_reputation recherche.chercheur_reputation ['donnees_reputation']
chercheur_registre recherche.chercheur_registre ['notes_registre']
chercheur_finances recherche.chercheur_finances ['notes_finances']
chercheur_reputation recherche.chercheur_reputation ['notes_reputation']
scoreur None ['score_risque']
analyste None ['analyse']
redacteur None ['rapport']
relecteur None ['critique']
…
État final : ['analyse', 'critique', 'donnees_finances', 'donnees_registre', 'donnees_reputation',
             'notes_finances', 'notes_registre', 'notes_reputation', 'rapport', 'score_risque']

Chaque chercheur a sa branche (recherche.chercheur_…) : il ne voit pas l'historique des deux autres, mais tous écrivent dans le même état.

Je sais faire

  • Écrire dans l'état avec output_key, tool_context.state ou state_delta
  • Lire l'état dans une instruction avec {cle} et {cle?}
  • Choisir la portée d'une clé (temp:, sans préfixe, user:, app:)
  • Éviter les collisions d'écriture entre agents parallèles
التالي