Naar de inhoud
NLEN
Illustratie: CI/CD voor Prompts met Geautomatiseerde Evals

CI/CD-Pipelines voor Prompts met Geautomatiseerde Evals

Door Ivo Donker — samengesteld met AI-ondersteuning (Claude & Gemini)

Het handmatig aanpassen van prompts in productie leidt onvermijdelijk tot onvoorspelbare regressies. Waar traditionele softwareontwikkeling leunt op compilers, linters en deterministische unittests, opereren taalmodellen probabilistisch. Een kleine wijziging in een systeemprompt om één specifiek randgeval op te lossen, kan ongemerkt drie andere bedrijfskritieke outputs breken. Zonder geautomatiseerde validatie ontdekken softwareteams fouten pas wanneer eindgebruikers ertegenaan lopen.

Om softwarekwaliteit te waarborgen, moeten prompts dezelfde releasecyclus doorlopen als applicatiecode. Dit vereist een geautomatiseerde CI/CD-pipeline waarin elke promptwijziging in Git getriggerd wordt, een representatieve testset doorloopt, en getoetst wordt aan vooraf gedefinieerde acceptatiecriteria. In dit artikel behandelen we de architectuur van zo'n evaluatiepipeline, de selectie van teststatistieken, cost-effective evaluatiestrategieën en harde merge gates binnen moderne ontwikkelstraten.

1. De anatomie van een CI/CD-pipeline voor prompts

Een CI/CD-pipeline voor prompts verschilt fundamenteel van een standaard build-pipeline. In plaats van een binaire compilatie of statische code-analyse, voert de runner een reeks gecontroleerde inferenties uit tegen vooraf vastgelegde model-endpoints. De architectuur bestaat uit vier opeenvolgende fasen: trigger & extractie, testuitvoering, kwantitatieve evaluatie, en rapportage met poortwachters (gates).

De eerste fase start bij een pull request waarin een prompt-sjabloon is gewijzigd. Om de context en werkwijze rond versiebeheer zuiver te houden, lees je het fundament in versiebeheer van prompts als code, waarin wordt uitgelegd waarom sjablonen in platte tekstbestanden binnen de repository moeten wonen. De CI-runner detecteert welke sjablonen gewijzigd zijn ten opzichte van de doeltak (bijvoorbeeld main) en selecteert uitsluitend de bijbehorende evaluatiedatasets. Hierdoor worden onnodige API-aanroepen en lange wachttijden voorkomen.

Vervolgens draait de pipeline de prompts parallel over een gouden dataset (golden dataset). De gegenereerde outputs worden opgevangen en doorgegeven aan een evaluatielaag. Deze laag berekent deterministische statistieken (zoals schema-validiteit en regex-matches) en model-gebaseerde statistieken (zoals semantische gelijkenis en contextgetrouwheid). Pas wanneer alle scores boven de gedefinieerde drempelwaarden vallen, geeft de pipeline groen licht voor een merge.

2. Evaluatiemetrics: deterministisch versus model-as-a-judge

Niet elke evaluatie vereist een duur LLM-oordeel. Een effectieve pipeline combineert snelle, goedkope deterministische controles met gerichte kwalitatieve model-evaluaties. Door deze scheiding strikt aan te brengen, blijft de pipeline snel en betaalbaar.

Evaluatietype Methode Snelheid & Kosten Doel & Toepassing
Syntactisch / Schema JSON Schema, Pydantic, Regex < 5 ms, €0,00 Validatie van veldnamen, types en verplichte sleutels in output.
Deterministische tekstmatches Levenshtein, Exact Match, Bleurt < 20 ms, €0,00 Vaste categorieën, classificatielabels en keyword-checks.
Semantische afstand Embedding Cosine Similarity ~50 ms, fractioneel Meten of de strekking overeenkomt met een referentie-antwoord.
LLM-as-a-Judge Geautomatiseerde prompts over criteria 500-2000 ms, API-kosten Toetsen op toon, feitelijke consistentie, hallucinaties en brontrouw.

Deterministische validatie fungeert als de eerste filterlaag. Faalt een prompt op een JSON-schemacheck, dan stopt de pipeline direct; er is geen reden om verdere LLM-judges aan te roepen voor een syntactisch ongeldige response. Pas wanneer de output syntactisch klopt, treedt de kwalitatieve scoring in werking. Hoe je deze numerieke drempelwaarden exact operationaliseert in teamverband lees je in je promptwijziging meten van testset naar cijfer, waarin dieper wordt ingegaan op het toekennen van gewichten aan deelscores.

