Agent!가 컨텍스트 창의 절반에서 압축하는 이유, 그리고 임계값을 16K로 줄여 버린 세 가지 버그
Agent!의 컨텍스트 압축이 긴 작업을 요약할 시점을 어떻게 결정하는지, 그리고 폴백 모델, Ollama, 272K Codex 창에 대한 9월 28일 수정 사항을 소개합니다.
긴 에이전트 작업에는 일종의 물리적인 한계가 있습니다. 파일 읽기, 빌드 로그, diff처럼 도구를 호출할 때마다 그 출력이 대화 기록에 쌓입니다. 결국 대화 기록은 모델의 컨텍스트 창에 들어가지 않게 되고, 제공업체는 요청을 거부합니다. 밤새 실행되는 에이전트라면 반드시 압축 을 해야 합니다. 중요한 내용은 유지하면서 대화를 줄이는 것입니다.
압축은 Agent/AgentViewModel/Messages/Compression.swift에 구현되어 있습니다. 9월 28일, 하룻저녁에 세 가지 수정이 들어갔는데, 모두 같은 증상을 해결하기 위한 것이었습니다. 훨씬 큰 창을 가진 모델에서 작업이 16K 토큰에서 압축되는 문제였습니다. 시스템이 어떻게 동작하는지, 그리고 무엇이 잘못되었는지 살펴보겠습니다.
언제 압축하는가: 창의 절반, 단 상한 있음
트리거는 CompactionState라는 구조체입니다. 그 핵심은 단 하나의 함수입니다.
static func threshold(for contextWindow: Int, maxTokens: Int = 0) -> Int {
let byFraction = Int(Double(min(contextWindow, compactionWindowCap)) * compactionFraction)
let reservedOutput = maxTokens > 0 ? min(maxTokens, contextWindow / 2) : 8_192
return max(2_000, min(byFraction, contextWindow - reservedOutput))
}
compactionFraction = 0.5, compactionWindowCap = 256_000일 때 이 코드의 의미는 다음과 같습니다.
- 창의 50%에서 압축합니다. 고정된 토큰 수가 아니라 비율입니다. 나머지 절반은 출력 예산이므로 입력과 출력이 항상 함께 들어갑니다.
- 128K를 넘지 않습니다. 256K보다 큰 창(Claude 1M, MiniMax 1M, Gemini와 Grok 2M)은 모두 128K에서 압축됩니다. 200K 창은 100K에서 압축됩니다.
- 응답을 위한 공간을 항상 남깁니다. 예약된 출력이 더 이상 들어가지 않을 만큼 임계값이 늦어질 수는 없습니다.
- 2K 아래로는 내려가지 않습니다. 아주 작은 로컬 모델을 위한 하한선입니다.
왜 2M 창을 128K로 제한할까요? 주석은 단도직입적입니다. 광고된 창이 거대할 때 상한 없는 비율을 쓰면 압축이 너무 늦어져서, 제공업체 측의 사소한 불일치도 압축이 아닌 치명적인 컨텍스트 초과로 이어집니다. 예를 들어 라우터가 보고한 것보다 짧은 창을 제공하거나, 시스템 프롬프트가 잘못 계산되는 경우입니다. 일찍 압축하면 요약 한 번의 비용이 듭니다. 너무 늦게 압축하면 작업 전체를 잃습니다.
측정: 먼저 제공업체를 믿고, 그다음 추정한다
임계값과 비교하려면 토큰 수가 필요합니다. 문자 수로 추측하면 오차가 크기 때문에, measuredTokens는 실제 값을 우선합니다. 바로 제공업체가 마지막 요청에 대해 보고한 input_tokens입니다. 이 수치에는 로컬 추정으로는 볼 수 없는 시스템 프롬프트와 도구 스키마가 포함됩니다. 그 보고 이후에 추가된 메시지만 추정합니다.
보고가 없으면 전통적인 "문자 수 ÷ 4" 추정으로 돌아가되, 25%를 더합니다. 밀도 높은 코드는 토큰당 3.3자에 더 가깝기 때문입니다. 추정치만으로 압축 비용을 치르기 전에, 루프는 온디바이스 카운터로 한 번 더 확인합니다.
어떻게 압축하는가: 단계별 처리
임계값에 도달하면 tieredCompact가 점점 강도를 높여 가며 단계를 밟습니다.
- Tier 0: 구조화된 요약. 활성 모델이 전체 대화 기록을 요약합니다. 요약하는 모델이 요약 대상인 도구 출력을 아직 볼 수 있도록 가장 먼저 실행됩니다.
- 마이크로 압축: 오래된 도구 결과를 짧고 복구 가능한 스텁으로 바꿉니다.
restore_tool_result도구로 언제든 되살릴 수 있습니다. - 이미지 제거: 스크린샷은 용량이 크고 요약도 잘 되지 않습니다.
- Tier 1: Apple Intelligence 요약. 빠르고 온디바이스로 처리됩니다.
- Tier 2: 공격적인 정리. 중간 메시지들을 요약 하나로 접어 넣습니다.
주석 하나가 이 철학을 잘 보여 줍니다. 구조적 압축은 "기능이 아니라 안전장치"라는 것입니다. Token Compression을 꺼도 구조적 압축은 실행됩니다. 이 토글을 따르는 것은 Apple Intelligence 단계뿐입니다.
압축 후에는 모델이 가장 아쉬워할 정보를 되돌려 줍니다. 아직 열려 있는 목표 기준, 활성 플랜 체크리스트, 그리고 작업 중에 편집한 파일 최대 다섯 개의 현재 내용(총 약 10K 토큰)이 여기에 포함됩니다. 읽기 중복 제거 캐시도 초기화되어 파일을 다시 읽을 수 있습니다.
마지막으로 서킷 브레이커 가 있습니다. 대화 기록을 줄이지 못한 압축이 세 번 연속 발생하면 루프는 시도를 멈춥니다. 하지만 영영 포기하지는 않습니다. 대화 기록이 마지막 실패 시점보다 25% 더 늘어나면 다시 시도합니다.
버그: 16K로 가는 세 갈래 길
9월 28일의 수정은 모두 같은 숫자로 귀결되었습니다. 32K 폴백 창은 min(16K, 32K − 8K) = 16K가 됩니다. 200K 이상의 모델에서 16K에 압축한다는 것은 거의 쉴 새 없이 요약하고, 방금 읽은 파일을 잊어버린다는 뜻입니다.
1. 폴백 모델이 엉뚱한 창을 빌려 썼다
Agent!는 폴백 체인을 지원합니다. 제공업체가 429를 반환하거나, 시간 초과가 나거나, 네트워크에서 끊기면 설정된 다음 제공업체에서 작업을 이어 갑니다. 탭마다 모델을 재정의할 수도 있습니다. 그런데 contextWindow(for:)는 실제로 실행 중인 모델이 아니라 제공업체에서 전역으로 선택된 모델의 창을 조회했습니다. 항목이 없는 폴백 모델이라면 32K 정적 크기로 떨어졌습니다.
수정 사항은 조회에 모델을 추가합니다.
func contextWindow(for provider: APIProvider, model: String? = nil) -> Int
이제 메인 루프, 탭 작업, 폴백 경로, 하위 에이전트 등 모든 호출 지점에서 실제 사용 중인 모델을 넘깁니다.
2. Ollama가 알아낸 것을 잊어버렸다
로컬 서버는 모델별 실제 창 크기를 비동기로 보고합니다. Ollama는 /api/show, LM Studio는 /api/v0/models, vLLM은 /v1/models를 통해서입니다. 그래서 루프는 첫 번째 이후 매 반복마다 refreshThreshold를 호출합니다. 작업이 시작된 뒤에 도착한 조회 결과도 그대로 반영됩니다.
그런데 Ollama에서는 조회한 창 크기가 저장되지 않아 압축이 계속 기본값인 16K에서 일어났습니다. 수정 후에는 조회한 컨텍스트 창을 저장하고, 창 크기를 모르는 경우 작업 시작 시점에 조회합니다. 이 수정은 새로운 OllamaContextWindowTests.swift 테스트 모음과 함께 배포되었습니다.
3. 넉넉한 출력 예산이 입력 예산을 잠식했다
이 버그는 미묘합니다. Codex는 GPT-6 Astra 모델의 창을 272K로 보고합니다. 사용자가 Max Output Tokens 를 256K로 설정합니다. 이전 코드는 출력 예산 전체를 예약했습니다.
272K − 256K = 16K → threshold = min(128K, 16K) = 16K
새 코드는 출력용으로 최대 창의 절반까지만 예약합니다.
let reservedOutput = maxTokens > 0 ? min(maxTokens, contextWindow / 2) : 8_192
같은 경우가 이제 min(128K, 272K − 136K) = 128K가 됩니다. 비율 상한은 여전히 상한이 적용된 창을 쓰지만, 출력이 들어가는지 확인할 때는 실제 창을 씁니다. 그렇지 않으면 1M 창에서 Claude의 기본 출력 예산 500K 때문에 window − maxTokens가 음수가 되어 임계값이 하한인 2K로 떨어질 것입니다.
여러분에게 중요한 이유
폴백 제공업체나 로컬 모델에서 긴 작업, 특히 밤샘 코딩 작업을 실행한다면 이제 훨씬 더 많은 작업 컨텍스트가 유지될 것입니다. "그 파일을 다시 읽어야 합니다" 같은 반복이 줄고, 놓치는 세부 사항이 줄고, 요약에 쓰이는 토큰도 줄어듭니다. 이 수정 사항들은 9월 28일 버전 1.1.77(빌드 277), 릴리스 후보 6과 함께 배포되었으며, 제공업체를 가리지 않는 컨텍스트 초과 및 max_tokens 오류 감지 기능도 함께 포함되었습니다.
에이전트 개발자를 위한 교훈은 보편적입니다. 메모리 크기는 실제로 실행 중인 모델을 기준으로 정하고, 자체 추정보다 제공업체의 토큰 수를 신뢰하며, 어떤 설정 하나가 나머지를 굶기지 않도록 모든 예산에 상한을 두세요.