Zum Hauptinhalt springen
Lektion 18 von 20 · Produktion, Evaluierung und Optimierung

CI/CD für KI: Automatisierte Evaluierungen

Lektion 18 von 20 Aktualisiert am 3. November 2026 Fortgeschritten
Auf dieser Seite
  1. Integration von Evaluierungen in Ihre CI/CD-Pipeline
  2. Das Prinzip: Ein Gate, kein Bericht
  3. Alles in GitHub Actions zusammenfügen
  4. Vergleichen statt in absoluten Begriffen zu urteilen
  5. Kernaussagen

Lektion aktualisiert am 3. November 2026 basierend auf der OpenAI-Dokumentation: Die Parameter temperature und top_p werden nur mit einem Reasoning-Effort von none akzeptiert, während Modelle der GPT-5.6-Serie standardmäßig mit medium rechnerisch arbeiten. Aufrufe, die diese Parameter setzen, müssen daher reasoning={"effort": "none"} (Responses API) oder reasoning_effort="none" (Chat Completions) angeben. Quellen: Verwendung von GPT-6 · GPT-5.6 Terra.

Integration von Evaluierungen in Ihre CI/CD-Pipeline

Das Bereitstellen einer Prompt-Änderung für die Produktion ohne Evaluierung ist vergleichbar mit dem Bereitstellen von Code ohne Tests. Die Analogie ist genauer, als es auf den ersten Blick erscheinen mag: In beiden Fällen scheint die Änderung trivial, wurde manuell an ein oder zwei Beispielen verifiziert, und die Nebenwirkung zeigt sich erst drei Tage später bei einem Nutzer, der Sie darüber nicht informieren wird. Der Unterschied besteht darin, dass eine Prompt-Regression keine Ausnahme auslöst – die Anwendung antwortet weiterhin, nur schlechter.

Das Ziel besteht darin, automatisierte Evaluierungen als zwingende Gate in Ihren Bereitstellungsprozess zu integrieren, um sicherzustellen, dass jede Änderung die Qualität aufrechterhält oder verbessert. Genau das werden wir im Folgenden behandeln.

Das Prinzip: Ein Gate, kein Bericht

Die Evaluierungs-Pipeline integriert sich in Ihren Bereitstellungsfluss unter Verwendung eines vertrauten Mechanismus. Ein Entwickler ändert einen Prompt oder einen Parameter, der Commit löst die CI-Pipeline aus, und die Evaluierungen werden automatisch gegen den Referenzdatensatz ausgeführt. Wenn die Scores die Schwellenwerte erfüllen, wird die Bereitstellung fortgesetzt; andernfalls wird sie blockiert, mit einem detaillierten Bericht, der angibt, welche Evaluierung gescheitert ist und um wie viel.

Der entscheidende Punkt ist das Blockierungsverhalten. Eine CI, die einen hübschen Bericht erstellt, ohne die Pipeline zu stoppen, wird nach zwei Wochen zu einem Artefakt, das niemand mehr prüft. Nur ein nicht-null Exit-Code verwandelt die Messung in eine Schutzschranke.

Der folgende Skript liest eine Konfiguration, lädt jeden Datensatz, führt die Fälle aus und vergleicht den Durchschnittsscore mit dem deklarierten Schwellenwert. Die Bewertungsmethode – einfache Keyword-Einschließung – ist absichtlich rudimentär: Eine CI-Prüfung muss schnell, deterministisch und unabhängig von einem Richter sein, dessen Score von einer Laufzeit zur nächsten variieren könnte.

import json
import sys
import openai
from pathlib import Path

client = openai.OpenAI()

def load_eval_config(path: str = "evals/config.json") -> dict:
    """Lädt die Evaluierungskonfiguration."""
    with open(path) as f:
        return json.load(f)

def load_dataset(path: str) -> list[dict]:
    """Lädt einen JSONL-Datensatz."""
    cases = []
    with open(path) as f:
        for line in f:
            cases.append(json.loads(line))
    return cases

