엔지니어링 노트대화 엔진

압축 임계값은 컨텍스트 윈도우가 아니라 입력 상한에서 잡는다

대화가 길어지면 요청은 언젠가 모델이 받을 수 있는 입력 상한에 닿습니다. 그 전에 오래된 턴을 요약하거나 덜어 내야 하는데, 이 일을 대화 압축(compaction)이라고 부릅니다. 압축은 입력이 임계값을 넘을 때 시작됩니다. 그런데 임계값을 컨텍스트 윈도우의 일정 비율로 잡으면 임계값이 입력 상한보다 위에 놓일 수 있습니다. 그러면 압축은 한 번도 시작되지 않고, 대화는 요청이 400으로 거절되면서 끝납니다. 입력창 옆의 컨텍스트 게이지도 100%를 채운 채 아무 일도 일으키지 않습니다.

이 글은 임계값을 입력 상한에서 잡는 방법을 다룹니다. 입력 상한을 어떻게 구하는지, 임계값과 상한 사이에 여유를 얼마나 두는지, OwlAI가 직접 압축할 때 그 여유 안에서 무엇부터 줄이는지, 그리고 컨텍스트 게이지가 압축기와 같은 숫자를 읽게 한 방법입니다.

컨텍스트 윈도우에는 출력 몫이 들어 있다

컨텍스트 윈도우는 한 요청의 입력과 출력을 합한 크기입니다. 입력에 쓸 수 있는 양은 윈도우에서 출력 몫을 뺀 만큼이고, 이것이 입력 상한입니다. 윈도우 비율로 잡은 임계값은 이 차이를 놓칩니다.

윈도우 40만 토큰, 최대 출력 12만 8천 토큰으로 선언된 모델에서 경계를 직접 재면 다음과 같습니다(2026년 8월 측정).

항목 토큰
컨텍스트 윈도우 400,000
통과한 입력 264,620
400으로 거절된 입력 285,000
입력 상한(윈도우 − 최대 출력) 272,000
출력 몫으로 윈도우의 8분의 1만 빼고 0.9를 곱한 임계값 315,000

마지막 줄의 임계값은 입력 상한보다 4만 3천 토큰 위에 있습니다. 대화가 자라면 입력은 임계값에 닿기 전에 상한부터 넘으므로, 이 식 아래에서는 압축이 일어날 자리가 없습니다.

출력 몫을 요청마다 넣는 max_output_tokens 값으로 잡고 싶어지기도 합니다. 출력을 적게 요청하면 입력을 더 실을 수 있을 것 같기 때문입니다. 같은 모델에서 max_output_tokens를 256으로 낮춰 다시 재도 260,024토큰은 통과하고 300,000토큰은 거절됐습니다. 상한은 요청값을 따라 움직이지 않습니다. 그래서 출력 몫에는 모델이 선언한 최대 출력을 씁니다.

임계값은 입력 상한에서 한 라운드 몫을 빼고 잡는다

OpenAI와 Anthropic에는 서버 측 압축이 있습니다. 요청에 임계값을 실어 보내면 입력이 그 값을 넘는 요청에서 프로바이더 서버가 앞부분을 요약합니다. 이 경로의 임계값은 세 줄로 정합니다.

  • 입력 상한 = 컨텍스트 윈도우 − 모델이 선언한 최대 출력
  • 여유 구간 = max(입력 상한 × 10%, 도구 결과 하나의 크기 상한)
  • 임계값 = 입력 상한 − 여유 구간

서버 측 압축은 입력이 [임계값, 입력 상한] 구간에 들어온 요청에서만 일어납니다. 압축하려면 서버의 모델이 원문을 한 번 읽어야 하므로, 상한을 넘은 요청은 압축으로 구하지 못합니다. 라운드(모델을 한 번 부르고 그 응답의 도구 호출을 실행하기까지) 하나에 붙는 양이 여유 구간보다 크면 입력은 구간을 건너뛰어 곧장 상한을 넘습니다. 도구 결과 하나는 따로 크기 상한(기본 5만 토큰)에 묶여 있으므로(「큰 도구 결과는 프로바이더가 자르기 전에 줄이고, 줄였다고 알린다」), 여유 구간을 적어도 그만큼 잡으면 상한 이하의 결과 하나는 반드시 구간 안에서 걸립니다. 컨텍스트 윈도우가 큰 모델은 병렬 도구 호출로 큰 결과가 한 라운드에 여럿 붙을 수 있어서 10% 쪽이 더 커지게 했습니다.

