درس 2 — Construire pas à pas

Anonymiser et bloquer les fuites

5 دقائق قراءة

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

Anonymiser et bloquer les fuites

1. Liste blanche plutôt que liste noire

Deux façons d'anonymiser :

ApprochePrincipeRisque
Liste noireon retire ce qu'on sait sensible (nom, âge…)tout ce qu'on a oublié passe
Liste blancheon ne garde que ce qui est utile au posteon peut retirer un peu trop (on l'ajoute alors exprès)

Le lab garde quatre sections, puis nettoie leur texte :

files/agent.py (anonymiser)
SECTIONS_UTILES = ["experience", "competences", "formation", "langues"]  # ni titre, ni loisirs, ni identité
FORME_UNIQUE = {"développeuse": "développeur", "ingénieure": "ingénieur", "diplômée": "diplômé",
                "certifiée": "certifié", "référente": "référent"}


def anonymiser(cv: dict) -> str:
    ident = cv["identite"]
    texte = "\n".join(f"{s.upper()} : {cv['sections'][s]}" for s in SECTIONS_UTILES)
    for mot in ident["nom"].split():                                   # prénom et nom, où qu'ils soient
        texte = re.sub(rf"\b{re.escape(mot)}\b", "[NOM]", texte, flags=re.I)
    texte = re.sub(r"[\w.+-]+@[\w-]+\.[\w.]+", "[EMAIL]", texte)
    texte = re.sub(r"(?:\+33|0)\d(?:[ .]?\d{2}){4}", "[TEL]", texte)
    texte = re.sub(r"\b(Mme|Mlle|M\.|Madame|Monsieur)\s", "", texte)
    for f, m in FORME_UNIQUE.items():
        texte = re.sub(rf"\b{f}(s?)\b", rf"{m}\1", texte)
        texte = re.sub(rf"\b{f.capitalize()}(s?)\b", rf"{m.capitalize()}\1", texte)
    return texte

Pourquoi la section « titre » n'est-elle pas transmise au modèle ?

2. Les termes qui ne doivent jamais passer

Pour chaque CV, on calcule la liste des valeurs sensibles de ce CV précis :

files/agent.py (termes_sensibles)
def termes_sensibles(cv: dict) -> list[str]:
    """Ce qui ne doit JAMAIS atteindre le modèle."""
    i = cv["identite"]
    return [*i["nom"].split(), i["email"], i["telephone"], i["date_naissance"][:4], i["nationalite"],
            i["adresse"].split(",")[-1].strip(), i["situation_familiale"]]

Pour C1, cela donne par exemple : Camille, Martin, l'e-mail, le téléphone, 1991, française, 44000 Nantes, mariée, 2 enfants.

3. Le garde-fou : before_model_callback

Un callback ADK est une fonction que le framework appelle à un moment précis. before_model_callback est appelé juste avant chaque appel au modèle, avec la requête complète (LlmRequest : instruction système + messages). S'il renvoie None, l'appel a lieu ; s'il renvoie une réponse, l'appel est remplacé ; s'il lève une exception, l'exécution s'arrête.

files/agent.py (garde_anonymat)
def garde_anonymat(callback_context: CallbackContext, llm_request: LlmRequest) -> LlmResponse | None:
    """before_model_callback : exécuté avant CHAQUE appel au modèle. Lever une exception arrête tout."""
    texte = str(llm_request.config.system_instruction or "") + " ".join(
        p.text or "" for c in llm_request.contents for p in c.parts or [])
    fuites = [t for t in callback_context.state.get("termes_sensibles", []) if t.lower() in texte.lower()]
    if fuites:
        raise PermissionError(f"appel bloqué, attributs sensibles présents : {fuites}")
    return None  # None = l'appel au modèle peut avoir lieu

Les termes sensibles sont placés dans l'état de la session à sa création, et lus par le callback via callback_context.state :

files/agent.py (evaluer_texte)
session = await runner.session_service.create_session(
    app_name="cv", user_id="recruteur", state={"termes_sensibles": sensibles})
sequenceDiagram
  participant P as evaluer_texte
  participant ADK as ADK
  participant CB as garde_anonymat
  participant G as Gemini
  P->>ADK: run_async(CV anonymisé)
  ADK->>CB: LlmRequest (instruction + CV)
  alt terme sensible trouvé
    CB-->>ADK: PermissionError
    ADK-->>P: exécution arrêtée, aucun appel au modèle
  else aucun terme
    CB-->>ADK: None
    ADK->>G: appel au modèle
  end

On envoie par erreur le CV brut (non anonymisé) de C1 à evaluer_texte avec ses termes sensibles. Que se passe-t-il ?

Exercice : bloquer aussi les e-mails inconnus

termes_sensibles ne connaît que l'e-mail déclaré dans l'identité. Ajoutez au callback une vérification générique : bloquer toute requête qui contient une adresse e-mail, quelle qu'elle soit.

Voir la solution
files/agent.py (ajout dans garde_anonymat)
EMAIL = re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+")

if EMAIL.search(texte):
    raise PermissionError("appel bloqué : adresse e-mail présente dans la requête")

Ajoutez un test : un CV dont l'expérience contient « contact : x@example.org » doit être bloqué si l'on saute l'anonymisation, et passer après anonymisation (l'adresse y devient [EMAIL]).

Résultat obtenu

Tests sans clé du 2026-10-15 : pour les 6 CV, aucun terme sensible ne subsiste après anonymisation, et le callback bloque bien un CV brut avant tout appel au modèle.

pytest -v (extrait)
test_agent.py::test_aucun_attribut_sensible_apres_anonymisation[C1] PASSED
test_agent.py::test_aucun_attribut_sensible_apres_anonymisation[C2] PASSED
test_agent.py::test_aucun_attribut_sensible_apres_anonymisation[C3] PASSED
test_agent.py::test_aucun_attribut_sensible_apres_anonymisation[C4] PASSED
test_agent.py::test_aucun_attribut_sensible_apres_anonymisation[C5] PASSED
test_agent.py::test_aucun_attribut_sensible_apres_anonymisation[C6] PASSED
test_agent.py::test_garde_bloque_l_appel_si_fuite PASSED

Je sais faire

  • J'anonymise par liste blanche de sections, puis je nettoie le texte restant
  • Je calcule les termes sensibles propres à chaque document
  • J'écris un before_model_callback qui inspecte la requête et bloque l'appel
  • Je passe des données au callback via l'état de session
التالي