Journaliser et mesurer la satisfaction
Pourquoi journaliser ?
Sans journal, vous ne savez pas ce que les gens demandent, ni où le chatbot se trompe. Le journal sert à trois choses : comprendre (quelles questions ?), corriger (quelles réponses ont reçu 👎 ?), surveiller (erreurs, lenteurs).
flowchart LR E["Échange terminé"] --> M["masquer() : e-mail, téléphone, IBAN"] M --> J["journal.jsonl (une ligne JSON par événement)"] A["Clic 👍 / 👎"] --> J J --> S["statistiques() → GET /stats"] S --> D["Décisions : compléter la fiche, ajuster le prompt"]
Le format JSONL (JSON Lines) met un objet JSON par ligne : on ajoute à la fin du fichier sans le relire, et n'importe quel outil (Python, jq, BigQuery) sait le lire.
Le code
EMAIL = re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+")
TELEPHONE = re.compile(r"(?:\+33|0)\s?[1-9](?:[\s.-]?\d{2}){4}")
IBAN = re.compile(r"\bFR\d{2}(?:\s?[0-9A-Z]{4}){5}\s?[0-9A-Z]{3}\b", re.I)
def masquer(texte: str) -> str:
"""Retire les données personnelles évidentes avant d'écrire le journal."""
texte = IBAN.sub("[IBAN]", texte)
texte = EMAIL.sub("[EMAIL]", texte)
return TELEPHONE.sub("[TEL]", texte)
def journaliser(evenement: dict) -> None:
evenement = {"ts": datetime.now(timezone.utc).isoformat(timespec="seconds"), **evenement}
ligne = json.dumps(evenement, ensure_ascii=False)
if os.environ.get("K_SERVICE"): # variable définie par Cloud Run :
print(ligne, flush=True) # stdout y part dans Cloud Logging
with JOURNAL.open("a", encoding="utf-8") as f:
f.write(ligne + "\n")Chaque échange produit un événement echange (question et réponse masquées, latence, modèle, erreur éventuelle), chaque clic un événement avis, relié par l'identifiant id reçu dans l'en-tête X-Echange-Id.
Les indicateurs
return {
"echanges": len(echanges),
"erreurs": sum(1 for e in echanges if e.get("erreur")),
"avis": len(avis),
"satisfaction": round(positifs / len(avis), 2) if avis else None,
"taux_avis": round(len(avis) / len(echanges), 2) if echanges else None,
"latence_mediane_ms": latences[len(latences) // 2] if latences else None,
}| Indicateur | Ce qu'il dit | Piège |
|---|---|---|
satisfaction | part des avis positifs | calculé sur ceux qui votent seulement |
taux_avis | part des réponses notées | sous quelques pourcents, la satisfaction est peu fiable |
erreurs | pannes du modèle | à surveiller avec une alerte |
latence_mediane_ms | temps typique d'une réponse complète | la médiane résiste aux valeurs extrêmes, contrairement à la moyenne |
Sur 1 000 échanges, 40 avis dont 36 positifs. Que conclure ?
La satisfaction (36/40 = 90 %) ne porte que sur les votants, qui ne sont pas forcément représentatifs. Le taux d'avis (4 %) rappelle de compléter par une relecture d'échantillons.
Pourquoi la médiane plutôt que la moyenne pour la latence ?
Une seule réponse à 30 s parmi des réponses à 3 s tire la moyenne vers le haut ; la médiane reste représentative de l'expérience typique. En production, on suit aussi le 95e centile.
Exercice : la part de « je ne sais pas »
Ajoutez à statistiques() un indicateur sans_reponse : la part des réponses qui renvoient vers l'accueil faute d'information. Indice : le prompt fait donner le contact de l'accueil dans ce cas.
Voir la solution
renvois = sum(1 for e in echanges if "accueil@" in e["reponse"] or "[EMAIL]" in e["reponse"])
# ...
"sans_reponse": round(renvois / len(echanges), 2) if echanges else None,Attention : le journal est masqué, donc l'e-mail de l'accueil y apparaît comme [EMAIL]. C'est une heuristique : certaines réponses donnent le contact même quand elles répondent. Pour un indicateur exact, faites ajouter au modèle un marqueur explicite, ou classez les réponses avec un second appel. Regardez cet indicateur par thème : il dit quoi ajouter à la fiche.
Résultat obtenu
Extrait réel de journal.jsonl après la session console du 2026-10-15 (l'IBAN, fictif, a été masqué avant écriture) :
{"ts": "2026-10-15T14:32:16+00:00", "type": "echange", "id": "234a8ba195de", "session": "40cbb89b5c8c4383bff688cc9161c838", "question": "Mon IBAN est [IBAN], vous pouvez le noter ?", "reponse": "Je ne peux pas prendre en note vos coordonnées bancaires sur ce chat. [...]", "latence_ms": 3945, "modele": "gemini-3.5-flash", "erreur": null}Et les statistiques renvoyées par GET /stats à la fin du test de bout en bout (test_http_reel.py), après 5 échanges et un 👍 :
stats : {'echanges': 5, 'erreurs': 0, 'avis': 1, 'satisfaction': 1.0, 'taux_avis': 0.2, 'latence_mediane_ms': 3209}Je sais faire
- Écrire un journal JSONL, un événement par ligne
- Masquer e-mails, téléphones et IBAN avant d'écrire
- Relier un avis à sa réponse par un identifiant d'échange
- Lire satisfaction, taux d'avis et latence médiane avec leurs pièges