Preparazione all'intervista di LLM e AI Generativa
Domande di Intervista per Ingegneri LLM e AI Generativa
15 domande selezionate di intervista su LLM e intelligenza artificiale generativa, raggruppate per livello di anzianità. Usale per ripassare i fondamenti, i compromessi pratici e il ragionamento di produzione a livello senior.
1Spiega la tokenizzazione a livello di subword e perché è preferita rispetto alla tokenizzazione a livello di parola o carattere nei moderni modelli linguistici.
La tokenizzazione a livello di subword è un approccio ibrido di segmentazione del testo che divide il testo in blocchi morfologici a lunghezza variabile o sottostringhe basate sulla frequenza (come 'un', 'break', 'able') anziché in parole intere o singoli caratteri. Algoritmi come Byte-Pair Encoding (BPE), WordPiece e Unigram LM apprendono un vocabolario a dimensione fissa da un corpus di addestramento in cui le parole frequenti rimangono intatte come singoli token, mentre le parole rare o mai viste vengono scomposte in unità di subword conosciute. La tokenizzazione a livello di subword è preferita nei moderni modelli linguistici perché bilancia la dimensione del vocabolario, la lunghezza della sequenza e la robustezza all'out-of-vocabulary (OOV). La pura tokenizzazione a livello di parola richiede un vocabolario eccessivamente grande (il che porta a matrici di embedding enormi) e soffre comunque di token OOV mappati a token generici '[UNK]'. Al contrario, la pura tokenizzazione a livello di carattere elimina i problemi OOV ma produce sequenze molto lunghe che aumentano drasticamente la complessità computazionale nei meccanismi di attenzione (che scalano quadraticamente con la lunghezza della sequenza) e diluiscono la densità semantica per token. La tokenizzazione a livello di subword raggiunge un compromesso ottimale mantenendo le lunghezze delle sequenze gestibili, le dimensioni del vocabolario pratiche (tipicamente da 32k a 128k token) e i tassi OOV a zero (specialmente se combinati con fallback a livello di byte).
2Spiega la differenza tra embedding di parole statici, embedding contestuali e stati nascosti dei transformer.
Gli embedding di parole statici, gli embedding contestuali e gli stati nascosti dei transformer rappresentano evoluzioni progressive nel modo in cui le rappresentazioni testuali catturano il significato e il contesto sintattico.
1. **Embedding di parole statici** (ad es., Word2Vec, GloVe, FastText) assegnano un singolo vettore fisso a ogni token del vocabolario indipendentemente dal suo contesto nella frase. In questo paradigma, parole polisemiche come 'bank' (sponda di fiume vs. banca finanziaria) o 'apple' (frutto vs. azienda tecnologica) hanno rappresentazioni vettoriali identiche in tutti i contesti, basandosi su una tabella di lookup statica.
2. **Embedding contestuali** (ad es., ELMo iniziale, rappresentazioni dei token di BERT, o embedding di frasi da Bi-Encoders) producono rappresentazioni in cui il vettore per un token è una funzione dinamica del suo contesto circostante. In BERT o ELMo, 'bank' in 'river bank' riceve un vettore di embedding completamente diverso da 'bank' in 'deposit money at the bank'.
3. **Stati nascosti dei transformer** si riferiscono alle rappresentazioni vettoriali intermedie prodotte in ogni singolo strato di una rete transformer durante un passaggio forward. Dati gli embedding dei token di input allo strato 0, ogni strato transformer successivo applica trasformazioni di self-attention e feed-forward, producendo una sequenza di vettori di stato nascosto h_l allo strato l. Mentre gli stati nascosti dello strato finale agiscono come embedding contestuali di alto livello, gli stati nascosti inferiori e intermedi catturano feature sintattiche, lessicali e strutturali di basso livello. Così, gli stati nascosti dei transformer comprendono l'intero continuum verticale delle rappresentazioni strato per strato attraverso la rete.
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())
3Spiega cosa rappresenta uno *spazio di embedding* e come la *similarità del coseno* viene interpretata per gli *embedding* testuali.
Uno *spazio di embedding* è uno spazio vettoriale continuo e ad alta dimensionalità R^d dove entità testuali discrete (parole, frasi o documenti) sono mappate in modo tale che le similarità semantiche, sintattiche o relazionali corrispondano a prossimità geometrica e relazioni direzionali. In questo spazio, distanze e angoli riflettono la correlazione semantica.
La *similarità del coseno* misura il coseno dell'angolo theta tra due vettori `u` e `v`, calcolata come:
`Similarità del Coseno(u, v) = (u . v) / (||u|| ||v||)`
La *similarità del coseno* viene interpretata negli *embedding* testuali come segue:
- **Intervallo e Orientamento**: Produce un valore scalare tipicamente delimitato in [-1, 1] (o [0, 1] per *embedding* non negativi). Un valore vicino a 1.0 indica che i due vettori puntano virtualmente nella stessa direzione, riflettendo un'alta similarità semantica o allineamento tematico. Un valore vicino a 0.0 implica ortogonalità (indipendenza semantica o non correlazione), e valori negativi indicano orientamenti opposti.
- **Invarianza dalla Magnitudine**: A differenza della distanza euclidea (distanza L2) o del prodotto scalare, la *similarità del coseno* normalizza per la lunghezza del vettore. Negli *embedding* testuali, la magnitudine del vettore può talvolta correlare con la lunghezza della sequenza, la frequenza dei *token* o la specificità del termine. Concentrandosi puramente sull'allineamento direzionale, la *similarità del coseno* isola l'orientamento semantico dalle differenze di magnitudine del vettore.
4Spiega la differenza tra gli embedding di token, gli embedding posizionali e gli embedding di segmento o di tipo negli input del trasformatore.
Negli input del trasformatore (in particolare nelle architetture in stile BERT), la rappresentazione di input per ogni token è tipicamente formata sommando elemento per elemento tre vettori di embedding distinti: 1. Embedding di token: Mappano gli ID discreti dei token del vocabolario in vettori densi che rappresentano l'identità semantica e lessicale fondamentale dei token. 2. Embedding posizionali: Iniettano informazioni sull'ordine dei token e sull'indice sequenziale nella rappresentazione, compensando il fatto che l'auto-attenzione è intrinsecamente invariante alla permutazione. 3. Embedding di segmento (o di tipo di token): Distinguono tra diversi intervalli di testo o frasi raggruppate in una singola sequenza di input (come la Frase A rispetto alla Frase B in compiti di classificazione a coppie o di risposta a domande). La combinazione di questi embedding fornisce un singolo tensore di input denso che codifica il significato del token, la posizione e il raggruppamento della sequenza prima di passare al primo strato del trasformatore.
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)
5Spiega l'attenzione come meccanismo per correlare i token in una sequenza, includendo query, chiavi, valori e multi-head attention.
L'attenzione è un meccanismo che permette ai token in una sequenza di indirizzare dinamicamente le informazioni e pesare la rilevanza di tutti gli altri token basandosi su una corrispondenza contestuale. Proiezioni lineari convertono l'input di ogni token in tre vettori:
- Query (Q): Rappresenta quali informazioni il token attuale sta cercando.
- Key (K): Rappresenta quali attributi o contenuti un token offre per corrispondere alle query.
- Value (V): Contiene il payload di informazioni effettivo da aggregare.
Nell'attenzione scalata prodotto-punto (scaled dot-product attention), i punteggi di attenzione vengono calcolati moltiplicando Query e Key ($Q K^T$), scalati per $\frac{1}{\sqrt{d_k}}$ per prevenire lo svanire del gradiente (gradient vanishing) attraverso grandi dimensioni, e normalizzati con una funzione softmax. L'output finale è la somma pesata dei Value: $$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$
La Multi-Head Attention (MHA) proietta $Q$, $K$ e $V$ in più sottospazi rappresentativi indipendenti (teste o *heads*) in parallelo. Questo consente al modello di partecipare contemporaneamente a diversi tipi di relazioni (ad esempio, struttura sintattica, coreferenza, dipendenze a lungo raggio) tra le posizioni. Gli output delle teste vengono concatenati e riproiettati linearmente alla dimensione del modello.
6Spiega come il mascheramento dell'attenzione differisce per i decoder causali rispetto agli encoder bidirezionali e quali comportamenti abilita o previene.
Il mascheramento dell'attenzione controlla quali token possono prestare attenzione a quali altri token impostando i logit dell'attenzione (punteggi prima del softmax) a $-\infty$ per le coppie non consentite, garantendo che il loro peso di attenzione post-softmax sia strettamente 0. 1. Decoder Causali (ad esempio, GPT, LLaMA): Utilizzano una maschera causale (autoregressiva) triangolare inferiore. Un token nella posizione $i$ può prestare attenzione solo alle posizioni $j \le i$. Questo impedisce l'attenzione ai token futuri, consentendo la generazione autoregressiva token per token durante l'inferenza e prevenendo la fuga di etichette dei token futuri durante l'addestramento parallelizzato. 2. Encoder Bidirezionali (ad esempio, BERT): Non utilizzano una maschera causale; ogni token può prestare attenzione a tutti i token passati e futuri lungo la sequenza. Utilizzano maschere di padding per impedire ai token validi di prestare attenzione ai token `[PAD]` vuoti nelle sequenze batch. L'attenzione bidirezionale produce rappresentazioni contestuali complete e ricche, ideali per i compiti di comprensione, ma impedisce la generazione di testo autoregressiva diretta in un unico passaggio.
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)
7Spiega la differenza tra le architetture transformer solo-encoder, solo-decoder e encoder-decoder per i compiti linguistici.
Le tre architetture transformer principali differiscono fondamentalmente nei loro pattern di mascheramento dell'attenzione e negli obiettivi operativi mirati: 1. Solo-Encoder (es. BERT, RoBERTa): Utilizza l'auto-attenzione bidirezionale in cui ogni token può prestare attenzione a tutti gli altri token nella sequenza contemporaneamente. Produce ricche rappresentazioni contestuali per un'intera sequenza di input, rendendola ideale per la classificazione, la QA estrattiva e la rappresentazione di feature. Non può generare naturalmente testo autoregressivo. 2. Solo-Decoder (es. GPT-3, Llama, Mistral): Utilizza l'auto-attenzione causale (unidirezionale), dove il token $i$ può prestare attenzione solo ai token nelle posizioni $j \le i$. Viene addestrato autoregressivamente utilizzando la predizione del token successivo e serve come architettura standard per modelli linguistici generativi, generazione di codice e conversazioni aperte. 3. Encoder-Decoder (es. T5, BART): Combina un encoder bidirezionale con un decoder causale autoregressivo. Oltre all'auto-attenzione causale sui token generati, il decoder utilizza strati di cross-attenzione che interrogano le rappresentazioni di output dell'encoder. Questa architettura è costruita appositamente per compiti di trasformazione sequence-to-sequence come la traduzione e il riassunto.
8Spiega come le scelte dei tokenizer a livello di byte, sensibili a Unicode e multilingue influenzano la qualità del modello, il costo e l'equità tra le lingue.
Le scelte di progettazione del tokenizer — come la segmentazione a livello di byte rispetto a quella sensibile a Unicode e l'allocazione del vocabolario multilingue — influiscono direttamente sulla qualità del modello a valle, sui costi di inferenza/addestramento e sull'equità linguistica. In termini di costo ed equità, i tokenizer addestrati prevalentemente su corpus in inglese o in scrittura latina allocano la maggior parte delle voci del vocabolario a parole e morfemi inglesi. Di conseguenza, l'inglese raggiunge un'alta compressione (ad esempio, circa 1,3 token per parola), mentre le scritture non latine (ad esempio, arabo, devanagari, tailandese, cinese) o le lingue a bassa risorsa sono spesso frammentate in più sottoparole o byte UTF-8 grezzi (spesso da 3 a 6 token per parola). Questa disparità è spesso definita 'tassa sui token' o 'squilibrio del tasso di fertilità': gli utenti non inglesi pagano significativamente di più per unità di contenuto semantico nella fatturazione delle API, esauriscono i limiti della finestra di contesto molto più velocemente e subiscono una latenza maggiore. In termini di qualità, i tokenizer a livello di byte (come Byte-level BPE in GPT-2/GPT-4 o SentencePiece con byte fallback in LLaMA) evitano completamente crash dovuti a caratteri non visti ed errori Out-of-Vocabulary (UNK), perché qualsiasi stringa UTF-8 valida si decompone in token di byte. Tuttavia, un'eccessiva frammentazione a livello di byte degrada la qualità della rappresentazione perché il trasformatore deve impiegare strati per ricombinare i frammenti di byte in concetti semantici prima di eseguire un ragionamento di alto livello. Aumentare la dimensione del vocabolario multilingue (ad esempio, espandendolo da 32k a oltre 128k token) bilancia i tassi di fertilità e migliora le prestazioni dei task a valle in diverse lingue, a costo di uno strato di embedding di input/output moderatamente più grande.
9Quali artefatti di tokenizzazione compaiono nel ragionamento numerico, nella generazione di codice o nel testo Unicode raro, e come possono i tokenizzatori specializzati ridurli?
Gli artefatti di tokenizzazione si verificano quando i tokenizzatori subword suddividono testo strutturato, numerico o raro in modo inconsistente, impedendo al modello di riconoscere la regolarità semantica o sintattica sottostante. Gli artefatti chiave includono: 1. Artefatti nel ragionamento numerico: i tokenizzatori BPE (Byte Pair Encoding) standard addestrati su testo generale dividono i numeri in segmenti di lunghezza arbitraria basati sulla frequenza (ad esempio, '12345' potrebbe essere tokenizzato come ['12', '345'] mentre '12346' come ['123', '46']). Questa raggruppamento inconsistente interrompe l'allineamento del valore posizionale (unità, decine, centinaia) e ostacola il ragionamento aritmetico. 2. Artefatti nella generazione di codice: l'indentazione (spazi/tab iniziali) e gli operatori multi-carattere (ad esempio, '==', '!=', '->') spesso si dividono irregolarmente attraverso i confini spazio-carattere, portando a errori di indentazione, conteggi di token eccessivi nel codice profondamente indentato e corruzioni della sintassi. 3. Artefatti con testo Unicode raro ed Emoji: le sequenze UTF-8 multi-byte (come emoji complessi con zero-width joiners o script rari) vengono suddivise in token di byte grezzi che non hanno significato semantico individuale, causando caratteri allucinati o rendering del glifo corrotto durante la generazione. I tokenizzatori specializzati riducono questi artefatti utilizzando regole di pre-tokenizzazione e vincoli di vocabolario personalizzati: - Divisione delle cifre: imporre la tokenizzazione a singola cifra (ad esempio, dividere ogni cifra `0-9` con espressioni regolari nel proprio token) assicura una rappresentazione uniforme del valore posizionale per il ragionamento matematico. - Token dedicati per spazi bianchi/indentazione: aggiungere token espliciti per indentazioni multi-spazio (ad esempio, 2, 4, 8 spazi) e preservare parole chiave/operatori dei linguaggi di programmazione. - Pre-tokenizzazione tramite Regex / Fallback a livello di byte: utilizzare splitter regex (come le regex di GPT-4/tiktoken) che separano punteggiatura, lettere e numeri in categorie rigorose prima che vengano calcolate le unioni BPE, impedendo unioni tra categorie diverse (ad esempio, impedendo a 'a=10' di unirsi in un singolo 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)])
10Discutere i compromessi nell'attenzione a contesto lungo, inclusa l'attenzione completa quadratica, l'attenzione a finestra scorrevole, l'attenzione sparsa o globale, il costo della KV-cache e la diluizione dell'attenzione.
Scalare l'attenzione a finestre di contesto lunghe presenta compromessi in termini di computazione, ingombro di memoria e fedeltà del modello: 1. Attenzione Completa Quadratica vs. Attenzione a Finestra Scorrevole / Sparsa: L'attenzione completa standard scala quadraticamente ($O(N^2)$) in termini di calcolo e memoria di attivazione con la lunghezza della sequenza $N$. L'attenzione a finestra scorrevole (locale) limita l'attenzione a un vicinato fisso $W$, riducendo la complessità a $O(N \cdot W)$, ma richiede più strati per propagare le informazioni tra token distanti. I pattern di attenzione sparsa o globale combinano finestre locali con token anchor globali selezionati per mantenere una scalabilità $O(N)$ consentendo la comunicazione a lungo raggio. 2. Costo della Memoria della KV-Cache: Durante la generazione autoregressiva, chiavi e valori per tutti i token precedenti vengono memorizzati nella cache per evitare calcoli ridondanti. La memoria della KV-cache scala linearmente con la lunghezza della sequenza ($O(B \cdot L \cdot H_{KV} \cdot D \cdot N)$). Per contesti molto lunghi (32k–128k+ token), la KV-cache consuma decine di gigabyte di VRAM della GPU per batch, limitando la dimensione massima del batch e la larghezza di banda della memoria. 3. Diluizione dell'Attenzione (Lost-in-the-Middle): Man mano che il contesto cresce, il denominatore softmax somma decine di migliaia di token, distribuendo la massa di probabilità in modo sottile su un contesto irrilevante. Questo aumento di entropia diluisce la nitidezza dell'attenzione, degradando la capacità del modello di richiamare in modo affidabile informazioni specifiche incorporate nel mezzo di prompt lunghi.
11Confronta Multi-Head Attention (MHA), Multi-Query Attention (MQA) e Grouped-Query Attention (GQA) e spiega come influenzano la memoria della KV-cache e il throughput di decodifica.
Multi-Head Attention (MHA), Multi-Query Attention (MQA) e Grouped-Query Attention (GQA) differiscono nel modo in cui le *head* di Key ($K$) e Value ($V$) sono condivise tra le *head* di Query ($Q$):
1. **Multi-Head Attention (MHA)**: Ha un numero uguale di *head* $Q$, $K$ e $V$ ($H_Q = H_{KV}$, rapporto 1:1). Ogni *head* di query si concentra sulle proprie rappresentazioni indipendenti di key/value. Sebbene espressiva, richiede la memorizzazione nella cache di matrici KV distinte per ogni *head*.
2. **Multi-Query Attention (MQA)**: Utilizza più *head* $Q$ ($H$) ma solo 1 *head* $K$ condivisa e 1 *head* $V$ condivisa (rapporto $H:1$). Questo riduce la dimensione della KV-cache di un fattore $H$, ma può portare a una leggera perdita di qualità o instabilità durante l'addestramento.
3. **Grouped-Query Attention (GQA)**: Raggruppa le *head* $Q$ in $G$ partizioni, dove ogni gruppo condivide una singola *head* $K$ e $V$ (ad esempio, 8 *head* $Q$ per *head* KV). GQA offre un compromesso ottimale, recuperando praticamente tutta la qualità di modellazione di MHA pur mantenendo i vantaggi in termini di memoria di MQA.
**Impatto su KV-Cache e Throughput di Decodifica:**
La generazione autoregressiva di token (decodifica) è limitata dalla larghezza di banda della memoria perché la GPU (Graphics Processing Unit) deve trasferire l'intera KV-cache dalla HBM (High-Bandwidth Memory) alla SRAM (Static Random-Access Memory) on-chip per ogni singolo token generato. Riducendo il numero di *head* KV di $H/G$ (ad esempio, $4\times$ a $8\times$ in GQA, o $32\times+$ in MQA):
- L'ingombro di memoria della KV-cache è ridotto proporzionalmente, consentendo dimensioni di *batch* di *serving* molto più grandi nella VRAM (Video Random-Access Memory) della GPU.
- Il traffico di lettura della memoria HBM per token diminuisce sostanzialmente, aumentando drasticamente il throughput di decodifica dei token.
# 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}%")
12Spiega come FlashAttention velocizza il calcolo esatto dell'attenzione senza modificare gli output dell'attenzione.
FlashAttention velocizza il calcolo dell'attenzione rendendo l'algoritmo ottimizzato per l'I/O (IO-aware), minimizzando il traffico di memoria in lettura e scrittura tra la lenta GPU High Bandwidth Memory (HBM) e la veloce Static Random-Access Memory (SRAM) on-chip, piuttosto che cercare di ridurre il conteggio totale di FLOP (Floating Point Operations) aritmetici. L'attenzione standard materializza matrici intermedie N x N di punteggio di attenzione e probabilità nella HBM, il che causa un collo di bottiglia significativo della larghezza di banda della memoria. FlashAttention supera questo problema attraverso tre meccanismi chiave:
1. **Suddivisione in blocchi (Tiling):** Divide le matrici Query, Key e Value in blocchi che si adattano interamente all'interno della SRAM on-chip della GPU.
2. **Softmax online:** Calcola il softmax in modo incrementale sui blocchi tracciando i massimi correnti e le somme dei normalizzatori, aggiornando gli output parziali senza la necessità della matrice N x N completamente materializzata in memoria.
3. **Ricalcolo Esatto:** Durante il passaggio all'indietro (backward pass), non legge le matrici di attenzione intermedie memorizzate dalla HBM; invece, le ricalcola al volo nella SRAM dalle statistiche correnti memorizzate. Poiché non vengono utilizzate approssimazioni, fattorizzazioni a rango basso o euristiche di eliminazione dei token, l'output è matematicamente esatto fino alla precisione numerica in virgola mobile, riducendo l'impronta di memoria della HBM da O(N^2) a O(N).
13Progettare workflow agentici deterministici utilizzando pianificatori, macchine a stati, DAG (Directed Acyclic Graph), stato intermedio tipizzato, tentativi limitati (bounded retries) e verifica dei risultati degli strumenti (tool-result verification) anziché cicli di agenti aperti.
I cicli di agenti aperti (ad esempio, cicli ReAct autonomi non vincolati) in produzione soffrono spesso di branching non deterministico, cicli infiniti, consumo eccessivo di token e "state drift". Un workflow agentico deterministico sostituisce i cicli a forma libera con un flusso di controllo strutturato e osservabile: 1. Macchine a Stati e DAG: Il flusso di controllo è definito come un DAG (Directed Acyclic Graph) esplicito o una macchina a stati finiti (ad esempio, LangGraph, Temporal, AWS Step Functions). Le transizioni dei nodi dipendono da condizioni esplicite e risultati tipizzati anziché da decisioni di modello aperte. 2. Stato Intermedio Tipizzato: Lo stato condiviso tra i nodi è modellato con schemi rigorosi (ad esempio, modelli Pydantic o dataclass). I nodi eseguono operazioni di lettura e scrittura validate, prevenendo lo "schema drift" o stati malformati. 3. Pianificatori: I pianificatori strutturati emettono un piano vincolato in anticipo (ad esempio, un elenco ordinato di passaggi supportati da enum) o scelgono da un set ristretto di transizioni di stato valide anziché decidere liberamente le azioni successive senza vincoli. 4. Verifica dei Risultati degli Strumenti: Gli output restituiti dagli strumenti vengono validati deterministicamente rispetto agli schemi e alle regole di business prima di aggiornare lo stato o passare ai passaggi successivi dell'LLM (Large Language Model). 5. Tentativi Limiti e Fallback: Ogni passaggio impone budget di tentativi espliciti, "exponential backoffs", timeout e transizioni di fallback (ad esempio, l'escalation a una revisione umana o l'attivazione di un'astensione sicura) per garantire la terminazione.
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
14Progettare una politica di confidenza e astensione per un assistente LLM che risponde a domande in settori regolamentati.
Nei settori regolamentati (come sanità, banche, legale e conformità), risposte errate comportano sanzioni normative, responsabilità legali e rischi per la sicurezza. Una robusta politica di confidenza e astensione combina un punteggio di confidenza calibrato su più segnali, soglie di risposta a più livelli e flussi di lavoro di escalation deterministici:
1. **Calibrazione della Confidenza Multi-Segnale**: Le probabilità logaritmiche (logprobs) grezze dei modelli linguistici di grandi dimensioni (LLM) sono spesso mal calibrate su query fuori dominio. Il punteggio di confidenza dovrebbe sintetizzare più segnali indipendenti:
* **Punteggio di Grounding del Recupero**: Similarità semantica e confidenza di re-ranking dei frammenti di prova recuperati.
* **Entailment a Livello di Affermazione (NLI)**: Modelli di inferenza del linguaggio naturale (NLI) che verificano che ogni affermazione estratta sia implicata dal contesto della sorgente recuperata.
* **Entropia Semantica / Auto-Consistenza**: Misurazione della consistenza semantica attraverso molteplici generazioni campionate.
* **Logprobs dei Token del Modello**: Logprobs minimi e medi su entità nominate chiave e token fattuali.
2. **Politica di Astensione a Livelli**:
* **Alta Confidenza (Punteggio >= Soglia Alta)**: Fornire direttamente la risposta generata con citazioni in linea.
* **Confidenza Media / Ambigua (Soglia Bassa <= Punteggio < Soglia Alta)**: Fornire una risposta conservativa con avvertenze esplicite, dichiarazioni di non responsabilità o chiedere all'utente dettagli chiarificatori.
* **Bassa Confidenza / Fuori Ambito (Punteggio < Soglia Bassa)**: Astensione categorica con un messaggio di rifiuto standardizzato.
3. **Escalation e Auditabilità della Conformità**:
* **Escalation Deterministica**: Le astensioni o le discrepanze critiche vengono automaticamente instradate a code con intervento umano (human-in-the-loop, HITL) o a sistemi di ticketing per agenti con contesto completo.
* **Percorso di Audit e Provenienza**: La telemetria completa — inclusi gli hash dei prompt, gli ID dei documenti recuperati, i punteggi dei singoli componenti di confidenza e le decisioni finali di routing — deve essere registrata per l'auditabilità normativa.
15Progettare una strategia di routing dei modelli che scelga tra modelli piccoli, medi e grandi in base alla complessità della richiesta, al costo, al rischio e ai requisiti di qualità.
Un'architettura di routing dei modelli di produzione indirizza le richieste in ingresso attraverso livelli di modelli piccoli (ad esempio, SLM (Small Language Models) da 1B a 8B), medi (ad esempio, modelli da 14B a 70B) e grandi (ad esempio, modelli all'avanguardia o MoE (Mixture of Experts) di grandi dimensioni), bilanciando complessità, latenza, rischio e costi computazionali. Il flusso di lavoro di routing generalmente combina regole statiche, routing predittivo e cascate di fallback dinamiche:
1. **Gate di Policy Deterministici/Statici**: Filtrare le richieste per livello di cliente, SLA (Service Level Agreement) di latenza stringenti, rischio normativo/di dominio (ad esempio, diagnosi medica o redazione legale instradate direttamente ai modelli di livello superiore) o attività semplici basate su regole (ad esempio, regex/formattazione di base a modelli piccoli).
2. **Routing della Complessità Predittiva**: Un classificatore veloce e leggero (come una ricerca di similarità di embedding, un cross-encoder o un piccolo router SLM) valuta la complessità della richiesta, la profondità di ragionamento e l'ambiguità del dominio per selezionare il livello più efficiente in termini di costi fin dall'inizio.
3. **Cascate di Esecuzione Dinamica ed Escalation**: Inviare il prompt prima a un modello più piccolo e valutare la confidenza dell'output (tramite logprobs/entropia dei token, validità dello schema strutturato o controlli guardrail). Se la confidenza è al di sotto della soglia o la validazione fallisce, il router scala a un modello medio o grande. I compromessi chiave del sistema includono l'overhead di latenza del router rispetto ai risparmi di calcolo, i budget di timeout del fallback sotto picchi di traffico e la valutazione continua (ad esempio, la valutazione shadow per tracciare la deriva della qualità dell'output tra i livelli).
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)