앞의 모델이면 입력 상한 272,000에서 5만을 빼 임계값이 222,000입니다. 윈도우 105만, 최대 출력 12만 8천인 모델이면 입력 상한이 922,000이고, 10%인 92,200이 5만보다 커서 임계값은 829,800입니다.

컨텍스트 윈도우 안의 입력 상한과 압축 임계값: 비율로 잡으면 상한 위에 놓인다
그림 1. 컨텍스트 윈도우 안의 입력 상한과 압축 임계값: 비율로 잡으면 상한 위에 놓인다

같은 토큰 수가 OpenAI에서는 context_management의 compact_threshold로, Anthropic에서는 context_management.edits의 트리거 값으로 실립니다. 압축이 일어나면 OwlAI는 서버가 돌려준 압축 블록을 압축 노드(압축한 맥락을 든, 부모 없이 새로 만든 노드)로 저장하고 다음 라운드부터 그 노드에서 대화 맥락을 이어 붙입니다(「고친 질문에 옛 답이 섞이지 않게 대화를 트리로 저장하기」).

직접 압축하는 경로는 요청을 보내기 전에 잰다

vLLM처럼 서버 측 압축이 없는 프로바이더에서는 OwlAI가 직접 압축합니다. 구축형 설정에서 모델마다 적는 spec.contextWindow(vLLM의 최대 길이)에서 출력 여유 outputHeadroomTokens(기본 8,192)를 빼고, 임계값 비율 thresholdRatio(기본 0.85)를 곱합니다. contextWindow가 131,072인 모델이면 입력 상한은 122,880토큰, 임계값은 104,448토큰입니다.

이 경로는 서버 측 압축과 재는 시점이 다릅니다. OwlAI가 호출마다 요청을 보내기 전에 이번 라운드에 붙을 도구 결과까지 포함한 입력의 토큰을 재고, 임계값을 넘으면 보내기 전에 줄입니다. 줄이는 방법은 싼 것부터 쓰고, 한 단계를 마칠 때마다 다시 재서 임계값 아래로 들어오면 멈춥니다.

단계 하는 일 모델 호출
0 크기 상한을 넘은 도구 결과를 줄인다. 대화 맥락을 다 비워도 상한을 넘으면 요약 전에 실패를 알린다 없음
A 최근 6개를 뺀 도구 결과 가운데 1,500토큰을 넘는 것을 뼈대만 남긴다 없음
B 최근 2턴을 뺀 앞부분을 같은 모델로 요약한다 1회
C 가장 오래된 턴부터 버린다 없음

A단계가 요약보다 먼저인 이유는 업무 질의에서 입력을 가장 많이 차지하는 것이 대화가 아니라 DB 조회 결과이기 때문입니다. A단계는 오래된 조회 결과를 컬럼 목록과 행 수만 남긴 뼈대로 바꾸고, 모델을 부르지 않으므로 같은 입력에는 늘 같은 결과를 냅니다. B단계의 요약 호출은 앞부분과 요약 지시만 읽습니다. 압축이 가장 필요한 순간은 전체 대화가 상한에 가까울 때라서, 전체를 다시 읽히면 요약 호출이 먼저 넘칩니다. C단계는 요약하고도 넘치거나 요약이 실패했을 때 쓰는 마지막 단계입니다.

요약 호출의 출력 상한에는 추론 몫을 더한다

요약문은 본문 약 1만 토큰으로 잡았습니다. 압축 뒤에는 이 요약이 앞선 대화의 유일한 맥락이 되므로, 짧게 아낀 만큼 지난 맥락이 사라집니다. 요약 호출의 출력 상한은 여기에 추론 몫 6천 토큰을 더한 1만 6천 토큰입니다.

추론 몫을 따로 얹는 이유는 출력 상한이 추론 토큰과 본문을 합쳐 세기 때문입니다. 출력 상한을 본문 분량에 맞추면 추론이 상한을 먼저 쓰고 본문은 잘리거나 비어서 돌아옵니다. 자체 호스팅 모델 하나로 6만 6천 토큰짜리 대화를 요약하게 했을 때, 출력 상한 2,000에서는 5회 가운데 4회가 빈 요약이었고 나머지 1회도 잘린 요약이었습니다. 요약이 비면 B단계를 건너뛰고 C단계로 넘어가 오래된 턴을 그냥 버립니다.

