0Pricing
AI Engineering Academy · Lekcja

Dlaczego aplikacje LLM trudno debugować

Poznaj powody, dla których tradycyjne logowanie nie wystarcza w aplikacjach LLM, dowiedz się, jakie informacje są potrzebne do diagnozowania awarii w potokach RAG i agentów, oraz poznaj model danych śledzenia.

Dlaczego aplikacje LLM trudno debugować to bezpłatna lekcja AI Engineering Academy na CoddyKit. To lekcja 1 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej AI Engineering Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs AI Engineering Academy zawiera 4 lekcji w sumie.

Wyjątkowe wyzwania związane z debugowaniem LLM

Tradycyjne oprogramowanie zawodzi w sposób deterministyczny: dla tych samych danych wejściowych zawsze generuje ten sam wynik, a stack trace wskazuje bezpośrednio wiersz, w którym wystąpił błąd. Aplikacje LLM podważają te założenia. Ten sam prompt może dawać różne wyniki przy kolejnych wywołaniach, błędy często są ciche (zamiast wyjątku pojawia się nieprawidłowa odpowiedź), a ich przyczyna może być ukryta w promptcie użytym pięć kroków wcześniej w łańcuchu. Standardowe narzędzia do logowania i debugowania nie są po prostu do tego przystosowane.

Niedeterminizm utrudnia odtwarzanie błędów

Wyniki LLM są domyślnie niedeterministyczne. Nawet przy temperature=0 ten sam prompt może dać nieco inne wyniki z powodu przetwarzania wsadowego i precyzji obliczeń numerycznych. Oznacza to, że błędy są sporadyczne: prompt, który kończy się błędem w 20% przypadków, przejdzie zestaw testów, jeśli zostanie uruchomiony tylko raz. Odtworzenie konkretnego błędu wymaga rejestrowania dokładnych danych wejściowych, parametrów modelu i wyniku w chwili wystąpienia błędu — nie tylko danych wejściowych.

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

Ciche błędy: niepoprawnie, a nie niesprawnie

Najbardziej podstępne błędy LLM to ciche błędy: wywołanie API kończy się powodzeniem (HTTP 200, bez wyjątku), ale odpowiedź jest niepoprawna, zmyślona, niepełna lub nie na temat. Aplikacja bez problemu przetwarza nieprawidłową odpowiedź i zwraca ją użytkownikowi, nie sygnalizując żadnego problemu. Tradycyjne monitorowanie, które wykrywa tylko wyjątki i kody błędów, nigdy ich nie znajdzie — potrzebne jest semantyczne monitorowanie jakości wyników.

# 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

Łańcuchy wieloetapowe: gdzie wystąpił błąd?

W potoku RAG lub łańcuchu agenta błąd w końcowej odpowiedzi może mieć źródło w etapie pobierania, który zwrócił nieistotne fragmenty. Ten problem może z kolei wynikać ze strategii dzielenia na fragmenty, która rozdzieliła ważne zdanie między dwa fragmenty, a ta — z modelu embeddingów, który nie radził sobie dobrze ze specjalistycznym żargonem. Bez śledzenia poszczególnych etapów widzisz tylko niepoprawną końcową odpowiedź i nie masz sposobu, aby wskazać etap, na którym pojawił się błąd.

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

Niespodzianki związane z liczbą tokenów i kosztami

Bez instrumentacji liczba tokenów i koszty pozostają niewidoczne aż do otrzymania miesięcznej faktury. Prompt systemowy, który wskutek przeoczenia urósł z 500 do 5000 tokenów, funkcja pobierania zwracająca 20 fragmentów zamiast 5 albo pętla wywołująca LLM 100 razy zamiast 10 — wszystkie te sytuacje po cichu zwielokrotniają koszty. Instrumentuj każde wywołanie LLM, aby rejestrować tokeny promptu, tokeny uzupełnienia i szacowany koszt; dzięki temu anomalie będą widoczne w czasie rzeczywistym.

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

Opóźnienie: który etap działa wolno?

Użytkownicy odbierają opóźnienie LLM jako pojedynczy czas oczekiwania, ale w rzeczywistości jest ono sumą wielu etapów: zapytania do wektorowej bazy danych, pobierania dokumentów, składania promptu, wywołania API przez sieć, generowania tokenów i analizowania odpowiedzi. Bez pomiaru czasu dla poszczególnych etapów nie można stwierdzić, czy wolna odpowiedź wynika z powolnego mechanizmu pobierania, czy z powolnego wywołania LLM. Instrumentuj każdy etap, mierząc jego opóźnienie, aby zidentyfikować rzeczywiste wąskie gardło.

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

Jakich informacji naprawdę potrzebujesz

