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

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

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

מהשרת

כדי להבין איך נראית תשובה בסטרימינג, ביקשתי מ-Gemini לספר לי בדיחה ארוכה באמצעות כלי שורת הפקודה curl. כדאי לעיין בקריאה הבאה ל-Gemini API. אם תנסו, הקפידו להחליף את {GOOGLE_API_KEY} בכתובת ה-URL במפתח Gemini API שלכם.

$ 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: ואחריה מטען הייעודי (payload) של ההודעה. הפורמט הספציפי לא חשוב, מה שחשוב זה חלקי הטקסט.

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

המטען הייעודי (payload) הראשון הוא 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"

...

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

מהלקוח

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

לדוגמה:

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

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

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

השלבים הבאים

רוצה לדעת איך לבצע רינדור של נתונים שמוזרמים בצורה יעילה ומאובטחת? כדאי לקרוא את השיטות המומלצות שלנו להצגת תשובות של מודלים גדולים של שפה.