3. Testsets opbouwen: golden datasets en synthetic generation

De betrouwbaarheid van een CI-pipeline staat of valt met de representativiteit van de testset. Een testset voor prompts moet drie categorieën testcases bevatten:

Standaardcases (happy path): Typische invoervarianten die 80% van het dagelijkse verkeer vertegenwoordigen. Deze bewaken de basisfunctionaliteit.

Randgevallen (edge cases): Onvolledige invoer, extreem lange teksten, ambigue vragen of ontbrekende velden. Deze toetsen of de prompt robuust faalt zonder ongeldige JSON of hallucinaties op te leveren.

Adversarial & regressiecases: Specifieke prompts die in het verleden productiefouten veroorzaakten of pogingen tot prompt injection bevatten. Telkens wanneer een gebruiker een fout meldt, wordt die case als regressietest toegevoegd aan de testset.

Het handmatig onderhouden van honderden testcases kost veel tijd. Daarom zetten teams synthetische data-generatie in. Een krachtiger model genereert op basis van historische context tientallen variaties van een gebruikersinvoer, inclusief opzettelijke spelfouten of afwijkende formuleringen. Voor de methodologische achtergrond van systematisch testen verwijzen we naar het artikel over regressietesten voor prompts op benchmark.llmnet.nl, dat aantoont hoe synthetische testsets blinde vlekken in prompt-aanpassingen blootleggen.

4. Implementatievoorbeeld: GitHub Actions workflow voor prompt-evals

Laten we een concrete implementatie bekijken met GitHub Actions en een Python-gebaseerde evaluatierunner. In deze opzet draait de testsuite automatisch bij elk pull request waarin bestanden in de map prompts/ worden gewijzigd.

name: Prompt CI/CD Regression Evaluation

on:
  pull_request:
    paths:
      - 'prompts/**'
      - 'evals/**'

jobs:
  evaluate-prompts:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          cache: 'pip'

      - name: Install dependencies
        run: |
          pip install -r evals/requirements.txt

      - name: Run automated prompt evaluations
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          python -m evals.runner \
            --prompts-dir ./prompts \
            --dataset ./evals/golden_dataset.jsonl \
            --threshold 0.85 \
            --output ./evals/report.json

      - name: Post Evaluation Summary to PR
        if: always()
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            if (fs.existsSync('./evals/report.json')) {
              const report = JSON.parse(fs.readFileSync('./evals/report.json', 'utf8'));
              const comment = `### 📊 Prompt Eval Rapport\n` +
                `- **Status:** ${report.passed ? '✅ PASSED' : '❌ FAILED'}\n` +
                `- **Score:** ${(report.mean_score * 100).toFixed(1)}% (Drempel: 85%)\n` +
                `- **Geteste samples:** ${report.total_samples}\n` +
                `- **Regressies gevonden:** ${report.regressions_count}`;
              github.rest.issues.createComment({
                issue_number: context.issue.number,
                owner: context.repo.owner,
                repo: context.repo.repo,
                body: comment
              });
            }

De pipeline draait een evaluatiescript dat zowel de oude versie van de prompt (op main) als de nieuwe versie (in de feature-branch) verwerkt over exact dezelfde testvector. Dit maakt directe differentiële scoring mogelijk.

5. Het evaluatiescript: differentiële scoring en asserties

Het Python-script achter de runner voert de werkelijke tests uit. Het script berekent een gewogen gemiddelde van semantische accuraatheid en harde schema-validaties. Hieronder staat een versimpelde, productieklare structuur van de evaluatierunner:

import json
import os
import sys
from typing import Dict, Any, List

def evaluate_sample(prompt_template: str, test_case: Dict[str, Any]) -> Dict[str, Any]:
    formatted_prompt = prompt_template.format(**test_case["inputs"])
    # Simuleer API-aanroep naar het te testen model
    raw_output = call_target_model(formatted_prompt)
    
    # Stap 1: Deterministische JSON-validatie
    schema_valid = validate_json_schema(raw_output, test_case["expected_schema"])
    if not schema_valid:
        return {"score": 0.0, "reason": "Schema mismatch"}
        
    # Stap 2: Model-as-a-judge kwaliteitsbeoordeling
    judge_score = run_llm_judge(
        system_input=test_case["inputs"],
        model_output=raw_output,
        reference=test_case["reference_output"]
    )
    return {"score": judge_score, "reason": "Success"}

