Langfuse wyrósł na standard obserwowalności i ewaluacji aplikacji LLM, ale w firmach, z którymi pracuję, infrastruktura CI od lat stoi na Jenkinsie. Stawianie osobnego serwera tylko dla modułów AI nie ma sensu: testy promptów i ewaluacje modeli da się wpleść w istniejący pipeline. Poniżej opisuję konfigurację, którą wdrażam u klientów, od poświadczeń, przez Jenkinsfile, po pułapkę z asynchronicznym wysyłaniem danych, na której zatrzymuje się większość zespołów zaczynających z LLMOps. O tym, gdzie modele językowe przydają się w samej pracy testera, piszę w tekście LLM w testowaniu.
Krok 1: klucze API poza repozytorium
Najczęstszy błąd, jaki widzę na starcie, to klucze Langfuse wpisane na sztywno w kodzie repozytorium albo w skrypcie pipeline’u. SDK potrzebuje trzech zmiennych środowiskowych:
- LANGFUSE_SECRET_KEY: klucz prywatny projektu,
- LANGFUSE_PUBLIC_KEY: klucz publiczny,
- LANGFUSE_HOST:
https://cloud.langfuse.comdla wersji chmurowej albo adres własnej instancji self-hosted.
W Jenkinsie zakładam je przez plugin Credentials Binding: Manage Jenkins → Credentials, nowy wpis typu „Secret text”, nazwa w stylu langfuse-secret-key-prod. Jenkins maskuje takie wartości w logach konsoli, więc klucz nie wycieka nawet przy gadatliwym buildzie.
Krok 2: Jenkinsfile z mapowaniem poświadczeń
Deklaratywny pipeline poniżej instaluje zależności, uruchamia testy w pytest i wysyła ślady (traces) do Langfuse. Zakładam, że agent ma już zainstalowanego Pythona.
pipeline {
agent any
environment {
// Mapowanie poświadczeń Jenkinsa na zmienne wymagane przez SDK Langfuse
LANGFUSE_SECRET_KEY = credentials('langfuse-secret-key')
LANGFUSE_PUBLIC_KEY = credentials('langfuse-public-key')
LANGFUSE_HOST = 'https://cloud.langfuse.com'
OPENAI_API_KEY = credentials('openai-api-key')
}
stages {
stage('Setup') {
steps {
sh 'pip install -r requirements.txt langfuse'
}
}
stage('LLM Evaluation') {
steps {
sh 'pytest tests/test_llm_logic.py'
}
}
}
}Sekcja environment mapuje poświadczenia z Jenkinsa na zmienne, których oczekuje SDK. W kodzie testów nic więcej nie konfiguruję: konstruktor Langfuse() wywołany bez argumentów czyta te zmienne sam.
Krok 3: flush(), czyli dlaczego dashboard jest pusty
Ten problem debugowałem u trzech różnych klientów: build zielony, pytest przeszedł, a w dashboardzie Langfuse pusto. Przyczyna siedzi w architekturze SDK. Zarówno wersja Python, jak i JS/TS wysyłają zdarzenia asynchronicznie w tle, żeby nie spowalniać głównego wątku aplikacji. Na serwerze produkcyjnym to zaleta. W CI kontener ginie natychmiast po zakończeniu komendy i SDK nie zdąża wypchnąć danych na serwer.
Wysłanie trzeba wymusić metodą flush() przed końcem procesu. Najwygodniej zrobić to raz, w pytest fixture o zasięgu sesji:
# conftest.py
import pytest
from langfuse import Langfuse
@pytest.fixture(scope="session", autouse=True)
def langfuse_client():
client = Langfuse()
yield client
client.flush() # bez tego dane nie dotrą do dashboarduDzięki autouse=True fixture wykona się przy każdym uruchomieniu testów i opróżni kolejkę po ostatnim z nich, niezależnie od tego, czy autor pojedynczego testu pamiętał o flushu.
Tagowanie buildów, zanim zrobi się bałagan
Gdy Jenkins zaczyna mielić kilkanaście buildów dziennie, trace’y z CI mieszają się z produkcją i lokalnym developmentem. Rozdzielam je od pierwszego dnia tagami zbudowanymi ze zmiennych, które Jenkins i tak udostępnia w każdym buildzie: BUILD_NUMBER i GIT_COMMIT.
import os
trace = langfuse.trace(
name="llm-ci-test",
tags=[
"jenkins",
f"build-{os.environ.get('BUILD_NUMBER', 'local')}",
os.environ.get('GIT_COMMIT', 'no-commit')[:8],
],
)W panelu Langfuse filtruję potem wyniki po numerze builda i widzę, która zmiana promptu położyła ewaluację. Zanim wprowadziłem tagi, samo dopasowanie trace’a do commita potrafiło zająć mi pół godziny.
Bramki jakości: build ma się wywalić, gdy model głupieje
Samo wysłanie danych do dashboardu niczego nie pilnuje. Jenkins musi dostać jednoznaczny sygnał, czy model odpowiada poprawnie, a brak wyjątku w kodzie testu o tym nie świadczy. Dlatego w skryptach ewaluacyjnych dodaję asercje na wyniki ocen (scores). Przy ewaluacji model-based, gdzie drugi model ocenia odpowiedzi pierwszego, skrypt pobiera średnią ocen i kończy się kodem błędu, gdy spadnie poniżej ustalonego progu, u mnie zwykle 0.8. Jenkins oznacza wtedy build jako FAILED i blokuje wdrożenie tym samym mechanizmem, który od lat zatrzymuje wadliwe release’y przy klasycznych testach, jak go poukładać, opisuję w tekście o pipeline CI/CD z testami automatycznymi.
Jedno zastrzeżenie praktyczne: ewaluacje model-based kosztują, bo za każdą ocenę płacimy tokenami. Pełnego zestawu nie uruchamiam więc przy każdym commicie. W Jenkinsie rozdzielam to na dwa joby: lekki smoke test promptów w każdym buildzie i pełną ewaluację raz dziennie, wyzwalaną triggerem cron. Rachunek za API spadł u jednego z klientów czterokrotnie, a regresje i tak wychodzą najpóźniej następnego ranka.
Całość (poświadczenia, Jenkinsfile, flush, tagi i bramka jakości) to dla zespołu znającego Jenkinsa jedno popołudnie pracy. W zamian każdy commit zmieniający prompt przechodzi tę samą kontrolę co reszta kodu, a regresję jakości modelu łapię w CI, zanim zobaczy ją użytkownik.