Zum Inhalt
wannacode

Der Prompt ist Code

Kurz gesagt: Ein Prompt ist der Text, der dem Modell eine Rolle, Regeln und Daten gibt; eine Vorlage mit Beispielen und Prüfungen der Antwort lebt wie jeder andere Code im Code, in Git und in den Tests.

Problem

Die Antwort, die gut klingt

Ein Kunde schreibt im Chat des Shops: „Wann kommt Bestellung 1042 an?“ Der Assistent ist wie in der Lektion über den API-Aufruf an ein Modell angeschlossen, und der System-Prompt ist eine Zeile:

const client = new FakeClient();
const order = { id: "1042", status: "versandt", eta: "3. November" };
const question = [{ role: "user", content: "Wann kommt Bestellung 1042 an?" }];
const reply = client.create("Du bist ein Shop-Assistent.", question);
console.log(`Prompt: ~${reply.inputTokens} Tokens`);
console.log(`Antwort: ${reply.text}`);
for (const line of check(reply.text, order)) {
console.log(` ${line}`);
}
Ausgabe
Prompt: ~13 Tokens
Antwort: Hallo! Danke, dass Sie sich an uns gewandt haben. Die Lieferung dauert meist 2 bis 5 Werktage, je nach Kurier und Adresse. Bestellung 1042 kommt höchstwahrscheinlich bis Freitag an. Wenn Sie weitere Fragen haben, helfe ich gern!
✗ höchstens 30 Wörter (37)
✗ das Datum aus den Daten (3. November)
✓ die Bestellnummer (1042)

Die Antwort ist höflich, aber sie hat 37 Wörter, wo im Chat Platz für zwei Sätze ist. „2 bis 5 Werktage“ ist allgemein, und „bis Freitag“ ist erfunden: In der Datenbank kommt die Bestellung am 3. November an. Der Kunde wartet am Freitag und schreibt dann wütend noch einmal.

Das Modell hat nichts falsch gemacht, was es hätte richtig machen können. Es weiß nichts über Bestellung 1042 und schreibt den wahrscheinlichsten Text, wie in der Lektion über die Grenzen des Modells. Das Problem ist der Prompt.

Die Idee

Der Prompt ist eine Spezifikation

Der Prompt sagt dem Modell, was es tun soll, so wie eine Aufgabenbeschreibung es einem neuen Kollegen sagt. Ein guter Prompt hat fünf Teile:

  1. Rolle: wer das Modell ist („der Support-Assistent des Shops“).
  2. Regeln: wie lang, in welchem Ton, was nicht erlaubt ist.
  3. Daten: alles, was das Modell wissen muss, vom Programm hineingelegt. Hier ist es die Bestellung aus der Datenbank.
  4. Eine Grenze um die Daten: Tags wie <order> und </order> zeigen, wo die Anweisungen enden und die Daten beginnen.
  5. Was zu tun ist, wenn es etwas nicht weiß: „wenn die Daten nicht reichen, gib an einen Kollegen weiter“. Ohne diese Regel füllt das Modell die Lücke mit Erfundenem.

Den Prompt baut eine Funktion mit Parametern, er wird nicht für jede Frage von Hand getippt. Diese Funktion ist eine Prompt-Vorlage (Prompt-Template). Solche Prompts zu schreiben und zu testen heißt Prompt Engineering: Der Prompt ist Code, den das Modell ausführt.

Sieh es dir an

Die Vorlage

Die Vorlage nimmt die Bestellung und gibt den System-Prompt zurück:

function supportPrompt(order) {
return `Du bist der Support-Assistent eines Shops.
Antworte höflich mit „Sie“, in höchstens zwei Sätzen.
Nutze nur die Daten zwischen <order> und </order>.
Wenn die Daten nicht reichen, gib an einen Kollegen weiter.
Verspreche keine Termine, die nicht in den Daten stehen.
<order>
Nummer: ${order.id}
Status: ${order.status}
voraussichtliche Lieferung: ${order.eta}
</order>`;
}
const order = { id: "1042", status: "versandt", eta: "3. November" };
const system = supportPrompt(order);
console.log(system);
console.log(`~${tokens(system)} Tokens`);
Ausgabe
Du bist der Support-Assistent eines Shops.
Antworte höflich mit „Sie“, in höchstens zwei Sätzen.
Nutze nur die Daten zwischen <order> und </order>.
Wenn die Daten nicht reichen, gib an einen Kollegen weiter.
Verspreche keine Termine, die nicht in den Daten stehen.
<order>
Nummer: 1042
Status: versandt
voraussichtliche Lieferung: 3. November
</order>
~67 Tokens

