فشرده‌سازی جلسه با Prompt API

منتشر شده: ۲۳ ژوئن ۲۰۲۶

هر جلسه LanguageModel دارای یک پنجره متن محدود است. با رشد یک مکالمه، مدل کل تاریخچه پیام را در متن خود جمع‌آوری می‌کند: هر پیام کاربر و هر پاسخ دستیار. وقتی پنجره پر می‌شود، مدیریت خودکار سرریز مرورگر شروع می‌شود. قدیمی‌ترین جفت پیام، یک جفت پیام و یک جفت پاسخ را به طور همزمان حذف می‌کند تا فضای کافی برای پیام جدید ایجاد شود. اگر پیام ورودی آنقدر بزرگ باشد که حذف کل تاریخچه مکالمه در آن جا نشود، فراخوانی با QuotaExceededError به طور کامل شکست می‌خورد.

فشرده‌سازی جلسه یک جایگزین پیشگیرانه است: تاریخچه مکالمه را با Summarizer API خلاصه کنید، سپس یک جلسه جدید را با استفاده از آن خلاصه‌ها به عنوان initialPrompts مجدداً راه‌اندازی کنید. مرورگر هرگز initialPrompts در حین مدیریت سرریز زمان اجرا حذف نمی‌کند، بنابراین خلاصه فشرده شده به طور دائم در متن مدل ثابت می‌ماند، تا زمانی که خود خلاصه‌ها هنگام فراخوانی create() در پنجره متن قرار گیرند. جلسه جدید همان رشته مکالمه را با کسری از هزینه توکن اصلی حمل می‌کند.

فشرده‌سازی جلسه به مکالمات LanguageModel با طول عمر بالا این امکان را می‌دهد که بدون از دست دادن پیوستگی، در پنجره‌ی زمینه باقی بمانند. مراحل کلیدی عبارتند از:

  1. contextUsage نسبت به contextWindow رصد کنید و آن را به کاربر نمایش دهید.
  2. به رویداد contextoverflow به عنوان یک هشدار اولیه گوش دهید.
  3. زبان هر پیام را با استفاده از API تشخیص زبان (Language Detector API) تشخیص دهید، سپس آن را با یک نمونه API Summarizer آگاه از زبان خلاصه کنید.
  4. جلسه قدیمی را از بین ببرید و یک جلسه جدید را با initialPrompts ایجاد کنید.
  5. یک کپی fullHistory را برای بازیابی خطا نگه دارید.

پیگیری استفاده از زمینه

رابط برنامه‌نویسی کاربردی Prompt دو ​​ویژگی برای نظارت بر میزان پر بودن محتوای یک جلسه ارائه می‌دهد:

  • session.contextUsage : تعداد توکن‌های مصرف‌شده در حال حاضر.
  • session.contextWindow : ظرفیت کل توکن‌های جلسه.

این را در یک عنصر <progress> منعکس کنید تا کاربران با یک نگاه بدانند که session چقدر به حد مجاز خود نزدیک شده است. value و max را مستقیماً روی تعداد توکن‌ها تنظیم کنید؛ مرورگر به طور خودکار نوار را مقیاس‌بندی می‌کند:

<progress id="token-bar" value="0" max="1"></progress>
<label for="token-bar" id="token-label">Context: — / — tokens</label>
function updateTokenDisplay(session) {
  const usage = session.contextUsage;
  const total = session.contextWindow;

  tokenBar.value = usage;
  tokenBar.max = total;
  tokenLabel.textContent =
    `${Math.round(usage)} / ${Math.round(total)} tokens ` +
    `(${Math.round((usage / total) * 100)}%)`;
}

بعد از هر پاسخ سریع، تابع updateTokenDisplay() را فراخوانی کنید تا نوار به‌روز بماند.

به سرریز متن گوش دهید

وقتی یک اعلان جدید از فضای باقی‌مانده تجاوز کند، مرورگر به‌طور خودکار بازیابی را آغاز می‌کند: قدیمی‌ترین جفت اعلان و پاسخ را یکی‌یکی حذف می‌کند تا فضای کافی آزاد شود. رویداد contextoverflow در لحظه شروع این حذف اجرا می‌شود. بلافاصله پس از ایجاد جلسه، یک کنترل‌کننده ثبت کنید:

session.addEventListener('contextoverflow', () => {
  showWarning('⚠ Context window nearly full. Consider compacting the session.');
});

