# LLM-integraties regressietesten in CI/CD-pipelines

[Naar de inhoud](#lm-inhoud)Netwerk/NL[EN](/en/)[Hubhub.llmnet.nlModellen vergelijken op taak, taal, kosten en licentie.](https://hub.llmnet.nl/)[Communitycommunity.llmnet.nlPrompttechnieken, patronen en systeemprompts.](https://community.llmnet.nl/)[APIapi.llmnet.nlLLM's robuust in software: rate limits, routing, structured output.](https://api.llmnet.nl/)[Consultancyconsultancy.llmnet.nlAI invoeren in een organisatie, van pilot tot productie.](https://consultancy.llmnet.nl/)[Nieuwsnieuws.llmnet.nlOntwikkelingen in AI, geduid voor Nederland.](https://nieuws.llmnet.nl/)[Benchmarkbenchmark.llmnet.nlZelf meten wat AI-kwaliteit is, voor jouw taken.](https://benchmark.llmnet.nl/)[Vacaturesvacatures.llmnet.nlAI-rollen, salarissen en carrièrepaden in Nederland.](https://vacatures.llmnet.nl/)[Lerenleren.llmnet.nlAI-concepten in gewoon Nederlands, van beginner tot bouwer.](https://leren.llmnet.nl/)[Gidsgids.llmnet.nlAI privé draaien op eigen Mac, pc, NAS of thuisserver.](https://gids.llmnet.nl/)[Directorydirectory.llmnet.nlHet AI-ecosysteem in kaart: tools, modellen, bedrijven.](https://directory.llmnet.nl/)[Radarradar.llmnet.nlSignalen uit X, onderzoek en communities voor indie developers.](https://radar.llmnet.nl/)[Appsapps.llmnet.nlReviews van AI-apps en open-source repo's, met tips voor wie zelf bouwt.](https://apps.llmnet.nl/)[llmnet.nl — hoofdsite](https://llmnet.nl/)[](https://x.com/intent/post?url=https%3A%2F%2Fapi.llmnet.nl%2Fllm-integraties-regressietesten-in-ci-cd-pipelines&text=LLM-integraties%20regressietesten%20in%20CI%2FCD-pipelines)[](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fapi.llmnet.nl%2Fllm-integraties-regressietesten-in-ci-cd-pipelines)[](https://www.reddit.com/submit?url=https%3A%2F%2Fapi.llmnet.nl%2Fllm-integraties-regressietesten-in-ci-cd-pipelines&title=LLM-integraties%20regressietesten%20in%20CI%2FCD-pipelines)[](#)[](https://x.com/intent/post?url=https%3A%2F%2Fapi.llmnet.nl%2Fllm-integraties-regressietesten-in-ci-cd-pipelines&text=LLM-integraties%20regressietesten%20in%20CI%2FCD-pipelines)[](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fapi.llmnet.nl%2Fllm-integraties-regressietesten-in-ci-cd-pipelines)[](https://www.reddit.com/submit?url=https%3A%2F%2Fapi.llmnet.nl%2Fllm-integraties-regressietesten-in-ci-cd-pipelines&title=LLM-integraties%20regressietesten%20in%20CI%2FCD-pipelines)[](#)

 
# LLM-integraties regressietesten in CI/CD-pipelines

 Door Ivo Donker — samengesteld met AI-ondersteuning (Claude & Gemini) · 20 augustus 2026

 Het integreren van taalmodellen in moderne softwarearchitecturen brengt een fundamentele verschuiving met zich mee voor traditionele softwarekwaliteitsborging: niet-deterministische output. Waar klassieke unit- en integratietests vertrouwen op harde, binaire vergelijkingen (zoals assert result == 42), introduceert een LLM een probabilistische component. Een ogenschijnlijk triviale aanpassing in een systeemprompt, een wijziging in de contextopbouw of een stille backend-update bij een modelleverancier kan ertoe leiden dat antwoorden plotseling subtiel afwijken in structuur, lengte, toon of semantische nauwkeurigheid. Zonder geautomatiseerde regressietesten in de CI/CD-pipeline sluipen deze kwaliteitslekken ongemerkt door naar de productieomgeving.

 Een doordachte CI/CD-pipeline voor LLM-applicaties balanceert drie tegenstrijdige belangen: snelle feedbackcycli voor ontwikkelaars, beheersbare API-kosten en diepgaande semantische kwaliteitscontrole. Voor wie de fundamenten van lokale mocks en unit-evaluaties wil opzetten, biedt de handleiding over [geautomatiseerd testen van LLM-integraties](https://api.llmnet.nl/testen-van-llm-integraties) een solide basis om individuele componenten modulair te isoleren. In dit artikel behandelen we de architectuur en operationalisering binnen continue integratie: hoe ontwerp je een gelaagde testsuite die zowel deterministische contractbreuken als statistische kwaliteitsregressies tijdig signaleert en blokkeert?

 
## De specifieke faalmodi van taalmodellen in productie

 Software die afhankelijk is van externe taalmodellen faalt op wezenlijk andere manieren dan traditionele microservices. Naast conventionele netwerkfouten en rate limits zijn er vier specifieke faalmodi die een continue testpipeline moet kunnen detecteren:

 
 
- Structurele syntax- en schemabreuken: Het model retourneert ongeldige JSON, vergeet verplichte velden, wijzigt datatypes (zoals een string in plaats van een integer) of voegt markdown-opmaakblokken toe rond de payload.
 
- Semantische drifting en degradatie: Door een promptwijziging beantwoordt het model een deelvraag niet meer, worden extracties onvolledig, of neemt het hallucinatieniveau bij randgevallen plotseling toe.
 
- Veiligheids- en instructie-erosie: Aangepaste instructies kunnen per ongeluk de weerbaarheid tegen prompt-injecties verzwakken, waardoor het model bedrijfsgeheimen lekt of vooraf gedefinieerde vangrails negeert.
 
- Latente providerwijzigingen: Externe API-leveranciers voeren regelmatig optimalisaties (zoals gewichtsveranderingen, kwantisatie of routing) door op bestaande modelaliassen, wat kan leiden tot afwijkend gedrag zonder dat de codebase zelf is aangepast.
 

 
## Gelaagde testarchitectuur in continuous integration

 Het direct aanroepen van live model-API's bij elke push of pull request is operationeel onhoudbaar. Het leidt tot trage pipelines, onvoorspelbare build-tijden, willekeurige netwerkfouten (flakiness) en torenhoge tokenkosten. Een effectieve pipeline verdeelt de testlast daarom strikt over drie lagen, elk met een eigen frequentie, trigger en isolatieniveau.

 
 
 
 
 Testlaag | 
 Trigger & Frequentie | 
 Uitvoeringsmethode | 
 Doel & Validatiecriteria | 
 

 
 
 
 Laag 1: Deterministic Unit & Contract | 
 Elke commit / PR-push (Fast Feedback) | 
 Lokale mocks, opgeslagen VCR-cassettes, Pydantic | 
 Payload-formattering, parsering, schemaconformiteit, foutafhandeling en fallback-paden. | 
 

 
 Laag 2: Integration Smoke & Sanity | 
 PR merge / Staging deployment | 
 Live API met minimale testset (< 15 items) | 
 Connectiviteit, API-sleutelpermissies, token-budgettering en basale modelinteractie. | 
 

 
 Laag 3: Full Evaluation & Benchmarking | 
 Nachtelijke build / Release-tag / Modelupgrade | 
 Live API over complete gouden dataset (> 200 items) | 
 Statistische kwaliteitsscores, cosine similarity, LLM-as-a-judge scores en kosten-latentieregistratie. | 
 

 
 
 

 
## Deterministische contractvalidatie met JSON Schema en Pydantic

 Wanneer een LLM fungeert als functionele component in een softwarepijplijn, genereert het vrijwel altijd gestructureerde gegevens voor downstream applicatielogica. Een onverwachte veldnaam of ongeldig type leidt direct tot een onverwerkte runtime-exceptie. Daarom vormt schema-validatie de snelste en meest kritieke verdedigingslinie in Laag 1.

 Als je wilt weten hoe je gegarandeerde JSON-schema's direct op protocolniveau afdwingt bij API-aanroepen, bekijk dan de documentatie over [betrouwbare structured output uit LLM's](https://api.llmnet.nl/structured-output) om te zien hoe schema-validatie direct op netwerkniveau wordt afgehandeld. Binnen de CI-omgeving valideren we zowel opgeslagen payloads als live gegenereerde modelresponsen tegen strikte Pydantic-modellen.

 import json
import pytest
from pydantic import BaseModel, Field, ValidationError

class ExtractedInvoiceItem(BaseModel):
 description: str = Field(..., min_length=2)
 quantity: int = Field(..., gt=0)
 unit_price_cents: int = Field(..., ge=0)
 vat_rate: float = Field(..., ge=0.0, le=1.0)

class InvoiceExtractionResponse(BaseModel):
 invoice_number: str = Field(..., pattern=r"^[A-Z0-9\-]{4,20}$")
 currency: str = Field(..., min_length=3, max_length=3)
 items: list[ExtractedInvoiceItem] = Field(..., min_items=1)
 total_amount_cents: int = Field(..., gt=0)

def parse_and_validate_payload(raw_json: str) -> InvoiceExtractionResponse:
 try:
 data = json.loads(raw_json)
 return InvoiceExtractionResponse.model_validate(data)
 except (json.JSONDecodeError, ValidationError) as err:
 raise AssertionError(f"Contractbreuk in LLM-output: {err}")

def test_invoice_payload_schema_compliance():
 sample_recorded_payload = """
 {
 "invoice_number": "INV-2026-0042",
 "currency": "EUR",
 "items": [
 {"description": "Cloud Hosting", "quantity": 1, "unit_price_cents": 12500, "vat_rate": 0.21}
 ],
 "total_amount_cents": 15125
 }
 """
 validated = parse_and_validate_payload(sample_recorded_payload)
 assert validated.invoice_number == "INV-2026-0042"
 assert len(validated.items) == 1
 assert validated.items[0].unit_price_cents == 12500

 Omdat deze tests uitsluitend lokale validatielogica uitvoeren, draaien honderden assertions binnen enkele milliseconden zonder enige externe netwerkafhankelijkheid of kosten.

 
## Mocks, caching en VCR-cassettes in continue integratie

 Om integratietests betrouwbaar en snel uit te voeren tijdens reguliere pull requests, maakt de testsuite gebruik van request-recording via bibliotheken zoals vcrpy of pytest-recording. Bij de eerste uitvoering worden het exacte HTTP-verzoek en het bijbehorende antwoord van de LLM-leverancier opgeslagen in een lokaal YAML-bestand (de cassette).

 import os
import pytest
from openai import OpenAI

@pytest.fixture
def api_client():
 api_key = os.getenv("OPENAI_API_KEY", "test-key-not-used-when-mocked")
 return OpenAI(api_key=api_key)

@pytest.mark.vcr
def test_summarization_integration_flow(api_client):
 response = api_client.chat.completions.create(
 model="gpt-4o-mini",
 messages=[
 {"role": "system", "content": "Vat de tekst samen in maximaal één zin."},
 {"role": "user", "content": "Kubernetes cluster auto-scaling vereist accurate metingen van resourcegebruik."}
 ],
 temperature=0.0
 )
 content = response.choices[0].message.content.strip()
 assert len(content) > 10
 assert "Kubernetes" in content

 In de standaard CI-stap draait pytest met --vcr-record=none. Als een ontwikkelaar de prompt-tekst of modelparameters in de code aanpast, matcht het uitgaande HTTP-verzoek niet meer met de bestaande cassette, waardoor de test direct faalt. Dit dwingt de ontwikkelaar om bewust nieuwe testcassettes te genereren en deze te reviewen in de pull request.

 
## Samenstelling en beheer van gouden evaluatiedatasets

 Voor de diepere evaluaties in Laag 3 is een zogenaamde gouden dataset (golden dataset) onmisbaar. Deze dataset bestaat uit een representatieve steekproef van productiescenario's met vooraf gevalideerde invoer, context en gewenste uitvoercriteria.

 Een complete gouden dataset voor CI/CD-regressietesten bevat vier specifieke categorieën:

 
 
- Hoofdscenario's (60-70%): Reguliere, eenduidige verzoeken die de kernfunctionaliteit van het systeem vertegenwoordigen.
 
- Complexe randgevallen (15-20%): Zeer lange documenten, ambigue instructies, meertalige invoer, ontbrekende context of zeldzame invoercombinaties.
 
- Adversarial & Safety inputs (10%): Doelbewuste pogingen tot indirecte prompt-injectie, systeeminstructie-extractie of het forceren van beleidsschendingen.
 
- Historische regressie-items (variabel): Elk afzonderlijk incident uit productie waarbij het model een foutief, misleidend of schadelijk antwoord gaf, wordt direct omgezet in een permanente testcase.
 

 Bewaar deze datasets als geversionneerde JSONL-bestanden binnen de repository (bijvoorbeeld in tests/fixtures/eval_golden_set.jsonl). Hierdoor doorlopen wijzigingen in testdata exact dezelfde peer-reviews en git-historie als de productiecode.

 
## Meetmethodes voor semantische evaluaties: omgaan met niet-determinisme

 Omdat taalmodellen zelfs bij temperature=0.0 kleine variaties kunnen vertonen in woordkeuze en zinsopbouw, leiden strikte stringvergelijkingen tot een onwerkbare hoeveelheid vals-positieve testresultaten. Voor kwalitatieve output hanteren we daarom een combinatie van deterministische heuristieken, embeddings-vergelijkingen en modelgebaseerde beoordelaars.

 Om te voorkomen dat de algehele nauwkeurigheid en stijl stilletjes afbrokkelen over opeenvolgende iteraties, lees je in het artikel over [regressietesten voor prompts en het voorkomen van kwaliteitsverlies](https://benchmark.llmnet.nl/regressietesten-prompts) hoe je degradatie over meerdere modelgeneraties kwantificeert. Binnen continuous integration vertalen we deze meetmethodes naar geautomatiseerde slagingsdrempels.

 
 
 
 
 Evaluatiemethode | 
 Doelwit & Toepassing | 
 Meetmethode & Formule | 
 Typische Slagingsdrempel in CI | 
 

 
 
 
 Exact Match & Regex | 
 Statuscodes, datums, categorieën | 
 String matching / Regex patterns | 
 100% (geen enkele afwijking toegestaan) | 
 

 
 Embedding Cosine Similarity | 
 Korte samenvattingen, antwoordintentie | 
 Cosine hoek tussen vectoren van output en referentie | 
 Gemiddelde score ≥ 0.88; geen enkel item < 0.75 | 
 

 
 ROUGE-L / BERTScore | 
 Documentextractie, parafrasering | 
 Langste gemeenschappelijke deelsequentie / Token-alignments | 
 ROUGE-L F1 ≥ 0.72 ten opzichte van baseline | 
 

 
 LLM-as-a-Judge (Rubric Scoring) | 
 Complexe redeneringen, toonzetting, feitelijkheid | 
 Gestructureerde beoordelingsprompt op schaal 1-5 | 
 Gemiddelde ≥ 4.2 / 5.0; 0 faalscores (score 1) | 
 

 
 
 

 
## Implementatie van een LLM-as-a-Judge evaluatiescript

 Voor niet-triviale taken (zoals juridische samenvattingen of klantenservice-afhandeling) biedt een krachtig referentiemodel als evaluator de meest betrouwbare kwaliteitsmeting. Hieronder staat een compleet evaluatiescript dat in de nachtelijke CI-pipeline draait:

 import json
import sys
from openai import OpenAI

JUDGE_SYSTEM_PROMPT = """
Je bent een onafhankelijke softwarekwaliteitsbeoordelaar. Beoordeel het gegenereerde antwoord 
op basis van de verstrekte gebruikersvraag, de broncontext en het referentie-antwoord.
Geef een score van 1 tot 5 op basis van feitelijke correctheid en volledigheid.

Antwoord uitsluitend in valide JSON:
{
 "score": <int 1-5>,
 "reasoning": "<korte motivatie in één zin>",
 "contains_hallucination": <true/false>
}
"""

def evaluate_golden_dataset(dataset_path: str) -> bool:
 client = OpenAI()
 total_score = 0
 failures = 0
 test_cases = []

 with open(dataset_path, "r", encoding="utf-8") as f:
 for line in f:
 if line.strip():
 test_cases.append(json.loads(line))

 for item in test_cases:
 eval_prompt = f"""
 Gebruikersvraag: {item['input']}
 Referentie-antwoord: {item['reference']}
 Gegenereerd modelantwoord: {item['candidate']}
 """

 response = client.chat.completions.create(
 model="gpt-4o",
 messages=[
 {"role": "system", "content": JUDGE_SYSTEM_PROMPT},
 {"role": "user", "content": eval_prompt}
 ],
 response_format={"type": "json_object"},
 temperature=0.0
 )

 result = json.loads(response.choices[0].message.content)
 total_score += result["score"]

 if result["score"] <= 2 or result["contains_hallucination"]:
 failures += 1
 print(f"FAIL [Case {item.get('id', 'N/A')}]: Score {result['score']} - {result['reasoning']}")

 avg_score = total_score / len(test_cases)
 print(f"\n--- Evaluatieresultaat ---")
 print(f"Totaal geëvalueerd: {len(test_cases)}")
 print(f"Gemiddelde score: {avg_score:.2f} / 5.0")
 print(f"Kritieke fouten: {failures}")

 # CI criteria: gemiddelde minimaal 4.0 en geen kritieke hallucinaties
 if avg_score < 4.0 or failures > 0:
 return False
 return True

if __name__ == "__main__":
 success = evaluate_golden_dataset("tests/fixtures/eval_golden_set.jsonl")
 if not success:
 sys.exit(1)
 sys.exit(0)

 
## Volledige GitHub Actions workflowconfiguratie

 Onderstaande configuratie demonstreert hoe de verschillende testlagen worden georkestreerd in GitHub Actions. Snelle controles draaien op elk pull request, terwijl intensieve evaluatieruns worden voorbehouden aan geplande nachtelijke builds of pull requests met een specifiek label.

 name: LLM CI/CD Quality & Regression Gate

on:
 pull_request:
 branches: [ main ]
 schedule:
 - cron: '0 2 * * *' # Elke nacht om 02:00 UTC
 workflow_dispatch:

concurrency:
 group: ${{ github.workflow }}-${{ github.ref }}
 cancel-in-progress: true

jobs:
 fast-deterministic-checks:
 name: Schema & Mocked Unit Tests
 runs-on: ubuntu-latest
 steps:
 - uses: actions/checkout@v4
 - name: Setup Python
 uses: actions/setup-python@v5
 with:
 python-version: '3.12'
 cache: 'pip'
 - name: Install dependencies
 run: pip install -r requirements-test.txt
 - name: Run Schema & Mock Tests (VCR Mode: None)
 run: pytest tests/unit tests/contracts --vcr-record=none -v

 live-evaluation-benchmark:
 name: Live Model Evaluation Run
 needs: fast-deterministic-checks
 if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'run-eval')
 runs-on: ubuntu-latest
 steps:
 - uses: actions/checkout@v4
 - name: Setup Python
 uses: actions/setup-python@v5
 with:
 python-version: '3.12'
 cache: 'pip'
 - name: Install dependencies
 run: pip install -r requirements-test.txt
 - name: Execute Golden Dataset Benchmark
 env:
 OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
 ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
 run: python scripts/run_eval_benchmark.py --dataset tests/fixtures/eval_golden_set.jsonl --output eval-summary.json
 - name: Archive Evaluation Report
 uses: actions/upload-artifact@v4
 with:
 name: evaluation-summary
 path: eval-summary.json
 retention-days: 14

 
## Koppeling met promptversiebeheer en rollout-strategieën

 Een regressietestsuite heeft pas echt waarde wanneer deze direct gekoppeld is aan het versiebeheer van prompts en parameters. Als prompts als losse strings door de codebase verspreid staan, is het vrijwel onmogelijk om te herleiden welke codewijziging verantwoordelijk is voor een kwaliteitsverschuiving.

 Voor een gestructureerd framework om prompts als versionable software-artefacten te behandelen, zie de gids over [promptversiebeheer en wijzigingen deployen zonder breuk](https://api.llmnet.nl/promptversiebeheer-wijzigingen-deployen-zonder-breuk) om te zien hoe semantische versienummering wordt toegepast. Door promptdefinities los te koppelen van applicatielogica en te voorzien van een strikt versienummer (zoals prompt-extract-invoice:v2.1.0), kan de CI-pipeline automatisch een differentiële evaluatie draaien: de prestaties van de nieuwe versie worden direct procentueel vergeleken met de actieve productie-baseline.

 
## Kosten, rate limits en budgetbeheer in testomgevingen

 Het structureel draaien van live evaluatietests op grote datasets brengt reële operationele kosten met zich mee. Zonder duidelijke vangrails kunnen geautomatiseerde loops of parallelle testrunners binnen korte tijd aanzienlijke budgetten consumeren.

 
 
 
 
 Strategie | 
 Mechanisme | 
 Kosten- en Risicoreductie | 
 

 
 
 
 Stratified Sampling | 
 Selecteer bij PR-runs een willekeurige steekproef van 10% per categorie uit de gouden dataset. | 
 Verlaagt de directe API-kosten per pull request met circa 90% terwijl grove regressies zichtbaar blijven. | 
 

 
 Asynchrone Batch API's | 
 Voer nachtelijke evaluaties uit via de batch-endpoints van providers (zoals OpenAI/Anthropic Batch). | 
 Biedt 50% korting op tokenkosten en omzeilt strikte gelijktijdige rate limits van realtime endpoints. | 
 

 
 Dedicated CI API-Keys | 
 Gebruik specifieke API-sleutels voor de CI-omgeving met een harde maandelijkse bestedingslimiet. | 
 Voorkomt dat een falende testloop ongemerkt het productiebudget of operationele tegoeden uitput. | 
 

 
 
 

 
## Beperkingen en valkuilen van geautomatiseerde LLM-evaluaties

 Het inrichten van regressietesten voor taalmodellen kent fundamentele uitdagingen die men helder voor ogen moet houden:

 
 
- Evaluator-bias bij LLM-as-a-judge: Het inzetten van een taalmodel als kwaliteitsrechter introduceert systematische fouten. Modellen vertonen vaak een voorkeur voor langere, breedsprakige antwoorden (verbosity bias), een voorkeur voor antwoorden die als eerste worden gepresenteerd (position bias) en een lichte voorkeur voor teksten die door dezelfde modelfamilie zijn gegenereerd (self-enhancement bias). Dit kan deels worden gemitigeerd door evaluatierubrieken strikt feitelijk te formuleren en paarsgewijze evaluaties met omgekeerde volgorde uit te voeren.
 
- Flakiness door te strakke drempelwaarden: Het hanteren van een 100% slaaggrens op semantische metrieken leidt onvermijdelijk tot willekeurig falende builds. Hanteer daarom statistische tolerantiegrenzen (bijvoorbeeld een minimale gemiddelde score van 4.2 over de gehele dataset) in plaats van harde eisen op elk afzonderlijk niet-deterministisch testitem.
 
- Data-contaminatie en drift in de gouden dataset: Een testset die niet regelmatig wordt herzien, verliest na verloop van tijd zijn representativiteit. Veranderingen in gebruikersgedrag, nieuwe productfeatures of verschuivende domeintermen vereisen een continu proces voor dataset-curatie.
 

 
## Stappenplan voor implementatie

 Het opzetten van een effectieve testpipeline hoeft niet in één keer volledig gerealiseerd te worden. We onderscheiden een logisch groeipad in vier fasen:

 
 
- Fase 1 (Dag 1): Introduceer Pydantic-contractvalidatie op alle JSON-outputs en neem VCR-cassettes op voor bestaande unit-tests. Dit kost minimale tijd en sluit 80% van de directe applicatiecrashes uit.
 
- Fase 2 (Week 1): Stel een compacte gouden dataset samen van 30 tot 50 representatieve voorbeelden (inclusief bekende eerdere productiefouten) en sla deze op als JSONL in de codebase.
 
- Fase 3 (Maand 1): Bouw een geautomatiseerd evaluatiescript met een referentiemodel en configureer een nachtelijke GitHub Actions-workflow die kwaliteitsrapporten genereert als build-artefact.
 
- Fase 4 (Kwartaal 1): Koppel de evaluatiestap aan promptversiebeheer en stel automatische regressiedrempels in die voorkomen dat een PR gemerged kan worden wanneer de gemiddelde kwaliteitsscore daalt.
