|

3줄 요약
- 컨텍스트 하강 이벤트 22건을 전수 재판정했더니 진짜 압축은 12건뿐이었고, 나머지는 advisor 왕복 3건과 resume 아티팩트 7건이었다(2026-08-09, 2개 프로젝트 전사).
- 진짜 압축인지 가르는 기준은 비율이 아니라 cache_write 절대값이다. 50,000 이상이면 진짜 압축, 10,000 이하면 다른 현상이다.
- 진짜 압축 12건 중 급증형 6건은 전부 상위 모델 자문(advisor) 호출 턴이었다. 여유를 넓히는 것보다 그 한 턴에 무엇을 넣느냐가 지배적이었다.

질문
왜 세션이 갑자기 압축되는가 누적이 아니라 특정 한 턴에 들어온 큰 입력이 임계를 넘긴 경우가 많다(진짜 압축 12건 중 급증형 6건)
진짜 압축인지 어떻게 아는가 cache_write ≳ 50,000이면 진짜 압축, ≲ 10,000이면 압축이 아니다
급증형 압축의 공통점은 확인된 급증형 6건은 전부 advisor 호출 턴이었다(6/6)
240k 캡을 걸면 240k에서 압축되나 아니다. 우리 환경 실측 발동선은 약 165,000토큰, 대략 캡의 0.69배다
무엇부터 손대야 하는가 캡 상향보다 먼저 "그 한 턴의 입력 크기"를 줄인다

이 글대로 하면 무엇이 달라지는가: 압축이 일어났을 때 "대화가 길어서"라고 넘기는 대신, 전사 로그에서 그 턴의 cache_write 한 값만 보고 진짜 압축인지 아닌지를 30초 안에 가르고, 압축을 부른 그 한 턴에 무엇이 들어갔는지를 지목할 수 있게 된다.

Claude Code 세션이 갑자기 요약되는 이유는 무엇인가?

세션 컨텍스트가 설정된 상한(캡)에 접근하면 Claude Code가 대화를 자동으로 요약한다(auto-compact). 그런데 무엇이 그 접근을 만드는지는 "대화가 길어져서"라는 통념과 달랐다. 두 프로젝트의 전사(transcript)에서 컨텍스트가 갑자기 내려앉은 이벤트 22건을 뽑아 전수 재판정한 결과(2026-08-09), 22건 중 진짜 압축은 12건이었고 3건은 advisor 왕복, 7건은 세션 재개(resume) 과정의 아티팩트였다. 그리고 진짜 압축 12건 중 절반인 6건은 몇 턴 전까지 여유가 있다가 특정 한 턴에서 한 번에 임계를 넘긴 급증형이었다.

자동 압축(auto-compact)이란 컨텍스트가 정책상 상한에 닿았을 때 대화를 요약해 재출발시키는 동작이며, 두 단계로 진행된다. 먼저 오래된 도구 출력을 폐기하고, 그래도 모자라면 대화 자체를 요약한다. 이 순서가 중요한 이유는 뒤에서 다시 다룬다.

"컨텍스트가 꽉 차서 압축된다"는 설명은 어디가 틀렸는가?

틀렸다기보다 절반만 맞다. 캡에 닿으면 압축이 걸리는 것은 사실이지만, 임계를 넘기는 주체가 "누적된 대화량"이 아니라 "직전 한 턴에 새로 들어온 입력"인 경우가 절반이었다. 진짜 압축 12건을 나누면 급증형 6건, 누적형 6건이다. 게다가 누적형 6건 중 다수는 캡을 걸지 않아 모델 한계(563k~999k)까지 흘러간 세션이었다. 캡을 씌운 체제에서 실제로 자주 만나는 쪽은 급증형이다.

이 구분이 실무에서 갈리는 지점은 처방이다. 누적형이라면 캡을 올리거나 /clear로 끊는 것이 유효하다. 급증형이라면 캡을 올려도 같은 한 턴이 다시 들어오는 순간 그대로 재발한다. 여유(캡 발동선에서 압축 후 바닥을 뺀 값)보다 큰 단일 턴 증가가 반복되면 압축이 몇 턴 만에 되풀이되는 thrashing이 되고, 한 바퀴마다 요약 출력과 프리픽스 재기록을 태우면서 진척은 0이 된다.

진짜 압축인지 아닌지는 어떻게 판별하는가?

cache_write 절대값 하나로 가른다. 우리가 처음 시도한 것은 비율 판별이었고, 그것이 실패한 과정이 이 글의 핵심이다.

