Cómo transmiten respuestas los LLM

Publicado: 21 de enero de 2025

Una respuesta de LLM transmitida consta de datos que se emiten 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. Considera la siguiente llamada a la API de Gemini. Si la pruebas, asegúrate de reemplazar {GOOGLE_API_KEY} en la URL por tu clave de la 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: seguido 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}}
Después de ejecutar el comando, se transmiten los fragmentos de resultados.

La primera carga útil es JSON. Analiza 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 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 pedirle chistes sobre tiranosaurios rex, le pides 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.

La salida ahora contiene el formato Markdown, comenzando con el bloque de código JavaScript. En el siguiente ejemplo, 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 que la situación sea aún más compleja, algunos de los elementos marcados 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 de forma individual con un analizador de Markdown.

Del cliente

Si ejecutas modelos como Gemma en el cliente con un framework como MediaPipe LLM, los datos de transmisión llegan a través 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 LanguageModel.create();
const stream = languageModel.promptStreaming(inputPrompt);
for await (const chunk of stream) {
  console.log(chunk);
}

Próximos pasos

¿Te preguntas cómo renderizar de forma segura y con un buen rendimiento los datos transmitidos? Lee nuestras prácticas recomendadas para renderizar respuestas de LLM.