def run_ci_evals(config: dict) -> dict:
    """Führt Evaluierungen für CI aus."""
    results = {}

    for eval_config in config["evaluations"]:
        name = eval_config["name"]
        dataset = load_dataset(eval_config["dataset"])
        threshold = eval_config["min_threshold"]
        model = eval_config["model"]
        prompt = Path(eval_config["prompt_file"]).read_text()

        scores = []
        for case in dataset:
            response = client.responses.create(
                model=model,
                reasoning={"effort": "none"},  # temperature erfordert effort none
                instructions=prompt,
                input=case["input"],
                temperature=0.0,
            )

            # Einfache Bewertung durch Keyword-Einschließung
            if "keywords" in case:
                response_lower = response.output_text.lower()
                found = sum(
                    1 for word in case["keywords"]
                    if word.lower() in response_lower
                )
                score = found / len(case["keywords"])
            else:
                score = 1.0  # Kein spezifisches Kriterium

            scores.append(score)

        avg_score = sum(scores) / len(scores)
        passed = avg_score >= threshold

        results[name] = {
            "score": avg_score,
            "threshold": threshold,
            "passed": passed,
            "num_cases": len(dataset),
        }

    return results

def ci_report(results: dict) -> tuple[str, bool]:
    """Erstellt einen Bericht für CI und gibt an, ob er in Ordnung ist."""
    lines = ["# AI Evaluation Report\n"]
    all_pass = True

    for name, r in results.items():
        status = "PASS" if r["passed"] else "FAIL"
        if not r["passed"]:
            all_pass = False
        lines.append(
            f"- [{status}] {name}: {r['score']:.2%} "
            f"(Schwellenwert: {r['threshold']:.0%}, {r['num_cases']} Fälle)"
        )

    report = "\n".join(lines)
    return report, all_pass

Beachten Sie, dass der Prompt aus einer Datei gelesen wird, anstatt direkt im Skript geschrieben zu werden. Dies ist der Grund, warum eine Auslösung möglich ist: Ein Prompt, der im Repository versioniert ist, erzeugt einen Diff, und ein Diff kann eine Evaluierung auslösen. Solange Ihre Prompts als String-Literalen verstreut in Ihrem Anwendungscode leben, ist eine ordnungsgemäße Automatisierung nicht möglich.

Die Konfiguration deklariert eine Evaluierung pro Aufgabe, jede mit ihrem eigenen Schwellenwert.

{
  "evaluations": [
    {
      "name": "ticket-classification",
      "dataset": "evals/datasets/classification.jsonl",
      "prompt_file": "prompts/classification.txt",
      "model": "gpt-5.6-terra",
      "min_threshold": 0.90
    },
    {
      "name": "resume-generation",
      "dataset": "evals/datasets/resumes.jsonl",
      "prompt_file": "prompts/resume.txt",
      "model": "gpt-5.6-terra",
      "min_threshold": 0.75
    }
  ]
}

Die Lücke zwischen diesen beiden Schwellenwerten ist das Herzstück der Methode. Ticket-Klassifizierung ist eine geschlossene Aufgabe: Unter 0,90 liefert die Funktion nicht den erwarteten Service. Zusammenfassung ist eine offene Aufgabe, die anhand von Keywords beurteilt wird, die ein guter Text möglicherweise anders formulieren könnte; ein Erfordernis von 0,90 würde jede Bereitstellung blockieren und schnell dazu führen, dass die Prüfung deaktiviert wird. Kalibrieren Sie jeden Schwellenwert basierend auf Scores, die in der Produktion beobachtet wurden, und nicht auf einer runden Zahl, die sich beruhigend anfühlt.

Alles in GitHub Actions zusammenfügen

Der Workflow wird nur für relevante Pfade ausgelöst – Prompts, Evaluierungen, KI-Integrationscode –, sodass eine Änderung am Stylesheet keine API-Aufrufe verbraucht.

# .github/workflows/eval-ai.yml
name: AI Evaluation

on:
  pull_request:
    paths:
      - "prompts/**"
      - "evals/**"
      - "src/ai/**"

jobs:
  evaluate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - run: pip install openai

      - name: Run evals
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: python evals/run_ci.py

      - name: Publish report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: eval-report
          path: evals/report.md

Zwei Details sind erwähnenswert. Der API-Schlüssel wird über Repository-Secrets weitergegeben, niemals über die Workflow-Datei, die von jedem mit Zugriff auf den Code einsehbar ist. Und die Veröffentlichung des Berichts verwendet if: always(), andernfalls würde das Artefakt nicht erstellt, wenn die Evaluierungen fehlschlagen – was genau der einzige Fall ist, in dem Sie es lesen müssen.

