Préparation à l'entretien en LLM et IA Générative

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.

Commencer un entretien d'IA en LLM et IA GénérativeAucune carte bancaire requise. 1 session gratuite disponible.
Préparation aux entretiens techniques en anglaisUn mode où les non-natifs peuvent s'entraîner à passer des entretiens techniques.

Questions Junior

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 en sous-mots découpe le texte en unités de longueur variable. Des méthodes comme BPE (fusion de paires d’octets), WordPiece ou Unigram apprennent un vocabulaire. Un mot fréquent peut tenir en un jeton, alors qu’un mot rare se décompose en unités connues. Les mots entiers exigeraient un vocabulaire plus grand et laisseraient davantage de formes inconnues. Les caractères seuls allongent souvent les séquences, ce qui augmente le coût de l’attention standard. Les sous-mots offrent un compromis. La couverture dépend de l’alphabet de base. Un repli sur les octets peut représenter les entrées non couvertes par le vocabulaire appris.

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained('gpt2')
text = 'unbelievable'
tokens = tokenizer.tokenize(text)
token_ids = tokenizer.encode(text)

print('Tokens:', tokens)
print('Token IDs:', token_ids)
Essayer de répondre à cette question avec un coach IA

2Quelle différence y a-t-il entre les plongements statiques de mots, les plongements contextuels et les états cachés d’un transformeur ?

Un plongement statique donne au même mot un vecteur indépendant de la phrase, comme avec Word2Vec. Un plongement contextuel dépend du texte autour du mot : *bank* peut avoir des représentations différentes dans les contextes d’une banque et d’une rive. Dans un transformeur, les états cachés sont les vecteurs calculés pour chaque position à chaque couche lors d’un passage avant. Un état d’une couche choisie peut donc servir de plongement contextuel. Un plongement de phrase peut aussi agréger plusieurs états. Les caractéristiques capturées varient selon la couche, le modèle et l’entraînement.

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())
Essayer de répondre à cette question avec un coach IA

3Expliquez ce que représente un espace d'embeddings et comment la similarité cosinus est interprétée pour les embeddings textuels.

Un espace de plongements associe à chaque texte un vecteur dans un espace de dimension élevée. Les proximités utiles dépendent du modèle et de son entraînement. Pour des vecteurs non nuls, la similarité cosinus vaut `cos(u, v) = (u · v) / (||u|| × ||v||)`. Elle varie de −1 à 1 et compare les directions indépendamment de la norme. Une valeur élevée signale souvent des textes proches selon le modèle. Zéro indique des vecteurs orthogonaux, et une valeur négative des directions en partie opposées. Cela ne prouve ni indépendance sémantique ni antonymie. Calibrez le seuil sur la tâche.

import numpy as np

def cosine_sim(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

# Hypothetical 3D embeddings
v_king = np.array([0.9, 0.1, 0.4])
v_queen = np.array([0.85, 0.15, 0.42])
v_apple = np.array([0.1, 0.9, -0.2])

print('Sim(king, queen):', round(cosine_sim(v_king, v_queen), 4))
print('Sim(king, apple):', round(cosine_sim(v_king, v_apple), 4))
Essayer de répondre à cette question avec un coach IA

4Expliquez la différence entre plongements de tokens, positionnels et de segment ou de type dans les entrées d’un transformeur.

Dans une architecture de type BERT, la représentation de chaque token est généralement la somme élément par élément de trois vecteurs. Le plongement de token transforme l’identifiant discret du vocabulaire en vecteur dense portant l’identité lexicale et sémantique. Le plongement positionnel fournit ordre et indice : sans information de position, l’auto-attention non masquée est équivariante aux permutations, donc permuter les entrées permute les sorties sans coder l’ordre. D’autres architectures utilisent des positions relatives ou rotatives au lieu d’un vecteur ajouté. Le plongement de segment ou de type distingue les portions regroupées, comme phrase A et phrase B dans classification par paires ou questions-réponses. Leur combinaison forme un tenseur dense unique de sens, position et groupe avant la première couche.

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)
Essayer de répondre à cette question avec un coach IA

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.

