AI Engineering Academy · Lektion

Warum sich LLM-Anwendungen schwer debuggen lassen

Verstehen Sie, warum herkömmliches Logging für LLM-Anwendungen nicht ausreicht, welche Informationen Sie zur Diagnose von Fehlern in RAG- und Agent-Pipelines benötigen und wie das Tracing-Datenmodell aufgebaut ist.

Lektion 1 von 413 Schritte

Warum sich LLM-Anwendungen schwer debuggen lassen ist eine kostenlose AI Engineering Academy-Lektion auf CoddyKit. Dies ist Lektion 1 von 4. Du kannst die komplette Lektion unten kostenlos lesen – dann übst du sie direkt im Browser mit einem integrierten Code-Editor und einem KI-Tutor rund um die Uhr. Sie ist Teil des AI Engineering Academy-Lernpfads, und dein Fortschritt wird über Web und CoddyKit-App synchronisiert. Der AI Engineering Academy-Kurs umfasst insgesamt 4 Lektionen.

Die besonderen Herausforderungen beim Debuggen von LLMs

Herkömmliche Software schlägt deterministisch fehl: Bei derselben Eingabe erzeugt sie immer dieselbe Ausgabe, und ein Stacktrace verweist direkt auf die fehlerhafte Zeile. LLM-Anwendungen brechen mit diesen Annahmen. Derselbe Prompt kann bei verschiedenen Aufrufen unterschiedliche Ausgaben erzeugen, Fehler bleiben oft unbemerkt (eine falsche Antwort statt einer Exception), und die Ursache kann in einem Prompt verborgen sein, der fünf Schritte zuvor in einer Kette verwendet wurde. Standardtools für Logging und Debugging sind dafür schlicht nicht ausgelegt.

Nichtdeterminismus erschwert die Reproduktion

LLM-Ausgaben sind standardmäßig nichtdeterministisch. Selbst bei temperature=0 kann derselbe Prompt aufgrund von Batchverarbeitung und numerischer Präzision leicht unterschiedliche Ausgaben erzeugen. Dadurch treten Bugs intermittierend auf: Ein Prompt, der in 20 % der Fälle fehlschlägt, besteht Ihre Testsuite, wenn Sie ihn nur einmal ausführen. Um einen bestimmten Fehler zu reproduzieren, müssen Sie die exakte Eingabe, die Modellparameter und die Ausgabe zum Zeitpunkt des Fehlers protokollieren – nicht nur die Eingabe.

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 response

Stille Fehler: Falsch statt defekt

Die tückischsten Fehler bei LLMs sind stille Fehler: Der API-Aufruf ist erfolgreich (HTTP 200, keine Exception), aber die Antwort ist falsch, halluziniert, unvollständig oder themenfremd. Ihre Anwendung verarbeitet die falsche Antwort problemlos weiter und gibt sie an den Benutzer zurück, ohne darauf hinzuweisen, dass etwas schiefgelaufen ist. Herkömmliches Monitoring, das nur Exceptions und Fehlercodes überwacht, erkennt diese Fehler nie – dafür benötigen Sie ein semantisches Monitoring der Ausgabequalität.

# 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 sophisticated

Mehrstufige Ketten: Wo ist der Fehler entstanden?

In einer RAG-Pipeline oder Agent-Kette kann ein Fehler in der abschließenden Antwort auf einen Retrieval-Schritt zurückzuführen sein, der irrelevante Textabschnitte geliefert hat. Dieser kann wiederum durch eine Chunking-Strategie verursacht worden sein, die einen wichtigen Satz auf zwei Textabschnitte verteilt hat, was wiederum auf ein Embedding-Modell zurückgeht, das mit technischem Fachjargon nicht gut umgehen konnte. Ohne Tracing pro Schritt sehen Sie nur die falsche abschließende Antwort und können nicht feststellen, in welchem Schritt der Fehler eingeführt wurde.

# 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?

Tokenanzahl und Kostenüberraschungen

