Aller au contenu

Multi-agents

Dans ce dernier chapitre de cette 2e partie, nous allons développer un système multi-agents composé de 3 agents : - un agent creator qui va rédiger l'énoncé d'un exercice - un agent reviewer qui va commenter l'énoncé de l'exercice et proposer des modifications - un agent corrector qui va modifier l'énoncé de l'exercice en se basant sur le travail de l'agent reviewer

Voici le schéma du graphe obtenu :

multi-agent

Normalement vous devriez être capable de comprendre le code ci-dessous, je vous laisse donc l'analyser par vous-même (vous trouverez tout de même quelques explications en dessous) :

from pydantic import BaseModel, Field
from langgraph.graph import StateGraph, START, END
from langchain_core.output_parsers import JsonOutputParser
from langchain.prompts import ChatPromptTemplate
from langchain_google_genai import ChatGoogleGenerativeAI
from dotenv import load_dotenv
from IPython.display import Markdown, display
from pypdf import PdfReader
import os

load_dotenv()
API_KEY = os.getenv('GOOGLE_API_KEY')

llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash")

counter = 0

class EvalExo(BaseModel):
  note : int = Field(description="Note sur 10 attribuée à l'exercice")
  comment : str = Field(description="Commentaire sur la qualité de l'exercice qui permet de justifier la note")
  modif : str = Field(description="Proposition de modifications afin d'améliorer la qualité de l'exercice")

class State(BaseModel):
    order : str
    statement : str
    reviewer_comment : EvalExo

parser_eval = JsonOutputParser(pydantic_object=EvalExo)

graph_builder = StateGraph(State)

reader = PdfReader("./prog_math_2de.pdf")
progMath_2de = ""
for page in reader.pages:
    text = page.extract_text()
    if text:
        progMath_2de += text

prompt_sys_creator = """
    Ton rôle est de créer un exercice de mathématiques.
    Tu dois bien veiller à ce que les questions posées soient en adéquation avec le programme de seconde.
    Les questions de l'exercice doivent être progressives
    Tu dois utiliser le markdown et les formules mathématiques doivent être au format LaTeX
    Tu ne dois pas faire d'introduction, tu dois uniquement proposer l'énoncé de l'exercice
    """
prompt_sys_reviewer = """
    Ton rôle est d'évaluer la qualité d'un exercice de mathématiques.
    Plus précisément, tu dois effectuer les opérations suivantes :\n
    - mettre une note sur 10. Ta note doit refléter la qualité globale de l'exercice (intérêt pédagogique, difficulté bien adaptée, rigueur mathématique, respect du programme...).
        Un exercice avec une ou plusieurs questions hors programme devra obtenir une note inférieure à 4.\n
    - Faire un commentaire sur la qualité et l'intérêt de l'exercice. Tu pourras, entre autres, préciser les points du programme que l'exercice permet de travailler\n
    - Faire des propositions de modifications qui permettront d'améliorer la qualité de l'exercice. 
        Si tu trouves que l'exercice est déjà de bonne qualité et qu'il n'a besoin d'aucune modification, tu dois laisser ce champ vide (modif = "").\n
    Le format de ta réponse doit être identique au format défini dans EVALEXO : \n
    ### EVALEXO
    {eval}
    Voici un exemple de ce que tu dois produire (au format JSON):
    {{"note" : 7, "comment" : "C'est un bon exercice", "modif" : "La question 4 est un peu difficile, il faudrait la modifier"}}
    Réponds uniquement avec l’objet JSON, sans balises Markdown, sans texte avant ou après (tu ne dois jamais utiliser ```json)
    """
prompt_sys_corrector = """
    Ton rôle est de modifier l'énoncé d'un exercice de mathématiques.
    Pour apporter ces modifications tu dois obligatoirement t'appuyer sur les commentaires rédigés par un expert.
    Tu dois uniquement rédiger une version modifiée de l'énoncé de l'exercice
    """

prompt_creator = ChatPromptTemplate([
    ("system", prompt_sys_creator),
    ("human", "Voici le programme de mathématiques de la classe de 2de :\n {prog_2de}"),
    ("human", "Voici les consignes de l'utilisateur pour la création de l'exercice :\n {order}"),
])

prompt_reviewer = ChatPromptTemplate([
    ("system", prompt_sys_reviewer),
    ("human", "Voici le programme de mathématiques de la classe de 2de :\n {prog_2de}"),
    ("human", "Voici les consignes de l'utilisateur pour la création de l'exercice :\n {order}"),
    ("human", "Voici l'énoncé de l'exercice que tu dois analyser : {statement}"),
])

