--- icon: lucide/replace --- # Fabriques de placeholders Un *placeholder* est le jeton synthétique qui prend la place d'une valeur détectée avant que le texte n'atteigne le LLM. Au lieu d'envoyer `Patrick`{ .pii } habite à `Paris`{ .pii } au LLM, le pipeline transmet `<>`{ .placeholder } habite à `<>`{ .placeholder }. Les valeurs originales restent dans la mémoire de conversation, le LLM ne les voit jamais. !!! note "Pourquoi le nom placeholder factory" *Placeholder* parce que le jeton tient la place de la valeur originale. Le nom anglais aurait pu être *token*, mais ce mot est déjà surchargé côté LLM (tokens de langage). *Factory* parce que le composant fabrique ces jetons à la volée, en fonction des entités détectées dans chaque message. Une **placeholder factory** décide de la forme de ces jetons et de la quantité d'information qu'ils transportent. Deux questions structurent le choix. 1. *Le jeton est-il unique par entité ?* `Patrick`{ .pii } et `Marie`{ .pii } ne doivent pas se ramener au même `<>`{ .placeholder } générique, sinon le LLM ne peut pas les distinguer. Un jeton unique par entité permet au modèle de raisonner sur les relations. La question *le manager est-il la même personne que `Patrick`{ .pii } ?* devient *`<>`{ .placeholder } est-il `<>`{ .placeholder } ?*, et elle a une réponse claire. 2. *Le jeton est-il réversible et retrouvable ?* Le jeton désigne-t-il une seule valeur dans la mémoire de la conversation, et peut-on le relocaliser dans un texte que le pipeline n'a pas produit ? La restauration a besoin de ces deux propriétés, qu'elle porte sur la réponse du modèle ou sur les arguments d'un outil. Si deux entités se confondent dans un même `<>`{ .placeholder }, on ne sait pas laquelle restaurer. Six familles de factories se placent à des points différents de ce spectre, et le choix a des conséquences directes sur les `ToolCallStrategy` utilisables sans risque. Voir [Stratégies d'appel outil](tool-call-strategies.md) pour le côté exécution. - **Aucune information** (`<>`{ .placeholder }) : un jeton constant qui ne révèle rien au LLM. Caviardage classique. Aucun raisonnement n'est possible sur les entités. Par exemple, le modèle ne peut pas voir que la valeur était une ville et décider d'appeler l'outil `get_weather`. - **Type seul** (`<>`{ .placeholder }, `<>`{ .placeholder }) : le type est révélé, pas l'identité. Plusieurs personnes dans une même conversation se confondent dans le même `<>`{ .placeholder }, donc les références croisées se cassent. - **Type + id (opaque)** (`<>`{ .placeholder }, `<>`{ .placeholder }) : type révélé, identité stable, jeton manifestement synthétique. Le LLM sait que `<>`{ .placeholder } et `<>`{ .placeholder } sont deux personnes différentes. Unique, donc réversible par remplacement de chaîne. - **Id seul** (`<>`{ .placeholder }) : un hash unique par entité, sans révéler le type. Le LLM voit qu'il y a deux entités distinctes mais ignore si ce sont des personnes, des emails ou des cartes. Garde la réversibilité côté outil sans donner d'indice sémantique au modèle. - **Valeur partielle** (`J*******`{ .placeholder } pour `Jonathan`{ .pii }) : une partie du contenu réel reste visible, ici la première lettre et la longueur. Le LLM voit le début de la valeur, pas la valeur complète. Plus risqué côté confidentialité (fragments réels) et côté réversibilité (collisions possibles). !!! note "Convention de format des jetons" Les jetons de cette documentation suivent une règle simple. - **Jeton synthétique** (qui ne ressemble à aucune valeur réelle), encadré par `<<` et `>>`. Par exemple `<>`{ .placeholder }, `<>`{ .placeholder }, `<>`{ .placeholder }, `<>`{ .placeholder }, `<>`{ .placeholder }. Les délimiteurs servent deux objectifs. Un LLM ou un humain qui relit ne confond jamais le jeton avec un mot du texte ou une balise HTML/XML émise par le modèle. Et le middleware peut retrouver le jeton pour faire son remplacement de chaîne, y compris repérer un jeton que le modèle aurait inventé. - **Jeton qui réplique le format d'une valeur réelle** (réaliste hashé, masqué), sans délimiteur. Par exemple `a1b2c3d4@anonymized.local`{ .placeholder }, `Patient_a1b2c3d4`{ .placeholder }, `j***@mail.com`{ .placeholder }. L'absence de délimiteur est délibérée. Le jeton doit paraître naturel, pour qu'un outil aval qui valide le format (regex email, longueur de carte) l'accepte. La règle vaut aussi pour toute factory que vous écrirez. Jeton purement opaque, encadrez-le. Jeton qui imite une vraie valeur, laissez-le brut. --- ## Détail des familles ### Aucune information, destruction totale Le jeton est un marqueur fixe, par exemple `<>`{ .placeholder }. Le LLM apprend *qu'une* information a été retirée mais rien sur son type, son nombre ni ses relations. La conversation perd toutes ses références internes. Un agent qui doit traiter *envoyer la facture au client* ne peut pas savoir si le client est celui cité plus tôt ou un nouveau. Utile pour le caviardage d'archive, inutile dès qu'un agent doit raisonner. - Intégrée : `RedactPlaceholderFactory` (sortie `<>`{ .placeholder }, délimiteurs paramétrables). - Tag de préservation : `PreservesNothing`. ### Type seul, identités confondues `<>`{ .placeholder }, `<>`{ .placeholder }. Le LLM sait qu'il s'agit d'une personne, d'un email, d'une carte, et peut répondre aux questions qui dépendent du seul type. Mais deux personnes différentes dans la même conversation se confondent dans le même jeton. Le mode d'échec classique est la référence croisée. La question *`Patrick`{ .pii } est-il la même personne que le manager cité plus tôt ?* devient *`<>`{ .placeholder } est-il le même que `<>`{ .placeholder } ?*, et cette question n'a pas de réponse. - Intégrée : `LabelPlaceholderFactory` (sortie `<>`{ .placeholder }). - Tag de préservation : `PreservesLabel`. ### Type + id (opaque) `<>`{ .placeholder }, `<>`{ .placeholder }. La chaîne n'est manifestement *pas* une personne, un email ou un numéro de carte, c'est un jeton. Le LLM ne peut pas la confondre avec une donnée réelle, les logs d'audit se parcourent facilement, et il y a **zéro chance** de collision avec une vraie valeur. Ses délimiteurs rendent aussi le jeton retrouvable. On peut ainsi repérer un jeton que le modèle aurait inventé. En contrepartie, un prompt ou un outil aval strict qui exige *l'argument doit ressembler à un email* rejettera ces jetons. - Intégrées : `LabelCounterPlaceholderFactory` (`<>`{ .placeholder }) et `LabelHashPlaceholderFactory` (`<>`{ .placeholder }). - Tag de préservation : `PreservesLabeledIdentityOpaque`. Les deux numérotent les entités par label, dans l'ordre. La première personne devient l'ordinal 1, la deuxième 2, et un email démarre son propre compte à 1. `LabelHashPlaceholderFactory` affiche cet ordinal sous forme de hash. Le hash est un sha256 de la chaîne `label:ordinal`, jamais de la valeur. Il ne sert qu'à donner une apparence opaque, pour que deux entités consécutives paraissent sans lien. ### Id seul, identité sans type `<>`{ .placeholder }. Le jeton garde la forme synthétique `<<...>>` mais ne révèle pas le label, tout en portant un hash unique par entité. Le LLM ignore si l'entité est une personne, un email ou une carte, mais voit que `<>`{ .placeholder } et `<>`{ .placeholder } sont deux entités différentes. C'est l'un des niveaux les plus protecteurs qui reste utilisable côté outil. Le remplacement de chaîne fonctionne, parce que le hash est unique. - Intégrée : aucune pour cette branche. - Tag de préservation : `PreservesIdentityOnly`, prévu pour une factory que vous écrivez, un caviardage hashé sans préfixe de label. Voir la section *Écrire la sienne* plus bas. ### Type + id (réaliste hashé) Une factory utilisateur peut produire des valeurs **qui ressemblent au format d'origine** mais dont le contenu est piloté par un hash, par exemple `a1b2c3d4@anonymized.local`{ .placeholder } pour un email, ou `Patient_a1b2c3d4`{ .placeholder } pour un nom. Le jeton passe la validation de format de base (regex email, longueur, caractères autorisés), donc les outils et les templates de prompt aval qui attendent une valeur d'apparence réelle continuent de fonctionner. Comme le contenu est un hash, le jeton est **unique et ne peut pas coïncider par hasard** avec une vraie valeur existante. - Intégrée : aucune. Voir la section *Écrire la sienne* plus bas pour un exemple complet. - Tag de préservation : `PreservesLabeledIdentityHashed`. !!! warning "Jeton non retrouvable" Ce tag n'est pas retrouvable. Le middleware ne peut donc pas repérer un jeton inventé sous cette forme. Tenez-en compte avant de l'utiliser sous middleware. ### Valeur partielle, un fragment fuit `J*******`{ .placeholder }, `j***@mail.com`{ .placeholder }, `****4567`{ .placeholder }. Le jeton conserve *une partie* de la valeur originale, par exemple le domaine de l'email, les quatre derniers chiffres d'une carte, la première lettre d'un nom. Le LLM peut raisonner au-delà du type, *l'email est sur le domaine de l'entreprise*, *la carte se termine en 4567*, *le nom commence par J*. Deux compromis viennent avec. 1. **Des fragments réels de la valeur atteignent le LLM.** Il ne peut pas reconstruire la valeur complète, mais `j***@mail.com`{ .placeholder } situe déjà l'utilisateur chez un fournisseur de mail connu. 2. **Des collisions sont possibles.** Deux cartes différentes terminant par `4567` se confondent dans `****4567`{ .placeholder }, deux emails partageant la première lettre et le domaine deviennent identiques. Le jeton est *majoritairement* unique, sans garantie. - Intégrée : `MaskPlaceholderFactory`, qui garde par défaut le premier caractère de la valeur et masque le reste avec `*`, donc `Jonathan`{ .pii } devient `J*******`{ .placeholder } et `jean@mail.com`{ .pii } devient `j************`{ .placeholder }. Les formes `j***@mail.com`{ .placeholder } et `****4567`{ .placeholder } demandent une factory que vous écrivez. - Tag de préservation : `PreservesShape`. Le middleware le rejette, à la vérification de types comme à l'exécution. Un jeton ambigu ne peut pas être restauré par remplacement de chaîne, et un masque n'a pas de grammaire que le middleware sache retrouver. --- ## Tags de préservation Chaque factory porte un **type fantôme** qui résume le niveau de préservation de ses jetons. Un type fantôme est un paramètre générique qui n'existe qu'à la vérification de types, il n'influe pas sur l'exécution. C'est ce tag que le vérificateur de types lit pour valider une factory face à ses consommateurs. Le tableau suivant donne un exemple de jeton et le tag de chaque famille. | Famille | Exemple | Tag | |---|---|---| | Aucune information | `<>`{ .placeholder } | `PreservesNothing` | | Type seul | `<>`{ .placeholder } | `PreservesLabel` | | Type + id (opaque) | `<>`{ .placeholder }, `<>`{ .placeholder } | `PreservesLabeledIdentityOpaque` | | Id seul | `<>`{ .placeholder } | `PreservesIdentityOnly` | | Type + id (réaliste hashé) | `a1b2c3d4@anonymized.local`{ .placeholder }, `Patient_a1b2c3d4`{ .placeholder } | `PreservesLabeledIdentityHashed` | | Valeur partielle | `J*******`{ .placeholder }, `****4567`{ .placeholder } | `PreservesShape` | Deux tableaux lisent ces familles sous deux angles. Le tableau **Confidentialité** montre ce qui fuit vers le LLM, du point de vue de l'attaquant et de la vie privée. Le tableau **Exploitation** montre ce que l'agent et le système peuvent faire avec le jeton, du point de vue des capacités fonctionnelles. La même réponse peut être bonne d'un côté et problématique de l'autre, et les deux tableaux rendent cette tension explicite. Les deux tableaux partagent le même code couleur, du meilleur au problématique, détaillé dans la légende sous le second tableau. #### Confidentialité (ce qui fuit vers le LLM)
FamilleType vu ?Valeurs distinguées ?Fuite de valeur ?Collision avec une vraie valeur ?
Aucune informationnonnonaucunenon
Type seulouinonaucunenon
Type + id (opaque)ouiouiaucunenon
Id seulnonouiaucunenon
Type + id (réaliste hashé)ouiouiaucunenon
Valeur partielleouiouipartiellerisque
#### Exploitation par le LLM et l'agent
FamilleRaisonner sur le typeSuivre les références entre entitésRéversible côté outilJeton retrouvable
Aucune informationnonnonnonoui
Type seulouinonnonoui
Type + id (opaque)ouiouiouioui
Id seulnonouiouioui
Type + id (réaliste hashé)ouiouiouinon
Valeur partielleouimajoritairementoui (collisions)non
Légende : meilleur correct partiel problématique Les tags forment une **hiérarchie d'héritage** que le vérificateur de types exploite via la covariance de `AnyPlaceholderFactory[PreservationT_co]`. Une factory taguée plus spécifiquement satisfait donc un consommateur qui en demande une plus lâche. Trois axes indépendants organisent la taxonomie : - *Label* : le jeton révèle le type. - *Identité* : le jeton est unique par entité. - *Retrouvable* : la factory peut retrouver son jeton dans un texte arbitraire. Un jeton délimité le permet, un jeton réaliste non. `PreservesLabeledIdentity` combine label et identity par multi-héritage. Une factory `<>`{ .placeholder } est donc à la fois un `PreservesLabel` *et* un `PreservesIdentity`. `PreservesRecognizableIdentity` croise l'identité et la retrouvabilité. Le middleware n'accepte que cette intersection. Un consommateur typé contre `PreservesRecognizableIdentity` trie les tags ainsi : - Accepte : `PreservesIdentityOnly` et `PreservesLabeledIdentityOpaque`. - Rejette : `PreservesLabel`, `PreservesShape` et `PreservesNothing`, qui n'ont pas la garantie d'unicité, ainsi que `PreservesLabeledIdentityHashed`, qui n'est pas retrouvable. ```mermaid classDiagram class PlaceholderPreservation { racine } class PreservesNothing { <<REDACT>> } class PreservesLabel { <<PERSON>> } class PreservesShape { "J*******" } class Recognizable { abstraction } class PreservesIdentity { abstraction } class PreservesRecognizableIdentity { abstraction } class PreservesIdentityOnly { <<REDACT:a1b2c3d4>> } class PreservesLabeledIdentity { abstraction } class PreservesLabeledIdentityOpaque { <<PERSON:1>> <<PERSON:a1b2c3d4>> } class PreservesLabeledIdentityRealistic { abstraction } class PreservesLabeledIdentityHashed { a1b2c3d4@anonymized.local Patient_a1b2c3d4 } PlaceholderPreservation <|-- PreservesNothing PlaceholderPreservation <|-- PreservesLabel PlaceholderPreservation <|-- Recognizable PlaceholderPreservation <|-- PreservesIdentity PreservesLabel <|-- PreservesShape PreservesIdentity <|-- PreservesRecognizableIdentity Recognizable <|-- PreservesRecognizableIdentity PreservesRecognizableIdentity <|-- PreservesIdentityOnly PreservesLabel <|-- PreservesLabeledIdentity PreservesIdentity <|-- PreservesLabeledIdentity PreservesLabeledIdentity <|-- PreservesLabeledIdentityOpaque PreservesRecognizableIdentity <|-- PreservesLabeledIdentityOpaque PreservesLabeledIdentity <|-- PreservesLabeledIdentityRealistic PreservesLabeledIdentityRealistic <|-- PreservesLabeledIdentityHashed ``` *Hiérarchie des tags de préservation. Chaque nœud porte un exemple de jeton, les nœuds abstraits servent d'intersection entre axes. Chaque flèche va d'un tag vers son parent et se lit "est un".* { .figure-caption } `PreservesLabeledIdentity` hérite à la fois de `PreservesLabel` et de `PreservesIdentity`. Cet héritage exprime la relation *A est un B mais tous les B ne sont pas des A*. Tout `PreservesLabeledIdentity` est aussi un `PreservesLabel` et un `PreservesIdentity`, mais un `PreservesLabel` n'est pas forcément un `PreservesLabeledIdentity`. `PreservesShape` étend `PreservesLabel`, parce qu'un jeton masqué implique le label par son format. Il ne garantit pas l'unicité, donc il ne descend pas de `PreservesIdentity`. Chaque tag est une sous-classe de `str`, si bien qu'un jeton est une vraie chaîne qui porte son niveau de préservation dans son propre type. Une factory déclare le tag **le plus spécifique** qui correspond à ses garanties. ```python class LabelCounterPlaceholderFactory( BaseCounterPlaceholderFactory ): ... # PreservesLabeledIdentityOpaque class LabelHashPlaceholderFactory( BaseCounterPlaceholderFactory ): ... # PreservesLabeledIdentityOpaque class LabelPlaceholderFactory(AnyPlaceholderFactory[PreservesLabel]): ... class MaskPlaceholderFactory(AnyPlaceholderFactory[PreservesShape]): ... class RedactPlaceholderFactory(AnyPlaceholderFactory[PreservesNothing]): ... # No built-in for the id-only branch nor the realistic hashed one, # implement your own with PreservesIdentityOnly or PreservesLabeledIdentityHashed. ``` --- ## Factories intégrées | Factory | Style | Mécanisme | Exemple de sortie | |---|---|---|---| | `RedactPlaceholderFactory` | Redact | aucun | `<>`{ .placeholder } | | `LabelPlaceholderFactory` | Label | aucun | `<>`{ .placeholder } | | `LabelCounterPlaceholderFactory` (défaut) | Label | Counter | `<>`{ .placeholder } | | `LabelHashPlaceholderFactory` | Label | Hash | `<>`{ .placeholder } | | `MaskPlaceholderFactory` | Mask | partiel | `J*******`{ .placeholder } | Le tag de chaque factory figure dans le tableau des familles, plus haut. Le nommage suit le schéma `