การย่อเซสชันด้วย Prompt API

เผยแพร่เมื่อ: 23 มิถุนายน 2026

เซสชัน LanguageModel ทุกเซสชันมีหน้าต่างบริบทที่จำกัด เมื่อการสนทนามีขนาดใหญ่ขึ้น โมเดลจะสะสมประวัติข้อความทั้งหมดไว้ในบริบท ซึ่งรวมถึงพรอมต์ของผู้ใช้ทุกรายการและการตอบกลับของผู้ช่วยทุกรายการ เมื่อหน้าต่างเต็ม ระบบจัดการการล้นอัตโนมัติของเบราว์เซอร์จะเริ่มทำงาน ระบบจะนำคู่ข้อความที่เก่าที่สุดออกทีละคู่ (พรอมต์ 1 รายการและการตอบกลับ 1 รายการ) เพื่อเพิ่มพื้นที่ว่างสำหรับพรอมต์ใหม่ หากพรอมต์ขาเข้ามีขนาดใหญ่มากจนการนำประวัติการสนทนาทั้งหมดออกก็ยังไม่พอ ระบบจะยกเลิกการเรียกโดยสมบูรณ์พร้อมแสดง QuotaExceededError

การบีบอัดเซสชัน เป็นทางเลือกเชิงรุก โดยคุณสามารถสรุปประวัติการสนทนา ด้วย Summarizer API แล้วเริ่มเซสชันใหม่ โดยใช้ข้อมูลสรุปเหล่านั้นเป็น initialPrompts เบราว์เซอร์จะไม่นำ initialPrompts ออกระหว่างการจัดการการล้นในรันไทม์ ดังนั้นข้อมูลสรุปที่บีบอัดแล้วจะยังคงอยู่ในบริบทของโมเดลอย่างถาวร ตราบใดที่ข้อมูลสรุปเองมีขนาดพอดีกับหน้าต่างบริบทเมื่อมีการเรียก create() เซสชันใหม่จะดำเนินการสนทนาในหัวข้อเดิมด้วยค่าใช้จ่ายโทเค็นเพียงเล็กน้อยเมื่อเทียบกับค่าใช้จ่ายเดิม

การบีบอัดเซสชันช่วยให้การสนทนา LanguageModel ที่มีระยะเวลานานสามารถอยู่ในหน้าต่างบริบทได้โดยไม่สูญเสียความต่อเนื่อง ขั้นตอนสำคัญมีดังนี้

  1. ตรวจสอบ contextUsage เทียบกับ contextWindow แล้วแสดงให้ผู้ใช้เห็น
  2. ฟังเหตุการณ์ contextoverflow เพื่อรับคำเตือนล่วงหน้า
  3. ตรวจหาภาษาของข้อความแต่ละรายการด้วย Language Detector API แล้วสรุปข้อความนั้นด้วยอินสแตนซ์ Summarizer API ที่รองรับภาษา
  4. ทำลายเซสชันเก่าและสร้างเซสชันใหม่ด้วย initialPrompts
  5. เก็บสำเนา fullHistory ไว้สำหรับการกู้คืนข้อผิดพลาด

ติดตามการใช้บริบท

Prompt API แสดงแอตทริบิวต์ 2 รายการสำหรับการตรวจสอบว่าบริบทของเซสชันเต็มแล้วหรือยัง ดังนี้

  • 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.');
});

ลักษณะการทำงานของการนำออกนี้มีพร็อพเพอร์ตี้สำคัญ 2 รายการ ดังนี้

  • ระบบจะไม่นำ initialPrompts ออกในรันไทม์ เบราว์เซอร์จะไม่นำออกเพื่อเพิ่มพื้นที่ว่างสำหรับพรอมต์ขาเข้า อย่างไรก็ตาม หากขนาดรวมของ the initialPrompts ที่ส่งไปยัง LanguageModel.create() มีขนาดใหญ่เกินกว่า ที่จะใส่ในหน้าต่างบริบทได้ create() จะปฏิเสธด้วย QuotaExceededError ดังนั้นโปรดตรวจสอบว่าการบีบอัดมีขนาดเล็กพอที่จะ ดำเนินการสนทนาต่อได้
  • การนำออกมีขีดจำกัด หากพรอมต์ขาเข้ามีขนาดใหญ่มากจนการนำการสนทนาก่อนหน้าทั้งหมดออกก็ยังไม่พอ การเรียก prompt() หรือ promptStreaming() จะล้มเหลวด้วย QuotaExceededError และระบบจะไม่นำสิ่งใดออก

อ่านเพิ่มเติมเกี่ยวกับการจัดการการล้นบริบท ในเอกสารประกอบ Prompt API

ใช้เหตุการณ์ contextoverflow เพื่อเตือนผู้ใช้ ปิดใช้ปุ่มส่ง หรือทริกเกอร์การบีบอัดโดยอัตโนมัติก่อนที่เบราว์เซอร์จะเริ่มทิ้งประวัติการสนทนาโดยไม่แจ้งให้ทราบ

บีบอัดเซสชัน

การบีบอัดมี 3 ขั้นตอน ดังนี้

  1. สรุปข้อความแต่ละรายการในประวัติการสนทนาด้วย Summarizer API
  2. ทำลายเซสชันเก่า
  3. สร้างเซสชันใหม่โดยใช้ข้อมูลสรุปเป็น initialPrompts

สรุปประวัติ

Summarizer API เหมาะอย่างยิ่งสำหรับการบีบอัดข้อความแชทแต่ละรายการ สำหรับข้อความแต่ละรายการ ให้ตรวจหาภาษาก่อนด้วย Language Detector API เพื่อให้กำหนดค่า 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

จากนั้นสร้าง Summarizer ที่กำหนดค่าไว้สำหรับภาษาที่ตรวจพบ เลือก 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];
}

การแคช Summarizer ต่อคู่ format+lang ช่วยหลีกเลี่ยงการเรียก create() ที่ซ้ำซ้อนเมื่อข้อความที่ต่อเนื่องกันใช้ภาษาเดียวกัน

อาร์กิวเมนต์ format ได้มาจากเนื้อหาของข้อความเอง การระบุ 'markdown' สำหรับข้อความธรรมดาอาจทำให้เกิดการจัดรูปแบบที่ไม่ต้องการ และการระบุ 'plain-text' สำหรับ Markdown จะลบโค้ดและตัวเน้น นิพจน์ทั่วไปขนาดเล็กจะแยกความแตกต่างระหว่าง 2 อย่างนี้

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 ที่ยุบได้ที่ด้านล่างของหน้า

ซอร์สโค้ดอยู่ใน GitHub