Dans l’auto-attention, chaque position produit une requête Q, une clé K et une valeur V. Le produit scalaire entre une requête et les clés calcule les scores des positions accessibles. Diviser par la racine de la dimension des clés limite l’amplitude des scores et la saturation du softmax. Après masquage éventuel, le softmax donne des poids. La sortie est une somme pondérée des valeurs : `softmax(QKᵀ / √dₖ) V` dans le cas sans masque. Plusieurs têtes utilisent des projections apprises distinctes. Leurs sorties sont concaténées puis projetées pour combiner les relations captées.

import torch
import torch.nn.functional as F

# Q, K, V: [batch_size, seq_len, head_dim]
Q = torch.randn(1, 4, 64)
K = torch.randn(1, 4, 64)
V = torch.randn(1, 4, 64)
d_k = Q.size(-1)

scores = torch.matmul(Q, K.transpose(-2, -1)) / (d_k ** 0.5)
attn_weights = F.softmax(scores, dim=-1)
output = torch.matmul(attn_weights, V)

print("Output shape:", output.shape)
Essayer de répondre à cette question avec un coach IA

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.

Un masque d’attention interdit certaines paires de positions, par exemple en ajoutant `−∞` au score avant le softmax. Dans un décodeur causal, la position `i` ne peut lire que les positions `j ≤ i`. Cela évite de révéler le jeton futur à prédire à l’entraînement et permet la génération pas à pas. Dans un encodeur bidirectionnel, une position peut utiliser les jetons valides des deux côtés. Un masque de remplissage peut écarter `[PAD]` dans les deux architectures. Les représentations bidirectionnelles conviennent notamment à l’analyse d’un texte déjà disponible.

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)
Essayer de répondre à cette question avec un coach IA

7Expliquez la différence entre les architectures de transformeurs à encodeur seul, à décodeur seul et à encodeur-décodeur pour les tâches linguistiques.

Un encodeur seul, comme BERT, utilise généralement l’auto-attention bidirectionnelle pour représenter un texte complet. Il convient à la classification, à l’extraction et aux représentations de texte. Un décodeur seul, comme GPT ou Llama, utilise un masque causal et génère par prédiction successive des jetons. Dans un modèle encodeur-décodeur comme T5, l’encodeur lit l’entrée et le décodeur produit la sortie. Il combine une auto-attention causale sur la sortie avec une attention croisée vers l’encodeur. Ce modèle s’adapte à la traduction ou au résumé.

# Causal mask (Decoder-Only) vs Full mask (Encoder-Only)
import torch

seq_len = 4
encoder_mask = torch.ones(seq_len, seq_len)  # Full bidirectional attention
decoder_causal_mask = torch.tril(torch.ones(seq_len, seq_len))  # Lower-triangular

print("Encoder Mask:
", encoder_mask)
print("Decoder Causal Mask:
", decoder_causal_mask)
Essayer de répondre à cette question avec un coach IA

Questions Middle

8Comment le découpage par octets, la prise en compte d’Unicode et le choix d’un vocabulaire multilingue influencent-ils la qualité, le coût et l’équité entre langues ?

Un découpage par octets peut représenter tout texte UTF-8 valide si les octets de base sont couverts. Des unités Unicode ou des règles adaptées aux écritures peuvent mieux préserver certains motifs. Le vocabulaire appris favorise les formes fréquentes dans son corpus. À contenu comparable, une langue plus fragmentée peut consommer davantage de contexte, de calcul et de budget lorsqu’il est facturé au jeton. Un vocabulaire multilingue mieux réparti peut réduire cet écart, mais son agrandissement augmente aussi la taille des tables du modèle. Mesurez les jetons par texte comparable et la qualité par langue, sans supposer qu’une écriture entraîne toujours le même coût.

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained('gpt2')

english_text = 'Hello world'
hindi_text = 'नमस्ते दुनिया'

print('English tokens:', tokenizer.tokenize(english_text))
print('Hindi tokens:', tokenizer.tokenize(hindi_text))
print('English token count:', len(tokenizer.encode(english_text)))
print('Hindi token count:', len(tokenizer.encode(hindi_text)))
Essayer de répondre à cette question avec un coach IA

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 ?

Un vocabulaire général peut découper deux nombres de même longueur en groupes différents, ce qui complique l’apprentissage des positions décimales. Dans le code, une indentation ou un opérateur peut occuper plusieurs jetons. Un caractère Unicode rare ou un emoji peut être représenté par plusieurs octets, qui se recomposent correctement si la séquence générée est valide. On peut choisir des règles régulières pour les chiffres, des jetons d’indentation, ou séparer chiffres, lettres et ponctuation avant les fusions. Un repli sur les octets maintient la couverture. Mesurez le coût et les erreurs : ces choix ne garantissent pas un calcul juste ni un code valide.

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)])
Essayer de répondre à cette question avec un coach IA