prompt_corrector = ChatPromptTemplate([
    ("system", prompt_sys_corrector),
    ("human", "Voici l'énoncé de l'exercice que tu dois modifier : {statement}"),
    ("human", "Voici les propositions de modifications de l'expert que tu dois suivre à la lettre :\n {reviewer_modif}"),
])

def creator(state : State):
    llm_prompt = prompt_creator | llm
    rep = llm_prompt.invoke({'prog_2de' : progMath_2de, 'order' : state.order})
    print("creator")
    with open("exo.md", "w", encoding="utf-8") as f:
        f.write("creator"+"\n\n"+rep.content+"\n\n")
    return {'order' : state.order, 'statement' : rep.content, 'reviewer_comment' : state.reviewer_comment}

def reviewer(state : State):
    global counter
    counter += 1
    llm_prompt = prompt_reviewer | llm | parser_eval
    rep = llm_prompt.invoke({'eval': parser_eval, 'prog_2de' : progMath_2de, 'order' : state.order, 'statement' : state.statement})
    print(f"reviewer n°{counter}")
    with open("exo.md", "a", encoding="utf-8") as f:
        f.write(f"reviewer n°{counter}"+"\n\n"+f"Note : {rep['note']}"+"\n\n"+f"Commentaire : {rep['comment']}"+"\n\n"+f"Modifications proposées : {rep['modif']}"+"\n\n")
    return {'order' : state.order, 'statement' : state.statement, 'reviewer_comment' : rep}

def corrector(state : State):
    llm_prompt = prompt_corrector | llm
    rep = llm_prompt.invoke({'statement' : state.statement, 'reviewer_modif' : state.reviewer_comment.modif})
    print("corrector")
    with open("exo.md", "a", encoding="utf-8") as f:
        f.write("corrector"+"\n\n"+rep.content+"\n\n")
    return {'order' : state.order, 'statement' : rep.content, 'reviewer_comment' : state.reviewer_comment}

def router(state : State):
    if state.reviewer_comment.modif == "" or counter > 4:
        print("FIN")
        return "END"
    else :
        return "corrector"

graph_builder.add_node("creator", creator)
graph_builder.add_node("reviewer", reviewer)
graph_builder.add_node("corrector", corrector)

graph_builder.add_edge(START, "creator")
graph_builder.add_edge("creator", "reviewer")

graph_builder.add_conditional_edges("reviewer",router,{"END": END, "corrector": "corrector" })
graph_builder.add_edge( "corrector", "reviewer")
graph_builder.add_edge("reviewer", END)
graph = graph_builder.compile()

user_order = "Rédige moi un exercice de 5 ou 6 questions sur la partie Nombres et calculs"

g = graph.invoke({"order" : user_order, "statement" : "", "reviewer_comment" : EvalExo(note = -1, comment = "", modif = "")})
[Out]
    creator
    reviewer n°1
    corrector
    reviewer n°2
    corrector
    reviewer n°3
    FIN

Quelques explications :

L'état (state)

Cette fois, nous avons un état un peu plus complexe que d'habitude, puisque l'on trouve : - order qui correspond aux instructions données par l'utilisateur de l'application (exemple : "Rédige moi un exercice de 5 ou 6 questions sur la partie Nombres et calculs") - statement qui correspond à l'énoncé de l'exercice rédigé par creator et modifié par corrector - reviewer_comment est un cas un peu particulier, parce que son type est EvalExo. EvalExo est aussi un objet Pydantic qui contient : note, comment et modif. Pour ces 3 attributs, nous avons utilisé Field qui permet de décrire le rôle de chacun.

Agent creator

Rien de particulier pour cet agent, que du déjà vu

Agent reviewer

Le LLM lié à cet agent doit produire un objet JSON correspondant à l'objet Pydantic EvalExo. Si ce n'est pas le cas, notre application plantera (nous faisons planter l'application si le format de sortie du LLM n'est pas correct grâce à l'utilisation d'un JsonOutputParser). Nous avons donc ici une contrainte extrêmement forte. Et malgré toutes les précautions prises (indications dans le prompt système, utilisation du JsonOutputParser toujours dans le prompt), il peut arriver que le LLM ne renvoie pas le format attendu ! Ce genre de problème nous amène à une réflexion plus profonde sur l'utilisation des LLM :

