Perché le app LLM sono difficili da sottoporre a debug
Comprenda perché il logging tradizionale non è sufficiente per le applicazioni LLM, quali informazioni servono per diagnosticare i problemi nelle pipeline RAG e negli agenti e qual è il modello dei dati di tracing.
Perché le app LLM sono difficili da sottoporre a debug è una lezione AI Engineering Academy gratuita su CoddyKit. Questa è la lezione 1 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento AI Engineering Academy, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso AI Engineering Academy include 4 lezioni in totale.
Le sfide uniche del debugging degli LLM
Il software tradizionale fallisce in modo deterministico: a parità di input produce sempre lo stesso output e uno stack trace indica direttamente la riga che ha causato il problema. Le applicazioni basate su LLM mettono in discussione queste assunzioni. Lo stesso prompt può produrre output diversi in chiamate differenti, i malfunzionamenti sono spesso silenziosi (una risposta errata invece di un'eccezione) e la causa può essere nascosta in un prompt utilizzato cinque passaggi prima all'interno di una catena. Gli strumenti standard di logging e debugging semplicemente non sono progettati per questo.
Il non determinismo rende difficile la riproduzione
Per impostazione predefinita, gli output degli LLM sono non deterministici. Anche con temperature=0, lo stesso prompt può produrre output leggermente diversi a causa dell'elaborazione in batch e della precisione numerica. Ciò significa che i bug sono intermittenti: un prompt che fallisce il 20% delle volte supererà la Sua suite di test se lo esegue una sola volta. Per riprodurre uno specifico malfunzionamento è necessario registrare l'input esatto, i parametri del modello e l'output al momento del malfunzionamento, non soltanto l'input.
import json
import time
def logged_llm_call(client, messages, model, temperature, **kwargs):
request_id = f'{int(time.time() * 1000)}-{id(messages)}'
response = client.chat.completions.create(
model=model,
messages=messages,
temperature=temperature,
**kwargs
)
# Log EVERYTHING needed to reproduce this exact call
log_entry = {
'request_id': request_id,
'model': model,
'temperature': temperature,
'messages': messages,
'response': response.choices[0].message.content,
'finish_reason': response.choices[0].finish_reason,
'usage': response.usage.model_dump(),
'timestamp': time.time()
}
write_to_trace_store(log_entry)
return responseMalfunzionamenti silenziosi: errato, non bloccato
I malfunzionamenti più insidiosi degli LLM sono i malfunzionamenti silenziosi: la chiamata API va a buon fine (HTTP 200, nessuna eccezione), ma la risposta è errata, frutto di allucinazione, incompleta o fuori tema. L'applicazione elabora tranquillamente la risposta errata e la restituisce all'utente senza alcuna indicazione che qualcosa sia andato storto. Il monitoraggio tradizionale, che controlla soltanto eccezioni e codici di errore, non li rileverà mai: è necessario un monitoraggio semantico della qualità dell'output.
# This succeeds with HTTP 200 but returns wrong information
response = client.chat.completions.create(
model='gpt-4o',
messages=[{'role': 'user', 'content': 'What is the boiling point of water at sea level?'}]
)
output = response.choices[0].message.content
# response.status_code: None (not relevant - always 200 if we got here)
# No exception thrown
# But if output is '90 degrees Celsius', it is WRONG and your app will serve bad data
# You need semantic validation:
def validate_boiling_point_answer(text: str) -> bool:
return '100' in text # Rough check - real validation is more sophisticatedCatene a più passaggi: dove si è verificato l'errore?
In una pipeline RAG o in una catena di agenti, un errore nella risposta finale può risalire a un passaggio di retrieval che ha restituito chunk irrilevanti; questo, a sua volta, può dipendere da una strategia di chunking che ha diviso una frase chiave tra due chunk, riconducibile a sua volta a un modello di embedding che non gestiva bene il gergo tecnico. Senza un tracing per passaggio, vede soltanto la risposta finale errata e non ha modo di individuare quale passaggio abbia introdotto l'errore.
# Without tracing: you see only the final wrong answer
def rag_pipeline_naive(query):
chunks = retrieve(query) # step 1 - might return bad chunks
context = format_context(chunks) # step 2 - might truncate key info
answer = generate(query, context) # step 3 - LLM gets bad context
return answer # WRONG - but why?
# With tracing: you can see each step's input and output
def rag_pipeline_traced(query):
with trace_span('retrieve') as span:
chunks = retrieve(query)
span.set_attribute('num_chunks', len(chunks))
span.set_attribute('top_chunk_score', chunks[0]['score'] if chunks else 0)
with trace_span('format_context') as span:
context = format_context(chunks)
span.set_attribute('context_length', len(context))
with trace_span('generate') as span:
answer = generate(query, context)
span.set_attribute('answer_length', len(answer))
return answer # Now you can diagnose: was retrieve the problem?Sorp理se nel conteggio dei token e nei costi
Senza strumentazione, il conteggio dei token e i costi restano invisibili fino all'arrivo della fattura mensile. Un system prompt passato da 500 a 5000 token a causa di una svista, una funzione di retrieval che restituisce 20 chunk invece di 5 o un ciclo che chiama l'LLM 100 volte invece di 10: tutti questi fattori moltiplicano silenziosamente i costi. Strumenti ogni chiamata LLM per registrare i token del prompt, i token del completamento e il costo stimato, così le anomalie saranno visibili in tempo reale.
COST_PER_1K = {'gpt-4o': {'input': 0.005, 'output': 0.015},
'gpt-4o-mini': {'input': 0.000150, 'output': 0.000600}}
def compute_cost(model: str, usage) -> float:
pricing = COST_PER_1K.get(model, {'input': 0.005, 'output': 0.015})
input_cost = (usage.prompt_tokens / 1000) * pricing['input']
output_cost = (usage.completion_tokens / 1000) * pricing['output']
return input_cost + output_cost
def instrumented_call(client, model, messages):
response = client.chat.completions.create(model=model, messages=messages)
cost = compute_cost(model, response.usage)
# Alert if single call is unexpectedly expensive
if cost > 0.10: # more than 10 cents for one call
print(f'WARNING: Expensive LLM call: ${cost:.4f} ({response.usage.prompt_tokens} prompt tokens)')
metrics.record('llm_cost_usd', cost, tags={'model': model})
metrics.record('llm_prompt_tokens', response.usage.prompt_tokens)
return responseLatenza: quale passaggio è lento?
Gli utenti percepiscono la latenza di un LLM come un unico tempo di attesa, ma in realtà essa è la somma di molti passaggi individuali: query al database vettoriale, recupero dei documenti, assemblaggio del prompt, chiamata di rete all'API, generazione dei token e analisi della risposta. Senza misurare i tempi di ogni passaggio, non può stabilire se una risposta lenta sia dovuta a un retriever lento o a una chiamata LLM lenta. Strumenti ogni passaggio con misurazioni della latenza per identificare il vero collo di bottiglia.
import time
from contextlib import contextmanager
@contextmanager
def timed(name: str, metrics_client):
start = time.monotonic()
try:
yield
finally:
elapsed_ms = (time.monotonic() - start) * 1000
metrics_client.histogram(f'step_latency_ms', elapsed_ms, tags={'step': name})
if elapsed_ms > 2000: # flag steps taking more than 2 seconds
print(f'SLOW STEP [{name}]: {elapsed_ms:.0f}ms')
# Usage
def rag_with_timing(query, metrics):
with timed('embed_query', metrics):
query_embedding = embed(query)
with timed('vector_search', metrics):
chunks = vector_db.search(query_embedding, top_k=5)
with timed('llm_generate', metrics):
answer = generate(query, chunks)
return answerQuali informazioni Le servono davvero
Per diagnosticare un malfunzionamento di un'applicazione LLM, deve acquisire e memorizzare: il prompt di input completo (system e tutti i messaggi), il modello e i parametri utilizzati (temperature, max_tokens), l'output completo, il conteggio dei token e il costo stimato, la latenza per passaggio, eventuali chiamate agli strumenti e i relativi risultati e un ID di sessione o di richiesta che colleghi tutti i passaggi di una singola richiesta dell'utente. Questo è il dataset minimo di tracing utilizzabile.
from dataclasses import dataclass, field
from typing import Optional
import time
@dataclass
class LLMTrace:
request_id: str
session_id: str
step_name: str
model: str
temperature: float
system_prompt: str
user_messages: list[dict]
response: str
finish_reason: str
prompt_tokens: int
completion_tokens: int
cost_usd: float
latency_ms: float
tool_calls: list[dict] = field(default_factory=list)
error: Optional[str] = None
timestamp: float = field(default_factory=time.time)
def is_anomalous(self) -> bool:
return (
self.cost_usd > 0.10 or
self.latency_ms > 10000 or
self.finish_reason == 'length' or # was cut off
self.error is not None
)Correlazione delle tracce con gli ID di richiesta
Una singola richiesta dell'utente può attivare 10 chiamate LLM attraverso servizi diversi. Senza un ID di correlazione che passi attraverso tutte queste chiamate, non può raggrupparle in un'unica traccia. Inserisca un ID di richiesta univoco all'ingresso di ogni richiesta dell'utente e lo trasmetta in ogni chiamata LLM downstream, query al database e messaggio di log. In questo modo è possibile ricostruire il percorso completo di esecuzione di qualsiasi richiesta specifica dell'utente.
import uuid
from contextvars import ContextVar
# Thread-safe request ID propagation using context variables
request_id_var: ContextVar[str] = ContextVar('request_id', default='unknown')
def handle_user_request(query: str):
# Set request ID at the entry point
req_id = str(uuid.uuid4())[:8]
request_id_var.set(req_id)
return rag_pipeline(query)
def get_current_request_id() -> str:
return request_id_var.get()
# Every LLM call logs with the same request_id
def log_llm_call(model, prompt, response):
logger.info('LLM call', extra={
'request_id': get_current_request_id(), # automatically correlates all calls
'model': model,
'prompt_length': len(prompt),
'response_length': len(response)
})Lo stack di osservabilità degli LLM
Lo stack di osservabilità degli LLM è costituito da tre livelli: il logging acquisisce record strutturati di ogni chiamata LLM (LangSmith, Langfuse, log personalizzati); le metriche monitorano valori aggregati nel tempo: numero di richieste, latenza media, tasso di errore e costo giornaliero; il tracing registra la catena causale dei passaggi all'interno di una singola richiesta. Insieme, questi tre pilastri offrono la visibilità necessaria per diagnosticare i malfunzionamenti, individuare le regressioni e ottimizzare le prestazioni.
Avvisi sul peggioramento della qualità
A differenza del software tradizionale, in cui gli errori sono binari (funzionante/non funzionante), la qualità degli LLM peggiora gradualmente. Una modifica al prompt può ridurre la qualità delle risposte dall'85% al 70% senza che venga generata alcuna eccezione. Monitori la qualità eseguendo ogni giorno una valutazione automatizzata (punteggio LLM-as-judge) su un campione delle risposte in produzione. Generi un avviso quando il punteggio medio progressivo scende al di sotto di una soglia, prima che gli utenti inizino a lamentarsi.
Come iniziare a occuparsi dell'osservabilità
Non aspetti che si verifichi un incidente in produzione prima di aggiungere l'osservabilità. Inizi con tre passaggi essenziali: (1) registri ogni chiamata LLM con input completo, output e conteggio dei token in un database o file, (2) assegni un ID di richiesta a ogni interazione dell'utente e lo includa in tutte le voci di log e (3) aggiunga la misurazione dei tempi per ogni passaggio della pipeline. Questi tre elementi, da soli, renderanno risolvibile l'80% delle attività di debugging in pochi minuti anziché in ore.
Verifica rapida
Verifichi la Sua comprensione dei motivi per cui le app basate su LLM sono difficili da sottoporre a debugging, come illustrato in questa lezione.
Riepilogo della lezione
In questa lezione ha imparato che il non determinismo rende i bug degli LLM intermittenti e difficili da riprodurre senza acquisire il contesto completo della richiesta; i malfunzionamenti silenziosi (risposte errate provenienti da chiamate API riuscite) eludono il monitoraggio tradizionale degli errori e richiedono controlli semantici della qualità; infine, il tracing per passaggio con correlazione tramite ID di richiesta è il minimo necessario per diagnosticare i malfunzionamenti nelle pipeline RAG e degli agenti a più passaggi. Nella prossima lezione implementeremo il tracing con LangSmith.
Impara Python con un tutor IA — gratis
Scrivi ed esegui vero codice nel tuo browser, ricevi aiuto istantaneo da un tutor IA disponibile 24/7, e riprendi da dove hai lasciato sul web o nell'app.
- Corsi
- 30
- Lezioni
- 120
Domande Frequenti
La lezione «Perché le app LLM sono difficili da sottoporre a debug» è gratuita?
Sì — il testo completo di «Perché le app LLM sono difficili da sottoporre a debug» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso AI Engineering Academy, passa a CoddyKit PRO. Il corso AI Engineering Academy include 4 lezioni in totale.
Cosa imparerò in «Perché le app LLM sono difficili da sottoporre a debug»?
Comprenda perché il logging tradizionale non è sufficiente per le applicazioni LLM, quali informazioni servono per diagnosticare i problemi nelle pipeline RAG e negli agenti e qual è il modello dei d… Eserciti AI Engineering Academy con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare AI Engineering Academy?
Non è richiesta alcuna esperienza precedente. AI Engineering Academy su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 1 di 4.
Quanto tempo richiede la lezione «Perché le app LLM sono difficili da sottoporre a debug»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione AI Engineering Academy?
Sì. Ogni lezione AI Engineering Academy include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- Perché le app LLM sono difficili da sottoporre a debug
- Tracing con LangSmith
- Langfuse per l'observability indipendente dal modello
- Avvisi su latenza, costi e peggioramento della qualità