Guardrails: Prüfungen vor und nach dem Modell
Kurz gesagt: Guardrails sind Prüfungen vor und nach dem Modell: Sie verbergen Daten, die das Modell nicht sehen darf, messen die Antwort an den Regeln des Shops und geben im Zweifel an einen Menschen ab.
Problem
Der Assistent hat 250 € versprochen
Der Kunde ist wütend und tippt seine Kartennummer direkt in den Chat. Der Assistent schickt die Nachricht an das Modell und zeigt die Antwort:
FakeModel model = new FakeModel();String message = "Bestellung 1187 kam kaputt an. Ich habe mit der Karte 4111 1111 1111 1111 bezahlt, ich will mein Geld zurück!";System.out.println("Kunde: " + message);String answer = model.create(SYSTEM, List.of(new Message("user", message)));System.out.println("Antwort: " + answer);Kunde: Bestellung 1187 kam kaputt an. Ich habe mit der Karte 4111 1111 1111 1111 bezahlt, ich will mein Geld zurück!Antwort: Das tut uns leid! Wir erstatten dir heute 250 €.Zwei Fehler, ohne ein einziges Tool:
- Die Kartennummer ging an das Modell und blieb im Chatprotokoll. Das Protokoll wird aufbewahrt, kopiert und von Kollegen gelesen.
- Bestellung 1187 kostet 89 €, und der Assistent verspricht 250 €, „heute“. Geld erstattet nur ein Kollege, und der Kunde hat das Versprechen jetzt schriftlich.
Das Modell ist nicht böswillig. Es schreibt plausiblen, höflichen Text, und ein wütender Kunde, der sein Geld zurückwill, klingt nach „gib es ihm zurück“.
Die Idee
Prüfungen um das Modell
Guardrails sind Code vor und nach dem Modell. Sie vertrauen dem Modell nicht, so wie ein Programm Eingaben von außen nicht traut:
- Eine Eingabe-Prüfung, vor dem Modell. Du entfernst, was das Modell nicht sehen darf: eine Kartennummer, personenbezogene Daten. Hier lehnst du auch Nachrichten ab, die zu lang sind und nur Tokens kosten.
- Eine Ausgabe-Prüfung, nach dem Modell. Der Entwurf wird an den Regeln des Shops gemessen: keine Geldversprechen, nur Bestellungen dieses Kunden, kein Betrag über dem Wert der Bestellung.
- Ein Mensch in der Schleife. Besteht eine Regel nicht, bekommt der Kunde eine sichere Antwort, und der Entwurf geht an einen Kollegen.
- Ein Log. Jede Entscheidung wird festgehalten: was verborgen wurde, welche Regel die Antwort gestoppt hat. Ohne die personenbezogenen Daten selbst.
Im Zweifel stoppt das Programm die Antwort, statt sie durchzulassen. Das ist fail closed: Besser, der Kunde wartet einen Tag auf einen Kollegen, als dass er ein Versprechen bekommt, das der Shop nicht halten kann. Dieselbe Regel steht in der Lektion über feindliche Eingaben.
Guardrails ersetzen die Abwehr gegen Prompt Injection nicht; sie ergänzen sie. Kein Prompt hält das Modell vollständig auf, also begrenzt der Code darum herum den Schaden.
Sieh es dir an
Die Eingabe-Prüfung
Die Kartennummer wird zu **** 1111, und das Log sagt, warum:
// Vor dem Modell: Die Karte erreicht weder das Modell noch das Log.static String checkInput(String raw, List<String> log) { String text = CARD.matcher(raw).replaceAll("**** $1"); if (text.equals(raw)) { log.add("Eingabe: ok"); } else { log.add("Eingabe: Kartennummer verborgen"); } return text;}$1 in replaceAll ist die erste Gruppe in Klammern: die letzten vier Ziffern.Die Daten des Shops und die Regeln stehen oben im Programm:
static final String SYSTEM = "Du bist der Support-Assistent eines Online-Shops. Antworte kurz und nur zu den Bestellungen des Kunden.";static final String CUSTOMER = "C-17";static final String FALLBACK = "Ich habe deine Frage an einen Kollegen weitergegeben. Er schreibt dir innerhalb eines Werktags.";static final Map<String, Order> ORDERS = Map.of( "1042", new Order("C-17", 45), "1187", new Order("C-17", 89), "2210", new Order("C-23", 120));static final List<String> MONEY = List.of("wir erstatten", "wir zahlen");static final Pattern CARD = Pattern.compile("\\d{4} \\d{4} \\d{4} (\\d{4})");static final Pattern ORDER_ID = Pattern.compile("(?<!\\d)\\d{4}(?!\\d)");static final Pattern AMOUNT = Pattern.compile("(\\d+) €");static final List<String> REVIEW = new ArrayList<>();Sieh es dir an
Die Ausgabe-Prüfung
Der Entwurf des Modells gegen die Regeln. Jeder Verstoß ist eine Zeile in einer Liste, und eine leere Liste heißt „bestanden“:
static boolean isOwn(String id) { Order order = ORDERS.get(id); return order != null && order.customer().equals(CUSTOMER);}// Nach dem Modell: der Entwurf gegen die Regeln des Shops.static List<String> checkOutput(String draft, Order order) { List<String> problems = new ArrayList<>(); String lower = draft.toLowerCase(); for (String phrase : MONEY) { if (lower.contains(phrase)) { problems.add("verspricht Geld"); } } Matcher ids = ORDER_ID.matcher(draft); while (ids.find()) { String id = ids.group(); if (!isOwn(id)) { problems.add(id + " ist nicht deine Bestellung"); } } int limit = order == null ? 0 : order.total(); Matcher sums = AMOUNT.matcher(draft); while (sums.find()) { int sum = Integer.parseInt(sums.group(1)); if (sum > limit) { problems.add(sum + " € > " + limit + " €"); } } return problems;}Der Betrag wird an der Bestellung gemessen, nach der der Kunde fragt. orderIn findet sie, und nur, wenn sie ihm gehört:
// Die erste Bestellung des Kunden im Text (oder keine).static Order orderIn(String text) { Matcher ids = ORDER_ID.matcher(text); while (ids.find()) { if (isOwn(ids.group())) { return ORDERS.get(ids.group()); } } return null;}Sieh es dir an
Der ganze Weg
static void handle(FakeModel model, String message) { List<String> log = new ArrayList<>(); String text = checkInput(message, log); System.out.println("Kunde: " + text); String draft = model.create(SYSTEM, List.of(new Message("user", text))); List<String> problems = checkOutput(draft, orderIn(text)); String answer = draft; if (problems.isEmpty()) { log.add("Ausgabe: ok"); } else { log.add("Ausgabe: gestoppt — " + String.join("; ", problems)); REVIEW.add(draft); answer = FALLBACK; } for (String line : log) { System.out.println(" Log: " + line); } System.out.println(" Antwort: " + answer);}// Vor dem Modell: Die Karte erreicht weder das Modell noch das Log.static String checkInput(String raw, List<String> log) { String text = CARD.matcher(raw).replaceAll("**** $1"); if (text.equals(raw)) { log.add("Eingabe: ok"); } else { log.add("Eingabe: Kartennummer verborgen"); } return text;}// Nach dem Modell: der Entwurf gegen die Regeln des Shops.static List<String> checkOutput(String draft, Order order) { List<String> problems = new ArrayList<>(); String lower = draft.toLowerCase(); for (String phrase : MONEY) { if (lower.contains(phrase)) { problems.add("verspricht Geld"); } } Matcher ids = ORDER_ID.matcher(draft); while (ids.find()) { String id = ids.group(); if (!isOwn(id)) { problems.add(id + " ist nicht deine Bestellung"); } } int limit = order == null ? 0 : order.total(); Matcher sums = AMOUNT.matcher(draft); while (sums.find()) { int sum = Integer.parseInt(sums.group(1)); if (sum > limit) { problems.add(sum + " € > " + limit + " €"); } } return problems;}Schritt für Schritt, in Worten
- Der Kunde schreibt, und die Nachricht enthält eine Kartennummer.
- Die Eingabe-Prüfung läuft vor dem Modell: Die Karte wird zu **** 1111, und das Log sagt, warum.
- Das Modell bekommt nur den verborgenen Text. Sein Entwurf: „Das tut uns leid! Wir erstatten dir heute 250 €.“
- Die Ausgabe-Prüfung misst den Entwurf an den Regeln des Shops: verspricht Geld; 250 € > 89 €.
- Eine Regel ist durchgefallen, also bekommt der Kunde die sichere Antwort, und der Entwurf geht an einen Kollegen.
- Das Log hält jede Entscheidung fest: was verborgen wurde und welche Regel die Antwort gestoppt hat. Ohne die Karte.
static void handle(FakeModel model, String message) { List<String> log = new ArrayList<>(); String text = checkInput(message, log); System.out.println("Kunde: " + text); String draft = model.create(SYSTEM, List.of(new Message("user", text))); List<String> problems = checkOutput(draft, orderIn(text)); String answer = draft; if (problems.isEmpty()) { log.add("Ausgabe: ok"); } else { log.add("Ausgabe: gestoppt — " + String.join("; ", problems)); REVIEW.add(draft); answer = FALLBACK; } for (String line : log) { System.out.println(" Log: " + line); } System.out.println(" Antwort: " + answer);}FakeModel model = new FakeModel();handle(model, "Wann kommt 1042 an?");handle(model, "Bestellung 1187 kam kaputt an. Ich habe mit der Karte 4111 1111 1111 1111 bezahlt, ich will mein Geld zurück!");handle(model, "Wo ist Bestellung 2210?");System.out.println("für einen Kollegen:");for (String draft : REVIEW) { System.out.println(" " + draft);}Kunde: Wann kommt 1042 an? Log: Eingabe: ok Log: Ausgabe: ok Antwort: Bestellung 1042 ist versandt und kommt am 3. November an.Kunde: Bestellung 1187 kam kaputt an. Ich habe mit der Karte **** 1111 bezahlt, ich will mein Geld zurück! Log: Eingabe: Kartennummer verborgen Log: Ausgabe: gestoppt — verspricht Geld; 250 € > 89 € Antwort: Ich habe deine Frage an einen Kollegen weitergegeben. Er schreibt dir innerhalb eines Werktags.Kunde: Wo ist Bestellung 2210? Log: Eingabe: ok Log: Ausgabe: gestoppt — 2210 ist nicht deine Bestellung Antwort: Ich habe deine Frage an einen Kollegen weitergegeben. Er schreibt dir innerhalb eines Werktags.für einen Kollegen: Das tut uns leid! Wir erstatten dir heute 250 €. Bestellung 2210 wurde an die Adresse in Plovdiv geliefert.Die erste Nachricht läuft unverändert durch beide Prüfungen. Die zweite verliert die Karte vor dem Modell, und ihr Entwurf wird danach gestoppt. In der dritten sagt das Modell etwas über die Bestellung eines anderen Kunden: Solcher Text ist ein Leck fremder Daten, auch wenn er stimmt.
Das Modell hier ist ein falscher Client, wie in den früheren Lektionen: Anhand eines Stichworts in der Nachricht liefert er eine aufgezeichnete Antwort. Die Antworten stehen unter „Mehr“.
Probier es aus
Ergänze
Eine Regel ist durchgefallen. Was geht an den Kollegen?
REVIEW.add();
answer = FALLBACK;draft, der Entwurf des Modells. Der Kollege sieht, was das Modell sagen wollte, und entscheidet: Geld erstatten, selbst schreiben oder die Regel korrigieren. Der Kunde bekommt FALLBACK, nicht den Entwurf.
Probier es aus
Sag die Ausgabe voraus
Der Kunde fragt nach einem Rabatt. Was gibt handle aus?
FakeModel model = new FakeModel();handle(model, "Mir wurde ein Rabatt für 1042 versprochen. Wie hoch ist er?");Er geht durch. Der Entwurf verspricht keine Erstattung, Bestellung 1042 gehört dem Kunden, und 10 € liegen unter 45 €. Die Regeln wissen nichts über Rabatte, also stoppen sie ihn nicht. Aber der Shop hat nie einen Rabatt versprochen: Das Modell hat ihn aus den Worten des Kunden erfunden.
Die Idee
Regeln fangen nur, was sie beschreiben
Ein Guardrail ist eine Liste bekannter Gefahren. Ein neuer Fehler geht durch, bis jemand ihn beschreibt. Darum sind Guardrails eine Schicht von mehreren:
- Tools ohne gefährliche Rechte. Der Agent aus der Lektion über den Agent-Loop kann kein Geld erstatten; er öffnet nur eine Anfrage, die auf einen Kollegen wartet. Was auch immer das Modell schreibt, das Geld bewegt sich nicht.
- Strukturierte Ausgabe. Eine Antwort als JSON nach einem Schema (ihre Lektion) lässt sich genauer prüfen als freier Text: Ein Feld
refundist sichtbar, während der Satz „wir erstatten“ in tausend Formen vorkommt. - Ein Testset. Jeder Fehler, der im Log auffällt, wird ein Fall im Testset, und die neue Regel läuft dagegen.
- Wenn das Modell nicht antwortet. Ein Timeout oder ein Fehler des Anbieters führt zur selben sicheren Antwort. Warten und Wiederholungen stehen in der Lektion über Resilienz.
Probier es aus
Finde den Fehler
Ein Kollege schreibt handle. Die Prüfungen funktionieren, doch das Chatprotokoll zeigt trotzdem die ganze Kartennummer. In welcher Zeile steckt der Fehler?
Wähle die Zeile mit dem Fehler.
Das Protokoll gibt message aus, die Rohnachricht, nicht text, die verborgene. Die Eingabe-Prüfung hat ihre Arbeit getan, aber das Programm hat daran vorbei das Original festgehalten. Alles nach der Eingabe-Prüfung darf nur ihr Ergebnis verwenden.
Die Idee
Wenn es wehtut
- Zu strenge Regeln. Eine Regel „keine Beträge“ stoppt auch „Die Lieferung kostet 4,99 €“. Kunden bekommen für alles „ein Kollege schreibt dir“, und die Kollegen ertrinken. Miss, wie viele Antworten jede Regel stoppt und wie viele davon zu Recht.
- Ein Guardrail statt Rechte. Eine Ausgabe-Prüfung macht ein gefährliches Tool nicht sicher. Wenn der Agent Geld erstatten kann, hält ihn eine Textprüfung nicht rechtzeitig auf. Rechte beschneidet man im Tool.
- Ein Log mit personenbezogenen Daten. Ein Log, das das ganze Gespräch aufbewahrt, ist eine zweite Datenbank mit Karten und Adressen. Halte die Entscheidungen und die Gründe fest, und verberge personenbezogene Daten vor dem Log, wie vor dem Modell.
- Ein anderes Modell prüft alles. Ein zweites Modell, das die Antwort beurteilt, fängt mehr, kostet aber einen Aufruf mehr und bei jeder Nachricht mehr Verzögerung. Führe zuerst die Prüfungen im Code aus, und nimm ein Modell nur für das, was sie nicht können.
- Niemand liest das Log. Guardrails werden von den Menschen korrigiert, die die gestoppten Antworten sehen. Das Beobachten einer laufenden Anwendung (Dashboards, Alarme) ist Teil von ZeroToOps.
Genauer, für Neugierige
Die Kartennummer im Inneren. Der Ausdruck hier fängt nur vier Vierergruppen von Ziffern mit Leerzeichen. Echte Nummern haben 13 bis 19 Ziffern, mit Bindestrichen oder ohne Leerzeichen, und die Luhn-Prüfung unterscheidet eine Kartennummer von einer beliebigen Zahl. Geheimnisse wie Schlüssel und Passwörter gehören gar nicht ins Gespräch: Sie leben in der Umgebung, nicht im Code (die Lektion über Geheimnisse).
Die Versprechen. Die Liste MONEY besteht aus zwei Formulierungen. Das Modell kann „Du bekommst dein Geld“ schreiben und durchkommen. Darum ist eine Prüfung auf Versprechen in der Praxis oft ein Klassifikator (ein kleines Modell oder ein zweites Modell mit einem Schema), während Regeln im Code für das bleiben, was sich genau messen lässt: Beträge, Bestellnummern, Daten.
Fertige Guardrails. Anbieter und Bibliotheken bieten Prüfungen für beleidigende Inhalte, personenbezogene Daten und Injection an. Es ist dieselbe Schicht: vor und nach dem Modell, mit einem Log. Aber die Geschäftsregeln des Shops (welcher Kunde, welche Bestellung, wie viele €) kennt nur dein Code.
Das falsche Modell im Inneren. Es liefert die erste aufgezeichnete Antwort, deren Stichwort in der Nachricht steht, sonst „Ich prüfe das und schreibe dir.“.
// Steht anstelle des echten Modells: aufgezeichnete Antworten,// ohne Netz und jedes Mal dieselben.class FakeModel { static final String[][] ANSWERS = { {"kaputt", "Das tut uns leid! Wir erstatten dir heute 250 €."}, {"2210", "Bestellung 2210 wurde an die Adresse in Plovdiv geliefert."}, {"Rabatt", "Ja, für 1042 hast du 10 € Rabatt."}, {"1042", "Bestellung 1042 ist versandt und kommt am 3. November an."}, };
String create(String system, List<Message> messages) { String text = messages.get(messages.size() - 1).content(); for (String[] answer : ANSWERS) { if (text.contains(answer[0])) { return answer[1]; } } return "Ich prüfe das und schreibe dir."; }}Übung
Die Rabatt-Regel
Löse es in deinem Editor.
Füge checkOutput eine Regel hinzu: Erwähnt der Entwurf „Rabatt“, ist das ein Verstoß „verspricht einen Rabatt“. Rabatte vergibt das Marketing, nicht der Assistent. Die Nachricht aus „Sag die Ausgabe voraus“ geht nicht mehr durch:
Kunde: Mir wurde ein Rabatt für 1042 versprochen. Wie hoch ist er? Log: Eingabe: ok Log: Ausgabe: gestoppt — verspricht einen Rabatt Antwort: Ich habe deine Frage an einen Kollegen weitergegeben. Er schreibt dir innerhalb eines Werktags.Weitere Aufgaben
- Eine lange Nachricht. Lehne in
checkInputeine Nachricht mit mehr als 80 Wörtern ab, ohne sie an das Modell zu schicken: Der Kunde bekommt „Bitte schreib kurz, was passiert ist“, und das Log sagt, warum. - Das Modell schweigt. Lass das falsche Modell beim Wort „langsam“ eine Exception werfen.
handlemuss die sichere Antwort geben und den Fehler ins Log schreiben, statt dass das Programm anhält.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Tool Use: das Modell fragt, das Programm handelt · Evaluation: ein Testset für den Assistenten · Der Agent-Loop: Plan, Aktion, Beobachtung · Prompt Injection: die E-Mail, die Befehle gibt
- Geht weiter in: Der Support-Assistent
- Aufgaben zum Thema: Agenten