Leçon 3 — Construire l'agent pas à pas

Priorité, réponse proposée et état

5 min de lecture

Priorité, réponse proposée et état

Un résultat structuré, pas une phrase

Le triage doit être exploitable par du code : quelle équipe, quelle priorité, quel brouillon. Plutôt que d'analyser le texte final du modèle, on lui fait appeler un outil dont les paramètres sont exactement les champs voulus.

files/agent.py
def enregistrer_triage(priorite: str, resume: str, reponse_proposee: str, tool_context: ToolContext) -> dict:
    """Enregistre le triage du ticket. À appeler UNE fois, en dernier.

    Args:
        priorite: "P1", "P2" ou "P3".
        resume: le problème en une phrase.
        reponse_proposee: brouillon de réponse au client (3 phrases max), relu par un humain avant envoi.
    """
    if priorite not in PRIORITES:
        return {"erreur": f"Priorité invalide : {priorite}. Valeurs : P1, P2, P3."}
    tool_context.state["triage"] = {"equipe": tool_context.agent_name, "priorite": priorite,
                                    "resume": resume, "reponse_proposee": reponse_proposee}
    tool_context.actions.skip_summarization = True  # fin du tour : pas d'appel modèle de plus
    return tool_context.state["triage"]
LigneRôle
if priorite not in PRIORITESrefuse « urgent » ou « haute » ; le modèle lit l'erreur et réessaie avec P1/P2/P3
"equipe": tool_context.agent_namel'équipe est le nom de l'agent qui appelle l'outil : le modèle ne peut pas se tromper ni mentir sur ce champ
tool_context.actions.skip_summarization = TrueADK considère le résultat de l'outil comme réponse finale : pas de nouvel appel au modèle pour « résumer »
flowchart LR
  M["Modèle du spécialiste"] -->|"enregistrer_triage(P2, ...)"| V{"priorité valide ?"}
  V -->|"non"| ER["{'erreur': ...} renvoyé au modèle"]
  ER --> M
  V -->|"oui"| ST[("state['triage'] : equipe = agent_name")]
  ST --> SK["skip_summarization : fin du tour"]

Pourquoi l'équipe est-elle tirée de tool_context.agent_name plutôt que d'un paramètre de l'outil ?

Les outils métier

Un spécialiste qui vérifie avant d'écrire fait de meilleurs brouillons. Deux outils simples, sur des données fictives :

files/agent.py
FACTURES = {"F-2026-0412": {"montant": 49.0, "statut": "payée", "prelevements": 2},
            "F-2026-0398": {"montant": 19.0, "statut": "impayée", "prelevements": 0}}
SERVICES = {"application web": "opérationnel", "api": "dégradé (latence élevée depuis 9 h)",
            "synchronisation mobile": "opérationnel", "export pdf": "en panne"}


def consulter_facture(numero: str) -> dict:
    """Retrouve une facture (montant, statut, nombre de prélèvements).

    Args:
        numero: par exemple "F-2026-0412".
    """
    return FACTURES.get(numero.upper(), {"erreur": "Facture introuvable."})
SpécialisteOutil métierCe qu'il permet de confirmer
facturationconsulter_facturele double prélèvement est réel (prelevements: 2)
techniqueetat_des_servicesl'export PDF est bien en panne : on peut l'écrire au client
compteaucundans la réalité : statut de la double authentification, dernières connexions

Le modèle appelle enregistrer_triage(priorite="urgent", ...). Que se passe-t-il ?

Lire le résultat

Après le run, le code lit simplement l'état de la session :

files/evaluer.py
s = await runner.session_service.get_session(app_name="triage", user_id="support", session_id=s.id)
triage = s.state.get("triage") or {"equipe": "aucune", "priorite": "-", "reponse_proposee": ""}

Si le routeur avait répondu lui-même sans transférer, triage serait absent : l'évaluation le compterait comme une erreur (obtenu = aucune).

Exercice : un champ « délai de réponse »

Ajoutez au triage un champ delai_cible calculé par le code à partir de la priorité (P1 : 1 h, P2 : 8 h, P3 : 48 h). Le modèle doit-il le fournir ?

Voir la solution

Non : c'est une règle déterministe, le code la connaît. On ne l'ajoute pas aux paramètres de l'outil.

DELAIS = {"P1": "1 h", "P2": "8 h", "P3": "48 h"}

def enregistrer_triage(priorite, resume, reponse_proposee, tool_context):
    if priorite not in PRIORITES:
        return {"erreur": ...}
    tool_context.state["triage"] = {"equipe": tool_context.agent_name, "priorite": priorite,
                                    "delai_cible": DELAIS[priorite],
                                    "resume": resume, "reponse_proposee": reponse_proposee}
    ...

Même principe que pour equipe : ne demandez au modèle que ce qu'il est seul à pouvoir juger (la priorité, le résumé, le brouillon).

Résultat obtenu

Trois triages réels issus de l'évaluation (gemini-3.5-flash, 2026-10-15) :

T01 facturation P1  Bonjour, nous avons bien constaté qu'un double prélèvement a eu lieu pour la
                    facture F-2026-0412. Notre équipe financière prend en charge votre dossier afin
                    de procéder à la régularisation. [...]
T06 facturation P3  Bonjour. Les virements bancaires peuvent nécessiter quelques jours ouvrés avant
                    d'être traités [...]. La facture F-2026-0398 figure actuellement comme impayée [...]
T15 compte      P1  Nous avons bien pris en compte votre signalement concernant l'accès non autorisé
                    à votre compte [...] transmis en priorité absolue à notre équipe de sécurité [...]

Dans T01, « nous avons bien constaté » s'appuie sur consulter_facture (prelevements: 2). T06 a été classé P3 alors que l'étiquette attendue est P2 : on y revient dans l'analyse d'erreurs.

Je sais faire

  • Obtenir un résultat structuré en faisant appeler un outil au modèle
  • Valider les valeurs d'un outil et laisser le modèle se corriger
  • Remplir un champ depuis tool_context plutôt que depuis le modèle
  • Terminer un tour avec skip_summarization
Suivant