Challenge 1.4: Text streamen
Warum siehst Du in ChatGPT die Antwort Wort für Wort statt alles auf einmal? Und was passiert unter der Haube, damit das funktioniert?
OVERVIEW
Abschnitt betitelt „OVERVIEW“Das LLM generiert Token für Token. Mit streamText bekommst Du jeden Token sofort, statt auf die komplette Antwort zu warten. Die gestrichelten Pfeile zeigen: Daten fliessen stueckweise zurück.
Ohne Streaming: User wartet 5-10 Sekunden auf eine leere Seite, denkt die App haengt. Erst wenn das LLM komplett fertig ist, erscheint die gesamte Antwort auf einmal. Bei langen Antworten fuehlt sich das an wie ein Absturz.
Mit Streaming: Sofortiges Feedback. Der erste Token erscheint nach wenigen Millisekunden. Der User sieht, dass etwas passiert, und kann schon lesen, waehrend das LLM noch generiert. Gefuehlte Geschwindigkeit steigt massiv.
WALKTHROUGH
Abschnitt betitelt „WALKTHROUGH“Schicht 1: streamText Basics
Abschnitt betitelt „Schicht 1: streamText Basics“streamText funktioniert wie generateText — selbe Parameter, selbes Interface. Der Unterschied: Es gibt sofort ein Stream-Objekt zurück, statt auf die komplette Antwort zu warten.
import { streamText } from 'ai';import { anthropic } from '@ai-sdk/anthropic';
const result = streamText({ // ← Kein await noetig! model: anthropic('claude-sonnet-4-5-20250514'), prompt: 'Erzaehle eine kurze Geschichte.',});Wichtig: streamText wird nicht mit await aufgerufen. Die Funktion gibt synchron ein Result-Objekt zurück, das mehrere Streams enthaelt. Das eigentliche Warten passiert beim Konsumieren der Streams.
Schicht 2: textStream — AsyncIterable für Text-Chunks
Abschnitt betitelt „Schicht 2: textStream — AsyncIterable für Text-Chunks“result.textStream ist ein AsyncIterable, das Dir jeden Text-Chunk einzeln liefert. Du konsumierst es mit for await...of:
for await (const chunk of result.textStream) { process.stdout.write(chunk); // ← Schreibt jedes Wort sofort ins Terminal}Warum process.stdout.write statt console.log? Weil console.log nach jedem Aufruf einen Zeilenumbruch einfuegt. process.stdout.write schreibt den Text genau so, wie er ankommt — Wort für Wort in derselben Zeile.
Schicht 3: fullStream — Alle Events, nicht nur Text
Abschnitt betitelt „Schicht 3: fullStream — Alle Events, nicht nur Text“textStream gibt Dir nur den Text. fullStream gibt Dir alle Events — inklusive Start, Finish und Token-Verbrauch:
for await (const part of result.fullStream) { switch (part.type) { case 'text-delta': // ← Ein Text-Chunk process.stdout.write(part.textDelta); break; case 'finish': // ← Stream ist fertig console.log('\n\nTokens:', part.usage.totalTokens); console.log('Finish Reason:', part.finishReason); break; case 'error': // ← Fehler im Stream console.error('Fehler:', part.error); break; }}Die wichtigsten Event-Typen:
| Event | Wann | Daten |
|---|---|---|
text-delta | Bei jedem Text-Chunk | part.textDelta |
finish | Stream ist fertig | part.usage, part.finishReason |
error | Bei einem Fehler | part.error |
tool-call | LLM ruft ein Tool auf | part.toolName, part.args (Level 3) |
finishReason: Die moeglichen Werte sindstop(Modell ist fertig),length(max. Tokens erreicht),content-filter,tool-calls,errorundother. Die vollständige Liste findest du in der AI SDK Dokumentation.
Schicht 4: toUIMessageStreamResponse — Vorschau auf spaetere Levels
Abschnitt betitelt „Schicht 4: toUIMessageStreamResponse — Vorschau auf spaetere Levels“Für Web-APIs (z.B. Next.js) gibt es toUIMessageStreamResponse(). Damit wird der Stream direkt in eine HTTP Response umgewandelt:
// In einer Next.js API Route (spaetere Levels):export async function POST(req: Request) { const result = streamText({ model: anthropic('claude-sonnet-4-5-20250514'), prompt: 'Hallo!', }); return result.toUIMessageStreamResponse(); // ← Stream als HTTP Response}Das brauchst Du jetzt noch nicht — aber es zeigt, warum Streaming so zentral ist: Es funktioniert vom Terminal bis zur Web-App mit demselben API.
Aufgabe: Streame Text ins Terminal mit textStream. Dann wechsle zu fullStream und logge zusätzlich das finish-Event.
import { streamText } from 'ai';import { anthropic } from '@ai-sdk/anthropic';
// TODO 1: Rufe streamText auf (ohne await!)// const result = streamText({// model: ???,// prompt: 'Erklaere in 3 Saetzen, warum TypeScript besser als JavaScript ist.',// });
// TODO 2: Konsumiere result.textStream mit for await...of// for await (const chunk of result.textStream) {// // Schreibe jeden Chunk sofort ins Terminal// }
// TODO 3 (Bonus): Ersetze textStream durch fullStream// Logge text-delta UND das finish-Event mit Token-VerbrauchCheckliste:
-
streamTextimportiert und aufgerufen (ohneawait) -
textStreammitfor await...ofkonsumiert - Text wird Chunk für Chunk ins Terminal geschrieben (mit
process.stdout.write) - Bonus:
fullStreamEvents geloggt (text-delta + finish)
Lösung anzeigen
Variante 1: textStream
import { streamText } from 'ai';import { anthropic } from '@ai-sdk/anthropic';
const result = streamText({ model: anthropic('claude-sonnet-4-5-20250514'), prompt: 'Erklaere in 3 Saetzen, warum TypeScript besser als JavaScript ist.',});
for await (const chunk of result.textStream) { process.stdout.write(chunk);}console.log(); // Zeilenumbruch am EndeVariante 2: fullStream mit Events
import { streamText } from 'ai';import { anthropic } from '@ai-sdk/anthropic';
const result = streamText({ model: anthropic('claude-sonnet-4-5-20250514'), prompt: 'Erklaere in 3 Saetzen, warum TypeScript besser als JavaScript ist.',});
for await (const part of result.fullStream) { switch (part.type) { case 'text-delta': process.stdout.write(part.textDelta); break; case 'finish': console.log('\n\n--- Stream beendet ---'); console.log('Tokens:', part.usage.totalTokens); console.log('Finish Reason:', part.finishReason); break; }}Erklärung: streamText gibt synchron ein Result-Objekt zurück. Der eigentliche API-Call startet erst, wenn Du den Stream konsumierst (mit for await). textStream liefert nur Text-Chunks, fullStream liefert alle Event-Typen inklusive Metadaten wie Token-Verbrauch.
Tipp: Alternativ zu
fullStreamkannst Du nach dem Stream auchawait result.usagenutzen — das Result-Objekt hatusageals Promise, das aufloest sobald der Stream fertig ist.
Ausfuehren:
npx tsx challenge-1-4.tsErwarteter Output (ungefaehr):
TypeScript ist besser als JavaScript weil es statische Typen...(Text erscheint Wort für Wort im Terminal)
--- Stream beendet ---Tokens: 78Finish Reason: stopCOMBINE
Abschnitt betitelt „COMBINE“Uebung: Kombiniere streamText mit der selectModel-Funktion aus Challenge 1.2. Streame denselben Prompt mit zwei verschiedenen Modellen nacheinander und vergleiche die gefuehlte Geschwindigkeit.
- Nutze
selectModelum ein Flash-Modell und ein Pro-Modell zu waehlen - Streame mit dem ersten Modell, dann mit dem zweiten
- Miss die Zeit mit
performance.now()oderDate.now()— welches Modell liefert den ersten Token schneller? - Nutze
fullStream, um am Ende die Token-Verbraeuche beider Modelle zu vergleichen
Optional Stretch Goal: Baue eine Funktion streamWithTimer(model, prompt), die den Stream konsumiert UND die “Time to First Token” (TTFT) misst — also die Zeit vom Aufruf bis zum ersten text-delta Event.