Das Einstiegsskript verbindet alles: Es schreibt den Bericht auf die Festplatte für das Artefakt, druckt ihn in die Protokolle und setzt den Exit-Code, der den Rest der Pipeline zulässt oder blockiert.

#!/usr/bin/env python3
"""Evaluierungsskript für CI — evals/run_ci.py"""

import sys
from pathlib import Path

def main():
    config = load_eval_config("evals/config.json")
    results = run_ci_evals(config)
    report, all_pass = ci_report(results)

    # Den Bericht schreiben
    Path("evals/report.md").write_text(report)
    print(report)

    # Exit-Code für CI
    if not all_pass:
        print("\nEinige Evaluierungen sind fehlgeschlagen. Bereitstellung blockiert.")
        sys.exit(1)
    else:
        print("\nAlle Evaluierungen bestehen.")
        sys.exit(0)

if __name__ == "__main__":
    main()

Vergleichen statt in absoluten Begriffen zu urteilen

Ein fester Schwellenwert beantwortet die Frage „Ist das gut genug?”. Er beantwortet nicht die Frage „Ist das besser als vorher?”, was oft die eigentliche Frage ist. Ein umgeschriebener Prompt kann über dem Schwellenwert bleiben, während er im Vergleich zur vorherigen Version drei Punkte verliert: Die CI wird grün, und die Verschlechterung gelangt in die Produktion, ohne dass jemand es bemerkt.

Der Regressionstest führt daher beide Versionen auf demselben Datensatz, in derselben Kampagne, aus und vergleicht die Durchschnitte. Eine Toleranz von 5 % absorbiert das Rauschen, das in generativen Ausgaben inhärent ist; darüber hinaus ist das Urteil explizit.

def prompt_regression_test(
    current_prompt: str,
    previous_prompt: str,
    dataset: list[dict],
    model: str,
    degradation_threshold: float = 0.05,
) -> dict:
    """Überprüft, dass ein neuer Prompt die Leistung nicht verschlechtert."""
    scores_current = []
    scores_previous = []

    for case in dataset:
        # Bewertung mit aktuellem Prompt
        r1 = client.responses.create(
            model=model, instructions=current_prompt,
            reasoning={"effort": "none"},  # temperature erfordert effort none
            input=case["input"], temperature=0.0,
        )
        # Bewertung mit vorherigem Prompt
        r2 = client.responses.create(
            model=model, instructions=previous_prompt,
            reasoning={"effort": "none"},  # temperature erfordert effort none
            input=case["input"], temperature=0.0,
        )

        s1 = evaluate_response(r1.output_text, case)
        s2 = evaluate_response(r2.output_text, case)
        scores_current.append(s1)
        scores_previous.append(s2)

    avg_current = sum(scores_current) / len(scores_current)
    avg_previous = sum(scores_previous) / len(scores_previous)
    delta = avg_current - avg_previous

    return {
        "current_score": avg_current,
        "previous_score": avg_previous,
        "delta": delta,
        "regression": delta < -degradation_threshold,
        "verdict": "OK" if delta >= -degradation_threshold else "REGRESSION",
    }

Dieser Test verdoppelt die Anzahl der Aufrufe und damit die Kosten und die Dauer. Reservieren Sie ihn für Prompt-Änderungen, lassen Sie ihn den Pfad gewöhnlicher Commits frei, und führen Sie ihn auf einem repräsentativen Teilmenge statt auf Ihrem gesamten Referenzdatensatz aus.

Kernaussagen

  • Integrieren Sie Evaluierungen in Ihre CI/CD-Pipeline wie Unit-Tests
  • Versionieren Sie Prompts in Dateien: Ohne einen Diff ist keine Auslösung möglich
  • Definieren Sie Mindestschwellenwerte pro Evaluierungstyp, kalibriert auf Produktionsdaten
  • Blockieren Sie Bereitstellungen über Exit-Code, wenn Scores unter Schwellenwerte fallen
  • Testen Sie auf Regressionen, indem Sie neue und alte Prompts auf demselben Datensatz vergleichen