Mesurer la dérive d’un copilot — le gate par étape ne protège pas la chaîne
Recycler la documentation du projet LivresAgités (en sommeil) vers le dépôt pédagogique. Ce notebook illustre le Copilot Gutenberg d’AI-Engine — la seconde et dernière fonctionnalité GenAI cœur du dossier qui n’avait pas de notebook compagnon (l’autre, AI Forms, a été traitée par le grain précédent). Il est le compagnon exécutable du Parcours 1 de AI-Engine-WordPress.
Thèse. Le Copilot propose six transformations de texte — résumé, enhancement, traduction, rewriting, image, alt text — et un gate humain (« valide ou rejette l’insertion ») à chaque étape. Ce gate donne l’illusion d’une sûreté pas-à-pas. Mais les six transformations n’ont pas le même effet informationnel : certaines ajoutent (image, alt text), d’autres réécrivent sans perdre (enhancement, rewriting), d’autres encore détruisent de l’information (résumé, traduction). Et l’effet d’une transformation dépend de ce sur quoi elle s’applique : un résumé d’un résumé collapse, une traduction d’un texte déjà résumé perd le peu qu’il restait. Le gate est local (étape par étape) ; la dérive est globale (chaîne). Un Copilot ne protège pas de la dérive — il la rend consentie.
Ce notebook ne dépend d’aucun service : pas de réseau, pas de modèle, pas de clé. Les transformations sont simulées par des fonctions déterministes sur des vecteurs (fréquences de termes) — ce qui est enseigné est la structure de la dérive, pas une mesure sur un modèle particulier. Le fixture est synthétique (« Maison Valmont »).
1. Le Copilot Gutenberg en une phrase
AI-Engine ajoute un panneau Copilot dans l’éditeur Gutenberg de WordPress. Le rédacteur écrit un brouillon ; le Copilot propose une transformation ; le rédacteur valide ou rejette. Six transformations sont offertes :
Transformation
Ce qu’elle fait
Résumé
Condense un texte long en un paragraphe
Enhancement stylistique
Clarifie, reformule, ajuste le ton
Traduction
Traduit en préservant le registre
Rewriting
Propose plusieurs variantes d’un paragraphe
Image d’en-tête
Génère une illustration depuis un prompt
Alt text
Décrit une image en quelques mots
La pièce maîtresse, pédagogiquement, n’est aucune de ces transformations prise isolément — c’est le gate humain. « Aucune réécriture automatique, le brouillon reste sous contrôle humain. » C’est vrai à l’échelle d’une transformation. Ce notebook demande : et à l’échelle d’une chaîne de transformations ?
2. Quatre natures informationnelles — pas six
Les six transformations se classent en quatre natures selon leur effet sur l’information du document source. Cette classification n’est pas cosmétique : elle détermine ce que le gate humain peut, ou ne peut pas, protéger.
Nature
Transformations
Effet sur l’original
Gate peut restaurer ?
Synthèse additive
image, alt text
Ajoute du contenu dérivé (axe nouveau)
Oui (on retire l’ajout)
Réécriture réversible
enhancement, rewriting
Bruit de forme, fond préservé
Oui (on garde l’original)
Projection partielle
traduction
Perte du registre stylistique
Non (le registre est détruit)
Réduction destructive
résumé
Perte des thèmes mineurs, irréversible
Non (l’info mineure est détruite)
La ligne qui sépare les deux premières des deux dernières est la frontière de réversibilité. En-deçà (synthèse, réécriture), le gate humain est une vraie garantie : on peut toujours revenir. Au-delà (projection, réduction), le gate est une approbation d’une perte : valider un résumé, c’est consentir à ne jamais revoir les nuances qu’il a tu — même si on rejette l’étape suivante.
3. Le fixture — un document et son vecteur
On représente un document par un vecteur de fréquences de termes sur un vocabulaire fixe. C’est une représentation rudimentaire (pas de syntaxe, pas d’ordre) mais suffisante pour mesurer la dérive : ce qui nous intéresse est combien de l’original survit, pas la qualité littéraire.
Le fixture est une page de catalogue Valmont : la fiche d’un livre, décrit par la fréquence de dix termes éditoriaux.
# Aucune dependance externe hors numpy.import numpy as np# Vocabulaire d'une page catalogue Valmont (10 termes editoriaux).VOCABULAIRE = ["recit", "personnage", "style", "intrigue", "plume","chapitre", "denouement", "voix", "rythme", "image"]# Document source : fiche d'un livre (frequence de chaque terme).# Note : 'image' est a 0 -- la page texte ne decrit pas d'image. Les axes# d'ajout (image, alt_text) sont donc vraiment orthogonaux a l'original.v0 = np.array([3.0, 2.0, 4.0, 2.0, 1.0, 2.0, 1.0, 3.0, 1.0, 0.0])print("Document source (sommet de la fiche du livre) :")for terme, freq inzip(VOCABULAIRE, v0):print(" "+ terme +" : "+str(int(freq)))print(" norme au carree : "+str(float(v0 @ v0)))
Si \(v\) ajoute du contenu orthogonal (une image sur un axe nouveau) : projection = 1,0 (la projection ne voit pas l’ajout — l’original est intact, juste dilué).
Si \(v\)atténue des composantes de \(v_0\) sans en amplifier d’autres : projection < 1,0.
Contre-exemple : supprimer une composante et en amplifier une autre peut ramener la projection à 1,0. Sur \(v_0 = (1, 1)\), le vecteur \((2, 0)\) donne une projection de 1,0 — la deuxième composante a entièrement disparu, mais le doublement de la première compense exactement la perte dans le produit scalaire. La projection n’est donc pas une fraction d’information conservée : c’est un chevauchement directionnel, trompable par compensation (les quatre cas-limites sont testés ci-dessous).
Pourquoi l’utiliser quand même ? Parce qu’elle sépare l’ajout de la perte, là où le cosinus confond perte et dilution (les deux le font baisser). Une synthèse additive (image) et une réduction (résumé) baissent toutes deux le cosinus, mais pour des raisons opposées — seule la projection les distingue.
Pour une lecture en « fraction conservée », il faut un indicateur composante par composante — le recouvrement défini ci-dessous, valable pour des fréquences non négatives. Les deux indicateurs ne sont pas interchangeables, et ni l’un ni l’autre ne mesure la fidélité factuelle d’une réécriture textuelle réelle : pour cela, il faudrait des faits de référence et une vérification dédiée.
def rappel(v_courant, v_original):"""Projection normalisee du vecteur courant sur l'original, plafonnee a 1. (v_courant . v_original) / (v_original . v_original), bornee par min(1, .).ATTENTION : ce n'est PAS une fraction d'information conservee -- une suppression compensee par une amplification (ex. (1,1) -> (2,0)) vaut 1.0 bien qu'une composante ait entierement disparu. Vaut 1.0 si l'original est intact ou si seul un contenu orthogonal a ete ajoute ; < 1.0 si des composantes de l'original sont attenuees sans compensation. Le nom `rappel` est historique : pour une lecture "fraction conservee", voir `recouvrement` ci-dessous. """ denom =float(v_original @ v_original)if denom ==0.0:return0.0# Plafonne a 1.0 : la projection normalisee ne depasse pas son support.# Le bruit additif (enhancement, rewriting) pousse legerement au-dessus ;# c'est un artefact, pas une retention superieure a l'original.returnmin(1.0, float(v_courant @ v_original) / denom)def cosinus(a, b):"""Cosinus entre deux vecteurs (mesure de dilution, pas de perte).""" na, nb = np.linalg.norm(a), np.linalg.norm(b)if na ==0or nb ==0:return0.0returnfloat(a @ b / (na * nb))# Sanity check.print("rappel(v0, v0) = "+str(rappel(v0, v0)) +" (doit valoir 1.0)")v_ajout = v0.copy()v_ajout[9] +=4.0# ajout orthogonal sur l'axe 'image'print("rappel(v0 + ajout_image, v0) = "+str(round(rappel(v_ajout, v0), 3)) +" (doit valoir 1.0 : ajout non penalise)")print("cosinus(v0 + ajout_image, v0) = "+str(round(cosinus(v_ajout, v0), 3)) +" (baisse : dilution)")
Les quatre cas-limites — projection contre recouvrement
La projection normalisée peut valoir 1,0 alors que de l’information a disparu. Pour le voir : quatre cas sur un jouet à deux axes occupés et un axe libre, comparés sur deux indicateurs — la projection, et le recouvrement composante par composante, valable pour des fréquences non négatives : la masse commune à chaque axe (le minimum des deux), rapportée à la masse totale de l’original. C’est lui — et seulement lui — qui se lit comme « fraction de la masse fréquencielle conservée ».
def recouvrement(v_courant, v_original):"""Recouvrement composante par composante (frequences non negatives) : sum(min(v_courant, v_original)) / sum(v_original). Se lit comme la fraction de la masse frequentielle conservee axe par axe -- ce que la projection normalisee n'est PAS (voir la metrique plus haut). """returnfloat(np.minimum(v_courant, v_original).sum() / v_original.sum())# Jouet : deux axes occupes (1, 1) + un axe libre (0).original_limites = np.array([1.0, 1.0, 0.0])cas_limites = {"identite (1, 1, 0)": np.array([1.0, 1.0, 0.0]),"suppression (1, 0, 0)": np.array([1.0, 0.0, 0.0]),"ajout orthogonal (1, 1, 2)": np.array([1.0, 1.0, 2.0]),"amplification compensee (2, 0, 0)": np.array([2.0, 0.0, 0.0]),}print("original = (1, 1, 0) | projection recouvrement")print(" "+"-"*58)for nom, v in cas_limites.items(): p = rappel(v, original_limites) r = recouvrement(v, original_limites)print(" "+ nom +" | "+str(round(p, 2)).ljust(10) +str(round(r, 2)))print()print("Derniere ligne : la projection vaut 1.0 -- le doublement de l'axe 1")print("compense exactement la disparition de l'axe 2 dans le produit scalaire")print("-- alors que la moitie de la masse frequentielle a disparu. Seul le")print("recouvrement (0.5) revele la perte. Les deux indicateurs ne sont pas")print("interchangeables.")
original = (1, 1, 0) | projection recouvrement
----------------------------------------------------------
identite (1, 1, 0) | 1.0 1.0
suppression (1, 0, 0) | 0.5 0.5
ajout orthogonal (1, 1, 2) | 1.0 1.0
amplification compensee (2, 0, 0) | 1.0 0.5
Derniere ligne : la projection vaut 1.0 -- le doublement de l'axe 1
compense exactement la disparition de l'axe 2 dans le produit scalaire
-- alors que la moitie de la masse frequentielle a disparu. Seul le
recouvrement (0.5) revele la perte. Les deux indicateurs ne sont pas
interchangeables.
4. Les six transformations, en vecteurs
Chaque transformation est modélisée par une fonction déterministe sur le vecteur. Ce ne sont pas de vraies transformations de texte — ce sont des fables vectorielles qui capturent la nature informationnelle de chacune. Le résumé seuille (garde les \(k\) plus grandes composantes) ; la traduction atténue les dimensions de registre ; l’image ajoute sur un axe nouveau. La reproductibilité est garantie par des graines fixes.
def resume(v, k=4):"""REDUCTION destructive : garde les k plus grandes composantes, annule les autres. L'information mineure est detruite.""" w = np.zeros_like(v) idx = np.argsort(v)[::-1][:k] w[idx] = v[idx]return wdef enhancement(v):"""REECRITURE reversible : bruit leger proportionnel au poids. L'information principale est preservee.""" rng = np.random.RandomState(7)return v +0.10* rng.randn(len(v)) * np.abs(v)def rewriting(v):"""REECRITURE plus marquee qu'enhancement. Reversible mais bruyante.""" rng = np.random.RandomState(13)return v +0.15* rng.randn(len(v)) * np.abs(v)def traduction(v):"""PROJECTION partielle : attenuation des dimensions de registre (style, plume, voix) + leger bruit. Perte du registre, fond preserve.""" w = v.copy()for i in [2, 4, 7]: # style, plume, voix w[i] *=0.5 rng = np.random.RandomState(11)return w +0.05* rng.randn(len(v)) * np.abs(v)def image(v):"""SYNTHESE additive : ajoute une description image sur un axe orthogonal.""" w = v.copy() w[9] +=4.0# axe 'image'return wdef alt_text(v):"""SYNTHESE additive courte : comme image, amplitude plus faible.""" w = v.copy() w[9] +=2.5return wTRANSFORMATIONS = {"resume": resume,"enhancement": enhancement,"rewriting": rewriting,"traduction": traduction,"image": image,"alt_text": alt_text,}
# Profil de chaque transformation appliquee SEULE au document source.def nature_transformation(v_original, v_courant):"""Classe une transformation par son effet informationnel. Le rappel seul ne distingue pas une reecriture (bruit, norme stable) d'une synthese additive (ajout, norme en hausse) -- les deux ont un rappel ~1.0. On ajoute donc le ratio de norme pour separer. """ r = rappel(v_courant, v_original)if r <0.85:return"perte (rappel < 0.85)" ratio_norme = np.linalg.norm(v_courant) / np.linalg.norm(v_original)if ratio_norme >1.05:return"synthese additive (norme en hausse)"return"reecriture reversible (norme stable)"print("Rappel de l'original apres une transformation unique :")print()print(" "+"transformation".ljust(14) +"rappel cosinus nature (rappel + norme)")print(" "+"-"*64)for nom, f in TRANSFORMATIONS.items(): w = f(v0) r = rappel(w, v0) c = cosinus(w, v0)print(" "+ nom.ljust(14) +str(round(r, 3)).ljust(9) +str(round(c, 3)).ljust(10) + nature_transformation(v0, w))
Le tableau rend la §2 lisible par les nombres. Trois groupes se séparent nettement sur le rappel :
Synthèse additive (image, alt_text) : rappel = 1,0 — l’original est intact, le vecteur a juste grossi sur un axe nouveau. Le cosinus baisse (dilution), mais c’est un artefact, pas une perte.
Réécriture réversible (enhancement, rewriting) : rappel élevé (~0,9+) — du bruit sur la forme, le fond préservé.
Perte (résumé, traduction) : rappel abaissé — de l’information de l’original a été effectivement détruite.
Une seule mesure (le rappel) fait le travail que la lecture du menu « six transformations » ne fait pas : elle révèle que le résumé et l’image, présentés côte à côte dans le panneau Copilot, n’ont rien à voir — l’un détruit, l’autre ajoute.
5. La chaîne — la dérive est cumulative
C’est ici que le gate humain montre sa limite. Appliquons non plus une transformation, mais une séquence — chacune validée par le rédacteur — et mesurons le rappel à la fin.
def rappel_apres_chaine(v_original, chaine):"""Applique une chaine de transformations (par nom) a l'original, renvoie la projection normalisee finale. """ v = v_original.copy()for nom in chaine: v = TRANSFORMATIONS[nom](v)return rappel(v, v_original)# Trois chaines candidates, chacune plausible pour un redacteur.chaines = {"A (destructrice)": ["resume", "traduction", "enhancement"],"B (benigne)": ["enhancement", "rewriting", "alt_text"],"C (collapse)": ["resume", "resume"],}print("Verdict final de chaque chaine (les deux indicateurs, cf. la metrique) :")print()for nom, chaine in chaines.items(): v_final = v0.copy()for t in chaine: v_final = TRANSFORMATIONS[t](v_final) p = rappel_apres_chaine(v0, chaine) r = recouvrement(v_final, v0)print(" "+ nom.ljust(22) +" -> projection "+str(round(p, 3)).ljust(7) +" | recouvrement "+str(round(r, 3)))
Verdict final de chaque chaine (les deux indicateurs, cf. la metrique) :
A (destructrice) -> projection 0.544 | recouvrement 0.424
B (benigne) -> projection 1.0 | recouvrement 0.988
C (collapse) -> projection 0.776 | recouvrement 0.632
Lecture
Les trois chaînes ont la même longueur (deux ou trois étapes), et chaque étape individuelle passait le gate humain — aucune n’aurait été rejetée à la lecture, car chaque transformation prise seule est légitime. Et pourtant :
Chaîne A (résumé → traduction → enhancement) : projection 0,54, recouvrement 0,42 — plus de la moitié de la masse fréquentielle a disparu (lecture sur le recouvrement, seul indicateur qui se lit en fraction conservée ; la projection ne mesure qu’un chevauchement directionnel). Le résumé a retiré les nuances ; la traduction a écrasé le registre qu’il restait ; l’enhancement a bruité le tout. Le rédacteur a validé trois fois, et plus de la moitié du document est partie.
Chaîne B (enhancement → rewriting → alt text) : projection au plafond (1,0) et recouvrement 0,99 — les fréquences de l’original sont quasi intactes. Trois transformations, mais toutes réversibles ou additives : le fond tient.
Chaîne C (résumé → résumé) : projection 0,78, recouvrement 0,63 — et les mêmes valeurs qu’un seul résumé, sur les deux indicateurs à la fois. Ce n’est pas que la chaîne soit sûre — c’est qu’il n’y a plus rien à perdre : le premier résumé a déjà tué les nuances, et le second bute sur le même plancher (resume est idempotente dans ce modèle top-k : résumer le résumé redonne le même vecteur). La destruction a un point bas, et le résumé d’un résumé le confirme.
C’est l’enseignement central : le gate humain est local, la dérive est globale. La chaîne A perd deux fois (deux destructrices complémentaires détruisent des informations différentes), la chaîne C ne perd qu’une fois (la deuxième destructive est redondante avec la première). Dans les deux cas, aucune des étapes n’aurait été rejetée isolément — et dans les deux cas, le résultat global n’est pas la somme des résultats locaux. Un Copilot sans vision de la chaîne — sans afficher un indicateur de conservation muni de sa définition — laisse le rédacteur approuver une perte qu’il n’aurait pas acceptée d’un coup.
6. Où la dérive se produit — le profil étape par étape
Le rappel final est un verdict ; le profil (rappel après chaque étape) est le diagnostic. Où la dérive se produit-elle ? D’un seul tenant, ou répartie ?
def profil_chaine(v_original, chaine):"""Renvoie la liste des rappels apres chaque etape (incluant l'etat initial 1.0 en tete). """ profil = [1.0] v = v_original.copy()for nom in chaine: v = TRANSFORMATIONS[nom](v) profil.append(rappel(v, v_original))return profilfor nom, chaine in chaines.items(): p = profil_chaine(v0, chaine) etiquettes = ["initial"] + chaine ligne =" "+ nom.ljust(22) +" "for e, r inzip(etiquettes, p): ligne += e +"="+str(round(r, 2)) +" "print(ligne)
A (destructrice) initial=1.0 resume=0.78 traduction=0.53 enhancement=0.54
B (benigne) initial=1.0 enhancement=1.0 rewriting=1.0 alt_text=1.0
C (collapse) initial=1.0 resume=0.78 resume=0.78
Lecture
Le profil révèle ce que le verdict final masque : la dérive n’est pas uniforme, elle a une géométrie. Trois formes caractéristiques apparaissent :
Chaîne A : un escalier qui descend à chaque destructrice. Le résumé ampute (marche 1), la traduction finit le travail sur le registre (marche 2), et l’enhancement — réversible — n’ajoute qu’un plat. La dérive est complète avant la dernière étape : un Copilot qui afficherait le profil permettrait au rédacteur de voir que le dommage est fait dès la traduction.
Chaîne B : une ligne plate au plafond. Trois étapes validées, zéro perte. C’est le profil d’une chaîne qui ne franchit jamais la frontière de réversibilité — exactement ce qu’on veut pour des contenus où le fond compte.
Chaîne C : une seule marche, puis un plat. Le deuxième résumé ne descend pas plus que le premier. Le profil plat après la première marche est la signature d’une transformation idempotente — il n’y a plus rien à extraire.
C’est la contre-mesure pédagogique : si le Copilot affichait le profil — « après cette validation, la conservation mesurée du document est de X, selon l’indicateur affiché et sa définition » — le rédacteur verrait le point de rupture (chaîne A), la préservation (chaîne B) ou le plancher (chaîne C), et pourrait décider en connaissance de cause. Le gate humain pourrait protéger la chaîne, à la condition expresse de l’instrumenter avec une mémoire de la chaîne. Sans cette mémoire, le gate est une suite d’approbations locales qui ne voient pas la dérive globale.
7. Provenance et limites
Ce que ce notebook mesure. La structure de la dérive d’un document soumis à une chaîne de transformations : quelles transformations détruisent (rappel baisse), lesquelles préservent (rappel stable), et comment l’effet cumulé d’une chaîne validée étape par étape diffère de la somme des effets individuels. Tout est déterministe sur fixture synthétique.
Ce qu’il ne mesure pas. La qualité réelle des transformations (un résumé vrai peut être excellent et sa perte acceptable ; un enhancement vrai peut dégrader le style). Les transformations sont des fables vectorielles qui capturent la nature informationnelle, pas le rendu littéraire. La représentation par fréquences de termes ignore l’ordre, la syntaxe, le sens — elle ne mesure que la matière informationnelle.
La limite du propos. Le gate humain n’est pas inutile — il empêche les dérives les plus visibles (un résumé faux, une traduction absurde). Ce notebook ne dit pas « le gate ne sert à rien » ; il dit « le gate local ne voit pas la dérive globale, et cette limite appelle une instrumentation ». C’est une leçon de conception, pas un réquisitoire.
Pour aller plus loin. - auditer-un-formulaire-conditionnel.ipynb — autre angle sur le « comportement émergent non-lisible sur la définition statique » : un formulaire conditionnel comme machine à états. - livresagites-parcours.md Parcours 1 — la prose dont ce notebook est l’illustration exécutable, et sa leçon transposable (« cartographier les flux de texte réels »).
8. Exercices
Les trois exercices suivants manipulent le document source, les six transformations (TRANSFORMATIONS) et la métrique (rappel). Les stub sont à compléter — return None ou pass.
Exercice 1 — rappel après une chaîne
Écrire une fonction rappel_chaine qui, étant donné un vecteur original et une liste de noms de transformations, renvoie le rappel final après application de la chaîne complète. (Vous pouvez vous aider de rappel_apres_chaine défini plus haut, ou le réécrire.)
def rappel_chaine(v_original, chaine):"""Renvoie le rappel de l'original apres application d'une chaine de transformations (liste de noms). """# TODO : appliquer chaque transformation de la chaine, puis mesurer le rappel.returnNone
Exercice 2 — la chaîne la plus destructrice
Étant donné un vecteur original et une liste de chaînes candidates (chacune une liste de noms de transformations), écrire chaine_la_plus_destructrice qui renvoie la chaîne dont le rappel final est le plus bas (celle qui détruit le plus d’information).
def chaine_la_plus_destructrice(v_original, candidates):"""Parmi plusieurs chaines candidates, renvoie celle dont le rappel final est le plus bas (la plus destructrice). """# TODO : calculer le rappel de chaque candidate, garder le min.returnNone
Exercice 3 — le profil étape par étape
Écrire une fonction profil qui renvoie la liste des rappels après chaque étape d’une chaîne (en tête la valeur 1,0 pour l’état initial). Permet de localiser le point de rupture — là où la dérive se concentre.
def profil(v_original, chaine):"""Renvoie la liste des rappels apres chaque etape, en tete 1.0 (etat initial). Le point de rupture est la marche ou le rappel chute le plus fort. """# TODO : accumuler le rappel apres chaque transformation.returnNone
9. Ce que ce notebook enseigne, en une ligne
Un gate humain à chaque étape ne protège pas de la dérive d’une chaîne. Le résumé détruit, l’image ajoute, l’enhancement bruite — et le rédacteur qui valide pas-à-pas ne voit pas qu’il a traversé une frontière de réversibilité. Le remède n’est pas plus de gate, c’est un gate qui se souvienne de la chaîne.