Lesson 4 — Tester, évaluer, exploiter

Tester sans clé, simuler un élève, mettre en service

6 min read

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

Tester sans clé, simuler un élève, mettre en service

Deux niveaux de tests

Tests unitaires (pytest)Test de bout en bout (--demo)
Clé d'APInonoui
ModèleFauxModele qui rejoue un scriptGemini, tuteur et élève
Ce qu'on vérifierègle d'adaptation, écriture de l'état, persistance SQLitecomportement réel : positionnement, retours, reprise
Durée, coût2 secondes, gratuitenviron une minute, une quinzaine d'appels
Déterministeouinon
flowchart LR
  subgraph U["pytest : sans clé"]
    F["FauxModele (script)"] --> AG["Agent + outils réels"] --> SQ[("SQLite temporaire")]
  end
  subgraph E["--demo : avec clé"]
    T["Tuteur (Gemini)"] <--> S["Élève simulé (Gemini)"]
    T --> DB[("tuteur.db")]
  end

Un faux modèle pour ADK

ADK accepte comme model n'importe quel objet qui hérite de BaseLlm. Il suffit d'implémenter generate_content_async :

files/test_tuteur.py
class FauxModele(BaseLlm):
    """Rejoue une liste de réponses : un appel d'outil puis un texte, etc."""
    script: list = []

    async def generate_content_async(self, llm_request, stream=False):
        yield LlmResponse(content=types.Content(role="model", parts=[self.script.pop(0)]))

Le test fait jouer au faux modèle les appels definir_niveau puis trois enregistrer_reponse, avec un vrai SqliteSessionService sur un fichier temporaire. Ensuite, il crée un nouveau service et une nouvelle session sur le même fichier, comme le ferait un second lancement du programme :

files/test_tuteur.py
etat = asyncio.run(_discuter(db, ["python", "r1", "r2", "r3"], script))
assert etat["user:progression"]["python"]["niveau"] == 3  # 2 bonnes d'affilée -> +1
assert etat["user:progression"]["python"]["a_revoir"] == ["dict"]

# Nouvelle session, nouveau service (= nouveau processus) : la progression « user: » est relue du disque.
etat2 = asyncio.run(_discuter(db, ["re-bonjour"], [types.Part(text="Bon retour !")]))
assert etat2["user:progression"]["python"]["bonnes"] == 2

Que prouve test_progression_persiste_entre_sessions que test_ajuster_niveau ne prouve pas ?

L'élève simulé

Pour tester sans taper les réponses, main.py --demo 1 1 1 1 0 fait répondre un second appel Gemini, qui joue un élève juste (1) ou faux (0) :

files/main.py
def eleve_simule(question: str, juste: bool) -> str:
    """Un « faux élève » : un appel Gemini direct qui répond juste ou faux exprès."""
    consigne = "correctement" if juste else "en faisant une erreur typique de débutant"
    client = genai.Client()  # garder une référence : sinon le client est fermé avant la requête
    r = client.models.generate_content(
        model=os.environ.get("MODEL", "gemini-3.5-flash"),
        contents=f"Tu joues un élève. Réponds en une ou deux phrases, {consigne}, "
                 f"à la DERNIÈRE question de ce message du tuteur :\n{question}")
    return r.text.strip()

Ce que l'on vérifie à la lecture : le positionnement a bien lieu, le tuteur appelle enregistrer_reponse avec correcte cohérent avec la consigne donnée à l'élève, il annonce les changements de niveau, et la session 2 rappelle le niveau et les notions à revoir.

Résultat obtenu

Tests du 2026-10-15, sans clé d'API :

test_tuteur.py::test_ajuster_niveau PASSED                               [ 50%]
test_tuteur.py::test_progression_persiste_entre_sessions PASSED          [100%]
============================== 2 passed in 1.85s ===============================

Test de bout en bout du même jour (extrait) : l'élève simulé « faux » a répondu A) 7 à une question sur les décorateurs ; le tuteur a appelé enregistrer_reponse {'theme': 'python', 'notion': 'décorateurs de fonction', 'correcte': False}, expliqué la bonne réponse B) 14, puis proposé une nouvelle question sur la même notion. Dans la session suivante : « Tu es actuellement au niveau 4 (avancé) en Python, et nous avons la notion des décorateurs de fonction à revoir. »

Mettre en service

En local : python main.py --user <identifiant> suffit pour un usage individuel. Le fichier tuteur.db est la seule chose à sauvegarder.

Sur Google Cloud (non exécuté dans ce lab sans GCP) : remplacez le modèle par Vertex AI (GOOGLE_GENAI_USE_VERTEXAI=TRUE, GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_LOCATION), servez l'agent avec adk deploy cloud_run et passez à un service de session partagé (DatabaseSessionService sur Cloud SQL, ou VertexAiSessionService). Le code des outils ne change pas : seul le service de session est remplacé.

Vous passez le tuteur sur Cloud Run avec plusieurs instances. Que faut-il changer dans agent.py ?

Exercice : tester la descente de niveau

Écrivez un test pytest qui place un élève au niveau 3 puis lui fait rater deux questions, et vérifiez qu'il redescend au niveau 2 avec deux notions à revoir.

Voir la solution
def test_descente(tmp_path):
    script = [appel("definir_niveau", theme="sql", niveau=3), types.Part(text="Q1"),
              appel("enregistrer_reponse", theme="sql", notion="jointures", correcte=False), types.Part(text="Q2"),
              appel("enregistrer_reponse", theme="sql", notion="group by", correcte=False), types.Part(text="Q3")]
    etat = asyncio.run(_discuter(str(tmp_path / "t.db"), ["sql", "r1", "r2"], script))
    fiche = etat["user:progression"]["sql"]
    assert fiche["niveau"] == 2 and fiche["serie"] == []
    assert fiche["a_revoir"] == ["jointures", "group by"]

Chaque message utilisateur consomme deux réponses du script : l'appel d'outil, puis le texte final.

Je sais faire

  • Écrire un faux modèle BaseLlm pour tester un agent sans clé
  • Tester la persistance avec un vrai SqliteSessionService sur un fichier temporaire
  • Simuler un utilisateur avec un second appel au modèle
  • Expliquer ce qui change (et ce qui ne change pas) pour passer sur Google Cloud