Publicado em: 21 de janeiro de 2025
Uma resposta transmitida do LLM consiste em dados emitidos de forma incremental e contínua. Os dados de streaming têm uma aparência diferente no servidor e no cliente.
Do servidor
Para entender como é uma resposta transmitida, pedi ao Gemini para contar uma piada longa usando a ferramenta de linha de comando curl
. Considere a seguinte chamada para a API Gemini. Se você testar, 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 seguinte saída (truncada), no
formato de fluxo de eventos.
Cada linha começa com data:
seguido da carga útil da mensagem. O formato concreto não é importante, o que importa são os trechos 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 os candidates[0].content.parts[0].text
destacados:
{
"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 novas linhas.
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 tiranossauros, você pedir algo um pouco mais complexo? Por exemplo, peça ao Gemini para criar uma função JavaScript
que determine 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. A amostra a seguir inclui as mesmas etapas de pré-processamento de antes.
"```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 piorar, alguns dos itens marcados começam em um bloco e terminam em outro. Parte da marcação está aninhada. No exemplo a seguir, a função
destacada é dividida entre duas linhas:
**isEven(
e number) function:**
. Combinados, o resultado é **isEven("number) function:**
. Isso significa que, se você quiser gerar Markdown formatado, não poderá processar cada parte individualmente com um analisador Markdown.
Do cliente
Se você executar modelos como o Gemma no cliente com um framework como o MediaPipe LLM, os dados de streaming vão chegar 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 partes ao iterar em um
ReadableStream
.
const languageModel = await 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 desempenho? Leia nossas práticas recomendadas para renderizar respostas de LLMs.