Hoe LLM's reacties streamen

Gepubliceerd: 21 januari 2025

Een gestreamde LLM-respons bestaat uit data die incrementeel en continu wordt verzonden. Streaming data ziet er voor de server anders uit dan voor de client.

Van de server

Om te begrijpen hoe een gestreamde respons eruitziet, heb ik Gemini gevraagd een lange mop te vertellen met behulp van de opdrachtregeltool curl . Bekijk de volgende aanroep van de Gemini API. Als je dit probeert, vervang dan {GOOGLE_API_KEY} in de URL door je Gemini API-sleutel.

$ 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."}]}]}'

Dit verzoek registreert de volgende (ingekorte) uitvoer in eventstream-formaat . Elke regel begint met data: gevolgd door de berichtlading. De concrete opmaak is niet echt belangrijk, het gaat om de tekstfragmenten.

//
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}}
Nadat de opdracht is uitgevoerd, stromen de resulterende stukken binnen.

De eerste payload is JSON. Bekijk de gemarkeerde candidates[0].content.parts[0].text :

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

Die eerste text is het begin van Gemini's antwoord. Wanneer u meer text extraheert, wordt het antwoord gescheiden door nieuwe regels.

Het onderstaande fragment toont meerdere text die het uiteindelijke antwoord van het model laten zien.

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

...

Maar wat gebeurt er als je, in plaats van voor T-rex-grappen, het model om iets complexers vraagt? Vraag Gemini bijvoorbeeld om een ​​JavaScript-functie te bedenken om te bepalen of een getal even of oneven is? De text: chunks zien er iets anders uit.

De uitvoer bevat nu Markdown -formaat, beginnend met het JavaScript-codeblok. Het volgende voorbeeld bevat dezelfde pre-processing stappen als voorheen.

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

...

Om het nog ingewikkelder te maken, beginnen sommige gemarkeerde items in één blok en eindigen ze in een ander blok. Een deel van de markup is genest. In het volgende voorbeeld is de gemarkeerde functie verdeeld over twee regels: **isEven( en number) function:** . Gecombineerd is de uitvoer **isEven("number) function:** . Dit betekent dat als u geformatteerde Markdown wilt uitvoeren, u niet elk blok afzonderlijk kunt verwerken met een Markdown-parser.

Van de klant

Als u modellen als Gemma op de client uitvoert met een framework als MediaPipe LLM , komen de streaminggegevens via een callback-functie binnen.

Bijvoorbeeld:

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

Met de Prompt API krijgt u streaminggegevens in brokken door te itereren over een ReadableStream .

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

Volgende stappen

Vraagt ​​u zich af hoe u gestreamde data performant en veilig kunt renderen? Lees onze best practices voor het renderen van LLM-reacties .