תאריך פרסום: 23 ביוני 2026
לכל סשן של LanguageModel יש חלון הקשר סופי. ככל שהשיחה מתארכת, המודל צובר בהקשר שלו את היסטוריית ההודעות המלאה: כל הנחיה של המשתמש וכל תשובה של ה-Assistant. כשהחלון מתמלא, מופעלת התכונה האוטומטית של הדפדפן לטיפול בגלישה מעבר לגבולות החלון. הוא מסיר את זוגות ההודעות הכי ישנים, כלומר הנחיה ותגובה, כדי לפנות מקום להנחיה החדשה. אם ההנחיה הנכנסת כל כך גדולה שאפילו אחרי הסרת כל היסטוריית השיחות היא לא נכנסת, השיחה נכשלת לגמרי עם QuotaExceededError.
דחיסת סשנים היא חלופה פרואקטיבית: אפשר לסכם את היסטוריית השיחות באמצעות Summarizer API, ואז להפעיל סשן חדש באמצעות הסיכומים האלה כ-initialPrompts. הדפדפן אף פעם לא מסלק את initialPrompts במהלך הטיפול בגלישה מעבר למגבלות הזיכרון בזמן הריצה, ולכן הסיכום הדחוס נשאר קבוע בהקשר של המודל, כל עוד הסיכומים עצמם מתאימים לחלון ההקשר כשקוראים ל-create(). הסשן החדש כולל את אותו שרשור שיחה, בעלות של חלק קטן מעלות הטוקנים המקורית.
דחיסת סשנים מאפשרת לשיחות ארוכות טווח של LanguageModel להישאר בחלון ההקשר בלי לאבד את הרציפות. השלבים העיקריים הם:
- מעקב אחרי
contextUsageביחס ל-contextWindowוהצגת המידע למשתמש. - כדאי להאזין לאירוע
contextoverflowכהתרעה מוקדמת. - לזהות את השפה של כל הודעה באמצעות Language Detector API, ואז לסכם אותה באמצעות מופע של Summarizer API שמודע לשפה.
- משמידים את הסשן הישן ויוצרים סשן חדש עם
initialPrompts. - שמירת עותק של
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 מופעל ברגע שמתחיל הפינוי. רושמים את ה-handler מיד אחרי שיוצרים את הסשן:
session.addEventListener('contextoverflow', () => {
showWarning('⚠ Context window nearly full. Consider compacting the session.');
});
יש שני מאפיינים חשובים של התנהגות ההוצאה הזו:
initialPromptsלא מפונים בזמן הריצה. הדפדפן לא מסיר אותם כדי לפנות מקום להנחיה נכנסת. עם זאת, אם הגודל המשולב שלinitialPromptsשעבר אלLanguageModel.create()גדול מדי מכדי להיכנס לחלון ההקשר,create()דוחה את הבקשה עםQuotaExceededError, לכן חשוב לוודא שהדחיסה קטנה מספיק כדי להמשיך את השיחה.- יש מגבלה על מספר הפעמים שאפשר להסיר משתמשים. אם ההנחיה הנכנסת גדולה מדי, כך שגם אחרי הסרת כל השיחה הקודמת היא עדיין לא מתאימה, הקריאה ל-
prompt()או ל-promptStreaming()נכשלת עםQuotaExceededErrorולא מוסר שום דבר.
במסמכי התיעוד של Prompt API אפשר לקרוא מידע נוסף על טיפול בהצפה של הקשר.
אפשר להשתמש באירוע contextoverflow כדי להזהיר את המשתמש, להשבית את לחצן השליחה או להפעיל דחיסה באופן אוטומטי לפני שהדפדפן מתחיל להסיר בשקט את היסטוריית השיחות.
כיווץ הסשן
תהליך הדחיסה כולל שלושה שלבים:
- מסכמים כל הודעה בהיסטוריית השיחות באמצעות Summarizer API.
- השמדת הסשן הישן.
- יוצרים סשן חדש עם התקצירים בתור
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 ולדחוס את הסשן בכל שלב. בסרגל הטוקנים מוצג שימוש בהקשר בזמן אמת, והצבע שלו משתנה ככל שההקשר מתמלא. אחרי כל דחיסה, רשומה ביומן מתעדת את מספר האסימונים לפני ואחרי, כדי שתוכלו לראות ישירות את ההפחתה.
בתחתית הדף, בקטע Debug: conversation JSON שאפשר לכווץ, אפשר לבדוק את קובץ ה-JSON המלא והדחוס של השיחה.