Die Daten kommen nicht vom Modell, sondern vom Programm: Status und Datum werden aus der Datenbank gelesen. Wenn die Bestellung morgen ihren Status ändert, ändert sich der Prompt mit.

Der Preis: Der vage Prompt hatte etwa 6 Tokens, dieser hat etwa 67. Jede Frage im Chat bezahlt diese Tokens erneut, denn die API hat kein Gedächtnis, und der Prompt geht mit jeder Anfrage mit. Was ein Token ist und warum der Preis in Tokens angegeben wird, hast du in der Lektion über Tokens gesehen.

Sieh es dir an

Eine Frage, zwei Prompts

const vague = "Du bist ein Shop-Assistent.";
const prompts = [vague, supportPrompt(order)];
for (const system of prompts) {
const reply = client.create(system, question);
console.log(`~${reply.inputTokens} Tokens`);
console.log(reply.text);
for (const line of check(reply.text, order)) {
console.log(` ${line}`);
}
}
Schritt für Schritt, in Worten
  1. Eine Frage eines Kunden: „Wann kommt Bestellung 1042 an?“ Zuerst der vage Prompt: eine Zeile ohne Daten, ~13 Tokens.
  2. Die Antwort ist höflich und lang. „Bis Freitag“ ist erfunden: Im Prompt steht kein Datum, aus dem das Modell den Termin nehmen könnte.
  3. Das Programm prüft die Antwort: 37 Wörter, kein Datum aus den Daten. Sie besteht 1 von 3.
  4. Die Vorlage: wer das Modell ist, wie lang es antworten soll, was es benutzen darf und was es tun soll, wenn die Daten nicht reichen.
  5. Die Daten der Bestellung kommen vom Programm, zwischen <order> und </order>. Der Prompt hat jetzt ~74 Tokens statt ~13.
  6. Dieselbe Frage, der neue Prompt: ein Satz mit dem Datum 3. November.
  7. Der vage Prompt besteht 1 Prüfung von 3, der klare 3 von 3. Der klare Prompt ist länger, aber seine Antwort lässt sich prüfen.

Das Programm schickt dieselbe Frage mit beiden Prompts und prüft jede Antwort mit drei Regeln: die Länge, das Datum aus den Daten und die Bestellnummer:

function check(answer, order) {
const words = answer.trim().split(/\s+/).length;
return [
`${mark(words <= 30)} höchstens 30 Wörter (${words})`,
`${mark(answer.includes(order.eta))} das Datum aus den Daten (${order.eta})`,
`${mark(answer.includes(order.id))} die Bestellnummer (${order.id})`,
];
}
function mark(ok) {
return ok ? "✓" : "✗";
}
const client = new FakeClient();
const order = { id: "1042", status: "versandt", eta: "3. November" };
const question = [{ role: "user", content: "Wann kommt Bestellung 1042 an?" }];
const vague = "Du bist ein Shop-Assistent.";
const prompts = [vague, supportPrompt(order)];
for (const system of prompts) {
const reply = client.create(system, question);
console.log(`~${reply.inputTokens} Tokens`);
console.log(reply.text);
for (const line of check(reply.text, order)) {
console.log(` ${line}`);
}
}
Ausgabe
~13 Tokens
Hallo! Danke, dass Sie sich an uns gewandt haben. Die Lieferung dauert meist 2 bis 5 Werktage, je nach Kurier und Adresse. Bestellung 1042 kommt höchstwahrscheinlich bis Freitag an. Wenn Sie weitere Fragen haben, helfe ich gern!
✗ höchstens 30 Wörter (37)
✗ das Datum aus den Daten (3. November)
✓ die Bestellnummer (1042)
~74 Tokens
Bestellung 1042 ist versandt und kommt voraussichtlich am 3. November an.
✓ höchstens 30 Wörter (11)
✓ das Datum aus den Daten (3. November)
✓ die Bestellnummer (1042)