자체 압축도 결과를 압축 노드로 저장합니다. 저장하지 않고 호출마다 다시 압축하면 요약문이 매번 조금씩 달라져 요청 앞부분이 라운드마다 바뀌고, 캐시를 계속 쓰지 못합니다. 압축 노드로 고정하면 캐시는 압축 직후 한 번 새로 쓰고 그 뒤로는 다시 적중합니다(「시스템 프롬프트 한 줄에 캐시 읽기가 0이 된다, 바뀌는 블록은 뒤로 보낸다」).

압축기와 컨텍스트 게이지는 같은 함수에서 숫자를 읽는다

임계값을 구하는 식이 두 경로로 갈리므로, 화면과 압축기가 각자 계산하면 한쪽만 고친 순간 둘이 어긋납니다. 각자 계산하면 서로를 몰라도 되지만, 식이 바뀔 때마다 두 곳을 함께 고쳐야 합니다. 그래서 컨텍스트 예산을 돌려주는 함수를 하나만 두고 압축기와 화면이 모두 이 함수를 읽게 했습니다. 이 함수는 모델마다 윈도우·입력 상한·임계값 세 숫자를 돌려주고, 셋 사이에는 늘 「임계값 ≤ 입력 상한 ≤ 윈도우」가 성립합니다. 어느 경로인지는 함수 안에서 프로바이더 종류로 가르고, API로는 세 숫자만 나갑니다.

컨텍스트 게이지는 이 숫자로 그립니다.

분자에 턴의 입력 합계 대신 마지막 호출의 입력을 쓰는 이유는, 한 턴이 라운드마다 대화 맥락을 다시 보내서 도구를 다섯 번 쓴 턴의 합계가 실제 맥락의 몇 배가 되기 때문입니다. 요약 호출의 입력도 대화 맥락이 아니라서 뺍니다. 분모는 다음 턴의 모델에서 오므로 모델을 바꾸면 같은 대화가 다른 비율로 보입니다. 다음 턴이 압축될 시점이 실제로 달라지니 이쪽이 맞습니다.

윈도우와 입력 상한은 데이터로 받지만 화면에 내지 않습니다. 앞의 모델에서 임계값과 윈도우를 나란히 보이면 222,000/400,000으로 56%가 되고, 사용자는 왜 절반을 조금 넘는 데서 압축하는지 묻게 됩니다. 사용자에게 뜻이 있는 최대는 임계값 하나라서 팝오버에는 현재와 최대(임계값)만 보입니다. 압축 대상이 아닌 모델이면 함수가 값을 돌려주지 않고 게이지도 숨깁니다. 없는 임계값을 추정해 그리면 사용자는 오지 않을 압축을 기다리게 됩니다.

설계에서 고른 것

게이지의 분자에는 추정값 대신 프로바이더가 돌려준 측정값을 씁니다. 대가는 게이지가 한 턴 늦다는 점입니다. 다음 질문과 도구 결과가 붙으면 실제 입력은 그보다 커서, 게이지가 90%일 때 큰 조회 하나가 붙으면 그 턴 안에서 압축이 일어납니다. 숫자가 틀리는 것보다 한 턴 늦는 쪽을 골랐습니다.

모델 사양에 윈도우나 최대 출력이 없으면 보수적인 기본값을 씁니다. 서버 측 압축 경로는 윈도우 40만, 출력 몫 12만 8천을 가정하고, vLLM 모델은 contextWindow가 없으면 131,072로 봅니다. 임계값이 낮게 틀리면 압축이 조금 일찍 일어날 뿐이지만, 높게 틀리면 상한 위로 올라가 압축이 다시 멈춥니다. 그래서 윈도우는 작게, 출력 몫은 크게 잡아 낮은 쪽으로 틀리게 했습니다.

서버 측 압축은 켜고 끄는 설정 없이 늘 켜 둡니다. 설정을 두면 문제가 생긴 계정에서 기능만 끌 수 있지만, 구축형 제품에서는 설정 조합마다 검증할 경우가 늘어납니다. 대신 프로바이더 계정에서 그 기능이 열려 있어야 하므로, 배포한 계정에서는 긴 대화로 압축이 한 번 일어나는지 확인하는 편이 좋습니다.

vLLM 모델을 붙인다면 contextWindow를 서빙 엔진의 실제 최대 길이와 같게 적는 것이 게이지와 압축 시점을 함께 맞추는 방법입니다. 실제보다 크게 적으면 임계값이 상한 위로 올라가 이 글 첫머리의 증상이 그대로 납니다.

어떤 문제를 해결하나요?

대화 엔진의 다른 글