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 responseCiche 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 responseOpóź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 answerJakich 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
- Dlaczego aplikacje LLM trudno debugować
- Śledzenie za pomocą LangSmith
- Langfuse do obserwowalności niezależnej od modelu
- Alerty dotyczące opóźnień, kosztów i spadku jakości