Aby zdiagnozować dowolny błąd aplikacji LLM, trzeba przechwytywać i przechowywać: pełny prompt wejściowy (systemowy oraz wszystkie wiadomości), użyty model i parametry (temperature, max_tokens), pełny wynik, liczbę tokenów i szacowany koszt, opóźnienie każdego etapu, wszelkie wywołania narzędzi i ich wyniki oraz identyfikator sesji lub żądania, który łączy wszystkie etapy jednego żądania użytkownika. To minimalny zestaw danych niezbędny do śledzenia działania.

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
        )

Korelacja śladów za pomocą identyfikatorów żądań

Pojedyncze żądanie użytkownika może wywołać 10 wywołań LLM w różnych usługach. Bez identyfikatora korelacyjnego, który przepływa przez wszystkie te usługi, nie można połączyć wywołań w jeden ślad. W punkcie wejścia każdego żądania użytkownika należy nadać mu unikalny identyfikator żądania i przekazywać go w każdym kolejnym wywołaniu LLM, zapytaniu do bazy danych oraz komunikacie dziennika. Umożliwia to odtworzenie pełnej ścieżki wykonania dowolnego konkretnego żądania użytkownika.

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

Stos obserwowalności LLM

Stos obserwowalności LLM ma trzy warstwy: rejestrowanie przechwytuje ustrukturyzowane informacje o każdym wywołaniu LLM (LangSmith, Langfuse, własne dzienniki), metryki śledzą zagregowane wartości w czasie: liczbę żądań, średnie opóźnienie, współczynnik błędów i dzienny koszt, a śledzenie rejestruje przyczynowy łańcuch etapów w ramach jednego żądania. Wspólnie te trzy filary zapewniają widoczność potrzebną do diagnozowania błędów, wykrywania regresji i optymalizowania wydajności.

Alarmy o pogorszeniu jakości

W przeciwieństwie do tradycyjnego oprogramowania, w którym błędy są binarne (działa/nie działa), jakość LLM pogarsza się stopniowo. Zmiana promptu może obniżyć jakość odpowiedzi z 85% do 70%, mimo że nie zostanie zgłoszony żaden wyjątek. Monitoruj jakość, codziennie uruchamiając automatyczną ewaluację (ocenianie przez LLM jako sędziego) na próbce odpowiedzi produkcyjnych. Ustaw alarm, gdy krocząca średnia ocena jakości spadnie poniżej określonego progu — zanim użytkownicy zaczną się skarżyć.

Rozpoczęcie praktyki obserwowalności

Nie czekaj z dodaniem obserwowalności do wystąpienia incydentu na produkcji. Zacznij od trzech podstawowych kroków: (1) rejestruj każde wywołanie LLM wraz z pełnymi danymi wejściowymi, wynikiem i liczbą tokenów w bazie danych lub pliku, (2) przypisuj identyfikator żądania każdej interakcji użytkownika i umieszczaj go we wszystkich wpisach dziennika oraz (3) mierz czas każdego etapu potoku. Same te trzy działania sprawią, że rozwiązanie 80% zadań debugowania zajmie minuty zamiast godzin.

Szybki sprawdzian

Sprawdź swoje rozumienie przyczyn, dla których aplikacje LLM są trudne do debugowania, omówionych w tej lekcji.

Podsumowanie lekcji

W tej lekcji nauczyłeś się, że niedeterminizm sprawia, iż błędy LLM występują sporadycznie i trudno je odtworzyć bez przechwycenia pełnego kontekstu żądania; ciche błędy (niepoprawne odpowiedzi z udanych wywołań API) omijają tradycyjne monitorowanie błędów i wymagają semantycznej kontroli jakości; a śledzenie poszczególnych etapów z korelacją za pomocą identyfikatora żądania to minimum niezbędne do diagnozowania błędów w wieloetapowych potokach RAG i agentów. Następnie zaimplementujemy śledzenie za pomocą LangSmith.

Często zadawane pytania

Czy lekcja „Dlaczego aplikacje LLM trudno debugować” jest bezpłatna?

Tak — pełny tekst „Dlaczego aplikacje LLM trudno debugować” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu AI Engineering Academy, przejdź na CoddyKit PRO. Kurs AI Engineering Academy zawiera 4 lekcji w sumie.

Co nauczysz się w „Dlaczego aplikacje LLM trudno debugować”?

Poznaj powody, dla których tradycyjne logowanie nie wystarcza w aplikacjach LLM, dowiedz się, jakie informacje są potrzebne do diagnozowania awarii w potokach RAG i agentów, oraz poznaj model danych… Ćwiczysz AI Engineering Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć AI Engineering Academy?

Nie wymagamy żadnego doświadczenia. AI Engineering Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 1 z 4.

Ile czasu zajmuje lekcja „Dlaczego aplikacje LLM trudno debugować”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji AI Engineering Academy?

Tak. Każda lekcja AI Engineering Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Dlaczego aplikacje LLM trudno debugować
  2. Śledzenie za pomocą LangSmith
  3. Langfuse do obserwowalności niezależnej od modelu
  4. Alerty dotyczące opóźnień, kosztów i spadku jakości
← Powrót do AI Engineering Academy