Zum Inhalt
wannacode

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:

const model = new FakeModel();
const message = "Bestellung 1187 kam kaputt an. Ich habe mit der Karte 4111 1111 1111 1111 bezahlt, ich will mein Geld zurück!";
console.log(`Kunde: ${message}`);
const answer = model.create(SYSTEM, [{ role: "user", content: message }]);
console.log(`Antwort: ${answer}`);
Ausgabe
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:

  1. 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.
  2. 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.
  3. Ein Mensch in der Schleife. Besteht eine Regel nicht, bekommt der Kunde eine sichere Antwort, und der Entwurf geht an einen Kollegen.
  4. 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.
function checkInput(raw, log) {
const text = raw.replace(CARD, "**** $1");
if (text === raw) {
log.push("Eingabe: ok");
} else {
log.push("Eingabe: Kartennummer verborgen");
}
return text;
}
$1 in replace ist die erste Gruppe in Klammern: die letzten vier Ziffern. Der reguläre Ausdruck hat g, also verbirgt er jede Karte, nicht nur die erste.

Die Daten des Shops und die Regeln stehen oben im Programm:

const SYSTEM = "Du bist der Support-Assistent eines Online-Shops. Antworte kurz und nur zu den Bestellungen des Kunden.";
const CUSTOMER = "C-17";
const FALLBACK = "Ich habe deine Frage an einen Kollegen weitergegeben. Er schreibt dir innerhalb eines Werktags.";
const ORDERS = {
"1042": { customer: "C-17", total: 45 },
"1187": { customer: "C-17", total: 89 },
"2210": { customer: "C-23", total: 120 },
};
const MONEY = ["wir erstatten", "wir zahlen"];
const CARD = /\d{4} \d{4} \d{4} (\d{4})/g;
const ORDER_ID = /(?<!\d)\d{4}(?!\d)/g;
const AMOUNT = /(\d+) €/g;
const REVIEW = [];

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“:

function isOwn(id) {
const order = ORDERS[id];
return order !== undefined && order.customer === CUSTOMER;
}
// Nach dem Modell: der Entwurf gegen die Regeln des Shops.
function checkOutput(draft, order) {
const problems = [];
const lower = draft.toLowerCase();
for (const phrase of MONEY) {
if (lower.includes(phrase)) {
problems.push("verspricht Geld");
}
}
for (const [id] of draft.matchAll(ORDER_ID)) {
if (!isOwn(id)) {
problems.push(`${id} ist nicht deine Bestellung`);
}
}
const limit = order === undefined ? 0 : order.total;
for (const [, digits] of draft.matchAll(AMOUNT)) {
const sum = Number(digits);
if (sum > limit) {
problems.push(`${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).
function orderIn(text) {
for (const [id] of text.matchAll(ORDER_ID)) {
if (isOwn(id)) return ORDERS[id];
}
return undefined;
}

Sieh es dir an

Der ganze Weg

function handle(model, message) {
const log = [];
const text = checkInput(message, log);
console.log(`Kunde: ${text}`);
const messages = [{ role: "user", content: text }];
const draft = model.create(SYSTEM, messages);
const problems = checkOutput(draft, orderIn(text));
let answer = draft;
if (problems.length === 0) {
log.push("Ausgabe: ok");
} else {
log.push(`Ausgabe: gestoppt — ${problems.join("; ")}`);
REVIEW.push(draft);
answer = FALLBACK;
}
for (const line of log) {
console.log(` Log: ${line}`);
}
console.log(` Antwort: ${answer}`);
}
Schritt für Schritt, in Worten
  1. Der Kunde schreibt, und die Nachricht enthält eine Kartennummer.
  2. Die Eingabe-Prüfung läuft vor dem Modell: Die Karte wird zu **** 1111, und das Log sagt, warum.
  3. Das Modell bekommt nur den verborgenen Text. Sein Entwurf: „Das tut uns leid! Wir erstatten dir heute 250 €.“
  4. Die Ausgabe-Prüfung misst den Entwurf an den Regeln des Shops: verspricht Geld; 250 € > 89 €.
  5. Eine Regel ist durchgefallen, also bekommt der Kunde die sichere Antwort, und der Entwurf geht an einen Kollegen.
  6. Das Log hält jede Entscheidung fest: was verborgen wurde und welche Regel die Antwort gestoppt hat. Ohne die Karte.
function handle(model, message) {
const log = [];
const text = checkInput(message, log);
console.log(`Kunde: ${text}`);
const messages = [{ role: "user", content: text }];
const draft = model.create(SYSTEM, messages);
const problems = checkOutput(draft, orderIn(text));
let answer = draft;
if (problems.length === 0) {
log.push("Ausgabe: ok");
} else {
log.push(`Ausgabe: gestoppt — ${problems.join("; ")}`);
REVIEW.push(draft);
answer = FALLBACK;
}
for (const line of log) {
console.log(` Log: ${line}`);
}
console.log(` Antwort: ${answer}`);
}
const 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?");
console.log("für einen Kollegen:");
for (const draft of REVIEW) {
console.log(` ${draft}`);
}
Ausgabe
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.push();
answer = FALLBACK;

Probier es aus

Sag die Ausgabe voraus

Der Kunde fragt nach einem Rabatt. Was gibt handle aus?

const model = new FakeModel();
handle(model, "Mir wurde ein Rabatt für 1042 versprochen. Wie hoch ist er?");
Antwort

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 refund ist 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.

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 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."],
];
create(system, messages) {
const text = messages[messages.length - 1].content;
for (const [key, answer] of FakeModel.ANSWERS) {
if (text.includes(key)) return answer;
}
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:

Ausgabe
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 checkInput eine 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. handle muss die sichere Antwort geben und den Fehler ins Log schreiben, statt dass das Programm anhält.

Test

Teste dich

1. Warum wird die Kartennummer vor dem Modell verborgen und nicht danach?
2. Die Ausgabe-Prüfung meldet „verspricht Geld“. Was bekommt der Kunde?
3. „Ja, für 1042 hast du 10 € Rabatt.“ besteht die Prüfungen. Warum?
4. Was sollte das Log über die zweite Nachricht sagen?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·