ضغط الجلسات باستخدام Prompt API

تاريخ النشر: 23 يونيو 2026

تتضمّن كل جلسة LanguageModel نافذة سياق محدودة. مع تطوّر المحادثة، يجمع النموذج سجلّ الرسائل الكامل في سياقه، أي كل طلب من المستخدم وكل ردّ من "مساعد Google". عندما تمتلئ النافذة، يبدأ المتصفّح تلقائيًا في التعامل مع المحتوى الزائد. ويؤدي ذلك إلى إزالة أقدم مجموعات الرسائل، أي طلب وردّ واحد في كل مرة، لإتاحة مساحة للطلب الجديد. إذا كان الطلب الوارد كبيرًا جدًا بحيث لا يمكن استيعابه حتى بعد إزالة سجل المحادثات بأكمله، ستتعذّر المكالمة تمامًا مع ظهور الخطأ QuotaExceededError.

ضغط الجلسات هو بديل استباقي: يمكنك تلخيص سجلّ المحادثات باستخدام Summarizer API، ثم إعادة تشغيل جلسة جديدة باستخدام هذه الملخّصات كـ initialPrompts. لا يزيل المتصفّح initialPrompts أبدًا أثناء معالجة تجاوز سعة وقت التشغيل، لذا يبقى الملخّص المضغوط ثابتًا بشكل دائم في سياق النموذج، طالما أنّ الملخّصات نفسها تتناسب مع قدرة استيعاب النموذج عند استدعاء create(). تحمل الجلسة الجديدة سلسلة المحادثات نفسها بجزء بسيط من تكلفة الرموز المميزة الأصلية.

تتيح ميزة "ضغط الجلسة" للمحادثات الطويلة الأمد LanguageModel البقاء ضمن نافذة السياق بدون فقدان الاستمرارية. في ما يلي الخطوات الرئيسية:

  1. مراقبة contextUsage مقارنةً contextWindow وعرضها للمستخدم
  2. استمع إلى الحدث contextoverflow كتحذير مبكر.
  3. رصد لغة كل رسالة باستخدام Language Detector API، ثم تلخيصها باستخدام مثيل من Summarizer API يراعي اللغة
  4. احذف الجلسة القديمة وأنشئ جلسة جديدة باستخدام initialPrompts.
  5. احتفِظ بنسخة fullHistory لاسترداد البيانات في حال حدوث خطأ.

تتبُّع استخدام السياق

تعرض Prompt API سمتَين لمراقبة مدى اكتمال سياق الجلسة، وهما:

  • session.contextUsage: عدد الرموز المميزة المستخدَمة حاليًا
  • session.contextWindow: إجمالي سعة الرموز المميزة للجلسة

يمكنك عرض ذلك في عنصر <progress> ليعرف المستخدمون بسرعة مدى اقتراب الجلسة من الحد الأقصى. اضبط 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() كبيرًا جدًا بحيث لا يمكن استيعابه في قدرة الاستيعاب، سيرفض create() الطلب مع عرض QuotaExceededError، لذا تأكَّد من أنّ عملية الضغط صغيرة بما يكفي لمواصلة المحادثة.
  • هناك حدّ أقصى لعمليات الإخلاء. إذا كان الطلب الوارد كبيرًا جدًا لدرجة أنّه لا يمكن استيعابه حتى بعد إزالة المحادثة السابقة بأكملها، ستتعذّر المكالمة prompt() أو promptStreaming() وسيظهر الخطأ QuotaExceededError ولن تتم إزالة أي شيء.

يمكنك الاطّلاع على مزيد من المعلومات حول التعامل مع تجاوز سعة السياق في مستندات Prompt API.

استخدِم حدث contextoverflow لتحذير المستخدم أو إيقاف زر الإرسال أو بدء عملية الضغط تلقائيًا قبل أن يبدأ المتصفّح في تجاهل سجلّ المحادثات بدون تنبيه.

تصغير الجلسة

تتضمّن عملية الضغط ثلاث خطوات:

  1. لخِّص كل رسالة في سجلّ المحادثات باستخدام Summarizer API.
  2. محو الجلسة القديمة
  3. أنشِئ جلسة جديدة تتضمّن الملخّصات كـ initialPrompts.

تلخيص السجلّ

تُعدّ Summarizer API أداة مناسبة لضغط رسائل المحادثة الفردية. بالنسبة إلى كل رسالة، عليك أولاً رصد لغتها باستخدام Language Detector API لكي تتمكّن من ضبط أداة التلخيص بشكل صحيح:

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 الكامل والمضغوط للمحادثة في القسم القابل للتصغير تصحيح الأخطاء: JSON للمحادثة في أسفل الصفحة.

يتوفّر الرمز المصدر على GitHub.