Die Antwort zeigen, während sie ankommt
Eine Chat-Seite mit Datenstrom zeigt die Antwort eines Chatbots Stück für Stück, wenige Worte auf einmal, noch während sie entsteht. Der Server schickt jedes Stück, sobald er es hat. Das ist wichtig, weil eine lange Antwort viele Sekunden dauern kann und eine leere Seite wirkt, als sei sie kaputt.
Der Server schickt kleine Stücke. Die Seite hängt jedes Stück an den Bildschirm an. Das Labor-Terminal hat keinen Browser, deshalb bauen Sie hier die Seite und den Datenstrom als Text. Auf Ihrem eigenen Rechner würden Sie sie in einem Browser öffnen.
Am Ende dieser Lektion
Notiz
Ich kann eine gestreamte Antwort in einer Webseite anzeigen, während sie ankommt.
Schlüsselwörter
| Begriff | Was er bedeutet |
|---|---|
| Datenstrom | eine Antwort, die in kleinen Stücken gesendet wird |
| Server-Sent Events | ein Weg, wie ein Server Zeilen an eine Seite schiebt |
| Handler | die Funktion, die eine Anfrage beantwortet |
| Maskieren | Zeichen so ändern, dass sie als Text erscheinen |
| Stück | ein kleiner Teil einer gestreamten Antwort |
Die Zellen, die Lernende ausfüllen, bleiben in dieser Vorschau leer.
Ein Server kann eine Antwort in Stücken senden
Bei Server-Sent Events hält der Server die Verbindung offen und schreibt Zeilen. Jedes Ereignis beginnt mit data: , trägt ein Stück als JSON und endet mit einer Leerzeile. Der Typ der Antwort ist text/event-stream.
Ein letztes Ereignis namens ende sagt der Seite, dass die Antwort fertig ist.
Beispiel: die Zeilen eines Datenstroms
| Gesendete Zeile | Was die Seite tut |
|---|---|
| data: {"stueck": "Hallo"} und eine Leerzeile | Hängt Hallo an den Bildschirm an |
| data: {"stueck": " und willkommen"} und eine Leerzeile | Hängt die nächsten Worte dahinter |
| event: ende, dann data: ende und eine Leerzeile | Schließt die Verbindung |
Die Zellen, die Lernende ausfüllen, bleiben in dieser Vorschau leer.
Die Seite hängt an und kann stoppen
Die Seite enthält ein kleines Skript. Es öffnet den Datenstrom und hängt bei jedem Ereignis das Stück an einen Absatz an. Eine Stopp-Taste schließt den Datenstrom, danach kommen keine Stücke mehr.
Ein Stück mit append und einem String anzuhängen fügt einfachen Text ein. Es fügt nie Markup ein.
Modelltext maskieren
Ein Modell kann wiederholen, was man ihm gab, auch <script>. Gelangt dieser Text als HTML in eine Seite, kann der Browser ihn ausführen.
Maskieren macht aus < ein < und aus den anderen Sonderzeichen sichere Codes. Maskieren Sie jeden Modelltext, der in HTML geht. Machen Sie & zuerst, sonst maskieren Sie Ihre eigenen Codes.
Ein häufiger Fehler
Warnung
Viele glauben, Modelltext sei sicher, weil sie den Prompt geschrieben haben. Das stimmt nicht. Das Modell kann die Worte einer Nutzerin oder einer Quelle wiederholen. Behandeln Sie jede Antwort als nicht vertrauenswürdig: Hängen Sie sie als Text an oder maskieren Sie sie vorher.
Die Modellhilfe schreiben, Teil 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const basis = process.env.AI_URL || \"http://ai:4000\";' 'async function fragen(nachrichten, probe, max) {' ' try {' ' const res = await fetch(basis + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: nachrichten, max_tokens: max || 200 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"Status \" + res.status);' ' return (await res.json()).choices[0].message.content;' > model.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Die Modellhilfe schreiben, Teil 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' } catch (fehler) {' ' console.log(\"Aufgezeichnete Probe: Kein Modell hat geantwortet (\" + fehler.message + \").\");' ' const liste = [].concat(probe);' ' const runde = nachrichten.filter(function (m) { return m.role === \"assistant\"; }).length;' ' return liste[Math.min(runde, liste.length - 1)];' ' }' '}' 'module.exports = { fragen };' >> model.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Seite und Datenstrom schreiben, Teil 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { fragen } = require(\"./model\");' '' 'function zitieren(text) { return JSON.stringify(text); }' 'function htmlMaskieren(text) {' ' return text.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\")' ' .replace(/\"/g, \""\").replace(/\x27/g, \"'\");' '}' > oberflaeche4.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Seite und Datenstrom schreiben, Teil 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function seitenHtml() {' ' const skript = [' ' \"const ausgabe = document.getElementById(\" + zitieren(\"antwort\") + \");\",' ' \"const strom = new EventSource(\" + zitieren(\"/strom\") + \");\",' ' \"strom.onmessage = function (e) { ausgabe.append(JSON.parse(e.data).stueck); };\",' ' \"strom.addEventListener(\" + zitieren(\"ende\") + \", function () { strom.close(); });\",' ' \"document.getElementById(\" + zitieren(\"stopp\") + \").onclick = function () { strom.close(); };\",' ' ].join(\"\n\");' ' return \"<html><body><p id=antwort></p><button id=stopp>Stopp</button><script>\" + skript + \"</script></body></html>\";' '}' >> oberflaeche4.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Seite und Datenstrom schreiben, Teil 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function sseZeile(stueck) { return \"data: \" + JSON.stringify({ stueck: stueck }) + \"\n\n\"; }' 'function stromKoerper(stuecke, stoppNach) {' ' let koerper = \"\";' ' stuecke.slice(0, stoppNach).forEach(function (stueck) { koerper = koerper + sseZeile(stueck); });' ' return stoppNach >= stuecke.length ? koerper + \"event: ende\ndata: ende\n\n\" : koerper;' '}' >> oberflaeche4.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Seite und Datenstrom schreiben, Teil 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function bearbeiten(anfrage, stuecke) {' ' if (anfrage.url === \"/\") return { status: 200, typ: \"text/html\", koerper: seitenHtml() };' ' if (anfrage.url === \"/strom\") {' ' return { status: 200, typ: \"text/event-stream\", koerper: stromKoerper(stuecke, anfrage.stoppNach || 99) };' ' }' ' return { status: 404, typ: \"text/plain\", koerper: \"Nicht gefunden\" };' '}' >> oberflaeche4.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Seite und Datenstrom schreiben, Teil 5
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function zeigen(stuecke) {' ' const start = bearbeiten({ url: \"/\" }, stuecke);' ' console.log(\"GET / ->\", start.status, start.typ, start.koerper.length + \" Zeichen\");' ' const live = bearbeiten({ url: \"/strom\" }, stuecke);' ' console.log(\"GET /strom ->\", live.status, live.typ);' ' console.log(live.koerper.split(\"\n\n\").join(\" | \"));' ' const gestoppt = bearbeiten({ url: \"/strom\", stoppNach: 2 }, stuecke);' ' console.log(\"Stopp nach 2 Stücken:\", gestoppt.koerper.split(\"\n\n\").length - 1, \"Stücke gesendet, keine Zeile ende\");' ' console.log(\"Maskiert:\", htmlMaskieren(\"<b>hallo</b> & danke\"));' '}' >> oberflaeche4.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Seite und Datenstrom schreiben, Teil 6
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' // Aufgezeichnete Probe, nur benutzt, wenn kein Modell antwortet.' ' const probe = \"Hallo und willkommen an Bord, wohin möchten Sie fahren?\";' ' const antwort = await fragen([{ role: \"user\", content: \"Begrüßen Sie einen Busfahrgast in einem kurzen Satz.\" }], probe, 40);' ' zeigen(antwort.split(\" \").map(function (wort, stelle) { return stelle === 0 ? wort : \" \" + wort; }));' '}' 'main();' >> oberflaeche4.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Den Handler aufrufen
docker exec -w /home/node/project agent node oberflaeche4.jsHier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Eine mögliche Ausgabe
| Zeile | Was sie zeigen kann |
|---|---|
| GET / | 200 text/html und die Größe der Seite |
| GET /strom | 200 text/event-stream, dann die Stücke einer Antwort, getrennt durch | |
| Stopp | 2 Stücke gesendet, keine Zeile ende |
| Maskiert | <b>hallo</b> & danke, immer gleich |
Die Zellen, die Lernende ausfüllen, bleiben in dieser Vorschau leer.