使用 Prompt API 壓縮對話

發布日期:2026 年 6 月 23 日

每個 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> 元素中反映這項資訊,讓使用者一目瞭然工作階段是否即將達到上限。直接將 valuemax 設為權杖計數,瀏覽器會自動調整長條:

<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 不會在執行階段遭到逐出。瀏覽器不會移除這些提示,以便接收新的提示。不過,如果傳遞至 LanguageModel.create()initialPrompts 合併大小本身太大,無法放入脈絡視窗,create() 就會以 QuotaExceededError 拒絕,因此請確保壓縮大小足夠小,可繼續對話。
  • 移除次數有限制。如果傳入的提示過大,即使移除先前的所有對話,仍無法容納該提示,prompt()promptStreaming() 呼叫就會失敗並顯示 QuotaExceededError,且不會移除任何內容。

如要進一步瞭解如何處理內容溢位,請參閱 Prompt API 說明文件。

在瀏覽器開始自動捨棄對話記錄前,使用 contextoverflow 事件警告使用者、停用傳送按鈕,或自動觸發壓縮。

壓縮工作階段

壓縮作業分為三個步驟:

  1. 使用 Summarizer API 摘要說明對話記錄中的每則訊息。
  2. 銷毀舊的工作階段。
  3. 建立以摘要做為種子的新工作階段,並將其命名為 initialPrompts

重點摘要記錄

摘要 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: 'auto'preference: 'speed'

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' 可能會產生不必要的格式,為 Markdown 指定 'plain-text' 則會移除程式碼圍欄和強調效果。使用小型規則運算式區分兩者:

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) 可收合部分,檢查完整和壓縮的對話 JSON。

原始碼位於 GitHub