Lesson 3 — Construire pas à pas

Le LoopAgent complet

6 min read

This content is not translated yet. Showing the original version.

Le LoopAgent complet

1. L'assemblage

files/agent.py (construire)
def construire(model=None) -> SequentialAgent:
    m = model if callable(model) else (lambda nom: model or (MODEL_REDACTEUR if nom != "critique" else MODEL))
    redacteur = Agent(name="redacteur", model=m("redacteur"), output_key="brouillon", include_contents="none", ...)
    critique = Agent(name="critique", model=m("critique"), output_schema=Evaluation, output_key="evaluation",
                     include_contents="none", ...)
    reviseur = Agent(name="reviseur", model=m("reviseur"), output_key="brouillon", include_contents="none", ...)
    boucle = LoopAgent(name="boucle_qualite", max_iterations=MAX_TOURS,
                       sub_agents=[critique, Arbitre(name="arbitre"), reviseur])
    return SequentialAgent(name="atelier_redaction", sub_agents=[redacteur, boucle])
flowchart TB
  subgraph SEQ["SequentialAgent atelier_redaction"]
    R["redacteur : écrit state['brouillon']"] --> LOOP
    subgraph LOOP["LoopAgent boucle_qualite, max_iterations=4"]
      C["critique : écrit state['evaluation']"] --> A["arbitre : écrit consignes, tours, historique, valide"]
      A --> V["reviseur : RÉÉCRIT state['brouillon']"]
    end
  end

Le rédacteur et le réviseur écrivent la même clé brouillon : c'est voulu. Chaque révision remplace la précédente, et le critique du tour suivant lit toujours la dernière version. L'historique des notes, lui, est conservé par l'arbitre dans historique.

2. Ce que chaque agent voit : include_contents="none"

Par défaut, un agent LLM reçoit l'historique de la conversation (les messages et réponses des agents précédents). Dans une boucle, cet historique grossit à chaque tour : coût en hausse, et un critique qui relit les anciennes versions risque de les confondre avec la nouvelle.

Avec include_contents="none", l'agent ne reçoit que son instruction. On lui passe exactement ce dont il a besoin par injection d'état :

files/agent.py (critique et réviseur)
critique = Agent(
    name="critique", ..., include_contents="none",
    instruction=f"Tu es un relecteur exigeant. Note ce texte de 1 à 5 sur chaque critère :\n{GRILLE}\n"
                "Sois sévère : 5 = rien à améliorer. Donne des remarques concrètes (quoi changer, où).\n"
                "BRIEF :\n{brief}\n\nTEXTE À ÉVALUER :\n{brouillon}")
reviseur = Agent(
    name="reviseur", ..., include_contents="none",
    instruction="Réécris le texte en appliquant TOUTES les consignes, sans rien inventer hors du brief. "
                f"Réponds uniquement par le nouveau texte ({MOTS_MIN} à {MOTS_MAX} mots, texte brut).\n"
                "BRIEF :\n{brief}\n\nCONSIGNES :\n{consignes}\n\nTEXTE ACTUEL :\n{brouillon}")
AgentVoit l'historique ?Reçoit par l'état
redacteurnonbrief
critiquenonbrief, brouillon
arbitre (Python)lit l'état directementevaluation, brouillon, tours, historique
reviseurnonbrief, consignes, brouillon

Que se passerait-il si le critique gardait include_contents par défaut ?

3. Un modèle par rôle

files/agent.py (modèles)
MODEL = os.environ.get("MODEL", "gemini-3.5-flash")
MODEL_REDACTEUR = os.environ.get("MODEL_REDACTEUR", "gemini-3.5-flash-lite")  # brouillons : modèle léger

Le rédacteur et le réviseur utilisent le modèle léger, le critique le modèle principal. Raisonnement : écrire selon des consignes précises est une tâche plus simple que juger finement un texte. Les noms viennent de variables d'environnement : vous pouvez tout passer sur un seul modèle sans toucher au code.

4. Lancer

files/agent.py (rediger)
session = await runner.session_service.create_session(
    app_name="redaction", user_id="u1", state={"brief": brief})
msg = types.Content(role="user", parts=[types.Part(text="Rédige l'offre décrite dans le brief.")])

Le brief est placé dans l'état à la création de la session ; le message utilisateur ne sert qu'à déclencher l'exécution.

Avec MAX_TOURS=4, combien d'appels au modèle au maximum pour une offre ?

Exercice : la variante avec exit_loop

Remplacez l'arbitre Python par l'outil intégré exit_loop : un critique sans output_schema, qui appelle exit_loop quand le texte lui convient, et sinon répond par ses remarques (rangées dans state["consignes"]).

Voir la solution
variante_exit_loop.py
from google.adk.agents import Agent, LoopAgent
from google.adk.tools import exit_loop

critique = Agent(
    name="critique", model=MODEL, tools=[exit_loop], output_key="consignes", include_contents="none",
    instruction=f"Évalue ce texte selon la grille :\n{GRILLE}\n"
                "Si CHAQUE critère mérite au moins 4/5, appelle l'outil exit_loop et ne réponds rien d'autre. "
                "Sinon, réponds uniquement par la liste des corrections à faire.\n"
                "TEXTE :\n{brouillon}")
boucle = LoopAgent(name="boucle_qualite", max_iterations=4, sub_agents=[critique, reviseur])

Nous avons vérifié ce mécanisme avec un faux modèle : au 2ᵉ tour, l'appel à exit_loop produit un événement escalate=True et le réviseur n'est plus appelé. Inconvénients par rapport à l'arbitre : la décision dépend du modèle (il peut valider trop tôt), et les contrôles de longueur ou de mentions obligatoires ne sont plus garantis. Un compromis courant : garder l'arbitre Python et donner exit_loop au critique seulement pour les cas où il estime le brief impossible à satisfaire.

Résultat obtenu

Texte final réel (2026-10-15), validé au tour 2 :

python agent.py (=== TEXTE FINAL ===)
Expert-comptable, ne laissez pas une panne informatique paralyser votre cabinet en période fiscale ! Vos saisies
ne peuvent pas attendre. Avec le contrat « Sérénité » d'Atelier Nimbus, PME de maintenance informatique à Nantes,
sécurisez votre infrastructure.

Notre offre s'adresse aux structures de 5 à 20 postes. Pour 89 EUR HT par poste et par mois, elle inclut la
supervision de vos équipements, la gestion de vos sauvegardes et une intervention sur site garantie sous 4 heures
ouvrées en cas d'incident. Profitez d'une formule flexible, sans engagement au-delà de 12 mois.

Anticipez les risques techniques qui menacent votre productivité. Réservez un audit gratuit de 30 minutes sur
nimbus.example/audit pour protéger l'activité de votre cabinet.

Tours : 2 | validé : True

Les deux mentions obligatoires sont présentes, et aucun chiffre n'a été inventé hors du brief.

Je sais faire

  • J'imbrique un LoopAgent dans un SequentialAgent
  • J'utilise include_contents="none" et l'injection d'état pour contrôler ce que voit chaque agent
  • Je distingue {{cle}} (f-string) et {cle} (chaîne normale) dans une instruction
  • Je sais remplacer l'arbitre par l'outil exit_loop et j'en connais les limites
Next