프롬프트 API를 사용한 세션 압축

게시일: 2026년 6월 23일

모든 LanguageModel 세션에는 유한한 컨텍스트 윈도우가 있습니다. 대화가 길어지면 모델은 컨텍스트에 전체 메시지 기록(모든 사용자 프롬프트와 모든 어시스턴트 대답)을 누적합니다. 창이 가득 차면 브라우저의 자동 오버플로 처리가 시작됩니다. 가장 오래된 메시지 쌍(프롬프트와 응답 쌍)을 한 번에 하나씩 삭제하여 새 프롬프트를 위한 공간을 확보합니다. 수신되는 프롬프트가 너무 커서 전체 대화 기록을 삭제해도 맞지 않으면 QuotaExceededError와 함께 호출이 완전히 실패합니다.

세션 압축은 사전 대안입니다. 요약 도구 API로 대화 기록을 요약한 다음 요약을 initialPrompts로 사용하여 새 세션을 다시 시작합니다. 브라우저는 런타임 오버플로 처리 중에 initialPrompts를 삭제하지 않으므로 create()가 호출될 때 요약 자체가 컨텍스트 윈도우 내에 맞는 한 압축된 요약은 모델의 컨텍스트에 영구적으로 고정됩니다. 새 세션은 원래 토큰 비용의 일부로 동일한 대화 스레드를 전달합니다.

세션 압축을 사용하면 연속성을 유지하면서 컨텍스트 윈도우 내에 유지할 수 있는 장기 LanguageModel 대화가 가능합니다. 주요 단계는 다음과 같습니다.

  1. contextWindow에 상대적인 contextUsage를 모니터링하고 사용자에게 표시합니다.
  2. contextoverflow 이벤트를 조기 경고로 수신합니다.
  3. Language Detector API로 각 메시지의 언어를 감지한 다음 언어 인식 Summarizer API 인스턴스로 요약합니다.
  4. 이전 세션을 소멸시키고 initialPrompts로 새 세션을 시드합니다.
  5. 오류 복구를 위해 fullHistory 사본을 보관합니다.

컨텍스트 사용량 추적

프롬프트 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로 실패하고 아무것도 삭제되지 않습니다.

프롬프트 API 문서에서 컨텍스트 오버플로 처리에 관해 자세히 알아보세요.

브라우저가 자동으로 대화 기록을 삭제하기 전에 contextoverflow 이벤트를 사용하여 사용자에게 경고하거나, 전송 버튼을 사용 중지하거나, 압축을 자동으로 트리거합니다.

세션 압축

압축에는 세 단계가 있습니다.

  1. Summarizer API를 사용하여 대화 기록의 각 메시지를 요약합니다.
  2. 이전 세션을 소멸시킵니다.
  3. 요약이 initialPrompts로 시드된 새 세션을 만듭니다.

기록 요약

요약 도구 API는 개별 채팅 메시지를 압축하는 데 적합합니다. 각 메시지의 경우 먼저 언어 감지기 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'를 지정하면 코드 펜스와 강조가 삭제됩니다. 작은 정규 표현식으로 두 가지를 구분할 수 있습니다.

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에 있습니다.