Data di pubblicazione: 21 gennaio 2025
Una risposta LLM in streaming è costituita da dati emessi in modo incrementale e continuo. I dati di streaming hanno un aspetto diverso sul server e sul client.
Dal server
Per capire come appare una risposta in streaming, ho chiesto a Gemini di raccontarmi
una barzelletta lunga utilizzando lo strumento a riga di comando curl
. Prendi in considerazione la
seguente chiamata all'API Gemini. Se lo provi, assicurati di sostituire
{GOOGLE_API_KEY}
nell'URL con la tua chiave API Gemini.
$ 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."}]}]}'
Questa richiesta registra il seguente output (troncato) nel
formato del flusso di eventi.
Ogni riga inizia con data:
seguito dal payload del messaggio. Il formato
concreto non è importante, ciò che conta sono i blocchi di testo.
//
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}}
Il primo payload è JSON. Diamo un'occhiata più da vicino ai
candidates[0].content.parts[0].text
evidenziati:
{
"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
}
}
La prima voce text
è l'inizio della risposta di Gemini. Quando estrai
più voci text
, la risposta è delimitata da una nuova riga.
Il seguente snippet mostra più voci text
, che mostrano la risposta finale
del modello.
"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?\""
...
Ma cosa succede se invece di chiedere barzellette sul T-rex, chiedi al modello qualcosa di
leggermente più complesso? Ad esempio, chiedi a Gemini di creare una funzione JavaScript
per determinare se un numero è pari o dispari. I blocchi text:
hanno un aspetto
leggermente diverso.
L'output ora contiene il formato Markdown, a partire dal blocco di codice JavaScript. Il seguente esempio include gli stessi passaggi di pre-elaborazione di prima.
"```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"
...
Per rendere le cose più difficili, alcuni degli elementi contrassegnati iniziano in un blocco e terminano in un altro. Alcuni markup sono nidificati. Nell'esempio seguente, la funzione
evidenziata è suddivisa in due righe:
**isEven(
e number) function:**
. Combinato, l'output è
**isEven("number) function:**
. Ciò significa che se vuoi generare un output in formato
Markdown, non puoi semplicemente elaborare ogni blocco singolarmente con un parser Markdown.
Dal cliente
Se esegui modelli come Gemma sul client con un framework come MediaPipe LLM, i dati di streaming vengono inviati tramite una funzione di callback.
Ad esempio:
llmInference.generateResponse(
inputPrompt,
(chunk, done) => {
console.log(chunk);
});
Con l'API Prompt,
ricevi i dati di streaming come blocchi iterando su un
ReadableStream
.
const languageModel = await LanguageModel.create();
const stream = languageModel.promptStreaming(inputPrompt);
for await (const chunk of stream) {
console.log(chunk);
}
Passaggi successivi
Ti stai chiedendo come eseguire il rendering in modo efficiente e sicuro dei dati in streaming? Leggi le nostre best practice per il rendering delle risposte LLM.