Ein Modell über eine API aufrufen
Kurz gesagt: Ein Sprachmodell über eine API aufzurufen heißt: eine Anfrage mit Modell, System-Prompt und Nachrichten mit Rollen schicken; die Antwort enthält den Text, den Grund fürs Aufhören und die Zahl der Tokens.
Problem
Der Assistent tritt vor die Kunden
Bisher waren die Modelle klein und lebten im Programm. Der Support-Assistent muss echten Kunden antworten, und dafür braucht es ein großes Modell bei einem Anbieter. Das Programm fragt es über eine API: Es schickt eine Anfrage über das Netzwerk und wartet auf eine Antwort.
Am ersten Tag des Sales sehen Kunden „Fehler 429“ statt einer Antwort. Ein anderer Kunde wird vom Assistenten nach der Bestellnummer gefragt, die er gerade genannt hat.
Wie sieht eine Anfrage aus, was liefert das Modell zurück, und was tust du, wenn die Antwort ein Fehler ist?
Die Idee
Anfrage und Antwort
Ein Modell aufzurufen ist eine HTTP-Anfrage mit JSON. Die Anfrage enthält:
model: welches Modell;max_tokens: aus höchstens wie vielen Tokens die Antwort bestehen darf;system: den System-Prompt, die Anweisung für Rolle und Ton;messages: die Nachrichten mit ihren Rollen,user(der Kunde) undassistant(frühere Antworten des Modells).
Die Antwort enthält:
content: eine Liste von Blöcken mit einemtype; der Text steht in einemtext-Block;stop_reason: warum das Modell aufgehört hat,end_turn(es war fertig) odermax_tokens(es hat die Obergrenze erreicht);usage: wie viele Tokens hineingingen (input_tokens) und wie viele herauskamen (output_tokens). Danach bezahlst du.
Die API hat kein Gedächtnis. Jede Anfrage steht für sich und trägt alles mit, was das Modell sehen muss, wie in der Lektion über das Kontextfenster. Die APIs verschiedener Anbieter haben dieselbe Form: Nachrichten mit Rollen, Tokens, Tools.
Sieh es dir an
Ein falscher Client mit aufgezeichneten Antworten
Die Programme der Lektion rufen kein echtes Modell auf: Ihre Prüfung hat kein Netzwerk, und die echte Antwort ist jedes Mal anders (die Lektion über die Grenzen). Statt des Clients des Anbieters gibt es deshalb einen Ersatz mit derselben Form: Er nimmt model, max_tokens, system und messages und liefert content, stop_reason und usage. Die Antworten sind vorab aufgezeichnet und werden nach einem Wort in der letzten Nachricht gewählt. Die Tokens werden grob gezählt, 4 auf 3 Wörter.
class ApiError extends Error { constructor(status, message) { super(message); this.status = status; }}// ein Ersatz für den echten Client: aufgezeichnete Antworten, kein Netzwerkclass FakeClient { constructor(failures = []) { this.failures = [...failures]; }
create({ model, max_tokens, system, messages }) { if (this.failures.length > 0) { const status = this.failures.shift(); throw new ApiError(status, reason(status)); } let text = answer(messages); let stop_reason = "end_turn"; if (tokens(text) > max_tokens) { // max_tokens: die Antwort bricht mittendrin ab text = text.split(" ").slice(0, Math.floor((max_tokens * 3) / 4)).join(" "); stop_reason = "max_tokens"; } let input_tokens = tokens(system); for (const m of messages) { input_tokens += tokens(m.content); } const usage = { input_tokens, output_tokens: tokens(text) }; return { content: [{ type: "text", text }], stop_reason, usage }; }}
// die aufgezeichnete Antwort: gewählt nach einem Wort in der letzten Nachrichtfunction answer(messages) { const last = messages[messages.length - 1].content; const hasOrder = messages.some((m) => m.content.includes("1042")); if (last.includes("zurück")) return "Du kannst die Ware bis 14 Tage nach Erhalt zurückgeben."; if (last.includes("kommt")) { return hasOrder ? "Bestellung 1042 ist beim Kurier und kommt morgen bis 18 Uhr." : "Wie lautet deine Bestellnummer?"; } return "Wie kann ich helfen: Bestellstatus, Lieferung oder Rückgabe?";}
// der Fehlertext zu seinem Codefunction reason(status) { const reasons = { 400: "ungültige Anfrage", 401: "ungültiger Schlüssel", 429: "zu viele Anfragen", }; return reasons[status] ?? "Serverfehler";}
// grob: 4 Tokens auf 3 Wörter (die echte Zahl liefert der Tokenizer)function tokens(text) { const words = text.trim().split(/\s+/).length; return Math.floor((words * 4 + 2) / 3);}Ein Aufruf, dann dasselbe mit max_tokens 8:
// die Textblöcke, dann warum es aufgehört hat und wie viele Tokensfunction show(response) { for (const block of response.content) { if (block.type === "text") console.log(block.text); } const u = response.usage; console.log(`stop_reason: ${response.stop_reason}, input_tokens: ${u.input_tokens}, output_tokens: ${u.output_tokens}`);}const client = new FakeClient();const system = "Du bist ein Support-Assistent eines Online-Shops. Antworte kurz.";const messages = [{ role: "user", content: "Wann kommt Bestellung 1042 an?" }];
let response = client.create({ model: MODEL, max_tokens: 1024, system, messages });show(response);
response = client.create({ model: MODEL, max_tokens: 8, system, messages });show(response);Bestellung 1042 ist beim Kurier und kommt morgen bis 18 Uhr.stop_reason: end_turn, input_tokens: 18, output_tokens: 15Bestellung 1042 ist beim Kurier undstop_reason: max_tokens, input_tokens: 18, output_tokens: 8Bei 8 Tokens bricht die Antwort mitten im Satz ab, und stop_reason ist max_tokens. Das Programm muss das prüfen: Eine abgeschnittene Antwort sieht beim Kunden wie ein Fehler des Shops aus.
Sieh es dir an
Bei dir ausführen: der echte Aufruf
Derselbe Aufruf an ein echtes Modell. Er gehört nicht zu den geprüften Programmen und läuft hier nicht: Du brauchst ein Konto bei einem Anbieter und einen Schlüssel in der Umgebung, und jeder Aufruf kostet Geld.
Über das Netzwerk geht JSON. Die Anfrage:
{ "model": "<Name des Modells>", "max_tokens": 1024, "system": "Du bist ein Support-Assistent eines Online-Shops. Antworte kurz.", "messages": [ { "role": "user", "content": "Wann kommt Bestellung 1042 an?" } ]}Und die Antwort (die Zahlen hier stammen vom falschen Client):
{ "content": [ { "type": "text", "text": "Bestellung 1042 ist beim Kurier und kommt morgen bis 18 Uhr." } ], "stop_reason": "end_turn", "usage": { "input_tokens": 18, "output_tokens": 15 }}Mit dem Standard-HTTP-Client der Sprache ist das eine einzige POST-Anfrage. Die Adresse, der Name des Modells und der Header mit dem Schlüssel sind Platzhalter: Die echten stehen in der Dokumentation des Anbieters. Der Schlüssel wird aus der Variable LLM_API_KEY gelesen.
const response = await fetch("https://<anbieter>/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.LLM_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "<Name des Modells>", max_tokens: 1024, system: "Du bist ein Support-Assistent eines Online-Shops. Antworte kurz.", messages: [{ role: "user", content: "Wann kommt Bestellung 1042 an?" }], }),});if (!response.ok) throw new Error(`HTTP ${response.status}`);const reply = await response.json();for (const block of reply.content) { if (block.type === "text") console.log(block.text);}Anbieter liefern auch offizielle Bibliotheken (SDKs) für Java, C#, Python und JavaScript mit derselben Form: Nachrichten mit Rollen, Tokens, Tools. Die genauen Namen der Klassen und Methoden stehen in ihrer Dokumentation. await: Der Aufruf geht über das Netzwerk und ist asynchron.
Die Idee
Der Schlüssel liegt in der Umgebung, nicht im Code
Der API-Schlüssel ist wie ein Passwort für das Konto: Wer ihn hat, gibt davon Geld aus. Im Code landet er in Git, bei jedem Kollegen und manchmal in einem öffentlichen Repository. Deshalb wird er aus einer Umgebungsvariable gelesen, hier LLM_API_KEY.
const key = process.env.LLM_API_KEY ?? "";if (key.trim() === "") { console.log("Kein Schlüssel: setze LLM_API_KEY in der Umgebung.");} else { console.log("Der Schlüssel wird aus der Umgebung geladen. Gib ihn nicht aus und schreib ihn nicht in den Code.");}Kein Schlüssel: setze LLM_API_KEY in der Umgebung.Das Programm sagt, ob der Schlüssel da ist, gibt ihn aber nie aus. Die Datei mit lokalen Geheimnissen (zum Beispiel .env) steht in der .gitignore.
Sieh es dir an
Eine API ohne Gedächtnis
ask hängt die Frage an den Verlauf an, schickt den ganzen Verlauf und behält die Antwort mit der Rolle assistant. Am Ende dieselbe Frage, allein geschickt:
// hängt die Frage an, schickt den ganzen Verlauf und behält die Antwortfunction ask(client, system, history, text) { history.push({ role: "user", content: text }); const r = client.create({ model: MODEL, max_tokens: 1024, system, messages: history }); const reply = r.content[0].text; history.push({ role: "assistant", content: reply }); return reply;}const client = new FakeClient();const system = "Du bist ein Support-Assistent eines Online-Shops. Antworte kurz.";const history = [];for (const text of ["Hallo, ich habe Bestellung 1042.", "Wann kommt sie an?"]) { console.log(`Kunde: ${text}`); console.log(`Assistent: ${ask(client, system, history, text)}`);}
const alone = client.create({ model: MODEL, max_tokens: 1024, system, messages: [{ role: "user", content: "Wann kommt sie an?" }],});console.log(`nur die Frage, ohne den Verlauf: ${alone.content[0].text}`);Kunde: Hallo, ich habe Bestellung 1042.Assistent: Wie kann ich helfen: Bestellstatus, Lieferung oder Rückgabe?Kunde: Wann kommt sie an?Assistent: Bestellung 1042 ist beim Kurier und kommt morgen bis 18 Uhr.nur die Frage, ohne den Verlauf: Wie lautet deine Bestellnummer?Mit dem Verlauf sieht das Modell die Nummer 1042 aus der ersten Nachricht. Ohne ihn fragt es danach. Das Gedächtnis des Gesprächs liegt im Programm, nicht im Modell, und der Verlauf wächst und wird in jeder Runde mitbezahlt.
Probier es aus
Finde den Fehler
Ein Kollege schreibt ask. Bei der zweiten Frage fragt der Assistent wieder nach der Bestellnummer. In welcher Zeile steckt der Fehler?
Wähle die Zeile mit dem Fehler.
Der Verlauf wird geführt, aber nicht geschickt: Die Anfrage trägt nur die letzte Nachricht. Das Modell erinnert sich nicht an frühere Anfragen, also sieht es eine Frage ohne Nummer. Der ganze Verlauf muss mitgeschickt werden.
Probier es aus
Ergänze
Mit welcher Rolle kommt die Antwort des Modells in den Verlauf?
function ask(client, system, history, text) {
history.push({ role: "user", content: text });
const r = client.create({ model: MODEL, max_tokens: 1024, system, messages: history });
const reply = r.content[0].text;
history.push({ role: , content: reply });
return reply;
}Die Antworten des Modells haben die Rolle "assistant", die Nachrichten des Kunden "user". So weiß das Modell bei der nächsten Anfrage, was es gesagt hat und was der Kunde gesagt hat.
Die Idee
Die Fehler der API
Das Netzwerk und der Server des Anbieters fallen aus wie jeder fremde Dienst. Die Fehler kommen mit einem HTTP-Code und fallen in zwei Gruppen:
- Vorübergehend: 429 (zu viele Anfragen, ein Limit erreicht), 500 und die anderen 5xx (der Server hat ein Problem oder ist überlastet), 408 und 409. Einen Moment später kann dieselbe Anfrage durchgehen. Diese lohnt es sich zu wiederholen.
- Dauerhaft: 400 (fehlerhafte Anfrage), 401 (ungültiger Schlüssel), 404. Eine Wiederholung liefert dasselbe. Diese meldet das Programm sofort.
Eine Wiederholung wartet zwischen den Versuchen, und die Pause verdoppelt sich: 1, 2, 4 Sekunden. Ein kurzes Problem geht schnell vorbei, und ein überlasteter Server bekommt nicht einen Strom neuer Anfragen. Das ist exponentielles Backoff. Nach einer festen Zahl von Versuchen gibt das Programm auf.
Sieh es dir an
Wiederholungen mit einer Wartezeit, die sich verdoppelt
// wiederholt die vorübergehenden Fehler, die Wartezeit verdoppelt sichfunction createWithRetry(client, system, messages) { const maxAttempts = 4; let delay = 1; for (let attempt = 1; ; attempt++) { try { return client.create({ model: MODEL, max_tokens: 1024, system, messages, }); } catch (e) { const last = attempt === maxAttempts; if (last || !retryable(e.status)) throw e; console.log(` Versuch ${attempt}: ${e.status},`, `warte ${delay} s`); // ein echtes Programm wartet hier: // await sleep(delay * 1000) delay *= 2; } }}
// vorübergehend: 408, 409, 429 und 5xxfunction retryable(status) { return status === 408 || status === 409 || status === 429 || status >= 500;}Schritt für Schritt, in Worten
- Die Anfrage: das Modell, der System-Prompt und die Nachrichten mit ihren Rollen. Hier ist es eine, vom Kunden (user).
- Der erste Versuch liefert 429: „zu viele Anfragen“. Ein vorübergehender Fehler.
- 429 steht bei den vorübergehenden Fehlern, also wartet das Programm 1 s und versucht es noch einmal.
- Der zweite Versuch: 500, „Serverfehler“. Auch vorübergehend.
- Die Wartezeit verdoppelt sich: 2 s. Der überlastete Server bekommt Luft.
- Der dritte Versuch klappt.
- Die Antwort: ein Textblock, stop_reason end_turn und usage: 18 Eingabe- und 15 Ausgabe-Tokens.
- 401 und 400 werden nicht wiederholt: Warten repariert keinen falschen Schlüssel. Nach 4 Versuchen gibt das Programm auf.
// wiederholt die vorübergehenden Fehler, die Wartezeit verdoppelt sichfunction createWithRetry(client, system, messages) { const maxAttempts = 4; let delay = 1; for (let attempt = 1; ; attempt++) { try { return client.create({ model: MODEL, max_tokens: 1024, system, messages, }); } catch (e) { const last = attempt === maxAttempts; if (last || !retryable(e.status)) throw e; console.log(` Versuch ${attempt}: ${e.status},`, `warte ${delay} s`); // ein echtes Programm wartet hier: // await sleep(delay * 1000) delay *= 2; } }}
// vorübergehend: 408, 409, 429 und 5xxfunction retryable(status) { return status === 408 || status === 409 || status === 429 || status >= 500;}Drei Szenarien: 429 und 500, dann eine Antwort; 401, keine Wiederholung; vier Mal 429, nach dem vierten Versuch wird aufgegeben. Die Pause wird nur ausgegeben, damit das Programm sofort läuft.
const system = "Du bist ein Support-Assistent eines Online-Shops. Antworte kurz.";const messages = [{ role: "user", content: "Wann kommt Bestellung 1042 an?" }];const scenarios = [[429, 500], [401], [429, 429, 429, 429]];for (const failures of scenarios) { console.log(`Szenario [${failures.join(", ")}]:`); try { const r = createWithRetry(new FakeClient(failures), system, messages); console.log(` Antwort: ${r.content[0].text}`); } catch (e) { console.log(` aufgegeben: ${e.status} ${e.message}`); }}Szenario [429, 500]: Versuch 1: 429, warte 1 s Versuch 2: 500, warte 2 s Antwort: Bestellung 1042 ist beim Kurier und kommt morgen bis 18 Uhr.Szenario [401]: aufgegeben: 401 ungültiger SchlüsselSzenario [429, 429, 429, 429]: Versuch 1: 429, warte 1 s Versuch 2: 429, warte 2 s Versuch 3: 429, warte 4 s aufgegeben: 429 zu viele AnfragenDie offiziellen SDKs der Anbieter machen das meist intern: Sie wiederholen die vorübergehenden Fehler selbst, mit wachsender Pause. Lies die Dokumentation und setze keine eigene Schleife obendrauf: Die Versuche vervielfachen sich. Eine eigene Schleife schreibst du, wenn du die API direkt über HTTP aufrufst, wie im Block „Bei dir ausführen“.
Timeouts, Wiederholungen mit Jitter und den Circuit Breaker findest du in der Lektion über Ausfallsicherheit.Probier es aus
Sag die Ausgabe voraus
Der falsche Client liefert zuerst 429, dann 400. system und messages sind wie oben. Was gibt das Programm aus?
const client = new FakeClient([429, 400]);try { const r = createWithRetry(client, system, messages); console.log(`Antwort: ${r.content[0].text}`);} catch (e) { console.log(`aufgegeben: ${e.status}`);}429 ist vorübergehend: Das Programm gibt den ersten Versuch aus, wartet 1 s und versucht es noch einmal. 400 ist dauerhaft: retryable(400) ist false, der Fehler verlässt createWithRetry sofort, ohne zweite Zeile über das Warten, und das Programm gibt aus, dass es mit dem Code 400 aufgegeben hat. Zu einer Antwort kommt es nie.
Die Idee
Was der Kunde sieht
Wiederholungen helfen nicht immer. Wenn auch der letzte Versuch scheitert, darf der Kunde nicht „ApiException: 503“ sehen. Das Programm fängt den Fehler ab und sagt etwas Höfliches und Wahres: „Ich kann gerade nicht antworten. Ich verbinde dich mit einem Kollegen.“ Der Fehler geht für das Team ins Log.
Genauer, für Neugierige
Jitter. Wenn Tausende Programme gleichzeitig scheitern und genau 1, 2, 4 Sekunden warten, versuchen sie es gleichzeitig wieder. Zur Pause kommt ein zufälliger Teil (Jitter), um sie zu verteilen.
Timeout. Die Anfrage kann auch hängen, deshalb hat jeder Aufruf über das Netzwerk eine Obergrenze für die Zeit bis zur Antwort. Eine lange Antwort ist langsam, also schützt max_tokens Zeit und Geld zugleich. Mehr zu Langsamkeit und Kosten steht in der Lektion über Kosten und Latenz.
Limits. 429 heißt, dass du das Limit der Anfragen oder Tokens pro Minute erreicht hast. Eine Wiederholung hilft bei einer Spitze. Wenn du es ständig erreichst, hilft nicht mehr Wiederholen, sondern weniger Anfragen, kürzere Anfragen oder ein höheres Limit.
Wie es weitergeht. Der System-Prompt ist der erste Schritt zu Prompts als Code. Eine Antwort, die das Programm liest und nicht nur anzeigt, ist strukturierte Ausgabe, und ein Modell, das das Programm selbst nach Daten fragt, ist Tool Use. Text von einem Nutzer oder aus einem Dokument, der die Anweisungen ändern will (Prompt Injection), kommt im Modul über Agenten weiter hinten im Kurs.
Übung
Eine Antwort oder eine höfliche Absage
Löse die Aufgabe in deinem Editor.
Schreibe askSupport(client, system, question): Sie ruft createWithRetry auf und liefert den Text der Antwort, und wenn auch der letzte Versuch scheitert (oder der Fehler dauerhaft ist), liefert sie „Ich kann gerade nicht antworten. Ich verbinde dich mit einem Kollegen.“. Starte sie mit der Frage „Kann ich die Jacke zurückgeben?“ und vier Szenarien des falschen Clients: ohne Fehler; vier Mal 503; 401; ein Mal 429.
Szenario []: Du kannst die Ware bis 14 Tage nach Erhalt zurückgeben.Szenario [503, 503, 503, 503]: Versuch 1: 503, warte 1 s Versuch 2: 503, warte 2 s Versuch 3: 503, warte 4 s Ich kann gerade nicht antworten. Ich verbinde dich mit einem Kollegen.Szenario [401]: Ich kann gerade nicht antworten. Ich verbinde dich mit einem Kollegen.Szenario [429]: Versuch 1: 429, warte 1 s Du kannst die Ware bis 14 Tage nach Erhalt zurückgeben.Weitere Aufgaben
- Ein Log für das Team. Gib bei einer Absage vor der höflichen Antwort eine eigene Zeile mit dem Code und dem Text des Fehlers aus. Der Kunde sieht nur die höfliche.
- Die abgeschnittene Antwort. Füge
createWithRetryeinen Parameter fürmax_tokenshinzu. Wennstop_reasongleichmax_tokensist, gib die Antwort mit „…“ am Ende zurück und logge eine Warnung. Probiere es mit 8.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Text in Stücken: Tokens · Das nächste Token vorhersagen · Das Kontextfenster · Die Grenzen des Modells
- Geht weiter in: Der Prompt ist Code · Strukturierte Ausgabe: JSON nach einem Schema · Tool Use: das Modell fragt, das Programm handelt · Evaluation: ein Testset für den Assistenten
- Aufgaben zum Thema: Anwendungen mit LLMs