Réponse inattendue du LLM

Au-delà du cas particulier que nous traitons ici, nous ne pouvons en aucune façon être sûr que la réponse d'un LLM correspondra à nos attentes. Cela doit nous amener à nous interroger sur les applications basées sur les LLM destinées aux élèves. Sachant que, par nature, la réponse du LLM est imprévisible, doit-on laisser les élèves "seuls" avec ce genre d'outil ? La réponse institutionnelle est claire : NON ! Ma réponse personnelle est un peu plus nuancée : il faut prendre toutes les précautions possibles, notamment en formant les élèves sur les "dangers" liés à l'utilisation de ces applications.

Agent corrector

Ici aussi, rien de vraiment nouveau

Le routeur

Vous pouvez constater qu'ici l'aiguillage au niveau de l'agent reviewer entre le nœud END et le nœud corrector n'est pas "automatique", il s'appuie sur la fonction router. Rien de très compliqué dans cette fonction, vous remarquerez tout de même la mise en place d'un compteur (variable counter) qui évite les aller-retour interminables entre le reviewer et le corrector.

Création d'un fichier exo.md

Le programme ci-dessus produit un fichier exo.md (utilisation des with open("exo.md", "a", encoding="utf-8") as f:) qui contient à la fin de l'exécution les différentes versions de l'énoncé de l'exercice créé ainsi que les commentaires produits par reviewer. Il est possible de visualiser le contenu de ce fichier en exécutant la cellule suivante.

with open("exo.md", "r", encoding="utf-8") as f:
    content = f.read()

display(Markdown(content))
[Out]

creator

Exercice : Nombres et calculs

  1. Encadrement et Approximation

    • Donner un encadrement du nombre \(\pi\) d'amplitude \(10^{-2}\) par deux nombres décimaux.
    • En déduire une valeur approchée de \(\pi\) à \(10^{-2}\) près par défaut.
  2. Nature de Nombres

    • Le nombre \(\frac{7}{3}\) est-il un nombre décimal ? Justifier votre réponse.
    • Le nombre \(\sqrt{9}\) est-il un nombre irrationnel ? Justifier votre réponse.
  3. Calcul Littéral et Identités Remarquables

    • Développer et simplifier l'expression suivante : \((2x - 3)^2 - (x + 2)(x - 2)\).
    • Factoriser l'expression suivante : \(4x^2 - 9\).
  4. Puissances et Racines Carrées

    • Simplifier l'expression suivante : \(\frac{(a^3b^{-2})^2}{a^{-1}b^3}\), où \(a\) et \(b\) sont des nombres réels non nuls.
    • Écrire sous la forme \(a\sqrt{b}\)\(a\) est un entier et \(b\) un entier le plus petit possible : \(2\sqrt{48} - \sqrt{75}\).
  5. Inégalités et Intervalles

    • Résoudre l'inéquation suivante : \(3x + 5 < 7x - 3\). Exprimer l'ensemble des solutions sous forme d'intervalle.
    • Représenter sur une droite numérique l'intervalle des nombres réels \(x\) tels que \(|x - 1| \leq 2\).
  6. Problème Modélisation

    • Un rectangle a pour longueur \(L = x + 3\) et pour largeur \(l = 5\), où \(x\) est un nombre réel positif. Déterminer les valeurs de \(x\) pour lesquelles le périmètre de ce rectangle est inférieur à 26.

reviewer n°1

Note : 8

Commentaire : L'exercice est globalement de bonne qualité et couvre plusieurs aspects du programme de Seconde relatifs aux nombres et calculs. Les questions sont variées et permettent de tester la compréhension des concepts et la maîtrise des techniques de calcul. Il y a un bon équilibre entre les questions théoriques et les questions de mise en application. L'exercice est bien adapté au niveau de la classe de seconde. La question 6 permet de faire le lien avec la géométrie.

Modifications proposées : Dans la question 4, il serait peut-être plus pertinent de préciser que a et b sont des réels strictement positifs.

corrector

