Lección 7 — Fiabiliser : garde-fous, tests, évaluation

Tester sans clé avec un faux modèle

5 min de lectura

Este contenido aún no está traducido. Se muestra la versión original.

Tester sans clé avec un faux modèle

Pourquoi un faux modèle ?

Un vrai modèle est lent, payant et non déterministe (la même question ne donne pas toujours la même réponse). Impossible d'en faire la base d'une intégration continue. Ce qu'on veut vérifier en CI, ce n'est pas la qualité du texte (c'est le rôle de l'évaluation, leçon suivante) mais la plomberie : les agents s'enchaînent, l'état circule, la boucle s'arrête, les garde-fous se déclenchent.

flowchart LR
  subgraph Prod["Production"]
    P1["construire_pipeline()"] --> G["Gemini (réseau, payant)"]
  end
  subgraph Test["Tests pytest"]
    P2["construire_pipeline(FauxModele())"] --> F["FauxModele (BaseLlm, local)"]
  end

La seule condition : que le modèle soit injectable. C'est pourquoi construire_pipeline(model=None) accepte un modèle en paramètre, et que LlmAgent(model=...) accepte aussi bien un nom ("gemini-3.5-flash") qu'un objet BaseLlm.

Ce qu'on testeCommentRéseau ?
Règles de score, masquage, contrôlesappel direct des fonctions puresnon
Callbacksappel direct avec un faux contexte (SimpleNamespace)non
Enchaînement, état, boucle, arrêtpipeline complet + FauxModelenon
Qualité des rapportsadk eval + vrai modèleoui

Le faux modèle

Il suffit d'implémenter generate_content_async(llm_request, stream=False), un générateur asynchrone qui produit des LlmResponse. Le nôtre lit la requête pour savoir quel agent l'appelle (d'après les outils disponibles et l'instruction) :

files/test_pipeline.py (extrait)
class FauxModele(BaseLlm):
    """Imite Gemini : appelle l'outil disponible, refuse la 1re version du rapport, approuve la 2e."""

    model: str = "faux-modele"
    relectures: int = 0
    appels: int = 0

    async def generate_content_async(self, llm_request, stream=False):
        self.appels += 1
        demande = llm_request.contents[0].parts[0].text
        fournisseur = re.search(r"fournisseur : (.+)$", demande).group(1)
        outils = list(llm_request.tools_dict)
        dernier = llm_request.contents[-1]
        if any(p.function_response for p in dernier.parts or []):
            yield _reponse(types.Part(text="- résumé factuel de l'outil"))
        elif outils and outils[0].startswith("consulter_"):
            yield _reponse(types.Part(function_call=types.FunctionCall(
                name=outils[0], args={"fournisseur": fournisseur})))
        elif "approuver_rapport" in outils:
            self.relectures += 1
            if self.relectures == 1:
                yield _reponse(types.Part(text="1. Ajouter la section ## Plan d'action."))
            else:
                yield _reponse(types.Part(function_call=types.FunctionCall(name="approuver_rapport", args={})))
        elif "Rédige" in str(llm_request.config.system_instruction):
            yield _reponse(types.Part(text=RAPPORT_COMPLET))
        else:
            yield _reponse(types.Part(text="Risque financier élevé."))

Le vrai ADK fait tout le reste : exécution des outils, callbacks, output_key, escalate. Le test le plus important vérifie le scénario complet :

files/test_pipeline.py (extrait)
def test_pipeline_complet_avec_faux_modele():
    faux = FauxModele()
    r = asyncio.run(due_diligence("Composants Arvenne", faux))
    assert r["score"]["score"] == 45
    assert r["statut"] == "approuvé" and r["passages"] == 2   # la boucle a tourné 2 fois
    assert "[e-mail masqué]" in r["rapport"]                    # after_model_callback appliqué
    assert faux.appels == 3 * 2 + 1 + 2 * 2                     # 3 chercheurs, analyste, 2 tours de boucle

Le compte d'appels est un test de coût : si quelqu'un ajoute par erreur un agent ou supprime skip_summarization, le test casse.

Pourquoi la dernière assertion compte-t-elle les appels au faux modèle ?

Avec ce faux modèle, le test « Cartonnerie Belval » échouerait sur le statut. Pourquoi ?

Lancer les tests

terminal
cd due_diligence
pytest -q        # aucune variable d'environnement nécessaire

Exercice : tester le garde-fou budget en conditions réelles

Le test test_garde_fou_budget appelle le callback directement. Écrivez un test de bout en bout qui fixe MAX_APPELS_LLM à 4 et vérifie que le rapport final contient le message du garde-fou.

Voir la solution
files/test_pipeline.py (ajout)
def test_budget_coupe_le_pipeline(monkeypatch):
    monkeypatch.setattr(agent, "MAX_APPELS_LLM", 4)
    r = asyncio.run(due_diligence("Composants Arvenne", FauxModele()))
    assert r["rapport"].startswith("[garde-fou]")
    assert r["statut"] == "non approuvé"

Avec 4 appels autorisés, les 3 chercheurs font leur appel d'outil (3), un seul obtient son résumé (4), puis tout le reste reçoit la réponse du garde-fou, y compris le rédacteur dont la sortie devient rapport. Le relecteur, lui aussi bloqué, n'appelle jamais approuver_rapport : la boucle fait ses 3 tours, mais sans aucun appel au modèle. Le score (45) est quand même calculé, car les trois outils avaient répondu avant la coupure.

Résultat obtenu

pytest -v dans files/, sans aucune clé, le 2026-10-15 :

test_pipeline.py::test_score_par_fournisseur PASSED                      [ 11%]
test_pipeline.py::test_score_indetermine_si_donnee_manquante PASSED      [ 22%]
test_pipeline.py::test_masquage_email_et_iban PASSED                     [ 33%]
test_pipeline.py::test_controle_outil_refuse_nom_vide PASSED             [ 44%]
test_pipeline.py::test_garde_fou_budget PASSED                           [ 55%]
test_pipeline.py::test_pipeline_complet_avec_faux_modele PASSED          [ 66%]
test_pipeline.py::test_pipeline_fournisseur_inconnu PASSED               [ 77%]
test_pipeline.py::test_verifier_rapport PASSED                           [ 88%]
test_pipeline.py::test_approbation_refusee_si_rapport_incomplet PASSED   [100%]
============================== 9 passed in 1.61s ===============================

Je sais faire

  • Rendre le modèle injectable dans un pipeline ADK
  • Écrire un faux modèle BaseLlm qui scripte appels d'outils et textes
  • Tester un callback avec un faux contexte
  • Transformer le nombre d'appels au modèle en assertion de coût
Siguiente