Comment les LLM diffusent-ils des réponses ?

Publié le 21 janvier 2025

Une réponse LLM en streaming se compose de données émises de manière incrémentielle et continue. Les données de streaming sont différentes du serveur et du client.

Du serveur

Pour comprendre à quoi ressemble une réponse lue en streaming, j'ai demandé à Gemini de me raconter une longue blague à l'aide de l'outil de ligne de commande curl. Prenons l'appel suivant à l'API Gemini. Si vous essayez, veillez à remplacer {GOOGLE_API_KEY} dans l'URL par votre clé 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."}]}]}'

Cette requête consigne la sortie (tronquée) suivante au format flux d'événements. Chaque ligne commence par data:, suivi de la charge utile du message. Le format concret n'est pas vraiment important. Ce qui compte, ce sont les blocs de texte.

//
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}}
Une fois la commande exécutée, les segments de résultats sont diffusés.

La première charge utile est au format JSON. Examinons de plus près l'candidates[0].content.parts[0].text en surbrillance:

{
  "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
  }
}

Cette première entrée text correspond au début de la réponse de Gemini. Lorsque vous extrayez plusieurs entrées text, la réponse est délimitée par un saut de ligne.

L'extrait de code suivant affiche plusieurs entrées text, qui montrent la réponse finale du modèle.

"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?\""

...

Mais que se passe-t-il si, au lieu de demander des blagues sur les T-Rex, vous demandez au modèle quelque chose de légèrement plus complexe ? Par exemple, demandez à Gemini de créer une fonction JavaScript pour déterminer si un nombre est pair ou impair. Les blocs text: sont légèrement différents.

La sortie contient désormais le format Markdown, en commençant par le bloc de code JavaScript. L'exemple suivant inclut les mêmes étapes de prétraitement que précédemment.

"```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"

...

Pour compliquer la tâche, certains des éléments marqués commencent dans un bloc et se terminent dans un autre. Certaines balises sont imbriquées. Dans l'exemple suivant, la fonction mise en surbrillance est répartie sur deux lignes : **isEven( et number) function:**. Combiné, le résultat est **isEven("number) function:**. Cela signifie que si vous souhaitez générer une sortie Markdown mise en forme, vous ne pouvez pas simplement traiter chaque bloc individuellement avec un analyseur Markdown.

Du client

Si vous exécutez des modèles tels que Gemma sur le client avec un framework tel que MediaPipe LLM, les données de streaming sont transmises via une fonction de rappel.

Exemple :

llmInference.generateResponse(
  inputPrompt,
  (chunk, done) => {
     console.log(chunk);
});

Avec l'API Prompt, vous obtenez des données de streaming sous forme de blocs en itérant sur un ReadableStream.

const languageModel = await self.ai.languageModel.create();
const stream = languageModel.promptStreaming(inputPrompt);
for await (const chunk of stream) {
  console.log(chunk);
}

Étapes suivantes

Vous vous demandez comment afficher des données en streaming de manière performante et sécurisée ? Consultez nos bonnes pratiques pour afficher les réponses du LLM.