Der vage Prompt besteht 1 Prüfung von 3, der klare 3 von 3. Die Prüfungen sind Code: Sie sagen nicht, ob die Antwort schön ist, nur ob sie die Regeln einhält, die sich prüfen lassen. Wie aus solchen Prüfungen ein Testset für die ganze Anwendung wird, steht in der Lektion über Evaluation.

Ehrlich zum Modell in den Programmen. Das Modell ist hier ein falscher Client: dieselbe Form wie die echte API (System-Prompt, Nachrichten, Antwort und Tokens), aber die Antworten sind aufgezeichnet. Seine Regel ist grob: Wenn der Prompt Bestelldaten hat, antwortet er daraus; wenn nicht, gibt er die typische allgemeine Antwort zurück. So läuft das Programm ohne Netz und gibt jedes Mal dieselbe Ausgabe. Ein echtes Modell gibt bei jedem Lauf andere Worte, aber der Unterschied zwischen den beiden Prompts bleibt derselbe. Der Code des falschen Clients steht unten unter „Genauer, für Neugierige“.

Probier es aus

Finde den Fehler

Ein Kollege baut die Vorlage für die Frage „wo sind meine Bestellungen?“ um und übergibt alle Bestellungen aus der Datenbank. Die Tests laufen durch: Die Antworten sind kurz und haben die richtigen Daten. In welcher Zeile steckt der Fehler?

Wähle die Zeile mit dem Fehler.

Die Idee

Beispiele statt Erklärungen

Manchmal geht die Antwort nicht an einen Kunden, sondern in Code. Die E-Mails im Support-Postfach sollen nach Thema sortiert werden: Lieferung, Rückgabe, Zahlung oder Sonstiges. Das Programm erwartet genau eins der vier Wörter, um die E-Mail in die richtige Warteschlange zu legen.

Du kannst das Format in Worten erklären („antworte nur mit der Kategorie“). Sicherer ist es, es zu zeigen: ein paar Kundennachrichten, jede mit der Antwort, die du erwartest. Sie stehen im Gesprächsverlauf vor der echten E-Mail, als frühere Züge des Modells.

Das heißt Beispiele im Prompt (Few-Shot). Das Modell setzt den Text nach dem Muster fort, das es sieht, deshalb halten Beispiele das Format besser als Erklärungen. Aus der Lektion über das nächste Token weißt du, warum: Das Modell gibt jeder Fortsetzung eine Wahrscheinlichkeit, und die Beispiele machen das genaue Format zur wahrscheinlichsten.

Sieh es dir an

Drei Beispiele, vier E-Mails

Die Anweisung und die erlaubten Kategorien:

const SYSTEM = "Bestimme das Thema der Kundennachricht. "
+ "Kategorien: lieferung, rückgabe, zahlung, sonstiges. "
+ "Antworte nur mit der Kategorie.";
const LABELS = ["lieferung", "rückgabe", "zahlung", "sonstiges"];

Die drei Beispiele sind Paare von Nachrichten: der Kunde (user) und die Antwort, die wir wollen (assistant). Die letzte Nachricht ist die echte E-Mail:

function withExamples(message) {
return [
{ role: "user", content: "Mein Paket ist noch nicht da, dabei ist schon eine Woche vergangen." },
{ role: "assistant", content: "lieferung" },
{ role: "user", content: "Ich möchte die Schuhe zurückgeben, sie sind zu klein." },
{ role: "assistant", content: "rückgabe" },
{ role: "user", content: "Meine Karte wurde zweimal belastet." },
{ role: "assistant", content: "zahlung" },
{ role: "user", content: message },
];
}

Das Programm akzeptiert nur eine exakte Kategorie. Ein wenig Rauschen erlaubt es: Leerzeichen, Großbuchstaben und einen Punkt am Ende:

function parseLabel(answer) {
let label = answer.trim().toLowerCase();
if (label.endsWith(".")) label = label.slice(0, -1);
return LABELS.includes(label) ? label : null;
}

Dieselben vier E-Mails ohne Beispiele und mit Beispielen:

const client = new FakeClient();
const inbox = [
"Der Kurier hat mich an der Adresse nicht gefunden.",
"Wie gebe ich die Jacke zurück, wenn ich das Etikett entfernt habe?",
"Die Zahlung mit Karte klappt nicht.",
"Habt ihr ein Geschäft in Hamburg?",
];
for (const examples of [false, true]) {
console.log(examples ? "mit Beispielen:" : "ohne Beispiele:");
let parsed = 0;
for (const message of inbox) {
const messages = examples ? withExamples(message) : [{ role: "user", content: message }];
const answer = client.create(SYSTEM, messages).text;
const label = parseLabel(answer);
if (label !== null) parsed++;
console.log(` "${answer}" → ${label ?? "?"}`);
}
console.log(` erkannt: ${parsed} von ${inbox.length}`);
}
Ausgabe
ohne Beispiele:
"Kategorie: lieferung" → ?
"Rückgabe der Ware" → ?
"Zahlung." → zahlung
"Das gehört in keine der Kategorien, ich würde „sonstiges“ sagen." → ?
erkannt: 1 von 4
mit Beispielen:
"lieferung" → lieferung
"rückgabe" → rückgabe
"zahlung" → zahlung
"sonstiges" → sonstiges
erkannt: 4 von 4

Ohne Beispiele trifft das Modell das Thema, verpackt es aber in Worte, und das Programm liest nur „Zahlung.“. Mit drei Beispielen haben die Antworten genau das nötige Format: 4 von 4.

Probier es aus

Sag die Ausgabe voraus

Was gibt parseLabel für diese drei Antworten aus?

for (const answer of [" Lieferung.", "Kategorie: lieferung", "rückgabe"]) {
console.log(parseLabel(answer));
}
Antwort

Probier es aus

Ergänze

Ein Beispiel ist ein Paar von Nachrichten. Welche Rolle hat die zweite, die Antwort, die wir sehen wollen?

{ role: "user", content: "Mein Paket ist noch nicht da, dabei ist schon eine Woche vergangen." },
{ role: "", content: "lieferung" },

Die Idee

Der Prompt ist Code

Ein Wort im Prompt ändert das Verhalten der ganzen Anwendung, genau wie eine Zeile Code. Deshalb behandelst du ihn wie Code:

  • Im Code, nicht in den Einstellungen. Die Vorlage ist eine Funktion oder eine Konstante im Projekt, kein Text, den jemand auf dem Server bearbeitet. So liegt sie in Git: Du siehst, wer was geändert hat, und kannst die alte Version zurückholen.
  • Die Daten getrennt von den Anweisungen. Die Regeln stehen im System-Prompt, die Daten zwischen Tags, und die Frage des Kunden ist eine eigene Nachricht mit der Rolle user. Der Text des Kunden wird nie in die Anweisungen geklebt.
  • Eine Prüfung vor dem Release. Eine Änderung am Prompt, die eine Antwort behebt, kann eine andere kaputt machen. Eine Reihe von Fragen mit Prüfungen, die du bei jeder Änderung laufen lässt, steht in der Lektion über Evaluation.
  • Was Code kann, macht der Code. Das Lieferdatum, die Summe und die Rückgabefrist werden im Programm berechnet und fertig in den Prompt gelegt. Das Modell erzählt sie nach; es rechnet sie nicht aus.

Die Antwort eines echten Modells ändert sich von Lauf zu Lauf. Deshalb schauen die Prüfungen auf Regeln (Länge, Datum, Format), nicht auf exakte Worte.

Die Idee

Wann es wehtut

  • Ein langer Prompt wird jedes Mal bezahlt. 67 Tokens mal 10.000 Fragen am Tag sind 670.000 Tokens allein für den Prompt. Lege in den Prompt, was diese Frage braucht, nicht das ganze Handbuch des Shops.
  • Viele Regeln streiten. „Antworte kurz“ und „erkläre die Begriffe ausführlich“ in einem Prompt geben eine zufällige Wahl zwischen beiden. Wenn die Regeln zwanzig erreichen, behält das Modell nur einige.
  • Beispiele ziehen. Wenn alle drei Beispiele von Lieferung handeln, sagt das Modell öfter „lieferung“. Die Beispiele müssen jeden Fall abdecken und dürfen nicht alle gleich sein.
  • Der Prompt ist kein Schloss. „Gib nie fremde Daten preis“ ist eine Bitte, kein Schutz. Was im Prompt steht, kann herauskommen, deshalb enthält er nur, was diese Person sehen darf. Der Text des Kunden gehört zu den nicht vertrauenswürdigen Eingaben; wie du eine Anwendung vor darin versteckten Anweisungen schützt, steht in der Lektion über Prompt Injection im Modul „Agenten“.
  • Der Prompt behebt keine schlechten Daten. Wenn das Datum in der Datenbank falsch ist, wiederholt der beste Prompt es genau.
