Lesson 2 — Construire pas à pas

Un sous-agent par canal

5 min read

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

Un sous-agent par canal

Les canaux sont des données, pas du code

Les différences entre canaux tiennent en deux informations : une longueur maximale et une consigne de style. On les range dans un dictionnaire ; ajouter un canal = ajouter une ligne.

files/agent.py
CANAUX = {
    "linkedin": (1300, "Post LinkedIn professionnel : une accroche forte en 1re ligne, "
                       "3 paragraphes courts, un appel à l'action, 3 hashtags maximum."),
    "instagram": (800, "Légende Instagram : ton chaleureux, 2 ou 3 emojis, phrases courtes, "
                       "5 hashtags en fin de texte."),
    "newsletter": (1500, "Bloc de newsletter : un objet d'e-mail (ligne 'Objet : ...'), "
                         "un titre, 2 paragraphes, un bouton d'action entre crochets."),
    "whatsapp": (350, "Message WhatsApp de diffusion : 2 à 3 phrases, vouvoiement, "
                      "1 emoji au plus, un lien factice https://exemple.fr/produit."),
}

Les règles de conformité sont communes et ajoutées à chaque instruction :

files/agent.py
REGLES = """Règles de conformité (obligatoires) :
- n'invente aucun chiffre, aucune caractéristique, aucun prix absent du brief ;
- aucune promesse absolue : pas de « garanti à vie », « 100 % », « sans risque »,
  « le meilleur du marché », « miracle », ni d'allégation de santé ;
- n'écris aucune donnée personnelle (e-mail, téléphone, nom d'un particulier) ;
- si le brief contient [EMAIL], [TELEPHONE]... ignore ces marqueurs."""

Fabriquer un agent par canal

files/agent.py
def agent_canal(canal: str, max_car: int, consigne: str, model=MODEL) -> Agent:
    return Agent(
        name=canal,
        model=model,
        description=f"Rédige la déclinaison {canal} du brief.",
        instruction=f"Tu es rédacteur marketing. À partir du brief produit de l'utilisateur, "
                    f"écris UNIQUEMENT le texte final pour ce canal, en français.\n{consigne}\n"
                    f"Longueur maximale : {max_car} caractères.\n{REGLES}",
        output_key=f"texte_{canal}",
        before_model_callback=moderer_entree,
        after_model_callback=moderer_sortie,
    )


def creer_agent(model=MODEL) -> ParallelAgent:
    return ParallelAgent(
        name="declinaisons",
        description="Produit en parallèle les 4 déclinaisons d'un brief.",
        sub_agents=[agent_canal(c, m, txt, model) for c, (m, txt) in CANAUX.items()],
    )

root_agent = creer_agent()
  • name=canal : chaque agent a un nom unique ; il servira aussi dans le journal de modération (callback_context.agent_name).
  • output_key=f"texte_{canal}" : ADK écrit la réponse finale de l'agent dans state["texte_linkedin"], etc. Pas besoin de parcourir les événements.
  • model= est un paramètre : les tests y passeront un faux modèle.
flowchart TB
  C["CANAUX (dictionnaire)"] --> F["agent_canal() x 4"]
  F --> A1["Agent linkedin, output_key texte_linkedin"]
  F --> A2["Agent instagram, output_key texte_instagram"]
  F --> A3["Agent newsletter, output_key texte_newsletter"]
  F --> A4["Agent whatsapp, output_key texte_whatsapp"]
  A1 & A2 & A3 & A4 --> P["ParallelAgent declinaisons"]

Que contient state["texte_newsletter"] après l'exécution ?

Lancer les quatre rédacteurs et lire l'état

files/agent.py
async def decliner(brief: str, agent=root_agent) -> dict:
    runner = InMemoryRunner(agent=agent, app_name="multicanal")
    s = await runner.session_service.create_session(app_name="multicanal", user_id="u")
    async for _ in runner.run_async(user_id="u", session_id=s.id, new_message=types.Content(
            role="user", parts=[types.Part(text=brief)])):
        pass                                   # tout ce qui nous intéresse arrive dans l'état
    s = await runner.session_service.get_session(app_name="multicanal", user_id="u",
                                                 session_id=s.id)
    return s.state

Le rapport lit les quatre clés, compare chaque longueur au maximum du canal et ajoute le journal de modération :

files/agent.py
def rapport(etat: dict) -> str:
    lignes = []
    for canal, (max_car, _) in CANAUX.items():
        texte = etat.get(f"texte_{canal}", "")
        statut = "BLOQUÉ" if texte.startswith(BLOQUE) else (
            "TROP LONG" if len(texte) > max_car else "ok")
        lignes += [f"## {canal} ({len(texte)}/{max_car} car., {statut})", texte, ""]
    ...

Vous ajoutez un canal « sms » limité à 160 caractères. Que faut-il modifier ?

Exercice : un relecteur après les rédacteurs

Ajoutez une étape après les quatre rédacteurs : un agent relecteur qui lit les quatre textes dans l'état et signale les incohérences de prix ou de date entre canaux.

Voir la solution

On enchaîne le ParallelAgent et le relecteur dans un SequentialAgent. Les accolades {texte_linkedin} dans l'instruction sont remplacées par ADK par la valeur de l'état.

files/agent.py (variante)
from google.adk.agents import SequentialAgent

relecteur = Agent(
    name="relecteur", model=MODEL, output_key="relecture",
    instruction="Compare ces 4 textes et liste les incohérences de prix, de date ou de "
                "caractéristique (ou réponds « aucune »).\n"
                "LinkedIn : {texte_linkedin}\nInstagram : {texte_instagram}\n"
                "Newsletter : {texte_newsletter}\nWhatsApp : {texte_whatsapp}",
    after_model_callback=moderer_sortie,
)
root_agent = SequentialAgent(name="chaine", sub_agents=[creer_agent(), relecteur])

Le parallélisme est conservé pour les rédacteurs ; seul le relecteur attend, car il a besoin de leurs résultats.

Résultat obtenu

Exécution réelle le 2026-10-15 (gemini-3.5-flash), python agent.py brief_produit.md : en-têtes du rapport et texte LinkedIn.

## linkedin (720/1300 car., ok)
Fini les bouteilles en plastique jetables au bureau !

Voici la Clairo 500, notre gourde isotherme en inox de 500 ml conçue pour accompagner le
quotidien des actifs urbains. Dotée d'un bouchon étanche et disponible en 4 couleurs, [...]
Côté performances, nos mesures internes indiquent un maintien de vos boissons au froid
pendant 24 h et au chaud pendant 12 h. [...]
Profitez de notre prix de lancement à 29,90 € jusqu'au 30 novembre 2026 (au lieu de 34,90 €).
Commandez la vôtre dès aujourd'hui : https://exemple.fr/produit
#ZeroDechet #VieDeBureau #GourdeIsotherme
## instagram (501/800 car., ok)   ## newsletter (816/1500 car., ok)   ## whatsapp (262/350 car., ok)

Je sais faire

  • Décrire des variantes d'agent dans un dictionnaire et générer les agents
  • Regrouper des sous-agents dans un ParallelAgent
  • Récupérer la réponse de chaque agent avec output_key
  • Contrôler par le code une contrainte que le modèle respecte mal (longueur)
Next