Questions d'entretien d'ingénieur en LLM et IA Générative
15 questions d'entretien sélectionnées sur les LLM et l'IA générative, regroupées par niveau d'ancienneté. Utilisez-les pour réviser les fondamentaux, les compromis pratiques et le raisonnement de production de niveau senior.
1Expliquez la tokenisation par sous-mots et pourquoi elle est préférée à la tokenisation au niveau du mot ou au niveau du caractère dans les modèles de langage modernes.
La tokenisation par sous-mots est une approche hybride de segmentation de texte qui divise le texte en morceaux morphologiques de longueur variable ou en sous-chaînes basées sur la fréquence (telles que 'un', 'break', 'able') plutôt qu'en mots entiers ou en caractères individuels. Des algorithmes comme Byte-Pair Encoding (BPE), WordPiece et Unigram LM apprennent un vocabulaire de taille fixe à partir d'un corpus d'entraînement où les mots fréquents restent intacts en tant que tokens uniques, tandis que les mots rares ou inconnus sont décomposés en unités de sous-mots connues. La tokenisation par sous-mots est préférée dans les modèles de langage modernes car elle équilibre la taille du vocabulaire, la longueur des séquences et la robustesse aux mots hors vocabulaire (OOV). Une tokenisation purement au niveau du mot nécessite un vocabulaire excessivement grand (ce qui entraîne d'énormes matrices d'intégration) et souffre toujours de tokens OOV mappés à des tokens génériques '[UNK]'. Inversement, une tokenisation purement au niveau du caractère élimine les problèmes OOV mais produit des séquences très longues qui augmentent considérablement la complexité computationnelle des mécanismes d'attention (qui évoluent quadratiquement avec la longueur de séquence) et diluent la densité sémantique par token. La tokenisation par sous-mots offre un compromis optimal en maintenant des longueurs de séquence gérables, des tailles de vocabulaire pratiques (typiquement de 32k à 128k tokens), et des taux OOV à zéro (surtout lorsqu'elle est combinée avec des solutions de repli au niveau de l'octet).
2Expliquez la différence entre les plongements de mots statiques (static word embeddings), les plongements contextuels (contextual embeddings) et les états cachés de transformateur (transformer hidden states).
Les plongements de mots statiques, les plongements contextuels et les états cachés de transformateur représentent des évolutions progressives dans la manière dont les représentations textuelles capturent la signification et le contexte syntaxique.
1. Les **plongements de mots statiques** (par exemple, Word2Vec, GloVe, FastText) attribuent un vecteur unique et fixe à chaque jeton de vocabulaire, indépendamment de son contexte dans la phrase. Dans ce paradigme, les mots polysémiques comme 'bank' (rive de la rivière vs banque financière) ou 'apple' (fruit vs entreprise technologique) ont des représentations vectorielles identiques dans tous les contextes, reposant sur une table de correspondance statique.
2. Les **plongements contextuels** (par exemple, les premiers ELMo, les représentations de jetons BERT, ou les plongements de phrases des Bi-Encodeurs) produisent des représentations où le vecteur pour un jeton est une fonction dynamique de son contexte environnant. Dans BERT ou ELMo, 'bank' dans 'river bank' reçoit un vecteur de plongement complètement différent de 'bank' dans 'deposit money at the bank'.
3. Les **états cachés de transformateur** font référence aux représentations vectorielles intermédiaires produites à chaque couche individuelle d'un réseau transformateur lors d'un passage avant (forward pass). Étant donné les plongements des jetons d'entrée à la couche 0, chaque couche de transformateur successive applique des transformations d'auto-attention (self-attention) et de réseau de neurones à propagation directe (feed-forward), produisant une séquence de vecteurs d'état cachés `h_l` à la couche `l`. Alors que les états cachés de la dernière couche agissent comme des plongements contextuels de haut niveau, les états cachés des couches inférieures et moyennes capturent des caractéristiques syntaxiques, lexicales et structurelles de bas niveau. Ainsi, les états cachés de transformateur englobent tout le continuum vertical des représentations couche par couche à travers le réseau.
import torch
from transformers import AutoTokenizer, AutoModel
tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased')
model = AutoModel.from_pretrained('bert-base-uncased', output_hidden_states=True)
text1 = 'River bank'
text2 = 'Bank deposit'
inputs1 = tokenizer(text1, return_tensors='pt')
inputs2 = tokenizer(text2, return_tensors='pt')
with torch.no_grad():
out1 = model(**inputs1)
out2 = model(**inputs2)
# Layer 0 (Input token embeddings - static lookup before self-attention)
static_bank_1 = out1.hidden_states[0][0, 2] # 'bank'
# Layer 12 (Final contextual hidden state after all attention layers)
contextual_bank_1 = out1.hidden_states[12][0, 2]
contextual_bank_2 = out2.hidden_states[12][0, 1]
print('Cosine similarity of bank in different contexts (Layer 12):',
torch.cosine_similarity(contextual_bank_1, contextual_bank_2, dim=0).item())
3Expliquez ce que représente un espace d'embeddings et comment la similarité cosinus est interprétée pour les embeddings textuels.
Un espace d'embeddings est un espace vectoriel continu et de haute dimension R^d où les entités textuelles discrètes (mots, phrases ou documents) sont mappées de telle sorte que les similarités sémantiques, syntaxiques ou relationnelles correspondent à la proximité géométrique et aux relations directionnelles. Dans cet espace, les distances et les angles reflètent la pertinence sémantique.<br><br>La similarité cosinus mesure le cosinus de l'angle thêta entre deux vecteurs u et v, calculée comme suit :<br>`Similarité Cosinus(u, v) = (u . v) / (||u|| ||v||)`<br><br>La similarité cosinus est interprétée dans les embeddings textuels comme suit :<br>- **Plage et orientation** : Elle produit une valeur scalaire généralement bornée dans [-1, 1] (ou [0, 1] pour les embeddings non négatifs). Une valeur proche de 1,0 indique que les deux vecteurs pointent virtuellement dans la même direction, reflétant une forte similarité sémantique ou un alignement thématique. Une valeur proche de 0,0 implique une orthogonalité (indépendance sémantique ou non-pertinence), et les valeurs négatives indiquent des orientations opposées.<br>- **Invariance à la magnitude** : Contrairement à la distance euclidienne (distance L2) ou au produit scalaire, la similarité cosinus normalise la longueur du vecteur. Dans les embeddings textuels, la magnitude du vecteur peut parfois être corrélée à la longueur de la séquence, à la fréquence des tokens ou à la spécificité des termes. En se concentrant purement sur l'alignement directionnel, la similarité cosinus isole l'orientation sémantique des différences de magnitude vectorielle.
4Expliquez la différence entre les embeddings de jetons (token embeddings), les embeddings de position (positional embeddings) et les embeddings de segment ou de type (segment or type embeddings) dans les entrées d'un transformeur.
Dans les entrées des transformeurs (notamment les architectures de type BERT), la représentation d'entrée pour chaque jeton est généralement formée par la sommation élément par élément de trois vecteurs d'embedding distincts :
1. **Embeddings de jetons** : Mappent les identifiants de jetons (token IDs) de vocabulaire discrets en vecteurs denses représentant l'identité sémantique et lexicale fondamentale des jetons.
2. **Embeddings de position** : Injectent des informations sur l'ordre des jetons et leur index séquentiel dans la représentation, compensant le fait que l'auto-attention est intrinsèquement invariante à la permutation.
3. **Embeddings de segment (ou de type de jeton)** : Distinguent différentes étendues de texte ou phrases regroupées dans une seule séquence d'entrée (comme la phrase A par rapport à la phrase B dans les tâches de classification par paires ou de réponse aux questions).
La combinaison de ces embeddings fournit un tenseur d'entrée dense unique encodant la signification, la position et le regroupement de séquence du jeton avant de passer dans la première couche du transformeur.
import torch
import torch.nn as nn
vocab_size, max_seq_len, num_segments, d_model = 30522, 512, 2, 768
tok_embed = nn.Embedding(vocab_size, d_model)
pos_embed = nn.Embedding(max_seq_len, d_model)
seg_embed = nn.Embedding(num_segments, d_model)
input_ids = torch.tensor([[101, 7592, 102, 2023, 102]]) # Token IDs
type_ids = torch.tensor([[0, 0, 0, 1, 1 ]]) # Segment IDs (Sentence A vs B)
positions = torch.arange(input_ids.size(1)).unsqueeze(0) # Indices: [0, 1, 2, 3, 4]
# Final representation is the element-wise sum
input_rep = tok_embed(input_ids) + pos_embed(positions) + seg_embed(type_ids)
print(input_rep.shape)
5Expliquez l'attention (attention) comme mécanisme pour relier les jetons dans une séquence, y compris les requêtes, les clés, les valeurs et l'attention multi-têtes.
L'attention est un mécanisme qui permet aux jetons d'une séquence d'acheminer dynamiquement l'information et de pondérer la pertinence de tous les autres jetons en fonction d'une correspondance contextuelle. Des projections linéaires convertissent l'entrée de chaque jeton en trois vecteurs :
- Requête (Q) : Représente l'information que le jeton actuel recherche.
- Clé (K) : Représente les attributs ou le contenu qu'un jeton offre pour correspondre aux requêtes.
- Valeur (V) : Contient la charge utile d'information réelle à agréger.
Dans l'attention par produit scalaire pondéré, les scores d'attention sont calculés en multipliant les Requêtes et les Clés ($Q K^T$), mis à l'échelle par $\frac{1}{\sqrt{d_k}}$ pour éviter la disparition du gradient à travers de grandes dimensions, et normalisés avec une fonction softmax. La sortie finale est la somme pondérée des Valeurs : $$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$
L'attention multi-têtes (MHA) projette Q, K et V dans plusieurs sous-espaces de représentation indépendants (têtes) en parallèle. Cela permet au modèle de s'attacher simultanément à différents types de relations (par exemple, structure syntaxique, coréférence, dépendances à longue portée) à travers les positions. Les sorties des têtes sont concaténées et projetées linéairement vers la dimension du modèle.
6Expliquez comment le masquage d'attention diffère pour les décodeurs causaux par rapport aux encodeurs bidirectionnels et quels comportements il permet ou empêche.
Le masquage d'attention contrôle quels jetons sont autorisés à prêter attention à quels autres jetons en définissant les logits d'attention (scores avant softmax) à $-\infty$ pour les paires non autorisées, garantissant que leur poids d'attention post-softmax est strictement 0.
1. **Décodeurs causaux (par exemple, GPT, LLaMA) :** Ils utilisent un masque causal (autorégressif) triangulaire inférieur. Un jeton à la position $i$ ne peut prêter attention qu'aux positions $j \le i$. Cela empêche l'attention aux jetons futurs, permettant la génération autorégressive jeton par jeton lors de l'inférence et empêchant la fuite d'étiquettes de jetons futurs lors de l'entraînement parallélisé.
2. **Encodeurs bidirectionnels (par exemple, BERT) :** Ils n'utilisent pas de masque causal ; chaque jeton peut prêter attention à tous les jetons passés et futurs à travers la séquence. Ils utilisent des masques de remplissage (padding) pour empêcher les jetons valides de prêter attention aux jetons `[PAD]` vides dans les séquences par lots. L'attention bidirectionnelle produit des représentations contextuelles riches et complètes idéales pour les tâches de compréhension, mais elle empêche la génération de texte autorégressive directe en une seule passe.
import torch
import torch.nn.functional as F
scores = torch.randn(3, 3)
# Create upper-triangular mask for future positions
causal_mask = torch.triu(torch.ones(3, 3, dtype=torch.bool), diagonal=1)
# Mask future positions with -inf before softmax
masked_scores = scores.masked_fill(causal_mask, float('-inf'))
atten_weights = F.softmax(masked_scores, dim=-1)
print(atten_weights)
7Expliquez la différence entre les architectures de transformeurs à encodeur seul, à décodeur seul et à encodeur-décodeur pour les tâches linguistiques.
Les trois architectures de transformeurs principales diffèrent fondamentalement par leurs modèles de masquage d'attention et leurs objectifs opérationnels ciblés :
1. **Encodeur seul** (par exemple, BERT, RoBERTa) : Utilise l'auto-attention bidirectionnelle où chaque token peut prêter attention à tous les autres tokens de la séquence simultanément. Il produit de riches représentations contextuelles pour une séquence d'entrée entière, ce qui le rend idéal pour la classification, les questions-réponses extractives et la représentation de caractéristiques. Il ne peut pas naturellement générer de texte autorégressif.
2. **Décodeur seul** (par exemple, GPT-3, Llama, Mistral) : Utilise l'auto-attention causale (unidirectionnelle), où le token $i$ ne peut prêter attention qu'aux tokens aux positions $j \le i$. Il est entraîné de manière autorégressive en utilisant la prédiction du prochain token et sert d'architecture standard pour les modèles de langage génératifs, la génération de code et la conversation ouverte.
3. **Encodeur-Décodeur** (par exemple, T5, BART) : Combine un encodeur bidirectionnel avec un décodeur causal autorégressif. En plus de l'auto-attention causale sur les tokens générés, le décodeur utilise des couches d'attention croisée qui interrogent les représentations de sortie de l'encodeur. Cette architecture est spécialement conçue pour les tâches de transformation séquence-à-séquence telles que la traduction et le résumé.
8Expliquez comment les choix de tokeniseur au niveau octet, compatibles Unicode et multilingues affectent la qualité du modèle, le coût et l'équité entre les langues.
Les choix de conception des tokeniseurs – tels que les segmentations au niveau octet ou compatibles Unicode et les allocations de vocabulaire multilingue – ont un impact direct sur la qualité du modèle en aval, le coût d'inférence/d'entraînement et l'équité linguistique. En termes de coût et d'équité, les tokeniseurs entraînés principalement sur des corpus en anglais ou en écriture latine allouent la plupart des entrées de vocabulaire aux mots et morphèmes anglais. Par conséquent, l'anglais atteint une compression élevée (par exemple, environ 1,3 jeton par mot), tandis que les écritures non latines (par exemple, l'arabe, le devanagari, le thaï, le chinois) ou les langues à faibles ressources sont souvent fragmentées en plusieurs sous-mots ou octets UTF-8 bruts (souvent 3 à 6 jetons par mot). Cette disparité est souvent appelée la « taxe de jeton » ou le « déséquilibre du taux de fertilité » : les utilisateurs non anglophones paient beaucoup plus par unité de contenu sémantique pour la facturation API, consomment les limites de la fenêtre contextuelle beaucoup plus rapidement et subissent une latence plus élevée. En termes de qualité, les tokeniseurs au niveau octet (comme le BPE au niveau octet dans GPT-2/GPT-4 ou SentencePiece avec repli d'octets dans LLaMA) évitent entièrement les plantages dus à des caractères inconnus et les erreurs hors-vocabulaire (UNK) car toute chaîne UTF-8 valide se décompose en jetons d'octets. Cependant, une fragmentation excessive d'octets dégrade la qualité de la représentation car le transformeur doit consacrer des couches à recombiner les fragments d'octets en concepts sémantiques avant d'effectuer un raisonnement de haut niveau. L'augmentation de la taille du vocabulaire multilingue (par exemple, l'expansion de 32k à 128k+ jetons) équilibre les taux de fertilité et améliore les performances des tâches en aval dans diverses langues, au prix d'une couche d'intégration d'entrée/sortie modérément plus grande.
9Quels artefacts de tokenisation apparaissent dans le raisonnement numérique, la génération de code ou le texte Unicode rare, et comment les tokeniseurs spécialisés peuvent-ils les réduire ?
Les artefacts de tokenisation se produisent lorsque les tokeniseurs de sous-mots partitionnent de manière incohérente du texte structuré, numérique ou rare, empêchant le modèle de reconnaître la régularité sémantique ou syntaxique sous-jacente. Les principaux artefacts comprennent :
1. **Artefacts de raisonnement numérique :** Les tokeniseurs BPE (Byte Pair Encoding) standard entraînés sur du texte général segmentent les nombres en tronçons de longueur arbitraire basés sur la fréquence (par exemple, '12345' pourrait être tokenisé comme ['12', '345'] tandis que '12346' est tokenisé comme ['123', '46']). Ce regroupement incohérent perturbe l'alignement de la valeur de position (chiffres, dizaines, centaines) et entrave le raisonnement arithmétique.
2. **Artefacts de génération de code :** L'indentation (espaces/tabulations en début de ligne) et les opérateurs multi-caractères (par exemple, '==', '!=', '->') sont souvent divisés irrégulièrement aux limites des caractères d'espacement, entraînant des erreurs d'indentation, un nombre de tokens excessif dans le code fortement indenté et des corruptions syntaxiques.
3. **Artefacts Unicode et Emoji rares :** Les séquences UTF-8 multi-octets (comme les emojis complexes avec des joncteurs sans chasse ou les scripts rares) sont divisées en tokens d'octets bruts qui ne portent aucune signification sémantique individuelle, causant des caractères hallucinés ou un rendu de glyphes corrompu lors de la génération.
Les tokeniseurs spécialisés réduisent ces artefacts en utilisant des règles de pré-tokenisation et des contraintes de vocabulaire personnalisées :
- **Segmentation des chiffres :** Imposer une tokenisation à un seul chiffre (par exemple, en utilisant une expression régulière pour segmenter chaque chiffre `0-9` en son propre token) assure une représentation uniforme de la valeur de position pour le raisonnement mathématique.
- **Tokens dédiés pour les espaces/indentations :** Ajouter des tokens explicites pour les indentations multi-espaces (par exemple, 2, 4, 8 espaces) et préserver les mots-clés/opérateurs des langages de programmation.
- **Pré-tokenisation par expressions régulières / Replis au niveau des octets :** Utiliser des séparateurs basés sur des expressions régulières (tels que les expressions régulières de GPT-4/tiktoken) qui séparent la ponctuation, les lettres et les nombres en catégories strictes avant que les fusions BPE ne soient calculées, empêchant ainsi les fusions inter-catégories (par exemple, empêchant 'a=10' de fusionner en un seul token).
import tiktoken
# tiktoken cl100k_base (GPT-4 / ChatGPT) enforces digit and whitespace handling
enc = tiktoken.get_encoding('cl100k_base')
num1 = '12345'
num2 = '12346'
code_indent = ' def foo():'
print('Tokens num1:', [enc.decode([t]) for t in enc.encode(num1)])
print('Tokens num2:', [enc.decode([t]) for t in enc.encode(num2)])
print('Tokens code:', [enc.decode([t]) for t in enc.encode(code_indent)])
10Discutez des compromis de l'attention à long contexte, y compris l'attention complète quadratique, l'attention à fenêtre glissante, l'attention éparse ou globale, le coût du cache KV, et la dilution de l'attention.
L'extension de l'attention aux longues fenêtres de contexte présente des compromis en termes de calcul, d'empreinte mémoire et de fidélité du modèle :
1. **Attention complète quadratique vs. Attention à fenêtre glissante / Éparse :** L'attention complète standard évolue quadratiquement ($O(N^2)$) en termes de calcul et de mémoire d'activation avec la longueur de séquence $N$. L'attention à fenêtre glissante (locale) restreint l'attention à un voisinage fixe $W$, réduisant la complexité à $O(N \cdot W)$, mais nécessite plusieurs couches pour propager l'information entre des tokens éloignés. Les modèles d'attention éparse ou globale combinent des fenêtres locales avec des tokens d'ancrage globaux sélectionnés pour conserver une mise à l'échelle en $O(N)$ tout en permettant une communication à longue portée.
2. **Coût mémoire du cache KV :** Pendant la génération autorégressive, les clés (keys) et les valeurs (values) de tous les tokens précédents sont mises en cache pour éviter les calculs redondants. La mémoire du cache KV évolue linéairement avec la longueur de séquence ($O(B \cdot L \cdot H_{KV} \cdot D \cdot N)$). Pour des contextes très longs (32k à 128k+ tokens), le cache KV consomme des dizaines de gigaoctets de VRAM GPU par lot, limitant la taille maximale des lots et la bande passante mémoire.
3. **Dilution de l'attention (Perdu au milieu) :** À mesure que le contexte s'allonge, le dénominateur de la fonction softmax somme sur des dizaines de milliers de tokens, répartissant la masse de probabilité de manière diffuse sur un contexte non pertinent. Cette augmentation de l'entropie dilue la netteté de l'attention, dégradant la capacité du modèle à rappeler de manière fiable des informations spécifiques intégrées au milieu de longues invites.
11Comparez MHA, MQA et GQA et expliquez comment ils affectent la mémoire du cache KV et le débit de décodage.
L'Attention Multi-Têtes (MHA), l'Attention Multi-Requêtes (MQA) et l'Attention à Requêtes Groupées (GQA) diffèrent par la manière dont les têtes de Clé ($K$) et de Valeur ($V$) sont partagées entre les têtes de Requête ($Q$):
1. **Attention Multi-Têtes (MHA)**: Possède un nombre égal de têtes $Q$, $K$ et $V$ ($H_Q = H_{KV}$, ratio 1:1). Chaque tête de requête s'appuie sur ses propres représentations indépendantes clé/valeur. Bien qu'expressive, elle nécessite de mettre en cache des matrices KV distinctes pour chaque tête.
2. **Attention Multi-Requêtes (MQA)**: Utilise plusieurs têtes $Q$ ($H$) mais seulement 1 tête $K$ partagée et 1 tête $V$ partagée (ratio $H:1$). Cela réduit la taille du cache KV d'un facteur $H$, mais peut entraîner une légère perte de qualité ou une instabilité lors de l'entraînement.
3. **Attention à Requêtes Groupées (GQA)**: Groupe les têtes $Q$ en $G$ partitions, où chaque groupe partage une seule tête $K$ et $V$ (par exemple, 8 têtes $Q$ par tête KV). Le GQA offre un compromis optimal, récupérant pratiquement toute la qualité de modélisation du MHA tout en conservant les avantages de mémoire du MQA.
**Impact sur le cache KV et le débit de décodage**:
La génération de jetons autorégressive (décodage) est limitée par la bande passante mémoire car le GPU (Graphics Processing Unit) doit transférer l'intégralité du cache KV de la HBM (High-Bandwidth Memory) vers la SRAM (Static Random-Access Memory) embarquée pour chaque jeton généré. En réduisant le nombre de têtes KV par $H/G$ (par exemple, $4\times$ à $8\times$ en GQA, ou $32\times+$ en MQA):
- L'empreinte mémoire du cache KV est réduite proportionnellement, ce qui permet des tailles de lot de service beaucoup plus importantes dans la VRAM (Video RAM) du GPU.
- Le trafic de lecture mémoire HBM par jeton diminue considérablement, augmentant de façon spectaculaire le débit de jetons de décodage.
# Model with 32 Query Heads
num_q_heads = 32
mha_kv_heads = 32 # 1:1 ratio
gqa_kv_heads = 8 # 4:1 ratio (4 query heads per KV head)
mqa_kv_heads = 1 # 32:1 ratio (1 shared KV head)
print(f"KV Cache Size Relative to MHA:")
print(f"MHA: {mha_kv_heads / mha_kv_heads * 100:.1f}%")
print(f"GQA: {gqa_kv_heads / mha_kv_heads * 100:.1f}%")
print(f"MQA: {mqa_kv_heads / mha_kv_heads * 100:.1f}%")
12Expliquez comment FlashAttention accélère le calcul exact de l'attention sans modifier les sorties d'attention.
FlashAttention accélère le calcul de l'attention en rendant l'algorithme conscient des E/S (Input/Output) — minimisant le trafic mémoire en lecture et écriture entre la mémoire à haute bande passante (HBM) lente du GPU et la SRAM (Static Random-Access Memory) rapide sur puce, plutôt que d'essayer de réduire le nombre total d'opérations en virgule flottante (FLOP) arithmétiques. L'attention standard matérialise les matrices intermédiaires N x N de scores et de probabilités d'attention dans la HBM, ce qui provoque un goulot d'étranglement majeur de la bande passante mémoire. FlashAttention surmonte ce problème grâce à trois mécanismes clés :
1. **Tuilage (Tiling) :** Il divise les matrices de Requête (Query), Clé (Key) et Valeur (Value) en blocs qui tiennent entièrement dans la SRAM sur puce du GPU.
2. **Softmax en ligne (Online Softmax) :** Il calcule le softmax de manière incrémentielle sur des blocs en suivant les maximums courants et les sommes de normalisation, mettant à jour les sorties partielles sans avoir besoin de la matrice N x N matérialisée complète en mémoire.
3. **Recalcul exact :** Lors de la passe arrière, il ne lit pas les matrices d'attention intermédiaires stockées à partir de la HBM ; au lieu de cela, il les recalcule à la volée dans la SRAM à partir des statistiques courantes stockées. Comme aucune approximation, factorisation de rang faible ou heuristique de suppression de tokens n'est utilisée, la sortie est mathématiquement exacte jusqu'à la précision numérique en virgule flottante, tout en réduisant l'empreinte mémoire HBM de O(N^2) à O(N).
13Concevez des flux de travail agentiques déterministes en utilisant des planificateurs, des machines à états, des DAGs, un état intermédiaire typé, des tentatives bornées et une vérification des résultats d'outils, plutôt que des boucles d'agents ouvertes.
Les boucles d'agents ouvertes (par exemple, les boucles ReAct (Reasoning and Acting) autonomes non contraintes) en production souffrent souvent de ramification non déterministe, de boucles infinies, de dépenses de tokens incontrôlées et de dérive d'état. Un flux de travail agentique déterministe remplace les boucles libres par un flux de contrôle structuré et observable :
1. **Machines à États et DAGs** : Le flux de contrôle est défini comme un graphe acyclique dirigé (DAG - Directed Acyclic Graph) explicite ou une machine à états finis (par exemple, LangGraph, Temporal, AWS Step Functions). Les transitions de nœuds dépendent de conditions explicites et de résultats typés plutôt que de décisions de modèle ouvertes.
2. **État Intermédiaire Typé** : L'état partagé entre les nœuds est modélisé avec des schémas stricts (par exemple, des modèles Pydantic ou des dataclasses). Les nœuds effectuent des opérations de lecture et d'écriture validées, prévenant la dérive de schéma ou un état mal formé.
3. **Planificateurs** : Les planificateurs structurés émettent un plan contraint à l'avance (par exemple, une liste ordonnée d'étapes basées sur des énumérations) ou choisissent parmi un ensemble restreint de transitions d'état valides plutôt que de décider librement des actions suivantes sans contraintes.
4. **Vérification des Résultats d'Outils** : Les sorties renvoyées par les outils sont validées de manière déterministe par rapport aux schémas et aux règles métier avant de mettre à jour l'état ou de les passer aux étapes LLM (Large Language Model) en aval.
5. **Tentatives Bornées et Solutions de Secours** : Chaque étape applique des budgets de tentatives explicites, des retours arrière exponentiels, des délais d'attente et des transitions de secours (par exemple, une escalade vers une révision humaine ou le déclenchement d'une abstention sûre) pour garantir la terminaison.
from pydantic import BaseModel
from typing import Optional, Literal
class WorkflowState(BaseModel):
user_query: str
extracted_id: Optional[str] = None
verification_status: Literal["PENDING", "VERIFIED", "FAILED"] = "PENDING"
retry_count: int = 0
max_retries: int = 3
def execute_validation_node(state: WorkflowState) -> WorkflowState:
if state.retry_count >= state.max_retries:
state.verification_status = "FAILED"
return state
try:
result = call_verification_service(state.extracted_id)
state.verification_status = "VERIFIED" if result.is_valid else "FAILED"
except Exception:
state.retry_count += 1
return state
14Concevez une politique de confiance et d'abstention pour un assistant LLM (grand modèle linguistique) répondant à des questions dans des domaines réglementés.
Dans les domaines réglementés (tels que la santé, la banque, le droit et la conformité), des réponses incorrectes entraînent des sanctions réglementaires, des responsabilités légales et des risques de sécurité. Une politique robuste de confiance et d'abstention combine une notation de confiance calibrée multi-signaux, des seuils de réponse étagés et des flux de travail d'escalade déterministes : 1. Calibrage de la confiance multi-signaux : Les logprobs (probabilités logarithmiques) brutes des LLM sont souvent mal calibrées sur les requêtes hors domaine. Le score de confiance doit synthétiser plusieurs signaux indépendants : - Score d'ancrage de la récupération : Similarité sémantique et confiance de re-classement des morceaux de preuves récupérées. - Implication au niveau de l'affirmation (NLI - Natural Language Inference) : Modèles d'inférence en langage naturel vérifiant que chaque affirmation extraite est impliquée par le contexte source récupéré. - Entropie sémantique / Auto-cohérence : Mesure de la cohérence sémantique à travers plusieurs générations échantillonnées. - Logprobs des tokens du modèle : Logprobs minimum et moyen sur les entités nommées clés et les tokens factuels. 2. Politique d'abstention à plusieurs niveaux : - Confiance élevée (Score >= Seuil haut) : Fournir directement la réponse générée avec des citations intégrées. - Confiance moyenne / Ambigüe (Seuil bas <= Score < Seuil haut) : Fournir une réponse prudente avec des mises en garde, des clauses de non-responsabilité explicites ou demander à l'utilisateur des précisions. - Confiance faible / Hors de portée (Score < Seuil bas) : Abstention stricte avec un message de refus standardisé. 3. Escalade et auditabilité de la conformité : - Escalade déterministe : Les abstentions ou les divergences critiques sont automatiquement acheminées vers des files d'attente d'humains dans la boucle (HITL) ou des systèmes de billetterie d'agents avec un contexte complet. - Piste d'audit et lignage : Une télémétrie complète – y compris les hachages d'invites, les identifiants de document récupérés, les scores des composants de confiance individuels et les décisions de routage finales – doit être enregistrée pour l'auditabilité réglementaire.
15Concevez une stratégie de routage de modèles qui choisit parmi des petits, moyens et grands modèles en fonction de la complexité de la requête, du coût, du risque et des exigences de qualité.
Une architecture de routage de modèles en production dirige les requêtes entrantes vers des niveaux de modèles petits (par exemple, 1B–8B SLM (Small Language Models)), moyens (par exemple, modèles 14B–70B) et grands (par exemple, modèles de pointe ou grands modèles MoE (Mixture of Experts)) en équilibrant la complexité, la latence, le risque et le coût de calcul. Le flux de travail de routage combine généralement des règles statiques, un routage prédictif et des cascades de repli dynamiques : 1. Portes de politique déterministes/statiques : Filtrez les requêtes par niveau client, accords de niveau de service (SLA) de latence stricts, risque réglementaire/de domaine (par exemple, diagnostic médical ou rédaction juridique acheminés directement vers les modèles de premier niveau), ou tâches simples correspondant à des règles (par exemple, regex/formatage basique vers de petits modèles). 2. Routage prédictif de complexité : Un classifieur rapide et léger (tel qu'une recherche de similarité d'embeddings, un encodeur croisé ou un petit routeur SLM) évalue la complexité de la requête, la profondeur du raisonnement et l'ambiguïté du domaine pour sélectionner le niveau le plus rentable en amont. 3. Exécution dynamique et cascades d'escalade : Envoyez l'invite d'abord à un modèle plus petit et évaluez la confiance de la sortie (via les logprobs/entropie des tokens, la validité du schéma structuré ou les vérifications des garde-fous). Si la confiance est en dessous du seuil ou si la validation échoue, le routeur escalade vers un modèle moyen ou grand. Les compromis clés du système incluent le surcoût de latence du routeur par rapport aux économies de calcul, les budgets de temps d'attente de repli en cas de pics de trafic, et l'évaluation continue (par exemple, l'évaluation fantôme pour suivre la dérive de la qualité des résultats entre les niveaux).
class DynamicModelRouter:
def __init__(self, small_client, medium_client, large_client, classifier, guardrail):
self.small = small_client
self.medium = medium_client
self.large = large_client
self.classifier = classifier
self.guardrail = guardrail
async def route_and_execute(self, request):
# 1. Deterministic Risk Gate
if request.risk_level == "high" or request.domain in ["legal", "medical_compliance"]:
return await self.large.generate(request.prompt)
# 2. Predictive Complexity Classifier
complexity = self.classifier.predict_complexity(request.prompt) # 0.0 to 1.0
if complexity < 0.35:
response = await self.small.generate(request.prompt)
if self.guardrail.is_acceptable(response):
return response
return await self.medium.generate(request.prompt) # Fallback
if complexity < 0.75:
response = await self.medium.generate(request.prompt)
if self.guardrail.is_acceptable(response):
return response
return await self.large.generate(request.prompt) # Fallback
# 3. High complexity frontier execution
return await self.large.generate(request.prompt)