Publicado em 21 de janeiro de 2025
Uma resposta LLM transmitida por streaming consiste em dados emitidos de forma incremental e contínua. Os dados de streaming são diferentes do servidor e do cliente.
Do servidor
Para entender como é uma resposta transmitida, pedi ao Gemini para me contar
uma piada longa usando a ferramenta de linha de comando curl
. Considere a
seguinte chamada para a API Gemini. Se você tentar, substitua
{GOOGLE_API_KEY}
no URL pela sua chave de 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."}]}]}'
Essa solicitação registra a saída (truncada) a seguir no formato de fluxo de eventos.
Cada linha começa com data:
seguida pelo payload da mensagem. O formato
específico não é importante. O que importa são os blocos de texto.
//
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}}
O primeiro payload é JSON. Confira o candidates[0].content.parts[0].text
destacado:
{
"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
}
}
A primeira entrada text
é o início da resposta do Gemini. Quando você extrai
mais entradas text
, a resposta é delimitada por nova linha.
O snippet a seguir mostra várias entradas text
, que mostram a resposta
final do modelo.
"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?\""
...
Mas o que acontece se, em vez de piadas sobre T-Rex, você pedir ao modelo algo
um pouco mais complexo. Por exemplo, peça para o Gemini criar uma função JavaScript
para determinar se um número é par ou ímpar. Os blocos text:
parecem
um pouco diferentes.
A saída agora contém o formato Markdown, começando com o bloco de código JavaScript. O exemplo a seguir inclui as mesmas etapas de pré-processamento anteriores.
"```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"
...
Para dificultar as coisas, alguns dos itens marcados começam em um bloco
e terminam em outro. Parte da marcação está aninhada. No exemplo abaixo, a função destacada é dividida entre duas linhas: **isEven(
e number) function:**
. A saída combinada é
**isEven("number) function:**
. Isso significa que, se você quiser gerar Markdown
formatado, não poderá processar cada bloco individualmente com um analisador de Markdown.
Do cliente
Se você executar modelos como o Gemma no cliente com uma estrutura como o MediaPipe LLM, os dados de streaming serão recebidos por uma função de callback.
Exemplo:
llmInference.generateResponse(
inputPrompt,
(chunk, done) => {
console.log(chunk);
});
Com a API Prompt,
você recebe dados de streaming como blocos iterando sobre um
ReadableStream
.
const languageModel = await self.ai.languageModel.create();
const stream = languageModel.promptStreaming(inputPrompt);
for await (const chunk of stream) {
console.log(chunk);
}
Próximas etapas
Você quer saber como renderizar dados transmitidos com segurança e performance? Leia nossas práticas recomendadas para renderizar respostas de LLM.