Ohne Instrumentierung bleiben Tokenanzahl und Kosten unsichtbar, bis die Monatsrechnung eintrifft. Ein System-Prompt, der aufgrund eines Versehens von 500 auf 5000 Tokens angewachsen ist, eine Retrieval-Funktion, die statt 5 nun 20 Textabschnitte zurückgibt, oder eine Schleife, die das LLM 100- statt 10-mal aufruft – all dies vervielfacht Ihre Kosten unbemerkt. Instrumentieren Sie jeden LLM-Aufruf, um Prompt-Tokens, Completion-Tokens und die geschätzten Kosten zu protokollieren, damit Anomalien in Echtzeit sichtbar werden.

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 response

Latenz: Welcher Schritt ist langsam?

Benutzer erleben die Latenz eines LLM als eine einzige Wartezeit, tatsächlich ist sie jedoch die Summe vieler einzelner Schritte: Abfrage der Vektordatenbank, Dokumentabruf, Zusammenstellung des Prompts, Netzwerkaufruf der API, Tokenerzeugung und Parsen der Antwort. Ohne Zeitmessung pro Schritt können Sie nicht feststellen, ob eine langsame Antwort durch einen langsamen Retriever oder einen langsamen LLM-Aufruf verursacht wird. Instrumentieren Sie jeden Schritt mit Latenzmessungen, um den tatsächlichen Engpass zu ermitteln.

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 answer

Welche Informationen Sie tatsächlich benötigen

Um einen Fehler in einer LLM-Anwendung zu diagnostizieren, müssen Sie Folgendes erfassen und speichern: den vollständigen Eingabe-Prompt (System-Prompt und alle Nachrichten), das verwendete Modell und seine Parameter (temperature, max_tokens), die vollständige Ausgabe, Tokenanzahlen und geschätzte Kosten, die Latenz pro Schritt, alle Tool-Aufrufe und deren Ergebnisse sowie eine Session- oder Request-ID, die alle Schritte einer Benutzeranfrage miteinander verknüpft. Dies ist der minimale Datensatz für ein funktionsfähiges Tracing.

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
        )

Trace-Korrelation mit Request-IDs

Eine einzelne Benutzeranfrage kann 10 LLM-Aufrufe über verschiedene Services hinweg auslösen. Ohne eine Korrelations-ID, die durch alle diese Aufrufe weitergereicht wird, können Sie sie nicht zu einem einzigen Trace gruppieren. Erzeugen Sie am Einstiegspunkt jeder Benutzeranfrage eine eindeutige Request-ID und übergeben Sie sie bei jedem nachgelagerten LLM-Aufruf, jeder Datenbankabfrage und jeder Log-Nachricht. So können Sie den vollständigen Ausführungspfad jeder bestimmten Benutzeranfrage rekonstruieren.

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)
    })

Der LLM-Observability-Stack

Der LLM-Observability-Stack besteht aus drei Ebenen: Logging erfasst strukturierte Datensätze zu jedem LLM-Aufruf (LangSmith, Langfuse, benutzerdefinierte Logs). Metriken verfolgen aggregierte Werte im Zeitverlauf: Anzahl der Anfragen, durchschnittliche Latenz, Fehlerrate und tägliche Kosten. Tracing zeichnet die kausale Kette der Schritte innerhalb einer einzelnen Anfrage auf. Zusammen bieten diese drei Säulen die nötige Transparenz, um Fehler zu diagnostizieren, Regressionen zu erkennen und die Leistung zu optimieren.

Qualitätsverschlechterung überwachen

Im Gegensatz zu herkömmlicher Software, bei der Fehler binär sind (funktioniert/defekt), verschlechtert sich die Qualität von LLMs schrittweise. Eine Änderung am Prompt kann die Antwortqualität von 85 % auf 70 % senken, ohne dass eine Exception ausgelöst wird. Überwachen Sie die Qualität, indem Sie täglich eine automatisierte Bewertung (LLM-as-judge) für eine Stichprobe der Antworten aus der Produktion durchführen. Lösen Sie einen Alarm aus, wenn der gleitende Durchschnitt der Qualitätsbewertung unter einen Schwellenwert fällt – bevor sich Benutzer beschweren.

Ihre Observability-Praxis beginnen

