컨텍스트 포화를 구조로 막기: Phase 격리 subagent와 파일 핸드오프
AI 코딩 에이전트로 여러 단계짜리 작업을 자동화하다 보면 누구나 한 번쯤 같은 발상을 떠올립니다. 컨텍스트가 가득 차면 그때 정리하고 다음 단계로 넘어가게 하면 되지 않나. 그런데 이 발상은 애초에 성립하지 않습니다. 모델은 자기 컨텍스트를 스스로 비울 수 없고, 한계에 다다라 auto-compaction이 한 번 터지면 그동안 쌓아온 토큰의 약 97%가 짧은 요약으로 증발해버립니다. 그래서 저는 방향을 반대로 틀었습니다. 비우는 방법을 고민하는 대신, 애초에 메인 세션이 차지 않게 만드는 구조를 짜기로 한 것입니다.
이 글은 그 과정에서 정리한 패턴 — Phase별로 작업을 격리 subagent에 맡기고, 단계 사이를 컨텍스트가 아니라 파일로 인계하는 오케스트레이션 — 에 대한 회고입니다. 곁들여 transcript 로그를 직접 까서 compaction의 실제 빈도와 손실률을 측정한 결과도 공유합니다. 이 글은 Claude Code 같은 에이전트 도구로 자동화를 해본 적이 있고, 컨텍스트 길이가 결국 한계에 부딪힌다는 사실을 어렴풋이 알고 있는 분을 가정하고 씁니다.
왜 '컨텍스트 비우고 다음 단계'는 불가능한가
먼저 가장 흔한 오해부터 짚고 가야겠습니다. 대화 컨텍스트는 본질적으로 append-only입니다. 한 번 들어간 메시지는 중간에서 골라 삭제할 수 없고, 새 메시지는 계속 뒤에 쌓이기만 합니다. 그래서 "필요 없어진 앞부분을 비워줘"라는 지시는 모델 입장에서는 수행할 수단이 없는 요청입니다.
그렇다면 한계에 다다르면 무슨 일이 벌어질까요. 도구가 알아서 auto-compaction을 수행합니다. 지금까지의 대화를 한 덩어리 요약으로 압축하고, 그 요약으로 컨텍스트를 새로 시작하는 것입니다. 문제는 이 압축이 lossy, 즉 손실 압축이라는 점입니다. 구체적인 코드, 시행착오의 맥락, 중간 의사결정의 근거가 대부분 사라지고 뼈대만 남습니다.
돌이켜 생각해보면, 컨텍스트를 실제로 새로 시작할 수 있는 경계는 딱 세 가지뿐이었습니다.
| 경계 | 성격 | 특징 |
|---|---|---|
| subagent 호출 | 격리된 새 컨텍스트 | 자식이 자기만의 컨텍스트에서 일하고, 부모에는 결과 요약만 돌아옴 |
/clear |
수동 초기화 | 사람이 직접 끊어야 함, 자동화 흐름 안에서는 부적절 |
| 새 세션 | 완전 재시작 | 이전 맥락을 파일/메모리로 다시 주입하지 않으면 단절 |
이 셋 중에서 자동화 흐름 안에 자연스럽게 끼워 넣을 수 있는 것은 subagent 호출뿐이었습니다. 자식 에이전트는 자기만의 컨텍스트에서 무거운 작업(탐색, 빌드, 시행착오)을 다 치르고, 부모에게는 결과 요약만 돌려줍니다. 자식의 컨텍스트는 작업이 끝나면 자식과 함께 사라집니다. 이 소멸이야말로 제가 원하던 '구조적 비우기'였습니다.
오케스트레이터 패턴: 파일로 인계한다
핵심 아이디어는 단순합니다. 여러 Phase로 쪼갠 작업에서, 각 Phase를 별도의 격리 subagent로 실행합니다. 그리고 단계와 단계 사이의 인계를 메인 컨텍스트가 아니라 파일(핸드오프 아티팩트)로 합니다.
여기서 가장 중요한 규칙은 subagent가 부모에게 무엇을 돌려주느냐입니다. Phase N의 subagent는 자기 작업 결과를 handoff 파일에 전부 기록하고, 부모에게는 10줄 이하의 요약과 그 파일 경로만 반환합니다. 작업의 실제 산출물(상세 결과, 코드, 로그)은 전부 디스크에 있고, 메인 세션에는 "무엇을 했고 결과가 어디에 있다"는 포인터만 남는 것입니다.
메인 오케스트레이터 (컨텍스트가 거의 안 늘어남)
├─ Phase 1 → 격리 subagent
│ ├─ 무거운 작업 (탐색·빌드·시행착오) ← 여기서 컨텍스트 소모
│ ├─ 결과를 handoff-phase1.md 에 기록
│ └─ 부모에 반환: 10줄 요약 + 파일 경로
├─ Phase 2 → 격리 subagent
│ ├─ handoff-phase1.md 를 읽어 이전 단계 인계
│ ├─ 작업 후 handoff-phase2.md 기록
│ └─ 부모에 반환: 10줄 요약 + 파일 경로
└─ Phase 3 → ...
이 구조의 효과는 분명합니다. 메인 세션의 컨텍스트는 Phase가 몇 개든 거의 늘어나지 않습니다. 단계마다 들어오는 것이 10줄짜리 요약뿐이기 때문입니다. 결과적으로 메인에서는 compaction 트리거 자체가 일어나지 않습니다. 무거운 맥락은 매번 자식과 함께 소멸하고, 오케스트레이터는 요약과 경로의 얇은 목록만 들고 끝까지 갑니다.
처음에는 "그냥 한 세션에서 Phase 1, 2, 3을 순서대로 진행하면 안 되나?"라고 생각했습니다. 다만 그렇게 하면 Phase 1의 무거운 탐색 맥락이 메인에 그대로 누적되고, Phase 3쯤 가면 compaction이 터져 앞 단계의 결정 근거가 흐려집니다. 결국 단계가 길어질수록 뒤로 갈수록 품질이 떨어지는 구조였습니다. 파일 핸드오프는 그 누적을 끊어내는 장치였습니다.
로그로 증명하기: compaction은 정말 얼마나 손해인가
여기까지는 설계 논리입니다. 솔직히 말하면 저도 "97%가 사라진다"는 말을 처음엔 막연한 인상으로만 갖고 있었습니다. 그래서 직접 transcript 로그를 까서 숫자로 확인해보기로 했습니다.
다행히 compaction은 transcript의 jsonl 기록에 구조화된 형태로 남습니다. compaction이 일어난 지점은 isCompactSummary: true인 user 메시지로 표시되고, 그 옆에 메타데이터가 붙습니다.
{
"isCompactSummary": true,
"compactMetadata": {
"trigger": "auto",
"preTokens": 332250,
"postTokens": 9919,
"durationMs": 125000
}
}
trigger는 manual(사람이 명령)인지 auto(한계 도달 자동)인지를 구분하고, preTokens와 postTokens는 압축 전후의 토큰 수입니다. 이 두 숫자만 비교하면 손실률이 그대로 나옵니다.
한 프로젝트의 수백 개 세션(대략 426~580개 범위)을 훑어 집계한 결과는 다음과 같았습니다.
| 지표 | 측정값 |
|---|---|
| compaction 발생 빈도 | 전체 세션의 약 0.7% (3건 수준) |
| 압축 전 토큰 (평균) | 약 332,250 |
| 압축 후 토큰 (평균) | 약 9,919 |
| 손실률 | 약 97% |
| 압축 소요 시간 (평균) | 약 125초 |
숫자를 직접 보고 나서야 두 가지를 분명히 깨달았습니다. 첫째, compaction은 자주 일어나지 않습니다. 1%도 안 됩니다. 둘째, 그러나 한 번 터지면 정말 처참합니다. 33만 토큰이 1만 토큰으로 줄어듭니다. 게다가 그 과정에 2분 가까운 시간이 통째로 소모됩니다.
흥미로운 대비도 있었습니다. 메인 세션에서 발생한 compaction은 전부 manual, 즉 사람이 의도적으로 호출한 것이었습니다. 반면 subagent들이 도는 sidechain에서는 1,315건 중 5건(약 0.38%)이 전부 auto 트리거였습니다. 다시 말해 한계에 부딪혀 어쩔 수 없이 터지는 자동 compaction은 메인이 아니라 subagent 쪽에서 일어나고 있었던 것입니다. 핸드오프 구조가 의도대로 작동해, 비싼 손실 압축이 메인을 비껴가 격리된 자식 쪽으로 밀려나 있었습니다.
subagent도 무한하지 않다 — 자가복구라는 안전장치
그럼 모든 무거운 일을 subagent에 던지면 끝일까요. 애석하게도 그렇지 않습니다. subagent의 컨텍스트도 유한합니다. 단일 Phase에 너무 큰 작업을 몰아넣으면, 그 자식 안에서 다시 auto-compaction이 터지거나, 심하면 작업 도중 침묵해버리는 일이 생깁니다. 격리는 손실을 메인에서 떼어낼 뿐, 손실 자체를 없애주지는 않습니다.
그래서 각 Phase가 자기 상태를 명시적으로 보고하도록 만들었습니다. subagent는 작업을 마칠 때 다음 네 가지 STATUS 중 하나를 반환합니다.
| STATUS | 의미 | 오케스트레이터의 대응 |
|---|---|---|
DONE |
해당 Phase 완료 | 다음 Phase로 진행 |
SPLIT_NEEDED |
한 번에 끝내기엔 너무 큼 | Phase를 더 잘게 쪼개 재실행 |
PARTIAL_RESUMABLE |
일부만 했고 이어서 가능 | 체크포인트부터 이어받기 |
BLOCKED_PREREQ |
선행 조건 미충족 | 중단하고 사람에게 에스컬레이션 |
여기에 핵심은 체크포인트의 append입니다. subagent는 진행하면서 중간 결과를 handoff 파일에 계속 덧붙입니다. 그래서 도중에 한계에 부딪혀 PARTIAL_RESUMABLE로 끊겨도, 다음 subagent가 그 파일을 읽고 끊긴 지점부터 이어갈 수 있습니다. 작업의 진실은 컨텍스트가 아니라 파일에 있으니, 컨텍스트가 날아가도 복구가 됩니다.
다만 이 자가복구를 그냥 풀어두면 무한 루프에 빠질 위험이 있습니다. 같은 Phase가 계속 SPLIT_NEEDED만 반환하면서 영원히 쪼개지기만 할 수 있기 때문입니다. 그래서 루프 가드를 두었습니다. 같은 Phase를 두 번 넘게 분할했거나, 분할했는데도 진전이 0이면, 더 시도하지 않고 사람에게 넘깁니다. 자동화가 끝없이 헛돌며 토큰만 태우는 상황을 막는 최소한의 제동장치입니다.
이 흐름을 언제 자동으로 발동시킬 것인가
마지막 고민은 트리거였습니다. 이 오케스트레이션을 매번 사람이 명시적으로 호출해야 한다면, 결국 "다단계 작업인데 또 직접 불러야 하네"라는 마찰이 남습니다. 그래서 어떤 신호를 보고 자동으로 발동시킬지를 정해야 했습니다.
두 가지 방식을 두고 저울질했습니다. 하나는 도구가 작업 설명을 보고 소프트하게 매칭해 알아서 발동하는 방식입니다. 편하지만, 빗나가거나 엉뚱한 데서 켜지는 오발동이 생깁니다. 다른 하나는 명시적인 룰을 박아 강하게 self-trigger하는 방식입니다. 결국 후자를 택했습니다. 발동 조건을 또렷하게 정의할수록 오발동이 줄기 때문입니다.
구체적으로는, 단계 단위로 쪼개진 계획 문서의 구조적 신호를 발동 기준으로 삼았습니다.
## Phase N형태의 단계 헤딩- 섹션별 복붙 프롬프트 헤딩 같은 단계 컨벤션
- '단계 → 커밋 매핑' 표처럼 단계가 두 개 이상 나열된 구조
반대로, '전체를 한 번에 착수'하는 항목이나 'PR 마감' 같은 경계 처리 항목은 단계가 아니므로 발동에서 의도적으로 제외했습니다. 그리고 한 가지 신호는 일부러 쓰지 않았습니다. 바로 '컨텍스트가 차오르는 것을 자가 감지'하는 방식입니다. 그럴듯해 보이지만 자가 감지는 불확실한 신호라, 켜야 할 때 안 켜지거나 멀쩡할 때 켜지는 변덕이 심합니다. 그래서 "다단계 계획 문서 + 실행 의도"라는 또렷한 외부 신호만 트리거로 남기고, 모호한 자가 감지는 배제했습니다.
정리하며
이 작업에서 제가 얻은 가장 큰 교훈은 반직관적입니다. 컨텍스트는 비우는 것이 아니라, 애초에 차지 않게 설계하는 것이라는 점입니다. 모델이 스스로 컨텍스트를 정리해주기를 기대하는 대신, Phase를 격리 subagent로 떼어내고 단계 사이를 파일로 인계하면, 무거운 맥락은 자식과 함께 사라지고 메인은 끝까지 가볍게 유지됩니다.
그리고 그 직관을 막연한 믿음으로 두지 않고 로그로 확인해본 것이 스스로에게도 의미가 컸습니다. compaction은 1%도 안 되게 드물지만, 한 번 터지면 토큰의 97%와 2분의 시간을 가져갑니다. 그 드물고 비싼 사건을 메인에서 밀어내는 것만으로도 긴 작업의 안정성은 눈에 띄게 달라졌습니다. 중요한 것은, 신뢰할 수 없는 자가 감지에 기대지 않고 또렷한 구조적 신호로만 자동화를 발동시키는 절제였습니다.
여기까지가 컨텍스트 포화를 구조로 막는 기본편입니다. 다음 편에서는 한 걸음 더 나아가, 서로 독립적인 트랙을 안전하게 병렬로 돌리는 방법 — 그리고 advocate와 critic을 붙여 '장밋빛 병렬화 계획'을 적대적으로 검증하고 보수적으로 자동 판정하는 과정 — 을 다뤄보려 합니다.
관련 글
Claude Code 소스 유출이 드러낸 아키텍처 — 3편: 메모리와 컨텍스트 압축
AI 에이전트가 장시간 작업할 때 가장 큰 적은 컨텍스트 한계입니다. Claude Code는 4계층 메모리, 200K 토큰 자동 압축, 자기 치유 메모리, 지연 로딩이라는 네 가지 전략으로 이 문제를 해결합니다.
큰 작업의 거처를 챗봇 UI에서 로컬 하네스로 — 옆에서 함께 옮긴 이야기
주변에서 외부 챗봇의 커스텀 기능 위에 무거운 작업을 굴리던 분이 매번 같은 자리에서 막히는 걸 보고, 그 작업의 거처를 Claude Code 위 로컬 하네스로 옮겨드린 과정. 옆에서 본 네 가지 막힘과 함께 갖춰둔 여섯 가지 패턴을 정리했습니다.
LLM 에이전트에 지식베이스를 통째로 먹이지 않는 법 — 1hop 인덱스 오버레이 패턴
지식베이스를 LLM 에이전트에 통째로 먹이면 컨텍스트 비용이 입력 규모에 비례해 폭발합니다. RAG를 끌어오기 전에 얇은 규칙과 단일 진입점 인덱스, 1hop 매핑만으로 도메인 메모리 주입 비용을 상수로 만든 구축기입니다.