10Analysez les compromis de l’attention sur contexte long : attention complète quadratique, fenêtre glissante, motifs clairsemés ou globaux, cache de clés et de valeurs (KV) et dilution de l’attention.

L’attention dense a un coût arithmétique quadratique en longueur N. Matérialiser les scores demande aussi une mémoire quadratique, mais des algorithmes exacts optimisés pour les entrées-sorties, comme FlashAttention, l’évitent. Une fenêtre locale W réduit le coût à O(N × W), mais plusieurs couches sont nécessaires pour relier des tokens éloignés. Des fenêtres fixes avec un nombre borné de tokens globaux donnent des motifs à croissance linéaire tout en permettant la communication distante. En génération autorégressive, les clés et valeurs précédentes sont conservées pour éviter les recalculs. Leur mémoire croît comme O(B × L × H_KV × D × N). Pour 32 000 à 128 000 tokens ou davantage, ce cache peut prendre des dizaines de gigaoctets par lot et limiter taille de lot et bande passante. Les longs contextes ajoutent distractions et difficultés de position ou généralisation. Le phénomène « perdu au milieu » est une faiblesse de récupération dépendant de la position, pas une conséquence universelle d’une entropie softmax accrue : l’attention peut rester concentrée. Mesurez la précision selon longueur et position des preuves plutôt que de la déduire du seul dénominateur.

def kv_cache_gb(batch_size, seq_len, layers=32, kv_heads=8, head_dim=128, bytes_per_elem=2):
    # 2 for Key and Value
    total_bytes = batch_size * seq_len * layers * kv_heads * head_dim * 2 * bytes_per_elem
    return total_bytes / (1024 ** 3)

print(f"32k context: {kv_cache_gb(4, 32768):.2f} GB")
print(f"128k context: {kv_cache_gb(4, 131072):.2f} GB")
Essayer de répondre à cette question avec un coach IA

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 multitête (MHA) a une tête de clé et de valeur pour chaque tête de requête. L’attention à requêtes multiples (MQA) partage une seule paire clé-valeur entre toutes les têtes de requête. L’attention à requêtes groupées (GQA) partage une paire par groupe. À dimensions, précision et longueur égales, le cache KV (clés et valeurs) est proportionnel au nombre de paires. Avec H têtes et G groupes, GQA utilise donc environ G/H de la mémoire KV de MHA. Réduire les lectures du cache peut accélérer le décodage et permettre de plus gros lots. Le gain réel et la qualité dépendent de l’implémentation, du matériel et du modèle.

# 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}%")
Essayer de répondre à cette question avec un coach IA

12Expliquez comment FlashAttention accélère le calcul exact de l'attention sans modifier les sorties d'attention.

FlashAttention traite les requêtes, clés et valeurs par blocs dans la mémoire rapide du processeur graphique. Un softmax calculé progressivement maintient le maximum et la normalisation de chaque ligne, ce qui évite d’écrire toute la matrice d’attention en mémoire à haute bande passante. Lors de la rétropropagation, certains intermédiaires sont recalculés à partir des statistiques conservées. L’attention dense reste quadratique en opérations pour une dimension de tête fixée. Le gain vient surtout de la réduction des transferts et du stockage intermédiaire. Le résultat mathématique est celui de l’attention exacte, avec de possibles écarts d’arrondi.

import torch

def online_softmax_step(m_prev, l_prev, out_prev, scores_block, v_block):
    # scores_block: (B, H, Br, Bc), v_block: (B, H, Bc, D)
    m_block = scores_block.max(dim=-1, keepdim=True).values
    m_new = torch.maximum(m_prev, m_block)
    
    # Rescale previous and current accumulators
    p_prev_scale = torch.exp(m_prev - m_new)
    p_block = torch.exp(scores_block - m_new)
    
    l_new = p_prev_scale * l_prev + p_block.sum(dim=-1, keepdim=True)
    out_new = (p_prev_scale * l_prev * out_prev + p_block @ v_block) / l_new
    return m_new, l_new, out_new
Essayer de répondre à cette question avec un coach IA