Warten Sie nicht auf einen Vorfall in der Produktion, bevor Sie Observability einführen. Beginnen Sie mit drei grundlegenden Schritten: (1) Protokollieren Sie jeden LLM-Aufruf mit vollständiger Eingabe, Ausgabe und Tokenanzahl in einer Datenbank oder Datei, (2) weisen Sie jeder Benutzerinteraktion eine Request-ID zu und nehmen Sie sie in alle Log-Einträge auf und (3) ergänzen Sie Ihre Pipeline um eine Zeitmessung pro Schritt. Allein diese drei Maßnahmen machen 80 % der Debugging-Aufgaben in Minuten statt in Stunden lösbar.

Kurzer Test

Testen Sie anhand dieser Lektion Ihr Verständnis dafür, warum LLM-Anwendungen schwer zu debuggen sind.

Zusammenfassung der Lektion

In dieser Lektion haben Sie gelernt: Nichtdeterminismus macht LLM-Bugs intermittierend und schwer reproduzierbar, wenn nicht der vollständige Kontext der Anfrage erfasst wird; stille Fehler (falsche Antworten trotz erfolgreicher API-Aufrufe) umgehen die herkömmliche Fehlerüberwachung und erfordern semantische Qualitätsprüfungen; und Tracing pro Schritt mit Request-ID-Korrelation ist die Mindestvoraussetzung, um Fehler in mehrstufigen RAG- und Agent-Pipelines zu diagnostizieren. Als Nächstes implementieren wir Tracing mit LangSmith.

Kostenlos starten

Lerne Python mit einem KI-Tutor — kostenlos

Schreibe und führe echten Code in deinem Browser aus, bekomme sofortige Hilfe von einem 24/7 KI-Tutor und setze dein Lernen im Web oder in der App fort.

Kurse
30
Lektionen
120

Häufig gestellte Fragen

Ist die Lektion „Warum sich LLM-Anwendungen schwer debuggen lassen“ kostenlos?

Ja — der vollständige Text von „Warum sich LLM-Anwendungen schwer debuggen lassen“ ist hier im Web kostenlos zu lesen. Um sie interaktiv zu üben (integrierter Code-Editor und 24/7 KI-Tutor) und den Rest des AI Engineering Academy-Kurses freizuschalten, upgrade auf CoddyKit PRO. Der AI Engineering Academy-Kurs umfasst insgesamt 4 Lektionen.

Was lerne ich in „Warum sich LLM-Anwendungen schwer debuggen lassen“?

Verstehen Sie, warum herkömmliches Logging für LLM-Anwendungen nicht ausreicht, welche Informationen Sie zur Diagnose von Fehlern in RAG- und Agent-Pipelines benötigen und wie das Tracing-Datenmodell… Du übst AI Engineering Academy mit praktischem Code, den du direkt im Browser ausführst, und ein 24/7 KI-Tutor beantwortet deine Fragen während du die Lektion bearbeitest.

Brauche ich Erfahrung, um AI Engineering Academy zu starten?

Keine Vorkenntnisse erforderlich. AI Engineering Academy auf CoddyKit ist für Anfänger bis fortgeschrittene Lernende strukturiert, sodass du hier starten oder von Anfang an beginnen und in deinem eigenen Tempo voranschreiten kannst. Dies ist Lektion 1 von 4.

Wie lange dauert die Lektion „Warum sich LLM-Anwendungen schwer debuggen lassen“?

Die meisten CoddyKit-Lektionen dauern etwa 5–10 Minuten. Jede ist kompakt und interaktiv, sodass du stetig Fortschritte machst und genau dort weitermachst, wo du aufgehört hast – im Web und in der App.

Kann ich in dieser AI Engineering Academy-Lektion Code schreiben und ausführen?

Ja. Jede AI Engineering Academy-Lektion enthält einen integrierten Code-Editor, sodass du echten Code direkt in deinem Browser schreibst und ausführst und sofort KI-Feedback erhältst — ohne lokale Einrichtung erforderlich.

Alle Lektionen in diesem Kurs

  1. Warum sich LLM-Anwendungen schwer debuggen lassen
  2. Tracing mit LangSmith
  3. Langfuse für modellunabhängige Observability
  4. Alarme bei Latenz, Kosten und Qualitätsverlust
← Zurück zu AI Engineering Academy