오진 1단계. 압축이면 프리픽스를 다시 써야 하니 cache_write / 직후 컨텍스트 비율이 1에 가까울 것이라고 봤다. 왜 틀렸나. 진짜 압축인 사례들의 이 비율이 0.58에서 1.00까지 흔들렸다. 연속으로 압축이 도는 중이면 시스템 프롬프트와 도구 스키마가 캐시 히트로 잡혀 비율이 0.6~0.75로 내려가고, 오랜만의 첫 압축이면 전량이 재기록돼 1.00에 가까워진다. 진짜 원인. 비율의 분모가 상황에 따라 달라졌다. 캐시 히트로 빠지는 고정분은 두 프로젝트에서 각각 116,310 − 84,459 = 31,851, 92,999 − 61,454 = 31,545로 약 31.5k에서 일치했고, 이 값은 프로젝트가 달라도 비슷했다. 즉 흔들리는 것은 압축 여부가 아니라 비율 계산 방식이었다.

그래서 비율을 버리고 절대값으로 바꿨다. 아래가 현재 쓰는 판별식이다.

판정 조건 정체
진짜 압축 cache_write ≳ 50,000 (압축 후 바닥 재기록량) 대화가 실제로 요약됐다
advisor 왕복(원복) cache_write ≲ 10,000 컨텍스트가 잠깐 부풀었다 되돌아온 것
resume 아티팩트 cache_write = 0 이고 직후 컨텍스트도 0 세션 재개 과정의 기록 형태일 뿐

하강 이벤트 22건을 어떻게 세었는가?

전사 파일을 무LLM 스크립트로 훑어 턴별 컨텍스트를 차분하고, 하강 지점마다 위 판별식을 적용했다. 대화 안에서 로그를 통째로 읽으면 그 행위 자체가 컨텍스트를 부풀리므로 계측은 대화 밖 스크립트로 돌린다.

  1. 계측 도구를 돌린다. 얼마나 썼는지는 scan_all.py, 어디서 새는지는 waste_scan.py가 답한다. 후자는 단일 턴 급증·진짜 압축·캐시 만료 재기록·폴링 턴·거대 도구결과·에이전트 보고 잔류를 턴 단위로 지목한다.
# 사용량 총량
python -X utf8 code/token_audit/scan_all.py --since 2026-08-04
# 낭비 지점 지목 (단일 턴 급증·압축·캐시 만료 재기록 등)
python -X utf8 code/token_audit/waste_scan.py --since 2026-08-04
  1. 전사를 읽을 때 requestId로 중복을 제거한다. 전사는 같은 요청을 2~3행으로 중복 기록하므로, 이 단계를 빼면 사용량이 약 1.9배로 과대 계상된다.
seen = set()
rows = []
for row in transcript_rows:          # 전사 한 줄 = assistant 응답 1건
    rid = row["requestId"]
    if rid in seen:                  # 같은 요청이 2~3행 중복 기록된다
        continue
    seen.add(rid)
    rows.append(row)
  1. 각 턴의 컨텍스트 점유를 계산한다. 세 값의 합이 그 턴이 실제로 이고 있던 컨텍스트다.
# ctx = input + cache_read + cache_creation
# 필드 이름은 자기 전사 스키마에 맞춰 바꾼다
def ctx_of(u):
    return u["input"] + u["cache_read"] + u["cache_creation"]
  1. 인접한 턴 사이에서 ctx가 내려간 지점만 하강 이벤트로 뽑는다.
  2. 각 하강 이벤트에 판별식을 적용한다. 순서를 지켜야 resume 아티팩트가 advisor 왕복으로 잘못 분류되지 않는다.
def classify(cw, ctx_after):
    if cw == 0 and ctx_after == 0:
        return "resume 아티팩트"
    if cw >= 50_000:
        return "진짜 압축"
    if cw <= 10_000:
        return "advisor 왕복(원복)"
    return "판정 보류"
  1. 진짜 압축으로 판정된 턴만 남기고, 그 직전 턴에 무엇이 들어갔는지를 본다. 여기서 급증형과 누적형이 갈린다.

우리 환경에서는 대조 수단이 하나 더 있었다. PreCompact 훅이 압축 시점마다 전사 전문을 아카이브에 남기기 때문에, 아카이브 파일명의 시각과 전사의 턴 시각을 맞춰 판정을 독립 검증할 수 있었다(파일명은 KST, 전사는 UTC 기준이라 시차를 맞추면 편차 2~6분). 이렇게 대조한 7건은 전부 일치했다.

압축을 부르는 급증은 어디서 오는가?

확인된 급증형 6건은 전부 advisor 호출 턴이었다. advisor는 대화 전체를 요청에 실어 보내는 상위 모델 자문 호출이라, 부르는 순간 그 턴의 컨텍스트가 2.05~2.20배가 된다(실측 6건). 그래서 기전이 단순하다. 호출 시점 컨텍스트에 2.2를 곱한 값이 발동선을 넘으면 그 자리에서 압축되고, 넘지 않으면 응답 후 원래대로 돌아온다.

