درس 4 — Tester, évaluer, exploiter

Tester, mesurer, exploiter

5 دقائق قراءة

هذا المحتوى غير مترجم بعد. تُعرض النسخة الأصلية.

Tester, mesurer, exploiter

1. Ce qu'il faut prouver

flowchart LR
  T1["test_grille_valide_et_refuse"] --> G["la grille accepte et refuse ce qu'il faut"]
  T2["test_boucle_s_arrete_des_que_la_grille_est_satisfaite"] --> S["escalate arrête la boucle, pas de révision inutile"]
  T3["test_boucle_bornee_par_max_iterations"] --> B["jamais plus de MAX_TOURS tours"]

Les trois propriétés qui comptent dans une boucle : elle juge juste, elle s'arrête quand c'est bon, elle s'arrête toujours.

2. Des faux modèles qui comptent leurs appels

files/test_agent.py (faux modèle)
class FauxModele(BaseLlm):
    model: str = "faux"
    script: list = []
    appels: int = 0

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

Le critique « répond » des notes en JSON, comme le ferait Gemini avec le schéma :

files/test_agent.py (arrêt sur succès)
def test_boucle_s_arrete_des_que_la_grille_est_satisfaite():
    modeles, f = faux([note(3), note(5)], ["v1 " + TEXTE_OK, "jamais utilisé"])
    etat = asyncio.run(a.rediger("brief", agent=a.construire(f)))
    assert etat["valide"] and etat["tours"] == 2
    assert etat["brouillon"].startswith("v1")
    assert modeles["reviseur"].appels == 1             # pas de réécriture après validation
    assert [h["valide"] for h in etat["historique"]] == [False, True]
files/test_agent.py (arrêt de sécurité)
def test_boucle_bornee_par_max_iterations():
    n = a.MAX_TOURS
    modeles, f = faux([note(2)] * n, [f"v{i} " + TEXTE_OK for i in range(1, n + 1)])
    etat = asyncio.run(a.rediger("brief", agent=a.construire(f)))
    assert not etat["valide"] and etat["tours"] == n and modeles["critique"].appels == n

Dans test_boucle_bornee_par_max_iterations, que vaut etat["valide"] et pourquoi ?

3. Exécution réelle

terminal
cd files && pip install -r requirements.txt
pytest -q                     # sans clé
cp .env.example .env          # mettez votre clé dans .env
python agent.py               # 3 à 9 appels au modèle

4. Mesurer avec l'historique

L'arbitre range chaque tour dans state["historique"]. Sur plusieurs briefs, c'est votre tableau de bord :

IndicateurComment le calculerCe qu'il révèle
Taux de validationpart des exécutions avec valide=Truegrille trop dure si trop bas
Tours moyensmoyenne de tourscoût : 2 × tours appels si validé, 1 + 2 × tours sinon
Gain par tourdifférence de moyenne des notes entre toursutile de boucler ?
Validé au tour 1part des premiers jets acceptésgrille trop indulgente si proche de 100 %

Sur 50 briefs, 49 textes sont validés dès le tour 1. Que conclure en priorité ?

5. Exploiter

  • Brief en entrée, texte et historique en sortie : stockez les deux. L'historique explique pourquoi un texte a été accepté.
  • Statut non validé : envoyez-le dans une file de relecture humaine plutôt que de le publier.
  • Mentions obligatoires : dans le lab elles sont en dur ; en production, mettez-les dans le brief (state["mentions"]) et lisez-les dans arbitrer().
  • Sur Google Cloud : le même pipeline tourne sur Cloud Run ou Vertex AI Agent Engine avec GOOGLE_GENAI_USE_VERTEXAI=TRUE, sans modifier le code.

Exercice : rendre les mentions obligatoires paramétrables

Modifiez le code pour que les mentions obligatoires viennent de l'état de session (state["mentions"], une liste) au lieu de la constante.

Voir la solution
files/agent.py (modifications)
def arbitrer(evaluation: dict, texte: str, mentions: list[str]) -> tuple[bool, list[str]]:
    ...
    consignes += [f"mention obligatoire absente : « {m} »" for m in mentions if m not in texte]
    ...

# dans Arbitre._run_async_impl
ok, consignes = arbitrer(s.get("evaluation") or {}, s.get("brouillon", ""), s.get("mentions", []))

# dans rediger()
state={"brief": brief, "mentions": mentions}

Pensez à mettre à jour les appels à arbitrer() dans test_agent.py.

Résultat obtenu

Tests sans clé (2026-10-15) :

pytest -v
test_agent.py::test_grille_valide_et_refuse PASSED
test_agent.py::test_boucle_s_arrete_des_que_la_grille_est_satisfaite PASSED
test_agent.py::test_boucle_bornee_par_max_iterations PASSED
============================== 3 passed in 1.27s ===============================

Historique de l'exécution réelle du même jour : 2 tours, 4 appels au modèle (1 rédacteur, 2 critiques, 1 réviseur ; l'arbitre est du code). La moyenne passe de 4,4 à 4,8 ; seul le critère clarte reste à 4.

python agent.py (historique)
{'tour': 1, 'evaluation': {'clarte': 4, 'concret': 5, 'appel_action': 5, 'ton': 4, 'correction': 4, ...}, 'valide': False}
{'tour': 2, 'evaluation': {'clarte': 4, 'concret': 5, 'appel_action': 5, 'ton': 5, 'correction': 5, ...}, 'valide': True}

Je sais faire

  • Je teste l'arrêt sur succès et l'arrêt de sécurité d'une boucle sans clé d'API
  • Je compte les appels d'un faux modèle pour détecter une boucle qui tourne trop
  • J'exploite l'historique pour mesurer le taux de validation et le coût par texte
  • Je route les textes non validés vers une relecture humaine