CI/CD für KI: Automatisierte Evaluierungen
Auf dieser Seite
Lektion aktualisiert am 3. November 2026 basierend auf der OpenAI-Dokumentation: Die Parameter
temperatureundtop_pwerden nur mit einem Reasoning-Effort vonnoneakzeptiert, während Modelle der GPT-5.6-Serie standardmäßig mitmediumrechnerisch arbeiten. Aufrufe, die diese Parameter setzen, müssen daherreasoning={"effort": "none"}(Responses API) oderreasoning_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