체제 advisor 호출 결과
캡 적용 후 5회 압축 5회
캡 적용 전(발동선이 모델 한계) 4회 원복 3회 · 압축 1회

"예전에는 advisor를 불러도 괜찮았다"는 기억은 반례가 아니라 다른 체제의 기록이다. 발동선이 1M이던 시절에는 2.2배를 곱해도 닿지 않았을 뿐이다.

advisor 다음으로 한 턴을 크게 부풀리는 후보는 대형 파일 전체 Read(123KB 파일이 약 35k 토큰), 로그 전체 Read, 여러 쪽짜리 PDF 통독, 대형 JSON 덤프, ls -R, 축약 없는 DB 조회다. 이번 22건에서 급증형으로 잡힌 것은 advisor뿐이지만, 크기 순서로는 같은 범주다. 컨텍스트 증가분의 1위가 사고(thinking)라는 실측과 겹쳐 읽으면, 한 턴의 크기를 통제하는 일이 컨텍스트 관리의 첫 레버라는 결론이 두 방향에서 확인된다.

압축 발동선과 여유는 어떻게 구하는가?

캡을 240,000으로 걸어도 240,000에서 잘리지 않는다. 우리 환경의 실측 발동선은 약 165,000토큰이었고, 미발동에서 발동으로 넘어가는 경계를 세 번 관측했다.

관측 미발동 시점 컨텍스트 발동 시점 컨텍스트
1 160,806 165,002
2 162,475 165,693
3 164,792 168,736

여기서 우리는 한 번 더 오진했다. 세 관측을 겹쳐 "발동선은 164,792 초과 165,002 이하, 폭 210토큰"이라고 적었다. 왜 틀렸나. 세 관측이 같은 조건이었다는 보장이 없고, 발동선이 캡만의 함수라는 증거도 없었다(모델과 도구 스키마 크기가 함께 섞여 있다). 남긴 값. 정밀도 주장은 폐기하고 두 개만 남겼다. 발동선 T ≈ 캡 × 0.69, 그리고 여유 = T − 압축 후 바닥이다.

바닥과 압축 비용은 프로젝트 상수다. 고정 선두부(전역 지침 파일, 프로젝트 지침 파일, 메모리)의 크기에 따라 달라지므로 남의 값을 가져다 쓰면 안 된다. 실측 예를 나란히 두면 차이가 분명하다.

프로젝트 고정 선두부 압축 후 바닥 여유(T − 바닥) 압축 1회 비용
A 89KB 약 130,000 약 35,000 약 125,000
B 69KB 약 91,000 약 102,000 약 85,000

압축 1회 비용은 요약 출력 약 25k에 바닥 재기록량을 더한 값이다. A 프로젝트의 실측 cache_write는 100,075와 96,662였고, B 프로젝트는 61,454와 58,838이었다. 여유가 약 35,000인 환경에서는 123KB짜리 파일 하나(약 35k 토큰)를 통째로 읽는 것만으로 여유가 찬다.

압축을 줄이려면 무엇부터 해야 하는가?

효과 순서대로 손댄다. 캡 상향은 목록의 위쪽이 아니다.

  • [ ] advisor 호출 횟수를 줄인다. 습관적인 "확인차 한 번 더"를 지운다. 다만 되돌리기 어려운 작업(DDL, 배포, 외부 시스템 변경) 앞에서는 비용을 따지지 말고 부른다. 잘못된 DDL 한 번이 압축 열 번보다 비싸다.
  • [ ] 단일 턴 최대 증가를 억제한다. 대형 파일 전체 Read 금지, Readoffset/limit로, 위치 탐색은 Grep으로.
  • [ ] 도구 출력을 통째로 받지 않는다. 로그는 grep·head·tail로 필요한 줄만 회수한다. 압축 1단계가 폐기하는 것이 정확히 이 도구 출력이므로, 출력 축약과 캡은 한 세트다.
  • [ ] 여유가 실제로 부족하면 캡을 올린다. 전역 설정 파일을 직접 고친다.
  • [ ] 고정 선두부를 줄인다. 바닥이 내려가면 여유와 압축 비용이 함께 내려간다.
  • [ ] 큰 산출물은 파일로 내보내고 요약 줄만 회수한다.
  • [ ] 서브에이전트 보고는 결론만 반환하게 한다(보고서는 부모 컨텍스트에 영구 잔류, 건당 약 3.8k 실측).

캡과 함께 걸어 두면 좋은 설정은 두 개다.

