Fecha de publicación: 21 de enero de 2025
Una respuesta de LLM transmitida consiste en datos emitidos de forma incremental y continua. Los datos de transmisión se ven diferentes en el servidor y en el cliente.
Desde el servidor
Para comprender cómo se ve una respuesta transmitida, le pedí a Gemini que me contara un chiste largo con la herramienta de línea de comandos curl
. Ten en cuenta la siguiente llamada a la API de Gemini. Si lo pruebas, asegúrate de reemplazar {GOOGLE_API_KEY}
en la URL por tu clave de API de 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."}]}]}'
Esta solicitud registra el siguiente resultado (truncado) en formato de flujo de eventos.
Cada línea comienza con data:
seguida de la carga útil del mensaje. El formato concreto no es importante, lo que importa son los fragmentos 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}}
La primera carga útil es JSON. Observa con más detalle el 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
}
}
Esa primera entrada de text
es el comienzo de la respuesta de Gemini. Cuando extraes
más entradas de text
, la respuesta se delimita con saltos de línea.
En el siguiente fragmento, se muestran varias entradas de text
, que muestran la respuesta final del 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?\""
...
Pero ¿qué sucede si, en lugar de chistes sobre T-Rex, le pides al modelo algo un poco más complejo? Por ejemplo, pídele a Gemini que cree una función de JavaScript para determinar si un número es par o impar. Los fragmentos de text:
se ven
un poco diferentes.
El resultado ahora contiene el formato Markdown, que comienza con el bloque de código de JavaScript. En la siguiente muestra, se incluyen los mismos pasos de procesamiento previo que 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 complicar aún más las cosas, algunos de los elementos con marcas comienzan en un fragmento y terminan en otro. Parte del lenguaje de marcado está anidado. En el siguiente ejemplo, la función destacada se divide en dos líneas: **isEven(
y number) function:**
. En conjunto, el resultado es **isEven("number) function:**
. Esto significa que, si deseas generar Markdown con formato, no puedes procesar cada fragmento por separado con un analizador de Markdown.
Desde el cliente
Si ejecutas modelos como Gemma en el cliente con un framework como MediaPipe LLM, los datos de transmisión provienen de una función de devolución de llamada.
Por ejemplo:
llmInference.generateResponse(
inputPrompt,
(chunk, done) => {
console.log(chunk);
});
Con la API de Prompt, obtienes datos de transmisión como fragmentos iterando sobre un ReadableStream
.
const languageModel = await self.ai.languageModel.create();
const stream = languageModel.promptStreaming(inputPrompt);
for await (const chunk of stream) {
console.log(chunk);
}
Próximos pasos
¿Te preguntas cómo renderizar datos transmitidos de forma segura y con un buen rendimiento? Lee nuestro artículo sobre prácticas recomendadas para renderizar respuestas de LLM.