Blog

Observability testów AI w praktyce: Langfuse i Playwright

Kategoria:

Aplikacje oparte na LLM nauczyły mnie porzucić asercje na dokładny tekst. Model zapytany dwa razy o to samo potrafi zwrócić dwie różne, równie poprawne odpowiedzi, więc sztywne expect(text).toBe() w Playwright zostawia dwa wyjścia: test flaky, który zespół nauczy się ignorować, albo regex tak luźny, że przepuści bzdurę. Poniżej opisuję, jak ustawiam observability testów AI na duecie Playwright + Langfuse: Playwright prowadzi użytkownika przez interfejs, a Langfuse ocenia, czy odpowiedź trzyma poziom.

Dlaczego porównywanie tekstu przestaje działać

Przy deterministycznym oprogramowaniu test E2E był prostą maszyną: klikam przycisk, oczekuję konkretnego napisu, porównuję wynik. W aplikacji z modelem językowym odpowiedź nie ma jednej poprawnej postaci, więc porównywanie napisów niczego nie gwarantuje. Zamiast zgodności tekstu sprawdzam jakość semantyczną odpowiedzi i przebieg całego łańcucha wywołań: od promptu, przez retrieval, po wygenerowany tekst. O tym, gdzie jeszcze modele językowe zmieniają pracę testera, piszę w tekście LLM w testowaniu.

Podział ról: Playwright symuluje, Langfuse ocenia

Rozdzielam dwie odpowiedzialności. Playwright odtwarza ścieżkę użytkownika: logowanie, wpisanie promptu, wysłanie zapytania i odebranie odpowiedzi w interfejsie. Langfuse pracuje po stronie backendu i rejestruje wnętrza: pełny ślad (trace) wywołań LLM, koszt każdego zapytania, latencję oraz automatyczną ocenę odpowiedzi w mechanizmie Scores. Test w Playwright nie musi rozstrzygać, czy odpowiedź jest mądra. Sprawdza, że system przeszedł całą ścieżkę, a jakość odczytuje z API Langfuse.

Korelacja śladów: jeden identyfikator na test

Bez korelacji zostaje worek logów w Langfuse i raport z Playwright, które nic o sobie nie wiedzą. Łączę je wstrzykniętym identyfikatorem: na starcie testu generuję UUID i dokładam go jako nagłówek HTTP do kontekstu przeglądarki. Backend odczytuje nagłówek i inicjalizuje trace w Langfuse z tym ID, więc po kliknięciu „Wyślij” wiem, o który przebieg pytać.

const traceId = crypto.randomUUID();

const context = await browser.newContext({
  extraHTTPHeaders: { 'X-Trace-Id': traceId },
});

// ...akcje w UI: prompt, wysłanie, odczyt odpowiedzi...

const trace = await langfuse.fetchTrace(traceId);
expect(trace.scores.hallucination).toBeLessThan(0.1);

Ten fragment to szkielet, w realnym projekcie odczyt score’ów opakowuję w polling z limitem czasu, bo ewaluacja w Langfuse kończy się chwilę po samym wywołaniu modelu.

LLM w roli sędziego

Ocenę merytoryczną deleguję do mechanizmu Scores: osobny model dostaje odpowiedź, kontekst i kryteria, po czym wystawia oceny liczbowe. Zaczynam od trzech kryteriów:

  • Halucynacje: czy odpowiedź trzyma się kontekstu dostarczonego przez RAG, czy dopisuje fakty od siebie.
  • Relevance: czy odpowiedź dotyczy pytania użytkownika, czy tylko brzmi mądrze obok tematu.
  • Bezpieczeństwo: czy model nie wygenerował treści szkodliwych albo niezgodnych z polityką produktu.

Asercja w teście robi się wtedy krótka: score halucynacji poniżej ustalonego progu, inaczej czerwono. Te same wyniki pokazuję potem biznesowi jako część bilansu jakości, dzięki czemu jakość funkcji AI widać w liczbach.

Datasety zamiast promptów w kodzie testów

Prompty wymyślane ad hoc w kodzie testów kończą się chaosem, a każde uruchomienie suity LLM kosztuje prawdziwe pieniądze za tokeny. Zbiór pytań z wzorcowymi odpowiedziami trzymam więc w Datasets w Langfuse. Playwright iteruje po tym zbiorze: pobiera prompt, wkleja go w UI aplikacji, a uzyskaną odpowiedź odsyła do Langfuse do porównania z wersją wzorcową. Jeden przebieg sprawdza dwie warstwy naraz: interfejs (czy aplikacja działa) i model (czy jakość nie spadła względem golden datasetu).

Dwie pułapki przy wpinaniu w CI/CD

Latencja. Odpowiedź LLM potrafi generować się kilkadziesiąt sekund, a domyślne 30-sekundowe timeouty Playwrighta ubijają test w połowie streamingu. Wydłużam czasy oczekiwania na elementy z odpowiedzią bota i czekam na ustanie strumieniowania, na przykład aż tekst w elemencie przestanie się zmieniać przez kilka sekund.

Koszt. Pełna suita z ewaluacją przy każdym pushu drenuje budżet API w kilka tygodni. U klientów ustawiam to tak: pełne testy jakościowe z oceną Langfuse w nocnych buildach i przed merge’em do gałęzi produkcyjnej, a na etapie pull requesta tylko smoke testy, czyli sprawdzenie, że LLM w ogóle odpowiada.

Jak pomagam w QA Sensei

Od 20 lat pracuję w IT, od siedmiu jako inżynier jakości, a odkąd klienci wkładają LLM do produktów, ustawiam u nich opisany wyżej układ: Playwright od ścieżek użytkownika, Langfuse od trace’ów i ewaluacji, podział na smoke i pełną suitę w CI/CD. Jeśli dopiero planujecie funkcje AI w produkcie, zacznijcie od tekstu o wdrożeniu AI w firmie, testowalność łatwiej zaprojektować przed pierwszą linijką kodu niż doklejać po fakcie. Pracuję zdalnie w całej Polsce, a w okolicach Wrocławia także u klienta.

Observability to jeden z elementów szerszej opieki nad jakością kodu z AI, QA dla kodu tworzonego z AI.