Genauer, für Neugierige

Der echte Aufruf (bei dir ausprobieren). Dieses Programm gehört nicht zur Prüfung der Lektion: Du brauchst ein Konto bei einem Modellanbieter, einen Schlüssel und Geld für jeden Aufruf. Der Schlüssel steht in der Umgebungsvariable LLM_API_KEY, nie im Code. Warum, steht in der Lektion über Geheimnisse im Code und in Git. Die Anbieter liefern offizielle SDKs für Java, C#, Python und JavaScript mit derselben Form (Nachrichten, Rollen, Tokens, Tools); die genauen Namen von Klassen und Methoden stehen in der Dokumentation des Anbieters. Hier ist die Anfrage als JSON, mit den üblichen Feldnamen und einem Platzhalter für den Namen des Modells:

{
"model": "<Name des Modells>",
"max_tokens": 1024,
"system": "Du bist der Support-Assistent eines Shops.\nAntworte höflich mit „Sie“, in höchstens zwei Sätzen.\nNutze nur die Daten zwischen <order> und </order>.\nWenn die Daten nicht reichen, gib an einen Kollegen weiter.\nVerspreche keine Termine, die nicht in den Daten stehen.\n\n<order>\nNummer: 1042\nStatus: versandt\nvoraussichtliche Lieferung: 3. November\n</order>",
"messages": [
{
"role": "user",
"content": "Wann kommt Bestellung 1042 an?"
}
]
}

Eine Beispielantwort: die Blöcke in content, stop_reason und die Tokens in usage:

{
"content": [
{
"type": "text",
"text": "Bestellung 1042 ist versandt und kommt voraussichtlich am 3. November an."
}
],
"stop_reason": "end_turn",
"usage": {"input_tokens": 74, "output_tokens": 15}
}

Ohne SDK geht die Anfrage mit dem Standard-HTTP-Client der Sprache hinaus. Die Adresse ist ein Platzhalter:

// requestJson ist das JSON der Anfrage oben.
// Der genaue Header für den Schlüssel steht in der Doku des Anbieters.
const response = await fetch("https://<anbieter>/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LLM_API_KEY}`,
"Content-Type": "application/json",
},
body: requestJson,
});
console.log(await response.text());

Der einzige Unterschied zu der Lektion über den Aufruf ist system: Statt einer Textzeile ist es das Ergebnis der Vorlage.

Der falsche Client von innen. Er behält die Form der Anfrage und der Antwort. Er zählt Tokens grob: Wörter × 1,3, aufgerundet. Die echte Zahl hängt vom Tokenizer ab und ist von Sprache zu Sprache verschieden.

// Steht für das echte Modell: gibt eine aufgezeichnete Antwort zurück,
// ohne Netz und jedes Mal dieselbe.
class FakeClient {
static VAGUE = "Hallo! Danke, dass Sie sich an uns gewandt haben. Die Lieferung dauert meist 2 bis 5 Werktage, je nach Kurier und Adresse. Bestellung 1042 kommt höchstwahrscheinlich bis Freitag an. Wenn Sie weitere Fragen haben, helfe ich gern!";
static VERBOSE = {
"lieferung": "Kategorie: lieferung",
"rückgabe": "Rückgabe der Ware",
"zahlung": "Zahlung.",
"sonstiges": "Das gehört in keine der Kategorien, ich würde „sonstiges“ sagen.",
};
create(system, messages) {
const text = this.answer(system, messages);
let input = tokens(system);
for (const m of messages) input += tokens(m.content);
return { text, stopReason: "end_turn", inputTokens: input, outputTokens: tokens(text) };
}
answer(system, messages) {
const last = messages.at(-1).content;
if (system.includes("Kategorien:")) {
const label = labelOf(last);
return messages.length > 1 ? label : FakeClient.VERBOSE[label];
}
const eta = field(system, "voraussichtliche Lieferung: ");
if (eta === null) return FakeClient.VAGUE;
const number = field(system, "Nummer: ");
const status = field(system, "Status: ");
return `Bestellung ${number} ist ${status} und kommt voraussichtlich am ${eta} an.`;
}
}
// Grob: Wörter × 1,3, aufgerundet.
function tokens(text) {
const t = text.trim();
return Math.floor(((t ? t.split(/\s+/).length : 0) * 13 + 9) / 10);
}
function labelOf(message) {
const m = message.toLowerCase();
if (m.includes("kurier") || m.includes("paket")) return "lieferung";
if (m.includes("zurück")) return "rückgabe";
if (m.includes("karte") || m.includes("zahlung")) return "zahlung";
return "sonstiges";
}
function field(system, name) {
const line = system.split("\n").find((l) => l.startsWith(name));
return line === undefined ? null : line.slice(name.length);
}