دو ویژگی مهم در این رفتار اخراج وجود دارد:

  • initialPrompts در زمان اجرا حذف نمی‌شوند. مرورگر آنها را برای ایجاد فضا برای اعلان ورودی حذف نمی‌کند. با این حال، اگر اندازه ترکیبی initialPrompts ارسالی به LanguageModel.create() خود برای جا شدن در پنجره context خیلی بزرگ باشد، create() با یک QuotaExceededError رد می‌شود، بنابراین مطمئن شوید که فشرده‌سازی به اندازه کافی کوچک است تا بتوانید مکالمه را ادامه دهید.
  • حذف محدودیت دارد. اگر اعلان ورودی آنقدر بزرگ باشد که حذف کل مکالمه قبلی هنوز در آن جا نشود، فراخوانی prompt() یا promptStreaming() با QuotaExceededError با شکست مواجه می‌شود و هیچ چیزی حذف نمی‌شود.

برای اطلاعات بیشتر در مورد مدیریت سرریز متن، مستندات Prompt API را مطالعه کنید.

از رویداد contextoverflow برای هشدار به کاربر، غیرفعال کردن دکمه ارسال یا فعال کردن خودکار فشرده‌سازی قبل از اینکه مرورگر شروع به حذف بی‌سروصدای تاریخچه مکالمات کند، استفاده کنید.

جلسه را فشرده کنید

فشرده‌سازی سه مرحله دارد:

  1. هر پیام در تاریخچه مکالمه را با استفاده از Summarizer API خلاصه کنید.
  2. جلسه قدیمی را نابود کنید.
  3. یک جلسه جدید ایجاد کنید که خلاصه‌ها را به عنوان initialPrompts در آن قرار دهید.

خلاصه کردن تاریخچه

رابط برنامه‌نویسی Summarizer برای فشرده‌سازی پیام‌های چت به صورت جداگانه مناسب است. برای هر پیام، ابتدا زبان آن را با رابط برنامه‌نویسی تشخیص زبان شناسایی کنید تا خلاصه‌ساز بتواند به درستی پیکربندی شود:

async function detectLanguage(text, threshold = 0.7) {
  const detector = await LanguageDetector.create();
  const results = await detector.detect(text);
  if (results.length > 0 && results[0].confidence >= threshold) {
    return results[0].detectedLanguage;
  }
  return null; // confidence too low — caller falls back to navigator.language
}

آستانه اطمینان 0.7 از اقدام در صورت تشخیص‌های نامشخص جلوگیری می‌کند. وقتی اطمینان کمتر از آستانه باشد، به navigator.language برگردید.

در مرحله بعد، یک خلاصه‌ساز ایجاد کنید که برای زبان شناسایی‌شده پیکربندی شده باشد. برای انتخاب مدل کوچک‌تر و کم‌تاخیرتر، preference: 'speed' را انتخاب کنید و اگر مدل سریع‌تر از زبان شناسایی‌شده پشتیبانی نمی‌کند، به preference: 'auto' برگردید:

const summarizers = {}; // cache, keyed by `${format}:${lang}`

async function getSummarizer(format, lang) {
  const key = `${format}:${lang}`;
  if (summarizers[key]) return summarizers[key];

  const baseOptions = {
    type: 'tldr',
    format, // 'markdown' or 'plain-text'
    length: 'short',
    expectedInputLanguages: [lang],
    expectedContextLanguages: [lang],
    outputLanguage: lang,
  };

  let options = { ...baseOptions, preference: 'speed' };
  let avail = await Summarizer.availability(options);

  if (avail === 'unavailable') {
    options = { ...baseOptions, preference: 'auto' };
    avail = await Summarizer.availability(options);
  }

  if (avail === 'unavailable') {
    throw new Error('Summarizer API unavailable on this device.');
  }

  summarizers[key] = await Summarizer.create(options);
  return summarizers[key];
}

ذخیره خلاصه‌سازها به ازای هر جفت format + lang از فراخوانی‌های تکراری create() در زمانی که پیام‌های متوالی زبان یکسانی دارند، جلوگیری می‌کند.

آرگومان format از خود محتوای پیام گرفته شده است. تعیین 'markdown' برای نثر ساده می‌تواند قالب‌بندی ناخواسته‌ای ایجاد کند، و تعیین 'plain-text' برای Markdown، حصارهای کد و تأکید را از بین می‌برد. یک عبارت منظم کوچک این دو را از هم متمایز می‌کند:

function looksLikeMarkdown(text) {
  return /(?:^#{1,6} |^[-*+] |\d+\. |\*\*|__|\[.+?\]\(|^> |^```)/m.test(text);
}

با مشخص شدن زبان و قالب، هر پیام را خلاصه کنید و یک رشته context ارسال کنید تا مدل متوجه شود که در حال فشرده‌سازی یک نوبت چت است، نه یک سند مستقل:

const compacted = [];

for (const msg of history) {
  const lang = (await detectLanguage(msg.content)) ?? navigator.language;
  const format = looksLikeMarkdown(msg.content) ? 'markdown' : 'plain-text';
  const summarizer = await getSummarizer(format, lang);

  const summary = await summarizer.summarize(msg.content.trim(), {
    context:
      `This is a ${msg.role} turn from a chat conversation. ` +
      `Preserve its key meaning as concisely as possible.`,
  });

  // Only use the summary if it's actually shorter.
  compacted.push({
    role: msg.role,
    content:
      summary.trim().length < msg.content.length ? summary.trim() : msg.content,
  });
}

جلسه قدیمی را از بین ببرید

قبل از ایجاد جایگزین، منابع جلسه قدیمی را آزاد کنید:

session.destroy();
session = null;

ایجاد یک جلسه جدید با تاریخچه فشرده

پیام‌های فشرده‌شده را به عنوان initialPrompts ارسال کنید تا جلسه جدید با زمینه مکالمه راه‌اندازی شود:

// Collect every language the detector was confident about.
const sessionLangs =
  confidentLangs.size > 0 ? [...confidentLangs] : [navigator.language];

session = await LanguageModel.create({
  expectedInputs: [{ type: 'text', languages: sessionLangs }],
  expectedOutputs: [{ type: 'text', languages: sessionLangs }],
  initialPrompts: compacted,
});

// Re-register the overflow handler on the new session.
session.addEventListener('contextoverflow', () => {
  /* ... */
});

جلسه جدید با contextUsage پایین‌تری شروع می‌شود. مکالمه از جایی که متوقف شده بود ادامه می‌یابد: مدل خلاصه‌ها را به عنوان زمینه قبلی خود دارد، بنابراین می‌تواند به سؤالات بعدی در مورد موضوعات قبلی پاسخ دهد.

مدیریت خطاها

اگر خلاصه‌سازی یا ایجاد جلسه پس از نابودی جلسه قبلی با شکست مواجه شود، کاربر توانایی چت کردن را از دست می‌دهد. یک آرایه fullHistory جداگانه که هرگز توسط فشرده‌سازی رونویسی نمی‌شود، نگهداری کنید و از آن به عنوان پشتیبان بازیابی استفاده کنید:

const history = []; // current session's view, replaced on each compaction
const fullHistory = []; // every original message, never overwritten

// In the catch block:
if (!session) {
  session = await LanguageModel.create({
    initialPrompts: fullHistory.map(({ role, content }) => ({ role, content })),
  });
  session.addEventListener('contextoverflow', () => {
    /* ... */
  });
}

بازیابی از fullHistory ممکن است دوباره متن را به ظرفیت نزدیک کند، اما کاربر حداقل به حالت کار برمی‌گردد و می‌تواند بلافاصله فشرده‌سازی دیگری را امتحان کند.

به صورت اختیاری از فشرده شدن برخی از محتوا جلوگیری کنید

اگر بخش‌های مهمی از یک پیام وجود دارد که باید همیشه در متن باقی بمانند، مثلاً نمونه‌های کد، آنها را جداگانه پردازش کنید. مثال زیر یک پیام را به بخش‌های متناوب نثر و کد-حصار تقسیم می‌کند، سپس فقط بخش‌های نثر را خلاصه می‌کند و بخش‌های کد را دست نخورده باقی می‌گذارد:

// Splits text into alternating prose and code-fence segments.
// Returns [{ type: 'prose'|'code', content: string }, …]
function splitByCodeFences(text) {
  const parts = [];
  const re = /^```[^\n]*\n[\s\S]*?^```[ \t]*$/gm;
  let lastIndex = 0;
  let match;
  while ((match = re.exec(text)) !== null) {
    if (match.index > lastIndex) {
      parts.push({
        type: 'prose',
        content: text.slice(lastIndex, match.index),
      });
    }
    parts.push({ type: 'code', content: match[0] });
    lastIndex = match.index + match[0].length;
  }
  if (lastIndex < text.length) {
    parts.push({ type: 'prose', content: text.slice(lastIndex) });
  }
  return parts;
}

نسخه آزمایشی را امتحان کنید

نسخه آزمایشی فشرده‌سازی جلسه به شما امکان می‌دهد با Prompt API چت کنید و جلسه را در هر زمانی فشرده‌سازی کنید. نوار توکن، میزان استفاده از متن را در لحظه نشان می‌دهد و با پر شدن متن، رنگ آن تغییر می‌کند. پس از هر فشرده‌سازی، یک ورودی لاگ، تعداد توکن‌ها را قبل و بعد ثبت می‌کند تا بتوانید مستقیماً کاهش را مشاهده کنید.

شما می‌توانید JSON مکالمه کامل و فشرده را در بخش Debug: conversation JSON که به صورت جمع‌شونده در پایین صفحه قرار دارد، بررسی کنید.

کد منبع در گیت‌هاب قرار دارد .