{
  "autoCompactWindow": 240000,
  "showClearContextOnPlanAccept": true
}

showClearContextOnPlanAccept는 계획을 승인할 때 탐색 과정에서 쌓인 컨텍스트를 버리는 선택지를 띄운다. 무료로 컨텍스트를 버릴 수 있는 수단이 /clear 말고 하나 더 생기는 셈이다. 설정 파일을 고칠 때 주의할 점이 있다. 설정 감시기는 세션이 시작될 때 설정 파일이 있던 디렉터리만 감시하므로, 이미 존재하던 전역 설정 파일의 편집은 즉시 반영되지만 없던 위치에 새로 만든 설정 파일은 다음 세션부터 적용된다.

압축이 이미 반복되고 있다면 순서가 다르다. 캡을 만지기 전에 직전 턴에 무엇을 넣었는지부터 본다. 그리고 같은 것을 다시 부르지 않는다. 같은 것을 다시 부르면 고리가 그대로 돈다.

이 판별식과 수치가 통하지 않는 조건은 무엇인가?

여기 적은 값 중 일반화할 수 있는 것은 판별식의 형태와 T ≈ 캡 × 0.69, 여유 = T − 바닥이라는 관계식뿐이다. 나머지는 우리 환경의 값이다. 압축 후 바닥, 여유, 압축 1회 비용은 고정 선두부의 함수라 프로젝트마다 다르고, 위 표의 두 프로젝트만 해도 바닥이 130,000과 91,000으로 갈렸다. cache_write ≳ 50,000 기준의 50,000도 "바닥 재기록량 정도"라는 뜻이라, 선두부가 훨씬 얇은 환경에서는 이 문턱을 자기 바닥 값에 맞춰 내려야 한다. 발동선 관측 자체도 캡·모델·도구 스키마 크기가 섞인 값이라 캡만의 함수로 볼 근거는 없다.

방법 쪽에도 조건이 있다. 이 재현 절차는 전사 파일에 접근할 수 있고 usage 계열 값이 턴별로 남아 있는 환경을 전제한다. 압축 시점을 독립 대조하려면 PreCompact 시점에 아카이브를 남기는 훅이 필요하고, 훅이 없으면 판정은 판별식 하나에만 기대게 된다. 그리고 캡 체제에서의 자연 압축 주기는 우리도 아직 관측하지 못했다. 그래서 advisor는 "언제 부를까"가 아니라 "몇 번 부를까"로 관리하고 있다. 관측 없는 수로 호출 시점을 정하지 않는다는 이 원칙 자체가 직접 측정한 것만 쓴다는 규칙에서 나왔다.

자주 묻는 질문

Q. Claude Code에서 대화가 갑자기 요약되는 이유는 무엇인가?
A. 컨텍스트가 서서히 쌓여서만이 아니라, 특정 한 턴에 들어온 큰 입력이 임계를 넘기는 경우가 많다. 하강 이벤트 22건 재판정에서 진짜 압축 12건 중 6건이 이 급증형이었고, 그 6건은 전부 advisor 호출 턴이었다.

Q. 진짜 압축인지 다른 현상인지 어떻게 구분하는가?
A. 그 턴의 cache_write 절대값을 본다. 50,000 이상이면 진짜 압축, 10,000 이하면 advisor 왕복이다. cache_write가 0이고 직후 컨텍스트도 0이면 세션 재개 아티팩트다.

Q. 비율로 판별하면 안 되는 이유는 무엇인가?
A. 진짜 압축도 cache_write / 직후 컨텍스트 비율이 0.58에서 1.00까지 흔들리기 때문이다. 연속 압축 중이면 시스템 프롬프트와 도구 스키마 약 31.5k가 캐시 히트로 빠져 비율이 낮아지고, 오랜만의 첫 압축이면 전량 재기록돼 1.00에 가까워진다.

Q. 캡을 240,000으로 걸면 240,000에서 압축되는가?
A. 아니다. 우리 환경 실측 발동선은 약 165,000토큰으로, 대략 캡의 0.69배 지점이었다. 미발동에서 발동으로 넘어가는 경계를 세 번 관측한 값이며, 그보다 정밀한 폭을 주장했던 이전 기록은 조건 동일성을 보장할 수 없어 폐기했다.

Q. 압축이 몇 턴 만에 반복될 때 무엇부터 해야 하는가?
A. 캡을 올리기 전에 직전 턴에 무엇이 들어갔는지 확인하고, 같은 것을 다시 부르지 않는다. 여유(발동선에서 압축 후 바닥을 뺀 값)보다 큰 단일 턴 입력이 반복되는 것이 거의 항상 원인이다.

Comments

Comments (0)

Leave a Comment

← Back to List