Exercice : Nombres et calculs

  1. Encadrement et Approximation

    • Donner un encadrement du nombre \(\pi\) d'amplitude \(10^{-2}\) par deux nombres décimaux.
    • En déduire une valeur approchée de \(\pi\) à \(10^{-2}\) près par défaut.
  2. Nature de Nombres

    • Le nombre \(\frac{7}{3}\) est-il un nombre décimal ? Justifier votre réponse.
    • Le nombre \(\sqrt{9}\) est-il un nombre irrationnel ? Justifier votre réponse.
  3. Calcul Littéral et Identités Remarquables

    • Développer et simplifier l'expression suivante : \((2x - 3)^2 - (x + 2)(x - 2)\).
    • Factoriser l'expression suivante : \(4x^2 - 9\).
  4. Puissances et Racines Carrées

    • Simplifier l'expression suivante : \(\frac{(a^3b^{-2})^2}{a^{-1}b^3}\), où \(a\) et \(b\) sont des nombres réels strictement positifs.
    • Écrire sous la forme \(a\sqrt{b}\)\(a\) est un entier et \(b\) un entier le plus petit possible : \(2\sqrt{48} - \sqrt{75}\).
  5. Inégalités et Intervalles

    • Résoudre l'inéquation suivante : \(3x + 5 < 7x - 3\). Exprimer l'ensemble des solutions sous forme d'intervalle.
    • Représenter sur une droite numérique l'intervalle des nombres réels \(x\) tels que \(|x - 1| \leq 2\).
  6. Problème Modélisation

    • Un rectangle a pour longueur \(L = x + 3\) et pour largeur \(l = 5\), où \(x\) est un nombre réel positif. Déterminer les valeurs de \(x\) pour lesquelles le périmètre de ce rectangle est inférieur à 26.

reviewer n°2

Note : 8

Commentaire : L'exercice est globalement bon et couvre plusieurs aspects du programme de seconde sur les nombres et le calcul. Les questions sont variées et permettent de tester la compréhension des concepts et la maîtrise des techniques de calcul. Il aborde l'encadrement, la nature des nombres, le calcul littéral, les puissances, les racines carrées, les inéquations et la modélisation. L'exercice est bien adapté au niveau de la seconde.

Modifications proposées : Dans la question 4, il serait peut-être pertinent de préciser que a et b sont des réels strictement positifs.

corrector

Exercice : Nombres et calculs

  1. Encadrement et Approximation

    • Donner un encadrement du nombre \(\pi\) d'amplitude \(10^{-2}\) par deux nombres décimaux.
    • En déduire une valeur approchée de \(\pi\) à \(10^{-2}\) près par défaut.
  2. Nature de Nombres

    • Le nombre \(\frac{7}{3}\) est-il un nombre décimal ? Justifier votre réponse.
    • Le nombre \(\sqrt{9}\) est-il un nombre irrationnel ? Justifier votre réponse.
  3. Calcul Littéral et Identités Remarquables

    • Développer et simplifier l'expression suivante : \((2x - 3)^2 - (x + 2)(x - 2)\).
    • Factoriser l'expression suivante : \(4x^2 - 9\).
  4. Puissances et Racines Carrées

    • Simplifier l'expression suivante : \(\frac{(a^3b^{-2})^2}{a^{-1}b^3}\), où \(a\) et \(b\) sont des nombres réels strictement positifs.
    • Écrire sous la forme \(a\sqrt{b}\)\(a\) est un entier et \(b\) un entier le plus petit possible : \(2\sqrt{48} - \sqrt{75}\).
  5. Inégalités et Intervalles

    • Résoudre l'inéquation suivante : \(3x + 5 < 7x - 3\). Exprimer l'ensemble des solutions sous forme d'intervalle.
    • Représenter sur une droite numérique l'intervalle des nombres réels \(x\) tels que \(|x - 1| \leq 2\).
  6. Problème Modélisation

    • Un rectangle a pour longueur \(L = x + 3\) et pour largeur \(l = 5\), où \(x\) est un nombre réel positif. Déterminer les valeurs de \(x\) pour lesquelles le périmètre de ce rectangle est inférieur à 26.

reviewer n°3

Note : 9

Commentaire : L'exercice est bien conçu et couvre plusieurs aspects du programme de seconde sur les nombres et calculs. Les questions sont variées et permettent de tester la compréhension des élèves sur les différents concepts abordés (encadrement, nature des nombres, calcul littéral, puissances, racines carrées, inégalités, intervalles, modélisation). La difficulté des questions est globalement bien adaptée au niveau de la seconde. Il permet de travailler les compétences : chercher, calculer, représenter, raisonner et communiquer.

Modifications proposées :

Exercice : Adaptez le programme pour qu'il produise des exercices dans la discipline de votre choix (et au niveau de votre choix)