Constrained Decoding
Le décodage contraint masque les jetons non valides à chaque étape de génération, afin qu’un LLM ne puisse émettre que des étiquettes autorisées, des correspondances avec une expression régulière ou du JSON conforme à un schéma.
Le décodage contraint est une technique de génération de texte qui limite les jetons qu’un modèle de langage peut produire à chaque étape, afin de garantir que le résultat final respecte un format requis : un élément d’un ensemble fixe d’étiquettes, une expression régulière, une grammaire ou un schéma JSON. Le modèle attribue toujours un score à chaque jeton de son vocabulaire. Avant de choisir un jeton, le décodeur élimine toutes les options qui enfreindraient le format. Les applications qui analysent les sorties de modèles, comme les backends d’API, les agents d’IA et les pipelines d’extraction de données, utilisent cette technique pour que chaque réponse puisse être analysée du premier coup.
Masquer les jetons non valides#
Un grand modèle de langage génère du texte un jeton à la fois. À chaque étape, il produit un score, ou logit, pour chaque jeton de son vocabulaire, puis une softmax transforme ces scores en probabilités. Le décodage contraint ajoute une opération avant l’échantillonnage : les jetons qui ne peuvent pas mener à une sortie valide reçoivent un logit égal à moins l’infini, ce qui leur donne une probabilité nulle, et la distribution des probabilités restantes est renormalisée sur les jetons valides.
import torch
# Raw next-token scores from a model over a toy vocabulary
vocab = ["approved", "rejected", "review", "maybe", "{", "}"]
logits = torch.tensor([1.2, 0.4, 0.9, 2.5, 0.1, 0.3])
# The schema allows only the three status labels at this position
allowed = torch.tensor([True, True, True, False, False, False])
masked_logits = logits.masked_fill(~allowed, float("-inf"))
# Softmax renormalizes probability over the valid tokens only
for name, raw, masked in zip(vocab, logits.softmax(-1), masked_logits.softmax(-1)):
print(f"{name:>9}: {raw:.2f} -> {masked:.2f}")Le modèle non contraint attribue 56 % de sa probabilité à « peut-être ». Une fois le masque appliqué, « peut-être » tombe à zéro et les trois étiquettes autorisées se partagent la probabilité au prorata de leurs scores initiaux. La probabilité de « approuvé » passe ainsi de 15 % à 46 %.
Les moteurs d’inférence de production suivent l’état de la contrainte au fil de la génération. Une liste de choix fixes ou une expression régulière se compile en automate à états finis. JSON et les autres formats imbriqués nécessitent une grammaire avec une pile, car une accolade fermante n’est valide qu’après l’accolade ouvrante correspondante. Après chaque jeton, le décodeur fait avancer l’état et recalcule l’ensemble autorisé. La tokenisation produit des fragments de sous-mots de longueur variable ; un seul jeton comme "}, peut donc couvrir plusieurs étapes de grammaire à la fois. Les bibliothèques pré-calculent ainsi les entrées du vocabulaire autorisées dans chaque état, ce qui maintient le coût par jeton à un niveau faible, même pour les vocabulaires de plus de 100 000 jetons. En 2023, Brandon Willard et Rémi Louf ont décrit cet indexage pour les expressions régulières et les grammaires hors contexte dans Efficient Guided Generation for Large Language Models, l’article à l’origine de la bibliothèque Outlines.
Choisir une contrainte#
La contrainte appropriée doit correspondre à la sortie réellement utilisée par l’application. Une liste de choix convient à la classification et au routage, où la réponse doit être l’une de quelques chaînes. Les expressions régulières couvrent les valeurs structurées courtes, comme les dates, les identifiants et les numéros de téléphone. Les grammaires hors contexte décrivent SQL, le code et les langages propres à un domaine ; le format de grammaire GBNF de llama.cpp en est un exemple largement utilisé. Pour les enregistrements comportant des champs nommés, JSON Schema spécifie les clés requises, les types, les énumérations et l’imbrication. Les API hébergées comme OpenAI Structured Outputs acceptent un schéma et le font respecter pendant la génération, généralement en prenant en charge un sous-ensemble documenté de la spécification complète JSON Schema.
Le décodage contraint est souvent confondu avec des techniques voisines :
- Sorties structurées : l’objectif plus général consiste à produire des réponses lisibles par machine, soit en utilisant le décodage contraint pendant la génération, soit en validant la sortie après coup et en réessayant en cas d’échec.
- Mode JSON : limite la sortie à un JSON syntaxiquement valide, quelle qu’en soit la structure ; les clés, les types et les valeurs doivent donc toujours être vérifiés.
- Ingénierie des prompts : demande un format dans les instructions, que les modèles respectent la plupart du temps. Les requêtes contraintes par un schéma décrivent généralement aussi les champs dans le prompt, afin que le modèle sache ce que chacun signifie.
- Appel de fonction : les noms et les arguments des outils comptent parmi les cibles les plus courantes des contraintes de schéma, car le code appelant attend des noms de champs exacts.
Compromis en pratique#
Un format garanti ne garantit rien sur le contenu. Un schéma peut imposer que {"total": 1290.00} soit un nombre, mais ce nombre peut quand même être incorrect. Un modèle contraint peut aussi halluciner avec aisance dans des champs valides. Les applications doivent toujours vérifier les règles métier après la génération.
Les contraintes peuvent aussi réduire la qualité des réponses lorsqu’elles éloignent le modèle du texte qu’il produirait naturellement. Un schéma qui exige que la réponse finale soit le premier champ ne laisse aucune place au raisonnement intermédiaire ; les schémas placent donc souvent un champ de raisonnement ou de notes en texte libre avant le champ de réponse. Il faut aussi tenir compte de la longueur de sortie : si la génération atteint la limite de jetons au milieu d’un objet, le résultat est invalide malgré la contrainte. C’est pourquoi les requêtes structurées fixent des longueurs maximales avec une marge suffisante pour contenir toute la structure. Les schémas très volumineux ou profondément récursifs sont plus longs à compiler, ce qui compte pour les systèmes qui en construisent un nouveau à chaque requête.
Les applications de vision utilisent le même mécanisme lorsqu’un modèle multimodal doit renvoyer des champs décrivant une image. L’interface LLM d’Ultralytics transmet les arguments de requête sans les modifier aux API compatibles avec OpenAI ; ainsi, un schéma JSON transmis avec une requête contenant une image limite la réponse d’un modèle vision-langage à des champs comme hazard et severity, prêts à être combinés aux détections de YOLO26 sur la même image.










