
Jak pisać testy dla aplikacji AI? Piramida testów, złoty dataset i CI/CD dla LLM
Testowanie aplikacji AI wymaga innych narzędzi niż klasyczny kod - LLM jest niedeterministyczny, więc zamiast sprawdzać równość stringów, testujesz semantykę, zachowanie i pokrycie edge case'ów. Właściwie zaprojektowany zestaw testów daje pewność przed deploymentem: testy semantyczne sprawdzają czy odpowiedź jest poprawna znaczeniowo, testy kontrakt-schematowe weryfikują format wyjścia, a testy regresyjne wykrywają, czy aktualizacja modelu lub promptu zepsuła istniejące zachowanie.
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.
W skrócie: zacznij od 30-50 przypadków z logów i zgłoszonych błędów, dodaj przypadki brzegowe, zapisuj kryterium zamiast dokładnej odpowiedzi („musi zawierać liczbę 14”), trzymaj zestaw w tests/fixtures/golden.json i dopisuj nowy przypadek po każdym błędzie produkcyjnym. Z czasem zestaw rośnie do 200-500 przykładów. Jak go zbudować z realnych awarii i dlaczego nie generować go modelem, opisuję szczegółowo w przewodniku po ewaluacji LLM. # test_integration.py import json, pytest from deepeval import assert_test from deepeval.test_case import LLMTestCase from deepeval.metrics import AnswerRelevancyMetric, FaithfulnessMetric from 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 × ok. 500 tokenów to ok. 50 tys. tokenów wejścia, czyli ok. $0.005 na tanim modelu (GPT-6 Luna, $0.10 za milion) i ok. $0.10 na modelu średniej klasy (GPT-6.1 Sol, $2 za milion), plus koszt metryk ocenianych przez model. To wciąż grosze w porównaniu z jednym błędem na produkcji.
Warstwa 3: Ewaluacja - gdy kryterium to "dobry", a nie "poprawny"
Dla złożonych wyjść (podsumowania, generowanie treści, konwersacje wieloturowe) stosuje się LLM-as-a-judge: drugi model ocenia jakość wyjścia według kryteriów. DeepEval ma do tego metrykę G-Eval, w której opisujesz kryterium zwykłym językiem. Sędzia powinien pochodzić z innej rodziny modeli niż model oceniany, bo modele zawyżają oceny własnych odpowiedzi.
Tutaj pokazuję tylko, jak wpiąć sędziego w testy. Jak napisać rubrykę, skalibrować sędziego na ręcznych ocenach i ograniczyć jego biasy, opisuję w artykule o ewaluacji LLM i LLM-as-a-judge.
# 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?
Aliasy modeli (np. wersje oznaczone „latest”) z czasem wskazują nowsze wersje, a starsze wersje są wycofywane według harmonogramu dostawcy. Żeby wiedzieć, że coś się zmieniło, potrzebujesz baseline - zapisanych wyników ewaluacji na złotym datasecie z daną wersją modelu. Dlatego w produkcji przypinaj konkretną wersję modelu, a zmianę traktuj jak każdy inny deployment.
- 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ę modelu, 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
Powiązane artykuły
---
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_SERVICES
Potrzebujesz wdrożenia tych koncepcji? Zobacz usługi powiązane z tym tematem.
Budowa Aplikacji AI
Dedykowane oprogramowanie AI i aplikacje webowe z silnikiem AI — MVP, full stack development, programowanie systemów AI od zera do produkcji.
Zobacz usługęUsługaTworzenie Stron i Aplikacji Web
Tworzę strony internetowe, sklepy i aplikacje webowe w Next.js i React: landing page, strony firmowe, sklepy headless, platformy B2B, interfejsy SaaS i 3D. 99/100 Lighthouse i SEO od pierwszego dnia.
Zobacz usługę/// RELATED_RECORDS
Modele embeddingów po polsku - bge-m3, multilingual-e5 czy PolDense? Test na 900 pytaniach
Który model embeddingów wybrać do polskiego RAG? Przetestowałem 10 modeli i BM25 na 900 prawdziwych pytaniach klientów: wyniki, odporność na brak polskich znaków i literówki, prefiks, który zabiera 30 punktów, ranking PIRB, ceny API, koszt przy dwóch skalach, licencje i kod.
Adopcja AI w zespole - dlaczego licencje leżą nieużywane i jak przekonać ludzi do AI
Według Gartnera tylko 5% firm po pilotażu Copilota planowało szersze wdrożenie, a z AI codziennie korzysta 8% pracowników. Pięć oporów i co na nie działa, rola menedżerów, program liderów AI, pomiar adopcji na trzech poziomach i plan na 90 dni.
Jak wybrać wykonawcę wdrożenia AI - 15 pytań, sygnały ostrzegawcze i zapisy w umowie
Według MIT NANDA 95% pilotaży AI nie daje efektu finansowego, ale wdrożenia z partnerami udają się w 67% przypadków. Zespół wewnętrzny, partner czy hybryda, 15 pytań do wykonawcy, sygnały ostrzegawcze, zapisy w umowie i pilotaż z kryteriami sukcesu.
Signal received?
Przerwij
Ciszę
Zainicjuj protokół. Nawiąż połączenie. Zbudujmy coś głośnego.
