چگونه LLM ها پاسخ ها را پخش می کنند

تاریخ انتشار: 21 ژانویه 2025

یک پاسخ جریانی LLM شامل داده هایی است که به صورت تدریجی و پیوسته منتشر می شود. جریان داده با سرور و مشتری متفاوت به نظر می رسد.

از سرور

برای اینکه بفهمم یک پاسخ جریانی چگونه به نظر می رسد، از Gemini خواستم با استفاده از ابزار خط فرمان curl یک جوک طولانی به من بگوید. تماس زیر را با Gemini API در نظر بگیرید. اگر آن را امتحان کردید، حتماً کلید Gemini API خود را جایگزین {GOOGLE_API_KEY} در URL کنید.

$ 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}}
پس از اجرای دستور، نتیجه به صورت استریم در می آید.

اولین 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 بخواهید که یک تابع جاوا اسکریپت را برای تعیین زوج یا فرد بودن یک عدد ارائه دهد. text: تکه ها کمی متفاوت به نظر می رسند.

خروجی اکنون حاوی فرمت Markdown است که با بلوک کد جاوا اسکریپت شروع می شود. نمونه زیر شامل همان مراحل پیش پردازش قبلی است.

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

...

برای چالش‌برانگیزتر کردن مسائل، برخی از موارد علامت‌گذاری شده در یک قسمت شروع می‌شوند و به قسمت دیگر ختم می‌شوند. برخی از نشانه گذاری ها تودرتو هستند. در مثال زیر، تابع برجسته شده بین دو خط تقسیم شده است: ** number) function:** **isEven( و عدد):**. به صورت ترکیبی، خروجی **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 بخوانید.