Prompt API を使用したセッションの圧縮

公開日: 2026 年 6 月 23 日

すべての LanguageModel セッションには、有限のコンテキスト ウィンドウがあります。会話が長くなるにつれて、モデルはコンテキストにメッセージの履歴全体(すべてのユーザー プロンプトとすべてのアシスタントの回答)を蓄積します。ウィンドウがいっぱいになると、ブラウザの自動オーバーフロー処理が開始されます。最も古いメッセージ ペア(プロンプトとレスポンスのペア)を一度に 1 つずつ削除して、新しいプロンプトのための空き容量を確保します。受信したプロンプトが大きすぎて、会話履歴全体を削除しても収まらない場合、呼び出しは QuotaExceededError で完全に失敗します。

セッションの圧縮は、プロアクティブな代替手段です。Summarizer API を使用して会話履歴を要約し、それらの要約を initialPrompts として使用して新しいセッションを再開します。ブラウザは実行時のオーバーフロー処理中に initialPrompts を削除しないため、create() が呼び出されたときに要約自体がコンテキスト ウィンドウ内に収まる限り、圧縮された要約はモデルのコンテキストに永続的に固定されます。新しいセッションでは、元のトークン費用のほんの一部で同じ会話スレッドが引き継がれます。

セッションの圧縮により、継続性を失うことなく、有効期間の長い LanguageModel 会話がコンテキスト ウィンドウ内に収まるようになります。主な手順は次のとおりです。

  1. contextWindow を基準に contextUsage をモニタリングし、ユーザーに表示します。
  2. 早期警告として contextoverflow イベントをリッスンします。
  3. Language Detector API を使用して各メッセージの言語を検出し、言語認識型の Summarizer API インスタンスを使用して要約します。
  4. 古いセッションを破棄し、initialPrompts で新しいセッションをシードします。
  5. エラー復旧用に fullHistory のコピーを保持します。

コンテキストの使用状況を追跡する

Prompt API は、セッションのコンテキストの充足度をモニタリングするための 2 つの属性を公開します。

  • 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() を呼び出して、バーを最新の状態に保ちます。

コンテキストのオーバーフローをリッスンする

新しいプロンプトが残りのコンテキストを超えると、ブラウザの自動復元が開始されます。十分なスペースが確保されるまで、最も古いプロンプトとレスポンスのペアが 1 つずつ削除されます。contextoverflow イベントは、この削除が開始されたときに発生します。セッションの作成直後にハンドラを登録します。

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

この削除動作には、次の 2 つの重要なプロパティがあります。

  • initialPrompts は実行時に削除されません。ブラウザは、受信したプロンプトのためにそれらを削除しません。ただし、LanguageModel.create() に渡される initialPrompts の合計サイズがコンテキスト ウィンドウに収まるほど小さくない場合、create()QuotaExceededError で拒否します。そのため、会話を続行できるほど圧縮が小さくなるようにしてください。
  • エビクションには上限があります。受信したプロンプトが大きすぎて、前の会話全体を削除しても収まらない場合、prompt() または promptStreaming() 呼び出しは QuotaExceededError で失敗し、何も削除されません。

Prompt API ドキュメントのコンテキスト オーバーフローの処理をご覧ください。

contextoverflow イベントを使用して、ブラウザが会話履歴の破棄をサイレントに開始する前に、ユーザーに警告したり、送信ボタンを無効にしたり、自動的に圧縮をトリガーしたりします。

セッションを圧縮する

圧縮には次の 3 つのステップがあります。

  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' を指定すると、不要な書式設定が導入される可能性があります。Markdown に 'plain-text' を指定すると、コードフェンスと強調が削除されます。次の小さな正規表現で 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 とチャットして、いつでもセッションを圧縮できます。トークンバーには、コンテキストの使用状況がリアルタイムで表示され、コンテキストが満たされるにつれて色が変わります。圧縮のたびに、ログエントリに圧縮前後のトークン数が記録されるため、削減を直接確認できます。

ページ下部の折りたたみ可能な [Debug: conversation JSON] セクションで、会話の完全な JSON と圧縮された JSON を確認できます。

ソースコードは GitHub にあります