Questions Senior

13Comment structurer un flux de travail d’agent avec un planificateur, une machine à états ou un DAG, un état typé, un nombre limité de tentatives et une vérification des résultats d’outils ?

Représentez le déroulement dans une machine à états ou un DAG (graphe orienté acyclique). Chaque étape déclare ses entrées et ses résultats dans un état typé. Le planificateur peut proposer une suite d’étapes, mais le moteur n’exécute que des étapes autorisées dont les préconditions et les transitions sont vérifiées. Une proposition du modèle peut varier entre deux exécutions, tandis que les règles de validation et de transition restent explicites. Avant de modifier l’état, vérifiez le schéma du résultat d’outil et ses invariants métier. Fixez un délai par appel, un nombre maximal de tentatives avec attente exponentielle pour les erreurs transitoires et un budget global d’étapes ou de temps. Définissez une issue terminale pour l’échec, par exemple un transfert à une personne ou une abstention. Journalisez les décisions pour pouvoir diagnostiquer et reproduire le chemin suivi.

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
Essayer de répondre à cette question avec un coach IA

14Comment concevoir une politique de confiance et d’abstention pour un assistant à grand modèle de langage qui répond dans un domaine réglementé ?

Calibrez les seuils sur des exemples représentatifs dont les réponses ont été vérifiées, séparément par catégorie de risque. Combinez des signaux comme la présence de sources pertinentes, l’appui de chaque affirmation par les sources, la stabilité des réponses et, si elles sont disponibles, les probabilités de jetons. Ces signaux restent imparfaits et peuvent être corrélés. Pour une demande autorisée et bien étayée, répondez avec des références. Si l’incertitude tient à une ambiguïté que l’utilisateur peut lever, demandez une précision. En cas de preuve insuffisante, de contradiction importante ou de demande hors périmètre, abstenez-vous ou transmettez à un spécialiste selon les règles définies. Certaines demandes doivent être examinées par une personne quel que soit le score. Conservez les versions, les références et les décisions nécessaires à l’audit avec des contrôles d’accès et une durée de conservation adaptée. Mesurez le taux d’erreurs acceptées et celui des abstentions injustifiées.

from dataclasses import dataclass
from typing import Literal

@dataclass
class DecisionResult:
    action: Literal["SERVE", "SERVE_WITH_CAVEAT", "ABSTAIN_AND_ESCALATE"]
    confidence_score: float
    reason: str

def evaluate_confidence_policy(retrieval_score: float, nli_entailment_score: float, semantic_entropy: float) -> DecisionResult:
    # Composite calibrated confidence index [0, 1]
    composite_score = (0.4 * retrieval_score) + (0.4 * nli_entailment_score) + (0.2 * (1.0 - semantic_entropy))
    
    if composite_score >= 0.85:
        return DecisionResult("SERVE", composite_score, "High evidence grounding")
    elif composite_score >= 0.60:
        return DecisionResult("SERVE_WITH_CAVEAT", composite_score, "Partial evidence support")
    else:
        return DecisionResult("ABSTAIN_AND_ESCALATE", composite_score, "Insufficient ground truth")
Essayer de répondre à cette question avec un coach IA

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é.

Commencez par des règles d’éligibilité liées aux données, au risque, au délai et aux capacités requises. Une demande qui exige un examen humain suit ce circuit quelle que soit la taille du modèle. Pour les demandes admissibles, un classificateur léger peut estimer la complexité et choisir le modèle le moins coûteux qui atteint le niveau de qualité mesuré pour ce type de tâche. Pour certaines tâches, lancez un petit modèle puis passez à un modèle intermédiaire ou plus puissant si la validation échoue ou si la demande se révèle plus complexe. Un schéma valide confirme la structure de la sortie. Vérifiez aussi le fond avec des critères adaptés à la tâche, comme l’appui sur des sources ou des tests exécutables. Fixez une limite totale de temps et de tentatives, puis prévoyez l’abstention ou une revue humaine si aucun modèle autorisé ne convient. Suivez la qualité, la latence, les erreurs de routage et le coût total des cascades.

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)
Essayer de répondre à cette question avec un coach IA

Préparez vos entretiens sur mobile : LLM and Generative AI

Répondez à voix haute et recevez les retours de l’IA dans l’app Android.

Télécharger EngineerSpeak sur Google Play
Scannez pour télécharger