Veröffentlicht: 21. Januar 2025
Eine gestreamte LLM-Antwort besteht aus Daten, die inkrementell und kontinuierlich gesendet werden. Streamingdaten sehen auf dem Server und auf dem Client unterschiedlich aus.
Über den Server
Um zu sehen, wie eine gestreamte Antwort aussieht, habe ich Gemini mit dem Befehlszeilentool curl
aufgefordert, mir einen langen Witz zu erzählen. Betrachten Sie den folgenden Aufruf der Gemini API. Ersetzen Sie dabei {GOOGLE_API_KEY}
in der URL durch Ihren Gemini API-Schlüssel.
$ curl "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:streamGenerateContent?alt=sse&key={GOOGLE_API_KEY}" \
-H 'Content-Type: application/json' \
--no-buffer \
-d '{ "contents":[{"parts":[{"text": "Tell me a long T-rex joke, please."}]}]}'
Diese Anfrage protokolliert die folgende (abgeschnittene) Ausgabe im Ereignisstream-Format.
Jede Zeile beginnt mit data:
, gefolgt von der Nachrichtennutzlast. Das konkrete Format ist nicht wichtig, es geht um die Textblöcke.
//
data: {"candidates":[{"content": {"parts": [{"text": "A T-Rex"}],"role": "model"},
"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],
"usageMetadata": {"promptTokenCount": 11,"candidatesTokenCount": 4,"totalTokenCount": 15}}
data: {"candidates": [{"content": {"parts": [{ "text": " walks into a bar and orders a drink. As he sits there, he notices a" }], "role": "model"},
"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],
"usageMetadata": {"promptTokenCount": 11,"candidatesTokenCount": 21,"totalTokenCount": 32}}
Die erste Nutzlast ist JSON. Sehen wir uns das hervorgehobene candidates[0].content.parts[0].text
genauer an:
{
"candidates": [
{
"content": {
"parts": [
{
"text": "A T-Rex"
}
],
"role": "model"
},
"finishReason": "STOP",
"index": 0,
"safetyRatings": [
{
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
"probability": "NEGLIGIBLE"
},
{
"category": "HARM_CATEGORY_HATE_SPEECH",
"probability": "NEGLIGIBLE"
},
{
"category": "HARM_CATEGORY_HARASSMENT",
"probability": "NEGLIGIBLE"
},
{
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
"probability": "NEGLIGIBLE"
}
]
}
],
"usageMetadata": {
"promptTokenCount": 11,
"candidatesTokenCount": 4,
"totalTokenCount": 15
}
}
Der erste text
-Eintrag ist der Beginn der Antwort von Gemini. Wenn Sie mehrere text
-Einträge extrahieren, wird die Antwort durch Zeilenumbrüche begrenzt.
Das folgende Snippet enthält mehrere text
-Einträge, die die endgültige Antwort des Modells zeigen.
"A T-Rex"
" was walking through the prehistoric jungle when he came across a group of Triceratops. "
"\n\n\"Hey, Triceratops!\" the T-Rex roared. \"What are"
" you guys doing?\"\n\nThe Triceratops, a bit nervous, mumbled,
\"Just... just hanging out, you know? Relaxing.\"\n\n\"Well, you"
" guys look pretty relaxed,\" the T-Rex said, eyeing them with a sly grin.
\"Maybe you could give me a hand with something.\"\n\n\"A hand?\""
...
Was passiert aber, wenn Sie das Modell nicht nach T-Rex-Witzen, sondern nach etwas etwas Komplexerem fragen? Sie können Gemini beispielsweise bitten, eine JavaScript-Funktion zu entwickeln, mit der ermittelt wird, ob eine Zahl gerade oder ungerade ist. Die text:
-Chunks sehen etwas anders aus.
Die Ausgabe enthält jetzt das Markdown-Format, beginnend mit dem JavaScript-Codeblock. Das folgende Beispiel enthält dieselben Schritte zur Vorverarbeitung wie zuvor.
"```javascript\nfunction"
" isEven(number) {\n // Check if the number is an integer.\n"
" if (Number.isInteger(number)) {\n // Use the modulo operator"
" (%) to check if the remainder after dividing by 2 is 0.\n return number % 2 === 0; \n } else {\n "
"// Return false if the number is not an integer.\n return false;\n }\n}\n\n// Example usage:\nconsole.log(isEven("
"4)); // Output: true\nconsole.log(isEven(7)); // Output: false\nconsole.log(isEven(3.5)); // Output: false\n```\n\n**Explanation:**\n\n1. **`isEven("
"number)` function:**\n - Takes a single argument `number` representing the number to be checked.\n - Checks if the `number` is an integer using `Number.isInteger()`.\n - If it's an"
...
Erschwerend kommt hinzu, dass einige der markierten Elemente in einem Block beginnen und in einem anderen enden. Einiges Markup ist verschachtelt. Im folgenden Beispiel ist die hervorgehobene Funktion auf zwei Zeilen verteilt: **isEven(
und number) function:**
. Zusammen ergibt das die Ausgabe **isEven("number) function:**
. Wenn Sie also formatiertes Markdown ausgeben möchten, können Sie nicht einfach jeden Chunk einzeln mit einem Markdown-Parser verarbeiten.
Vom Kunden
Wenn Sie Modelle wie Gemma mit einem Framework wie MediaPipe LLM auf dem Client ausführen, werden Streamingdaten über eine Callback-Funktion gesendet.
Beispiel:
llmInference.generateResponse(
inputPrompt,
(chunk, done) => {
console.log(chunk);
});
Mit der Prompt API kannst du Streamingdaten als Chunks abrufen, indem du über einen ReadableStream
iterierst.
const languageModel = await self.ai.languageModel.create();
const stream = languageModel.promptStreaming(inputPrompt);
for await (const chunk of stream) {
console.log(chunk);
}
Nächste Schritte
Du möchtest wissen, wie du gestreamte Daten leistungsstark und sicher rendern kannst? Best Practices für die Darstellung von LLM-Antworten