So streamen LLMs Antworten

Veröffentlicht am 21. Januar 2025

Eine gestreamte LLM-Antwort besteht aus Daten, die inkrementell und kontinuierlich ausgegeben werden. Streamingdaten sehen auf dem Server und auf dem Client unterschiedlich aus.

Vom Server

Um zu sehen, wie eine gestreamte Antwort aussieht, habe ich Gemini über das Befehlszeilentool curl aufgefordert, mir einen langen Witz zu erzählen. Sehen Sie sich den folgenden Aufruf der Gemini API an. Wenn Sie es ausprobieren, müssen Sie {GOOGLE_API_KEY} in der URL durch Ihren Gemini API-Schlüssel ersetzen.

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

Bei dieser Anfrage wird die folgende (gekürzte) Ausgabe im Event-Stream-Format protokolliert. Jede Zeile beginnt mit data:, gefolgt von der Nutzlast der Nachricht. Das konkrete Format ist nicht wichtig, es kommt auf die Textblöcke an.

//
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}}
Nach der Ausführung des Befehls werden die Ergebnisblöcke gestreamt.

Die erste Nutzlast ist JSON. Sehen Sie sich die hervorgehobenen candidates[0].content.parts[0].text genauer an:

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

Der erste text-Eintrag ist der Beginn der Antwort von Gemini. Wenn Sie mehr als text Einträge extrahieren, wird die Antwort durch Zeilenumbrüche getrennt.

Das folgende Snippet enthält mehrere text-Einträge, die die endgültige Antwort des Modells zeigen.

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

...

Was passiert aber, wenn Sie das Modell nicht nach Witzen über Tyrannosaurus Rex, sondern nach etwas Komplexerem fragen? Sie können Gemini beispielsweise bitten, eine JavaScript-Funktion zu erstellen, mit der ermittelt wird, ob eine Zahl gerade oder ungerade ist. Die text:-Abschnitte sehen etwas anders aus.

Die Ausgabe enthält jetzt das Markdown-Format, beginnend mit dem JavaScript-Codeblock. Das folgende Beispiel enthält dieselben Vorverarbeitungsschritte wie zuvor.

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

...

Erschwerend kommt hinzu, dass einige der markierten Elemente in einem Chunk beginnen und in einem anderen enden. Einige Markups sind verschachtelt. Im folgenden Beispiel ist die hervorgehobene Funktion auf zwei Zeilen aufgeteilt: **isEven( und number) function:**. Die kombinierte Ausgabe ist **isEven("number) function:**. Wenn Sie also formatiertes Markdown ausgeben möchten, können Sie nicht einfach jeden Chunk einzeln mit einem Markdown-Parser verarbeiten.

Vom Kunden

Wenn Sie Modelle wie Gemma auf dem Client mit einem Framework wie MediaPipe LLM ausführen, werden Streamingdaten über eine Callback-Funktion bereitgestellt.

Beispiel:

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

Mit der Prompt API erhalten Sie Streamingdaten als Chunks, indem Sie eine ReadableStream durchlaufen.

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

Nächste Schritte

Sie fragen sich, wie Sie gestreamte Daten leistungsstark und sicher rendern können? Best Practices für das Rendern von LLM-Antworten