Challenge 7.3: Stream Transforms
Hast Du bemerkt, dass LLM-Streams manchmal stottern — einzelne Buchstaben statt fluessiger Woerter? Und was wenn Du den Stream nachbearbeiten willst, bevor er beim User ankommt — z.B. bestimmte Woerter ersetzen oder filtern?
OVERVIEW
Abschnitt betitelt „OVERVIEW“Stream Transforms sitzen zwischen dem LLM und Deiner UI. Sie transformieren, filtern oder glaetten die Chunks, bevor sie beim User ankommen. Du kannst Built-in Transforms wie smoothStream nutzen oder eigene schreiben.
Ohne Transforms: Der Raw-Stream vom LLM kommt manchmal buchstabenweise (“H”, “e”, “l”, “l”, “o”), manchmal in grossen Bloecken. Die Ausgabe stottert, fuehlt sich unpoliert an. Wenn Du den Stream nachbearbeiten willst (filtern, formatieren, anreichern), musst Du das manuell nach dem Empfang tun — unflexibel und fehleranfaellig.
Mit Transforms: smoothStream() buffert einzelne Zeichen und gibt sie in natuerlichen Wortgruppen aus. Custom Transforms lassen Dich jeden Chunk modifizieren, bevor er weitergeleitet wird. Und mit der experimental_transform Option kannst Du mehrere Transforms verketten wie eine Pipeline.
WALKTHROUGH
Abschnitt betitelt „WALKTHROUGH“Schicht 1: smoothStream — das Built-in Smoothing
Abschnitt betitelt „Schicht 1: smoothStream — das Built-in Smoothing“smoothStream() ist eine eingebaute Transform-Funktion. Sie sammelt einzelne Zeichen und gibt sie in laengeren, natuerlicheren Chunks aus:
import { smoothStream, streamText } from 'ai';import { anthropic } from '@ai-sdk/anthropic';
const result = streamText({ model: anthropic('claude-sonnet-4-5-20250514'), prompt: 'Erklaere Stream Transforms.', experimental_transform: smoothStream(), // ← Eine Zeile, grosser Effekt});
for await (const chunk of result.textStream) { process.stdout.write(chunk); // ← Fluessigere Ausgabe}Ohne smoothStream: "S" "t" "r" "e" "a" "m" " T" "r" "a" "n" "s" …
Mit smoothStream: "Stream " "Transforms " "sind ein " "Mechanismus..." …
smoothStream akzeptiert optionale Konfiguration:
experimental_transform: smoothStream({ delayInMs: 10, // ← Verzoegerung zwischen Chunks (Default: 10) chunking: 'word', // ← 'word' oder 'line' oder RegExp}),| Option | Default | Beschreibung |
|---|---|---|
delayInMs | 10 | Millisekunden zwischen Chunk-Emissionen |
chunking | 'word' | Wie gruppiert wird: 'word', 'line' oder eine eigene RegExp |
Schicht 2: Custom TransformStream
Abschnitt betitelt „Schicht 2: Custom TransformStream“Ein Custom Transform ist eine Funktion, die eine TransformStream-Factory zurueckgibt. Jeder Chunk durchlaeuft die transform-Methode:
// Custom Transform: Alle Buchstaben in Grossbuchstabenconst upperCase = () => // ← Factory-Funktion (options: { tools: Record<string, unknown> }) => // ← Bekommt Tool-Infos new TransformStream({ transform(chunk, controller) { // Nur text-delta Chunks transformieren, Rest durchlassen if (chunk.type === 'text-delta') { controller.enqueue({ // ← Modifizierten Chunk weiterleiten ...chunk, textDelta: chunk.textDelta.toUpperCase(), // ← Text transformieren }); } else { controller.enqueue(chunk); // ← Andere Chunks unveraendert } }, });Die Struktur ist immer gleich:
- Aeussere Funktion: Gibt die Factory zurück (Konfigurationsebene)
- Innere Funktion: Bekommt Options (Tools etc.), gibt
TransformStreamzurück - transform-Methode: Verarbeitet jeden einzelnen Chunk
Schicht 3: Transforms verketten
Abschnitt betitelt „Schicht 3: Transforms verketten“Mit experimental_transform als Array kannst Du mehrere Transforms hintereinanderschalten. Jeder Chunk durchlaeuft alle Transforms in Reihenfolge:
import { smoothStream, streamText } from 'ai';
// Custom Transform: Filtert Chunks die "[INTERN]" enthaltenconst filterInternal = () => (options: { tools: Record<string, unknown> }) => new TransformStream({ transform(chunk, controller) { if (chunk.type === 'text-delta' && chunk.textDelta.includes('[INTERN]')) { // Chunk verschlucken — nicht weiterleiten return; } controller.enqueue(chunk); }, });
const result = streamText({ model: anthropic('claude-sonnet-4-5-20250514'), prompt: 'Erklaere Streaming.', experimental_transform: [ // ← Array = Pipeline smoothStream(), // ← Erst glaetten filterInternal(), // ← Dann filtern ],});Die Reihenfolge ist wichtig: Der erste Transform verarbeitet den Raw-Stream, der zweite bekommt den Output des ersten. Wie Pipes in der Unix-Shell.
Schicht 4: Praxisbeispiel — Sensitive Daten filtern
Abschnitt betitelt „Schicht 4: Praxisbeispiel — Sensitive Daten filtern“Ein realistischer Use Case: Du willst verhindern, dass E-Mail-Adressen im Stream erscheinen:
const redactEmails = () => (options: { tools: Record<string, unknown> }) => new TransformStream({ transform(chunk, controller) { if (chunk.type === 'text-delta') { const redacted = chunk.textDelta.replace( /[\w.-]+@[\w.-]+\.\w+/g, // ← Simple E-Mail RegExp '[EMAIL REDACTED]', ); controller.enqueue({ ...chunk, textDelta: redacted }); } else { controller.enqueue(chunk); } }, });
const result = streamText({ model: anthropic('claude-sonnet-4-5-20250514'), prompt: 'Nenne mir Kontaktdaten von Vercel.', experimental_transform: [ smoothStream(), redactEmails(), // ← E-Mails werden maskiert ],});Achtung: Stream Transforms arbeiten chunk-weise. Eine E-Mail-Adresse könnte über zwei Chunks verteilt sein (“user@” + “example.com”). Für robuste Filterung brauchst Du einen Buffer im Transform — das ist eine fortgeschrittene Technik.
Aufgabe: Wende smoothStream an, dann schreibe einen Custom Transform, der alle Text-Chunks in Grossbuchstaben umwandelt.
Erstelle die Datei stream-transforms.ts:
import { smoothStream, streamText } from 'ai';import { anthropic } from '@ai-sdk/anthropic';
// TODO 1: Schreibe einen upperCase Transform// const upperCase = () => (options) =>// new TransformStream({// transform(chunk, controller) {// // TODO: text-delta Chunks transformieren, Rest durchlassen// },// });
// TODO 2: Nutze experimental_transform mit smoothStream UND upperCase// const result = streamText({// model: anthropic('claude-sonnet-4-5-20250514'),// prompt: 'Erklaere in 2 Saetzen, was Stream Transforms sind.',// experimental_transform: [???],// });
// TODO 3: Konsumiere den Stream// for await (const chunk of result.textStream) {// process.stdout.write(chunk);// }Checkliste:
-
smoothStream()als erster Transform konfiguriert - Custom
upperCaseTransform geschrieben mitTransformStream - Nur
text-deltaChunks transformiert, andere Typen durchgelassen - Beide Transforms in
experimental_transformArray verkettet - Output zeigt glatten, grossgeschriebenen Text
Fuehre aus: npx tsx stream-transforms.ts
Lösung anzeigen
import { smoothStream, streamText } from 'ai';import { anthropic } from '@ai-sdk/anthropic';
// Custom Transform: Grossbuchstabenconst upperCase = () => (options: { tools: Record<string, unknown> }) => new TransformStream({ transform(chunk, controller) { if (chunk.type === 'text-delta') { controller.enqueue({ ...chunk, textDelta: chunk.textDelta.toUpperCase(), }); } else { controller.enqueue(chunk); } }, });
const result = streamText({ model: anthropic('claude-sonnet-4-5-20250514'), prompt: 'Erklaere in 2 Saetzen, was Stream Transforms sind.', experimental_transform: [ smoothStream(), // Erst glaetten upperCase(), // Dann in Grossbuchstaben ],});
for await (const chunk of result.textStream) { process.stdout.write(chunk);}console.log(); // Zeilenumbruch am EndeErklärung: smoothStream() sammelt die einzelnen Buchstaben vom LLM und gibt sie in Wortgruppen weiter. Dann transformiert upperCase() jeden Text-Chunk in Grossbuchstaben. Nicht-Text-Chunks (wie finish oder tool-call) werden unveraendert durchgereicht. Die Reihenfolge im Array bestimmt die Pipeline-Reihenfolge.
Erwarteter Output (ungefaehr):
STREAM TRANSFORMS SIND EIN MECHANISMUS, DER CHUNKS ZWISCHEN DEM LLMUND DEINER UI VERARBEITET. SIE ERMOEGLICHEN ES, DEN TEXT ZU GLAETTEN,ZU FILTERN ODER ZU TRANSFORMIEREN, BEVOR ER BEIM USER ANKOMMT.Der gesamte Text erscheint in Grossbuchstaben und fliesst in Wortgruppen statt einzelnen Buchstaben. Der genaue Text variiert (LLM-Output).
COMBINE
Abschnitt betitelt „COMBINE“Uebung: Kombiniere Stream Transforms mit Custom Data Parts aus Challenge 7.1. Baue einen Stream, der:
smoothStream()für fluessige Ausgabe nutzt- Einen Custom Transform hat, der bestimmte Woerter (z.B. “TODO”, “FIXME”) aus dem Text filtert und durch “[REDACTED]” ersetzt
- Per
createDataStreameinen Data Part sendet, der zaehlt, wie viele Woerter gefiltert wurden
Optional Stretch Goal: Baue einen wordCounter Transform, der mitzaehlt, wie viele Woerter insgesamt gestreamt wurden, und alle 50 Woerter ein Data Part mit dem aktuellen Zaehlerstand sendet.
Quellen
Abschnitt betitelt „Quellen“- Vercel AI SDK: smoothStream
- Vercel AI SDK: streamText — experimental_transform
- MDN: TransformStream
- ai-hero-dev Exercises 07.01-07.04 (Stream Transforms basiert auf der offiziellen AI SDK Dokumentation)