Warum Tags. <order> und </order> sind keine besondere Syntax. Das Modell hat viel Text mit solchen Tags gesehen und versteht, wo der Block beginnt und endet. ### oder Anführungszeichen funktionieren genauso gut, solange sie überall gleich sind.

„Denk Schritt für Schritt“. Bei Aufgaben mit mehreren Schritten (eine Summe, ein Vergleich) hilft es, wenn das Modell zuerst seine Überlegung schreibt und dann die Antwort. Das kostet mehr Tokens und Zeit, und die Antwort muss von der Überlegung getrennt werden, bevor sie das Programm erreicht. Die strukturierte Ausgabe aus der nächsten Lektion macht diese Trennung zuverlässig.

Ein Template-Literal mit Backticks behält die Einrückung wörtlich. Deshalb haben die Zeilen der Vorlage keine Einrückung, obwohl sie in einer Funktion stehen.

Übung

Eine Vorlage für Rückgaben

Löse die Aufgabe in deinem Editor.

Schreibe eine Vorlage returnPrompt(order) für Fragen zu Rückgaben. Die Bestellung hat eine Nummer, ein Lieferdatum (oder keins, wenn sie noch nicht geliefert ist) und eine Summe in Euro. Die Vorlage hat drei Regeln und die Daten zwischen <order> und </order>. Wenn es kein Datum gibt, steht in den Daten „noch nicht geliefert“: Der Code entscheidet, was gesagt wird, nicht das Modell. Führe sie für beide Bestellungen aus und gib nach jedem Prompt --- aus.

Ausgabe
Du bist der Support-Assistent eines Online-Shops.
Der Kunde kann die Bestellung bis zu 14 Tage nach der Lieferung zurückgeben.
Nutze nur die Daten zwischen <order> und </order>.
<order>
Nummer: 1187
geliefert am: 21.10
Summe: 89.90 €
</order>
---
Du bist der Support-Assistent eines Online-Shops.
Der Kunde kann die Bestellung bis zu 14 Tage nach der Lieferung zurückgeben.
Nutze nur die Daten zwischen <order> und </order>.
<order>
Nummer: 1203
geliefert am: noch nicht geliefert
Summe: 34.50 €
</order>
---

Weitere Aufgaben

  • Die Frist im Code. Füge den Daten eine Zeile „Rückgabe möglich bis:“ hinzu, mit dem Datum 14 Tage nach der Lieferung, im Programm berechnet.
  • Noch eine Prüfung. Füge in check die Regel „kein Versprechen eines Wochentags“ hinzu: Die Antwort enthält nicht „Montag“, „Dienstag“ und so weiter bis „Sonntag“. Wie viele Prüfungen besteht dann die vage Antwort?

Test

Teste dich

1. Der Assistent verspricht „bis Freitag“, aber in der Datenbank steht 3. November. Welche Korrektur ist richtig?
2. Das Programm erwartet eine von vier Kategorien, und das Modell liefert „Kategorie: lieferung“. Was hilft am meisten?
3. Die Vorlage legt die Bestellungen jedes Kunden in den Prompt, um „vollständiger“ zu sein. Wo liegt das Problem?
4. Warum lebt der Prompt im Code, statt von Hand in die Einstellungen des Servers getippt zu werden?

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