איך מודלים גדולים של שפה (LLM) משדרים תשובות

תאריך פרסום: 21 בינואר 2025

תגובה של LLM בסטרימינג מורכבת מנתונים שנפלטים באופן מצטבר ורציף. נתוני הסטרימינג נראים שונים מהשרת ומהלקוח.

מהשרת

כדי להבין איך נראית תשובה בסטרימינג, ביקשתי מ-Gemini לספר לי בדיחה ארוכה באמצעות הכלי בשורת הפקודה curl. עיינו בקריאה הבאה ל-Gemini API. אם תנסו את זה, חשוב להחליף את הערך {GOOGLE_API_KEY} בכתובת ה-URL במפתח ה-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."}]}]}'

הבקשה הזו מתעדת ביומן את הפלט הבא (קטוץ), בפורמט של מקור אירועים. כל שורה מתחילה ב-data: ואחריה עוקבת המטען הייעודי של ההודעה. הפורמט הספציפי לא חשוב, מה שחשוב הם קטעי הטקסט.

//
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}}
אחרי הרצת הפקודה, קטעי התוצאה מגיעים בזרם.

המטען הייעודי הראשון הוא JSON. נבחן מקרוב את הערך המודגש 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
  }
}

הרשומה הראשונה של text היא תחילת התשובה של Gemini. כשחוטפים עוד רשומות text, התשובה מחולקת לפי שורות חדשות.

בקטע הקוד הבא מוצגות כמה רשומות text, שמציגות את התגובה הסופית מהמודל.

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

...

אבל מה קורה אם במקום לבקש מהמודל בדיחות על טי-רקס, מבקשים ממנו משהו קצת יותר מורכב? לדוגמה, אפשר לבקש מ-Gemini ליצור פונקציית JavaScript כדי לקבוע אם מספר הוא זוגי או אי-זוגי. הקטעים של text: נראים קצת שונים.

הפלט מכיל עכשיו את הפורמט Markdown, שמתחיל בבלוק הקוד של JavaScript. הדוגמה הבאה כוללת את אותם שלבי עיבוד מראש כמו קודם.

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

...

כדי להקשות על העניין, חלק מהפריטים שסומנו מתחילים בחלק אחד ומסתיימים בחלק אחר. חלק מהתגים של ה-Markup נמצאים בתוך תגים אחרים. בדוגמה הבאה, הפונקציה המודגשת מחולקת לשתי שורות: **isEven( ו-number) function:**. הפלט המצטבר הוא **isEven("number) function:**. כלומר, אם רוצים להפיק פלט של Markdown בפורמט, אי אפשר פשוט לעבד כל מקטע בנפרד באמצעות מנתח Markdown.

מהלקוח

אם אתם מפעילים מודלים כמו Gemma בצד הלקוח באמצעות מסגרת כמו MediaPipe LLM, נתוני הסטרימינג מגיעים דרך פונקציית קריאה חוזרת (callback).

לדוגמה:

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

באמצעות Prompt API, אפשר לקבל נתונים בסטרימינג כקטעים על ידי איטרציה על ReadableStream.

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

השלבים הבאים

רוצים לדעת איך לבצע רינדור של נתונים בסטרימינג בצורה בטוחה ויעילה? שיטות מומלצות להצגת תשובות של LLM