Hvorfor LLM-applikasjoner er vanskelige å feilsøke
Forstå hvorfor tradisjonell logging ikke er tilstrekkelig for LLM-applikasjoner, hvilken informasjon du trenger for å diagnostisere feil i RAG- og agentpipelines, og datamodellen for sporing.
Hvorfor LLM-applikasjoner er vanskelige å feilsøke er en gratis leksjon i AI Engineering Academy på CoddyKit. Dette er leksjon 1 av 4. Du kan lese hele leksjonen gratis nedenfor – og deretter øve praktisk i nettleseren med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt. Den er en del av læringsløpet i AI Engineering Academy, og fremdriften din synkroniseres mellom nettet og CoddyKit-appen. Kurset i AI Engineering Academy inneholder totalt 4 leksjoner.
De unike utfordringene ved feilsøking av LLM-er
Tradisjonell programvare feiler deterministisk: Med samme inndata produserer den alltid samme utdata, og en stack trace peker direkte på linjen som feilet. LLM-applikasjoner bryter med disse forutsetningene. Den samme prompten kan gi ulike resultater ved forskjellige kall, feil er ofte stille (et feil svar i stedet for et unntak), og årsaken kan ligge skjult i en prompt som ble brukt fem trinn tidligere i en kjede. Standardverktøy for logging og feilsøking er ganske enkelt ikke laget for dette.
Ikke-determinisme gjør reproduksjon vanskelig
LLM-utdata er som standard ikke-deterministiske. Selv med temperature=0 kan den samme prompten gi litt forskjellige resultater på grunn av batchbehandling og numerisk presisjon. Det betyr at feil er intermitterende: En prompt som feiler 20 % av gangene, vil bestå testpakken hvis De bare kjører den én gang. For å gjenskape en bestemt feil må De logge nøyaktig inndata, modellparametere og utdata da feilen oppstod – ikke bare inndataene.
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 responseStille feil: Feil, ikke ødelagt
De mest lumske LLM-feilene er stille feil: API-kallet lykkes (HTTP 200, uten unntak), men svaret er feil, hallusinert, ufullstendig eller irrelevant. Applikasjonen behandler uten videre det feilaktige svaret og returnerer det til brukeren uten å indikere at noe gikk galt. Tradisjonell overvåking som bare ser etter unntak og feilkoder, vil aldri oppdage dette – De trenger semantisk overvåking av kvaliteten på utdataene.
# 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 sophisticatedKjeder med flere trinn: Hvor gikk det galt?
I en RAG-pipeline eller agentkjede kan en feil i det endelige svaret skyldes et hentetrinn som returnerte irrelevante tekstbiter. Dette kan igjen skyldes en oppdelingsstrategi som delte en viktig setning mellom to tekstbiter, og dette kan igjen skyldes en embedding-modell som ikke håndterte teknisk sjargong godt. Uten sporing per trinn ser De bare det feilaktige endelige svaret, uten mulighet til å finne ut hvilket trinn som introduserte feilen.
# 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?Overraskelser knyttet til tokenantall og kostnader
Uten instrumentering er tokenantall og kostnader usynlige helt til månedsregningen kommer. En systemprompt som har vokst fra 500 til 5000 token på grunn av en forglemmelse, en hente funksjon som returnerer 20 tekstbiter i stedet for 5, eller en løkke som kaller LLM-en 100 ganger i stedet for 10 – alt dette kan i stillhet mangedoble kostnadene. Instrumenter hvert LLM-kall slik at prompttoken, fullføringstoken og beregnet kostnad logges og avvik blir synlige i sanntid.
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 responseForsinkelse: Hvilket trinn er tregt?
Brukere opplever LLM-forsinkelse som én samlet ventetid, men den består egentlig av mange individuelle trinn: spørring mot vektordatabasen, henting av dokumenter, sammensetting av prompten, nettverkskall til API-et, generering av token og tolking av svaret. Uten tidsmåling per trinn kan De ikke vite om et tregt svar skyldes en treg hentekomponent eller et tregt LLM-kall. Instrumenter hvert trinn med målinger av forsinkelsen for å finne den faktiske flaskehalsen.
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 answerHvilken informasjon De faktisk trenger
For å diagnostisere en feil i en LLM-applikasjon må De samle inn og lagre: den fullstendige inputprompten (systemprompten og alle meldinger), modellen og parameterne som ble brukt (temperature, max_tokens), den fullstendige utdataen, tokenantall og beregnet kostnad, forsinkelse per trinn, eventuelle verktøykall og resultatene deres, samt en økt- eller forespørsels-ID som knytter alle trinnene i én brukerforespørsel sammen. Dette er det minste datasettet som trengs for sporing.
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
)Sporingskorrelasjon med forespørsels-ID-er
En enkelt brukerforespørsel kan utløse 10 LLM-kall på tvers av ulike tjenester. Uten en korrelasjons-ID som følger alle kallene, kan De ikke gruppere dem i én sporing. Sett inn en unik forespørsels-ID ved inngangen til hver brukerforespørsel, og send den med i alle etterfølgende LLM-kall, databaseforespørsler og loggmeldinger. Da kan De rekonstruere hele kjøringsforløpet for en bestemt brukerforespørsel.
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)
})Observability-stakken for LLM-er
Observability-stakken for LLM-er har tre lag: Logging samler strukturerte poster for hvert LLM-kall (LangSmith, Langfuse, egendefinerte logger). Målinger følger aggregerte tall over tid: antall forespørsler, gjennomsnittlig forsinkelse, feilrate og daglige kostnader. Sporing registrerer den årsaksmessige kjeden av trinn i én enkelt forespørsel. Til sammen gir disse tre søylene Dem innsynet De trenger for å diagnostisere feil, oppdage regresjoner og optimalisere ytelsen.
Varsling ved kvalitetsforringelse
I motsetning til tradisjonell programvare, der feil er binære (fungerer/fungerer ikke), forringes LLM-kvaliteten gradvis. En endring i prompten kan redusere svarkvaliteten fra 85 % til 70 % uten at det oppstår noen unntak. Overvåk kvaliteten ved å kjøre automatisert evaluering (LLM-as-judge-vurdering) på et utvalg produksjonssvar hver dag. Send et varsel når det rullerende gjennomsnittet for kvalitetsscoren faller under en terskel, før brukerne begynner å klage.
Slik kommer De i gang med observability
Ikke vent til det oppstår en produksjonshendelse før De legger til observability. Begynn med tre grunnleggende trinn: (1) logg hvert LLM-kall med fullstendige inndata, utdata og tokenantall i en database eller fil, (2) tildel en forespørsels-ID til hver brukerinteraksjon, og ta den med i alle loggoppføringer, og (3) legg til tidsmåling per trinn i pipelinen. Disse tre tiltakene alene vil gjøre 80 % av feilsøkingsoppgavene løselige på minutter i stedet for timer.
Kort kontroll
Test forståelsen Deres av hvorfor LLM-applikasjoner er vanskelige å feilsøke, basert på denne leksjonen.
Oppsummering av leksjonen
I denne leksjonen lærte De at ikke-determinisme gjør LLM-feil intermitterende og vanskelige å gjenskape uten at hele konteksten for forespørselen samles inn, at stille feil (feil svar fra vellykkede API-kall) omgår tradisjonell feilovervåking og krever semantiske kvalitetssjekker, og at sporing per trinn med korrelasjon via forespørsels-ID er det minste som kreves for å diagnostisere feil i RAG- og agentpipelines med flere trinn. Deretter implementerer vi sporing med LangSmith.
Lær deg Python med en AI-veileder – gratis
Skriv og kjør ekte kode i nettleseren, få umiddelbar hjelp fra en AI-veileder som er tilgjengelig døgnet rundt, og fortsett der du slapp – på nettet eller i appen.
- Kurs
- 30
- Leksjoner
- 120
Ofte stilte spørsmål
Er leksjonen «Hvorfor LLM-applikasjoner er vanskelige å feilsøke» gratis?
Ja – hele teksten i «Hvorfor LLM-applikasjoner er vanskelige å feilsøke» er gratis å lese her på nettet. For å øve interaktivt med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt, og for å låse opp resten av AI Engineering Academy-kurset, kan du oppgradere til CoddyKit PRO. Kurset i AI Engineering Academy inneholder totalt 4 leksjoner.
Hva lærer jeg i «Hvorfor LLM-applikasjoner er vanskelige å feilsøke»?
Forstå hvorfor tradisjonell logging ikke er tilstrekkelig for LLM-applikasjoner, hvilken informasjon du trenger for å diagnostisere feil i RAG- og agentpipelines, og datamodellen for sporing. Du øver på AI Engineering Academy med praktisk kode som du kjører direkte i nettleseren, mens en AI-veileder som er tilgjengelig døgnet rundt, svarer på spørsmålene dine mens du jobber deg gjennom leksjonen.
Trenger jeg erfaring for å begynne med AI Engineering Academy?
Ingen tidligere erfaring er nødvendig. AI Engineering Academy på CoddyKit er lagt opp for både nybegynnere og viderekomne, så De kan begynne her eller helt fra start og lære i Deres eget tempo. Dette er leksjon 1 av 4.
Hvor lang tid tar leksjonen «Hvorfor LLM-applikasjoner er vanskelige å feilsøke»?
De fleste CoddyKit-leksjoner tar omtrent 5–10 minutter. Hver leksjon er kort og interaktiv, slik at De gjør jevne fremskritt og kan fortsette akkurat der De slapp – både på nettet og i appen.
Kan jeg skrive og kjøre kode i denne AI Engineering Academy-leksjonen?
Ja. Alle AI Engineering Academy-leksjoner har en innebygd kodeeditor, slik at De kan skrive og kjøre ekte kode direkte i nettleseren og få umiddelbar tilbakemelding fra AI – uten lokal konfigurering.
Alle leksjonene i dette kurset
- Hvorfor LLM-applikasjoner er vanskelige å feilsøke
- Sporing med LangSmith
- Langfuse for modellagnostisk observerbarhet
- Varsling ved forverring av ventetid, kostnad og kvalitet