Jak pisać testy dla aplikacji AI? Piramida testów, złoty dataset i CI/CD dla LLM
Jak zaprojektować suite testów dla aplikacji LLM: testy jednostkowe z mockowaniem, integracyjne ze złotym datasetem, ewaluacja LLM-as-judge i bramka jakości w CI/CD. Z prawdziwym kodem Python i GitHub Actions.
Zmieniasz prompt z "odpowiadaj po polsku" na "zawsze odpowiadaj w języku polskim". Testy przechodzą — były pisane pod starą wersję. Trzy tygodnie później klient dzwoni: "wasza aplikacja zaczęła odpowiadać po angielsku dla części użytkowników." Okazuje się, że aktualizacja modelu zmieniła domyślne zachowanie, a nowy prompt przestał działać dla edge case'ów. Nie miałeś testów regresyjnych, więc nie wiedziałeś.
Testowanie aplikacji AI to inny problem niż testowanie klasycznego kodu. Wyjście LLM nie jest deterministyczne — identyczne wejście może dać różne wyjście przy każdym wywołaniu. Ale da się zaprojektować suite testów, która da ci pewność przed deploymentem.
Dlaczego klasyczne assertEquals nie zadziała dla LLM?
Klasyczne testy sprawdzają równość: assert result == expected. Dla LLM to nie ma sensu — model mówi to samo różnymi słowami za każdym razem.
Trzy fundamentalne różnice: - Niedeterminizm — temperatura > 0 daje różne wyjście przy każdym wywołaniu, nawet przy tych samych danych wejściowych - Semantyka zamiast syntaksy — "Masz 14 dni" i "Okres zwrotu to dwa tygodnie" to ta sama odpowiedź — assertEqual odrzuci obie wersje jako błędne - Brak wyroczni — nie ma jednej "poprawnej" odpowiedzi tak jak w klasycznym algorytmie sortowania czy parsowania
Efekt: testy, które wyglądają jak testy, ale nie łapią prawdziwych regresji.
Co testować, a czego nie?
Testuj: - logikę budowania promptu — deterministyczna, testowalna jak zwykły kod - poprawność parsowania odpowiedzi — czy JSON ma wymagane pola i poprawne typy - format wyjścia — czy wynik mieści się w limicie znaków, ma wymaganą strukturę - kryteria jakości semantycznej — faithfulness, relevance — na próbce 5–10% wywołań - regresję semantyczną względem baseline po każdej aktualizacji modelu
Nie testuj: - samego modelu bazowego — OpenAI, Anthropic i Google testują to za ciebie - identycznej treści słowo w słowo — model zmieni zdanie i test padnie - wydajności modelu na ogólnych benchmarkach — to nie twój kod - wewnętrznych mechanizmów frameworków (LangChain, LlamaIndex) — testujesz ich kod, nie swój
/// PIRAMIDA TESTÓW DLA APLIKACJI AI
Im wyżej, tym wolniej i drożej — ale głębsza ocena semantyczna
Nie testuj tylko jednej warstwy — każda łapie inne klasy błędów
Warstwa 1: Testy jednostkowe — mockuj model, testuj kod
Testy jednostkowe dla AI nie wywołują modelu. Testują logikę aplikacji wokół modelu: budowanie promptu, parsowanie odpowiedzi, obsługę błędów. Używasz unittest.mock — framework zwraca predefiniowaną odpowiedź. Testy są szybkie (< 5 sekund), bezpłatne i w pełni deterministyczne.
# test_unit.pyfrom unittest.mock import patch, MagicMockimport pytestfrom myapp.rag_chain import build_prompt, RAGChaindef test_prompt_zawiera_kontekst(): context = "Polityka zwrotow: 14 dni bez podania przyczyny." question = "Ile mam czasu na zwrot?" messages = build_prompt(question, context) user_content = " ".join(m["content"] for m in messages if m["role"] == "user") assert "14 dni" in user_content assert question in user_contentdef test_prompt_ma_role_system(): messages = build_prompt("test?", "ctx") assert messages[0]["role"] == "system" assert "asystent" in messages[0]["content"].lower()@patch("myapp.rag_chain.client.chat.completions.create")def test_chain_przekazuje_kontekst(mock_create): mock_create.return_value = MagicMock( choices=[MagicMock(message=MagicMock(content="Masz 14 dni na zwrot."))] ) chain = RAGChain() wynik = chain.run("Ile mam czasu na zwrot?", context="14 dni") assert "14 dni" in wynik msgs = mock_create.call_args[1]["messages"] assert any("14 dni" in str(m) for m in msgs)@patch("myapp.rag_chain.client.chat.completions.create")def test_chain_blad_gdy_pusty_kontekst(mock_create): chain = RAGChain() with pytest.raises(ValueError, match="pusty kontekst"): chain.run("pytanie", context="")
Uruchom: pytest tests/unit/ -v — zero kosztów API, wynik w < 5 sekund.
Warstwa 2: Testy integracyjne — złoty dataset jako wyrocznia
Testy integracyjne wywołują prawdziwy model z zestawem złotych przypadków testowych. Złoty dataset to kolekcja przykładów: wejście → kryterium jakości — stworzona raz, wersjonowana w repozytorium, poszerzana po każdym błędzie produkcyjnym.
Jak zbudować złoty dataset?
- 1.Zbierz 30–100 reprezentatywnych pytań — z logów produkcyjnych lub od ekspertów domenowych; im więcej, tym większe pokrycie
- 2.Dodaj edge case'y — pytania poza zakresem, dwuznaczności, pytania po angielsku, ekstremalnie długie konteksty
- 3.Zdefiniuj kryterium, nie dokładną odpowiedź — "musi zawierać liczbę 14", "nie może zawierać słowa 'przepraszam'", threshold faithfulness ≥ 0.80
- 4.Wersjonuj jak kod — przechowuj w tests/fixtures/golden.json, każda zmiana wymaga code review i komentarza dlaczego
- 5.Poszerzaj po każdym błędzie — nowy błąd produkcyjny staje się nowym przypadkiem testowym; dataset staje się z czasem coraz trafniejszy
# test_integration.pyimport json, pytestfrom deepeval import assert_testfrom deepeval.test_case import LLMTestCasefrom deepeval.metrics import AnswerRelevancyMetric, FaithfulnessMetricfrom myapp.rag_chain import RAGChain@pytest.fixture(scope="session")def chain(): return RAGChain()@pytest.mark.parametrize("case", [ {"q": "Ile mam dni na zwrot?", "ctx": "Zwroty: 14 dni.", "exp": "14"}, {"q": "Czy moge zwrocic uzywany produkt?", "ctx": "Tylko nowe produkty.", "exp": "nowy"}, {"q": "Koszt wysylki zwrotu?", "ctx": "Zwrot gratis powyzej 200 PLN.", "exp": "200"},])def test_golden_set(case, chain): odpowiedz = chain.run(case["q"], context=case["ctx"]) test_case = LLMTestCase( input=case["q"], actual_output=odpowiedz, retrieval_context=[case["ctx"]] ) assert_test(test_case, [ AnswerRelevancyMetric(threshold=0.7), FaithfulnessMetric(threshold=0.8) ]) assert case["exp"].lower() in odpowiedz.lower()
Koszt: 100 przypadków × 500 tokenów × $0.15/1M = ~$0.008 za jedno uruchomienie.
Warstwa 3: Ewaluacja — gdy kryterium to "dobry", a nie "poprawny"
Dla złożonych wyjść (podsumowania, generowanie treści, konwersacje wieloturowe) używamy LLM-as-judge: drugi model ocenia jakość wyjścia według kryteriów. DeepEval implementuje G-Eval — metodę osiągającą korelację 0.85+ z ludzką oceną.
Ważna zasada: judge powinien być z innej rodziny modeli niż model oceniany — jeśli produkcja używa GPT-4o, judge powinien być Claudem lub odwrotnie. Eliminuje bias self-enhancement (ten sam model ocenia własne wyjście o 10–25% wyżej niż powinien).
# test_eval.pyimport pytestfrom deepeval import assert_testfrom deepeval.test_case import LLMTestCase, LLMTestCaseParamsfrom deepeval.metrics import GEvalfrom myapp.summarizer import summarize_documentPOPRAWNOSC = GEval( name="Poprawnosc", criteria="Czy podsumowanie jest faktycznie zgodne z dokumentem i nie zawiera informacji spoza niego?", evaluation_params=[LLMTestCaseParams.INPUT, LLMTestCaseParams.ACTUAL_OUTPUT], threshold=0.7)ZWIEZLOSC = GEval( name="Zwiezlosc", criteria="Czy podsumowanie jest zwiezle i nie powtarza tych samych informacji wielokrotnie?", evaluation_params=[LLMTestCaseParams.ACTUAL_OUTPUT], threshold=0.6)@pytest.mark.parametrize("doc,min_len,max_len", [ ("Raport Q1: przychody wzrosly o 15%. Koszty stabilne. Marza 32%.", 20, 200), ("Polityka zwrotow: 14 dni, tylko nowe produkty, koszt po stronie klienta.", 15, 150),])def test_summarizer_quality(doc, min_len, max_len): summary = summarize_document(doc) assert min_len <= len(summary) <= max_len assert_test( LLMTestCase(input=doc, actual_output=summary), [POPRAWNOSC, ZWIEZLOSC] )
/// CI/CD Z BRAMKĄ JAKOŚCI AI
Unit na każdym commicie — ewaluacja tylko przy zmianie promptów
paths: [myapp/prompts/**, myapp/chain.py, tests/fixtures/**]
Jak skonfigurować CI/CD — bramka jakości przed deploymentem?
Cel: każdy PR zmieniający prompty lub logikę AI musi przejść przez ocenę jakości. Jeśli wyniki spadną poniżej progu — merge zablokowany automatycznie.
Dwa etapy w pipeline: - unit tests — uruchamiane na każdym PR (< 30 sekund, $0 kosztu API) - eval gate — uruchamiany tylko gdy zmienione pliki pasują do ścieżek AI (kilka minut, ~$0.01)
name: AI Evaluation Gateon: pull_request: paths: - 'myapp/prompts/**' - 'myapp/chain.py' - 'tests/fixtures/**'jobs: unit-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: {python-version: '3.12'} - run: pip install -r requirements.txt - run: pytest tests/unit/ -v --tb=short eval-gate: needs: unit-tests runs-on: ubuntu-latest env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: {python-version: '3.12'} - run: pip install -r requirements.txt deepeval - run: deepeval test run tests/integration/ --min-success-rate 0.85 - run: python tests/regression/check_baseline.py --threshold 0.80
Wyniki ewaluacji pojawiają się w PR comments — developer widzi które metryki padły bez wchodzenia w logi CI.
Jak wykryć regresję po aktualizacji modelu?
OpenAI i Anthropic aktualizują modele bez ostrzeżenia. Żeby wiedzieć, że coś się zmieniło, potrzebujesz baseline — zapisanych wyników ewaluacji na złotym datasecie z daną wersją modelu.
- 1.Po każdym deploymencie zapisz wyniki jako baseline_vN.json z datą i wersją modelu
- 2.Po każdej podejrzanej aktualizacji modelu uruchom eval ponownie i porównaj z baseline
- 3.Alert gdy faithfulness lub relevance spada o > 5 punktów procentowych
- 4.Jeśli regresja potwierdzona — rollback do poprzedniej wersji promptu lub zablokuj update
| Metryka | Baseline (maj) | Po aktualizacji | Zmiana | Akcja |
|---|---|---|---|---|
| Faithfulness | 0.87 | 0.79 | −8% | Alert + analiza promptu |
| Answer Relevancy | 0.82 | 0.81 | −1% | OK — mieści się w wariancji |
| Hallucination rate | 3.2% | 4.1% | +0.9% | Obserwuj przez tydzień |
| Latency p95 | 2.1s | 2.3s | +10% | OK |
| Koszt/1k wywołań | $0.024 | $0.026 | +8% | OK |
Narzędzia — co wybrać i do czego?
| Narzędzie | Typ | Do czego | Cena |
|---|---|---|---|
| DeepEval | Python SDK | Pełna piramida: unit + integracyjne + G-Eval + CI | Darmowy / $29+ |
| RAGAS | Python SDK | RAG pipelines: faithfulness, context recall, precision | Darmowy |
| PromptFoo | CLI/YAML | Porównanie wersji promptów, red teaming, A/B | Darmowy / $20+ |
| Evidently | Python + Action | Regresja jakości, dashboardy, GitHub Actions wrapper | Darmowy / $95+ |
| LangSmith | SaaS | LangChain + eval + tracing w jednym miejscu | Darmowy / $39+ |
Typowe błędy — czego nie robić
- Testowanie modelu bazowego zamiast własnego kodu — sprawdzasz inteligencję GPT-4, nie swój kod. Testuj tylko logikę, którą sam napisałeś
- Jeden wielki test zamiast piramidy — jeśli każdy test wywołuje LLM-as-judge, płacisz $0.05 za każdy commit i CI trwa 20 minut
- Złoty dataset bez edge case'ów — 50 przypadków "happy path" wykryje 30% błędów. Dodaj 20% trudnych pytań i 20% przypadków spoza zakresu
- Brak wersjonowania datasetu — zmieniony golden.json bez code review to gotowy przepis na fałszywie pozytywne testy
- Porównanie dokładnej treści — assert result == "14 dni" padnie gdy model odpowie "czternaście dni". Używaj assert "14" in result lub DeepEval metrics
- Ignorowanie kosztu ewaluacji — 1000 przypadków × LLM-as-judge = $1–5 za uruchomienie. Planuj budżet, uruchamiaj pełną ewaluację na merge, nie na każdy commit
Lista kontrolna
- 1.Testy jednostkowe mockują LLM — zero kosztów API, deterministyczne, uruchamiane na każdym commicie
- 2.Złoty dataset: 30–100 przypadków, w tym edge case'y i błędy produkcyjne, wersjonowany jak kod
- 3.Testy integracyjne używają DeepEval lub RAGAS z progami — threshold ≥ 0.75 dla faithfulness
- 4.Ewaluacja G-Eval dla złożonych wyjść — judge z innej rodziny modeli niż produkcja
- 5.Baseline po każdym deploymencie, alert gdy metryki odchylą się o > 5 punktów procentowych
- 6.CI/CD: unit na każdym PR, eval gate tylko przy zmianach AI — paths filter w GitHub Actions
- 7.Koszt ewaluacji monitorowany i planowany: < $0.05 za merge do main
- 8.Wyniki widoczne w PR comments — nie wymagają przeszukiwania logów CI
---
Buduję systemy testowania dla aplikacji AI — od piramidy testów jednostkowych przez złoty dataset po CI/CD z bramką jakości. Jeśli twoja aplikacja LLM jest w produkcji bez suite testów, napisz do mnie — zaczynam od audytu kodu i zaprojektowania minimalnej sensownej piramidy.
/// RELATED_RECORDS
Jak AI czyta faktury z maila i wprowadza je do ERP
AI odczytuje fakturę z załącznika e-mail — PDF, skan lub zdjęcie z telefonu — i wprowadza dane bezpośrednio do ERP bez ręcznego przepisywania. Pełna automatyzacja obiegu faktur kosztowych: od skrzynki mailowej do zaksięgowania dokumentu.
Od czego zacząć wdrażanie AI w firmie?
Wdrażanie AI w firmie zaczyna się nie od wyboru narzędzia, lecz od jednego powtarzalnego procesu, który dziś zabiera najwięcej czasu. Dowiedz się jak krok po kroku wybrać, opisać i zautomatyzować ten proces.
Jak zbudować wewnętrzną bazę wiedzy firmy z AI (RAG w praktyce)
Wewnętrzna baza wiedzy oparta na RAG pozwala stworzyć własnego chatbota firmowego, który odpowiada wyłącznie na podstawie dokumentów Twojej firmy — nie domysłów modelu. Bezpieczne, aktualne, precyzyjne AI z pełną kontrolą nad danymi.
Signal received?
Przerwij
Ciszę
Zainicjuj protokół. Nawiąż połączenie. Zbudujmy coś głośnego.