def main():
    with open("evals/golden_dataset.jsonl", "r") as f:
        dataset = [json.loads(line) for line in f]
        
    with open("prompts/customer_support_v2.txt", "r") as f:
        current_prompt = f.read()
        
    scores: List[float] = []
    for case in dataset:
        result = evaluate_sample(current_prompt, case)
        scores.append(result["score"])
        
    mean_score = sum(scores) / len(scores) if scores else 0.0
    passed = mean_score >= 0.85
    
    report = {
        "mean_score": mean_score,
        "total_samples": len(dataset),
        "passed": passed,
        "regressions_count": len([s for s in scores if s < 0.5])
    }
    
    with open("evals/report.json", "w") as f:
        json.dump(report, f, indent=2)
        
    if not passed:
        print(f"Eval mislukt: score {mean_score:.2f} ligt onder drempel 0.85")
        sys.exit(1)

if __name__ == "__main__":
    # call_target_model, validate_json_schema en run_llm_judge logica
    pass

Om te zien hoe dergelijke tests op lokaal Git-niveau worden ingericht voordat ze naar CI worden gepusht, bekijk je geautomatiseerde regressietests voor prompts in Git, waarin pre-commit hooks en lokale teststappen worden behandeld.

6. Kosten- en latentiebeheersing in continue teststraten

Een hardnekkige valkuil bij het draaien van prompt-evals in CI/CD zijn de oplopende kosten en wachttijden. Wie bij elke commit 500 complexe prompts door een frontier-model jaagt, creëert een dure en trage pipeline die ontwikkelaars ontmoedigt om regelmatig te testen.

Er zijn drie effectieve strategieën om kosten en doorlooptijd te beperken:

Gelaagde testsuites (tiering): Verdeel de evaluatieset in een smoke test (20 essentiële samples, draait bij elke commit, duurt < 30 seconden) en een full regression suite (300+ samples, draait alleen bij het openen van een pull request of nightly).

Slimme modelcascades voor judges: Gebruik kleinere, gekwantiseerde of snelle modellen (zoals Haiku of Flash-varianten) voor triviale rubric-beoordelingen. Schakel alleen over naar zwaardere modellen wanneer de beoordeling genuanceerde logische deductie vereist.

LLM Response Caching: Cache de responses van ongewijzigde componenten. Als alleen de gebruikersinstructie verandert maar de systeemprompt en context gelijk blijven, kan prefix-caching de tokenkosten aanzienlijk drukken.

Voor een bredere integratie binnen de software-architectuur en API-pipelines lees je de technische handleiding over LLM-integraties regressietesten in CI/CD-pipelines op api.llmnet.nl, waar mock-strategieën en stubbing van externe providers gedetailleerd aan bod komen.

7. Omgaan met niet-deterministische output en eval-drift

Omdat LLM-inferentie inherent stochastisch is (tenzij temperature op exact 0 staat, en zelfs dan kunnen floating-point verschillen tussen GPU-clusters minimale variaties veroorzaken), kan een afzonderlijke test run toevallig slagen of falen. Het blindelings afkeuren van een build op basis van één marginaal gefaalde testcase leidt tot 'flaky' builds en frustratie binnen engineeringteams.

In plaats van te testen op binaire gelijkheid per individueel sample, evalueert een robuuste pipeline op statistische distributies. Draai kritieke prompts driemaal bij twijfelgevallen en meet het slagingspercentage. Als de gemiddelde score van de dataset over de gehele distributie binnen een betrouwbaarheidsinterval van 95% blijft, wordt de wijziging geaccepteerd.

Daarnaast treedt na verloop van tijd eval-drift op: het fenomeen waarbij evaluatiesets verouderen omdat gebruikersvragen in productie veranderen. Hoe je herkent dat een testsuite niet langer de werkelijkheid weerspiegelt, wordt uitgelegd in de gids over eval-drift en stilletjes degraderende prompts, waarin monitoring van productiedata centraal staat.

8. Harde merge gates en governance in productieomgevingen

Zodra de pipeline betrouwbare cijfers produceert, worden branch protection rules geactiveerd in platforms als GitHub of GitLab. Een pull request mag niet gemerged worden naar de hoofdbranch tenzij aan harde kwaliteitseisen is voldaan.

Een effectief governance-beleid voor prompts hanteert de volgende acceptatiecriteria:

Door prompts op te nemen in geautomatiseerde CI/CD-processen verandert prompt engineering van een intuïtieve gok in een voorspelbare, meetbare softwarediscipline. Regressies worden in een vroeg stadium opgevangen in de pull request, deployments verlopen zonder verrassingen en teams kunnen met vertrouwen optimaliseren op kwaliteit, latentie en kosten.