세션 사이에서 세션 안으로, 컨텍스트 엔지니어링
이전 글에서 세션 경계 문제를 harness로 해결하는 방법을 이야기했다. 그런데 곧 다른 병목을 마주했다. 세션 안에서, 컨텍스트 윈도우에 여유가 남아 있는데도 에이전트가 흐려지는 현상이었다.
Anthropic은 이 현상을 컨텍스트 부패(context rot)라 부른다. 토큰이 늘어날수록 모델의 정보 회상 능력이 떨어지는 현상이며, 모든 LLM에서 공통적으로 나타난다. 10분짜리 회의와 3시간짜리 회의에서 우리가 같은 밀도로 발언을 기억하지 못하는 것과 같다. LLM도 제한된 어텐션 예산(attention budget) 안에서 어디에 가중치를 둘지 매 추론마다 결정하고, 새 토큰이 들어올 때마다 이 예산은 조금씩 깎인다.
어텐션 예산이 유한하다면, 고민되는 부분은 “무엇을 더 넣을까”가 보다 “무엇을 넣지 않을 것인가”다. 1편이 세션 사이에 무엇을 파일로 남길지를 다뤘다면, 이 글은 세션 안에서 무엇을 컨텍스트 윈도우에 들이지 않을지를 다룬다. Anthropic은 이 골라내는 기술을 context engineering이라 부른다.
1. Summary
원문은 context engineering을 prompt engineering의 자연스러운 진화로 놓는다. 프롬프트를 어떻게 쓸 것인가에서, 추론 시점에 어떤 토큰 집합을 넣을 것인가로 질문이 바뀐다. 원문의 뼈대는 세 덩어리다. 어텐션 예산이 왜 유한한지에 대한 아키텍처 근거, 유한한 예산 안에서 정적 컨텍스트를 짜는 세 층의 curation, 그리고 세션이 길어질 때 동적으로 관리하는 세 가지 long-horizon 기법이다.
왜 어텐션 예산은 유한한가. 근거는 세 가지다. 첫째, transformer 아키텍처에서 n개의 토큰은 n²개의 쌍(pairwise relationship)을 만든다. 토큰이 늘수록 관계망은 제곱으로 불어나고, 모델이 각 관계를 포착하는 능력은 그만큼 나빠진다. 둘째, 훈련 데이터에 짧은 시퀀스가 훨씬 많으므로, 모델은 긴 컨텍스트에 대해 상대적으로 적은 경험을 갖는다. 셋째, position encoding interpolation으로 더 긴 시퀀스를 처리할 수는 있지만, 적응 과정에서 위치 이해의 정밀도가 떨어진다.
이 세 요인이 만드는 것은 급격한 절벽이 아니라 점진적인 성능 저하(performance gradient)다. 길어질수록 정보 검색과 장거리 추론의 정밀도가 서서히 떨어진다. 이 구분이 중요한 이유는 2.2 절 compaction의 전제가 되기 때문이다 — 성능이 서서히 내려가므로, 지금은 눈에 띄지 않지만 나중에 중요해지는 정보를 섣불리 버리면 대가가 미묘한 품질 저하로 돌아온다.
1.1 세 층의 curation
어텐션 예산이 유한하다면, 그 안에 무엇을 넣을지가 곧 엔지니어링이다. 원문은 정적 컨텍스트를 세 층으로 나눈다.
System prompt — altitude. 핵심은 “적절한 고도(altitude)”다. 한쪽 극단은 if-else 로직을 하드코딩하는 것(취약하고 유지보수 비용이 올라간다), 다른 극단은 막연한 지침만 주는 것(구체적 신호가 전달되지 않는다). 올바른 고도는 행동을 유도할 만큼 구체적이면서, 모델이 스스로 판단할 여지를 남길 만큼 유연한 지점이다.
Tools — 겹치는 도구를 없애라. 가장 흔한 실패 원인은 역할이 비슷한 도구가 너무 많아 어느 것을 써야 할지 애매해지는 상황이다. “사람 엔지니어가 어떤 도구를 써야 하는지 확정적으로 말할 수 없다면, AI 에이전트가 더 잘할 거라고 기대할 수 없다.” 도구 설계의 기준은 모델의 능력이 아니라 사람의 판단 명확성에 있다.
Examples — 표준적인 소수. 모든 규칙을 나열하지 말고, 기대하는 행동을 보여주는 다양하고 표준적인 예시 소수를 제공하라. 그런 예시는 천 마디 말보다 나은 “그림”이 된다.
1.2 세 가지 long-horizon 기법
정적 컨텍스트를 아무리 잘 짜도, 세션이 길어지면 컨텍스트 윈도우를 넘는 정보가 쌓인다. 원문은 세 가지 기법을 제시한다.
Compaction. 컨텍스트 윈도우가 한계에 다다르면 대화 내용을 요약하고 새 컨텍스트를 시작한다. Claude Code는 메시지 히스토리에서 아키텍처 결정·미해결 버그·구현 세부사항은 보존하되 중복된 도구 출력은 버리고, 압축된 컨텍스트에 최근 파일 5개를 더해 이어간다. 핵심은 요약이 아니라 무엇을 유지하고 무엇을 버릴지의 선택이다. 원문에서는 recall을 먼저 최대화한 뒤, precision을 반복적으로 개선하라. 라고 이야기한다.
Structured note-taking. 에이전트가 컨텍스트 윈도우 바깥에 주기적으로 노트를 쓰고 나중에 다시 불러오는 기법이다. Claude Code의 to-do 리스트나 NOTES.md 패턴이 이에 해당한다. 1편의 claude-progress.txt가 정확히 이 구현체다.
Sub-agent architecture. 전문화된 서브 에이전트들이 깨끗한 컨텍스트 윈도우로 집중된 작업을 수행하고, 메인 에이전트는 상위 계획을 조율한다. 각 서브 에이전트는 수만 토큰을 탐색하더라도 1,000~2,000 토큰짜리 압축된 결과만 돌려보낸다. 탐색의 상세 컨텍스트는 서브 에이전트 안에 격리되고, 리드 에이전트는 종합과 분석에만 집중한다.
2. 읽고 알게 된 것들
원문은 컨텍스트를 가져오는 전략의 흐름을 소개한다. 기존의 pre-computed retrieval — 임베딩 기반 검색처럼 추론 전에 관련 정보를 미리 뽑아두는 방식이다. 새로운 just-in-time retrieval — 에이전트가 파일 경로, 쿼리, URL 같은 가벼운 식별자만 들고 있다가 필요한 시점에 도구로 직접 데이터를 끌어오는 방식이다.
2.1 Just-in-time retrieval이 가능하게 하는 것
흥미로운 것은 트레이드오프 자체보다, just-in-time이 가능하게 하는 것 — 에이전트가 탐색 과정에서 단계적으로 이해를 쌓아간다는 점이었다.
원문은 모델이 디렉토리의 구조적 메타데이터를 사람처럼 읽을 수 있다는 점을 짚는다. 사람이 프로젝트 디렉토리를 열었을 때 폴더 이름과 파일 배치만으로 대략의 구조를 파악하듯, 모델도 같은 신호를 읽는다. test_utils.py라는 같은 파일이 tests/ 폴더에 있을 때와 src/core_logic/에 있을 때 의미가 다르다는 원문의 예시가 이를 잘 보여준다. 파일의 내용을 열어보기 전에, 위치와 이름만으로 이미 판단이 시작된다.
이 전략으로 점진적인 밝혀냄(progressive disclosure)이 가능해진다. 에이전트가 파일 시스템을 탐색할 때, 각 단계에서 얻는 메타데이터가 다음 결정을 안내한다. 파일 크기가 복잡도를 암시하고, 네이밍 컨벤션이 용도를 힌트하고, 타임스탬프가 관련성의 프록시가 된다. 에이전트는 한 번에 모든 것을 컨텍스트에 올리지 않고, 한 층씩 이해를 쌓아가면서 필요한 것만 작업 기억(working memory)에 유지한다. 우리가 정보의 전체를 외우지 않고, 필요한 것만 메모하거나 중요한 곳에 스티커를 붙여 필요할 때 보는 것과 동일하다.
이런 단계적 탐색은 데이터를 미리 처리해두는 방식으로는 얻을 수 없다. 탐색의 각 단계가 다음 단계를 안내하는 구조이기 때문이다. 원문은 하이브리드 전략을 권한다. CLAUDE.md에 변하지 않는 정보는 미리 넣어 속도를 확보하고, 탐색이 필요한 정보는 에이전트가 런타임에 직접 찾아가게 두자. 모델에게 자율성을 주는 것이 더 효율적인 컨텍스트를 만든다.
2.2 Compaction — 되돌릴 수 없는 결정
원문은 compaction의 핵심이 요약이 아니라 선택에 있다고 말한다. 요약은 compaction이 하는 일이지만, 잘하고 못하고를 가르는 것은 무엇을 남기고 무엇을 버릴지의 판단이다. 그리고 이 판단이 어려운 진짜 이유는 되돌릴 수 없다는 데 있다.
1장에서 이야기 했던 점진적인 성능 저하의 원인이 여기에 있다. 지금 컨텍스트에서 거의 주목받지 못하는 정보가 열 번의 작업 뒤에는 핵심이 될 수 있다. 문제는 그땐 이미 compaction으로 사라진 뒤라는 것이다. 대가는 즉시 나타나지 않고 미묘한 품질 저하로 훨씬 나중에야 드러난다. 버린 시점에서는 올바른 판단처럼 보였지만 되돌릴 방법은 없다.
이 되돌릴 수 없는 결정에 대해 원문이 엔지니어에게 주는 메세지는 이렇다. — Recall을 먼저 최대화하고, precision을 나중에 개선하라. — 정확도를 먼저 올리려고 과감하게 쳐내면, 나중에야 중요해지는 컨텍스트를 영구히 잃는다. 되돌릴 수 없는 결정을 최대한 늦추자. 넉넉하게 남겨두면 나중에 정밀하게 다듬을 기회가 있지만, 일찍 쳐내면 그 기회 자체가 사라진다.
그렇다면 실무에서 에이전트와 긴 세션을 돌릴 때, 개발자가 할 수 있는 가장 직접적인 대응은 무엇일까. 버려지기 전에 빼두는 것이다. Compaction은 에이전트가 자동으로 수행하고, 개발자가 무엇이 남고 무엇이 사라지는지를 통제하기 어렵다. 하지만 compaction이 일어나기 전에 중요한 정보를 컨텍스트 윈도우 바깥 — 파일이나 노트 — 에 선제적으로 기록해두면, 되돌릴 수 없는 결정의 대가를 줄일 수 있다. 아키텍처 결정의 근거, 시도했다가 실패한 접근과 그 이유, 아직 해결하지 못한 문제의 현재 상태. 이런 것들은 compaction에서 “지금은 불필요해 보이지만 나중에 핵심이 될” 정보의 전형이다.
흥미로운 것은, 1편에서 만들었던 claude-progress.txt가 정확히 이 역할을 하고 있었다는 점이다. 1편에서는 세션 사이를 잇는 장치로 소개했지만, 2편의 관점에서 다시 보면 compaction의 위험성에 대한 보험이기도 하다. 컨텍스트 윈도우 안에서 사라질 수 있는 정보를 윈도우 밖에 복제해두는 것이니까.
2.3 1편 artifact들의 재해석
claude-progress.txt만 그런 게 아니다. 1편에서 만들었던 다른 artifact들도 2편의 이론으로 다시 보면 더 정교한 역할을 하고 있었다.
feature_list.json을 보자. 1편에서 이 파일의 본질은 권한 분리였다. 사람이 정한 할 일 목록을 에이전트가 조용히 고쳐 쓰지 못하게 JSON 스키마로 잠그는 장치. 하지만 2편의 관점에서 보면 — compaction이 일어났을 때 가장 먼저 복구되어야 할 정보다. 기능 목록, 각 기능의 완료 여부, passes 필드의 true/false. 이 정보를 단순한 텍스트로 전달하면 compaction 과정에서 망가질 수 있다. 1편에서 효율의 부산물이라고 했던 JSON의 낮은 해석 비용이, 2편에서는 compaction 내성이라는 다른 이점으로 해석된다.
init.sh도 마찬가지다. 1편에서 이 스크립트는 세션 시작 시 레포지토리 상태를 점검하는 관문이었다. 2편의 관점에서 보면 이 스크립트는 오염된 컨텍스트를 리셋하고 새 세션으로 넘어갈 때 환경 무결성을 증명하는 계약이다. Compaction이든 세션 전환이든, 에이전트가 새로운 컨텍스트 윈도우로 시작할 때 가장 먼저 필요한 것은 “지금 코드베이스가 정상인가”에 대한 확인이다. init.sh가 통과하면 에이전트는 이전 컨텍스트에서 무엇이 사라졌든 코드베이스 자체는 신뢰할 수 있다는 전제 위에서 작업을 시작할 수 있다. 이 스크립트가 없으면 에이전트는 새 컨텍스트에서 “이전 세션이 뭘 망가뜨리지 않았나”를 확인하는 데 어텐션 예산을 써야 한다.
1편의 문제(세션 사이)와 2편의 문제(세션 안)는 별개가 아니라, 같은 해법이 두 문제를 동시에 푸는 구조였던 셈이다.
3. 내 작업에 어떻게 적용할 것인가
3.1 Spring Boot 회원가입 예시: altitude와 하이브리드 retrieval
1편에서 POST /users 회원가입 API를 예시로 각 artifact를 구체화했다. 같은 예시를 이어 쓴다. 이번에 답해야 할 질문은 “CLAUDE.md에 무엇을 넣고 무엇을 넣지 않을 것인가?” 이다. 원문의 하이브리드 전략을 적용하면 판단 기준은 명확하다. 변하지 않는 것은 미리 넣고(pre-computed), 탐색이 필요한 것은 에이전트에게 맡긴다(just-in-time). Pre-computed로 CLAUDE.md에 박을 것들 — 프로젝트의 패키지 구조, 네이밍 컨벤션, DB 연결 정보와 테스트 프로파일 설정, 빌드·테스트 명령어. 이것들은 프로젝트 수명 동안 거의 바뀌지 않는다. Just-in-time으로 에이전트에게 맡길 것들 — 기존 엔티티의 필드 구성, 유사한 기존 엔드포인트의 구현 패턴, 테스트 패턴. 이것들은 코드가 변할 때마다 같이 변하므로 미리 적어두면 금방 낡는다. 이 전력으로 CLAUDE.md를 작성하면 다음과 같은 모양이 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# CLAUDE.md
## 아키텍처 (pre-computed — 거의 바뀌지 않는다)
- 헥사고날 레이어: domain / application.required / application.provided / application.service / adapter.in.web / adapter.out.persistence
- 네이밍: `*UseCase`, `*Port`, `*Adapter`
- 테스트 프로파일: application-test.yml -> H2 인메모리
## 빌드·테스트 (pre-computed)
- 빌드: `./gradlew bootRun`
- 테스트: `./gradlew test`
- 헬스체크: `curl localhost:8080/actuator/health`
## 코드 탐색 가이드 (just-in-time — 에이전트가 직접 찾는다)
- 새 엔드포인트를 만들 때: 기존 유사 엔드포인트를 grep으로 찾아 패턴을 따를 것
- 예) `grep -r "@PostMapping" src/adapter/in/web/`
- 엔티티 필드 확인: 코드를 직접 읽을 것. 여기에 적지 않는다 — 필드는 자주 바뀐다
- 예) `grep -r "@Column" src/domain/`
- 테스트 방식: 기존 테스트 파일을 확인할 것
- 예) `ls src/test/**/adapter/in/web/`
패키지 구조와 빌드 명령어는 고정값이니 프롬프트에 박는다. 엔티티 필드나 테스트 방식은 코드에서 직접 읽게 하되, 어디서 찾아야 하는지의 힌트만 준다. 에이전트가 grep과 ls로 탐색할 경로를 알려주는 것이 just-in-time retrieval의 실체다 — 데이터 자체가 아니라 데이터로의 포인터를 넣는 것이다.
원문은 하이브리드 전략에서 도메인의 특성이 비중을 결정한다고 짚는다 — 법률이나 금융처럼 콘텐츠 변경이 적은 도메인은 pre-computed의 비중이 커야 하고, 변화가 빠른 도메인은 just-in-time를 더 커야 한다. Spring 기반 백엔드는 후자에 가깝다. 패키지 구조와 컨벤션은 프로젝트 초기에 정해지면 천천히 변하고, 프레임워크 자체의 관례(@RestController, @Service, @Repository)는 버전이 올라가도 안정적이다. 그래서 CLAUDE.md에 넉넉히 박아두는 것이 에이전트의 어텐션 예산을 아끼는 합리적 선택이 된다.
3.2 어느 기법을 언제 쓸 것인가
1장에서 정리한 세 가지 long-horizon 기법 — compaction, note-taking, sub-agent — 은 서로 배타적이지 않다. 원문도 “the choice between these approaches depends on task characteristics”라고만 말하고 단일 정답을 제시하지 않는다. 하지만 실무에서 에이전트에게 어떤 작업을 맡길 때, 어느 기법에 무게를 둘지의 감각은 필요하다. 백엔드 개발에서 자주 만나는 네 가지 시나리오를 세 기법에 매핑해 본다.
| 시나리오 | 주력 기법 | 보조 기법 | 이유 |
|---|---|---|---|
| 레거시 코드 마이그레이션 | Note-taking | Sub-agent | 이전 구조와 새 구조의 매핑이 세션 전체를 관통해야 한다 |
| 새 기능 개발 | Compaction | Note-taking | 대화 흐름이 곧 작업 흐름이다 — 요약을 잘하면 충분하다 |
| 프로덕션 버그 추적 | Sub-agent | Note-taking | 원인 후보가 여럿이고, 각각을 깊이 파야 한다 |
| 대규모 리팩토링 | Sub-agent | Note-taking | 변경 범위가 넓어 단일 컨텍스트로 전체를 조망하기 어렵다 |
레거시 코드 마이그레이션. 오래된 서블릿 기반 코드를 Spring Boot로 옮기는 작업을 생각해 보자. 이 작업의 특징은 “이전 구조의 X가 새 구조의 Y에 대응한다”는 매핑 테이블이 세션 내내 참조되어야 한다는 점이다. 이 매핑은 compaction에서 쉽게 사라진다. 요약 모델 입장에서 열 개의 클래스 매핑 중 어느 것이 나중에 중요해질지 판단하기 어렵기 때문이다. 그래서 note-taking이 주력이다. 매핑 테이블을 파일로 유지하고, compaction 이후에도 에이전트가 참조할 수 있게 한다. 파일 단위 변환처럼 독립적인 작업은 서브 에이전트에게 넘길 수 있다.
새 기능 개발. 3.2절의 회원가입 API 같은 작업이다. 요구사항 확인 → 엔티티 설계 → 레이어별 구현 → 테스트. 대화의 흐름이 곧 구현의 흐름이고, 각 단계가 이전 단계를 자연스럽게 이어받는다. 이런 구조에서는 compaction이 가장 효과적이기에 이전 단계의 결정사항을 요약해 들고 가면 다음 단계를 시작하기에 충분하다. 다만 아키텍처 결정의 근거(“왜 별도 DTO를 만들었는가”, “왜 이 밸리데이션을 도메인 레이어에 뒀는가”)는 compaction에서 먼저 잘리기 쉬우므로, 핵심 결정은 노트로 빼두면 안전하다.
프로덕션 버그 추적. 장애가 발생했을 때 에이전트에게 원인 분석을 맡기는 경우다. 로그를 읽고, 관련 코드를 추적하고, 재현 조건을 좁혀가는 과정인데, 원인 후보가 여럿일 때가 많다. 후보 A를 파다가 막히면 후보 B로 넘어가야 하는데, 단일 에이전트가 이 전환을 하면 후보 A를 파던 컨텍스트가 후보 B의 탐색을 오염시킨다. 서브 에이전트가 각 후보를 독립된 컨텍스트에서 추적하고, 리드 에이전트가 결과를 종합하는 구조가 맞다. 각 서브 에이전트의 탐색 결과는 노트로 남겨 리드 에이전트가 참조한다.
대규모 리팩토링. 패키지 구조 변경이나 공통 모듈 추출처럼, 변경이 코드베이스 전체에 걸치는 작업이다. 단일 컨텍스트 윈도우에 모든 변경 대상을 올릴 수 없으므로, 서브 에이전트가 모듈 단위로 변경을 수행하고 리드 에이전트가 모듈 간 일관성을 확인하는 구조가 자연스럽다. 레거시 마이그레이션과 비슷하지만, 차이는 목표가 구조 변환이 아니라 구조 개선이라는 점이다 — 기존 동작을 보존해야 한다. 서브 에이전트 A가 모듈 X를, 서브 에이전트 B가 모듈 Y를 각각 고칠 때, B가 모듈 Y의 공개 메서드 시그니처를 바꿔버리면 모듈 X가 깨진다. 각 서브 에이전트는 자기 컨텍스트 안에서만 보기 때문에 다른 모듈이 자신에게 어떻게 의존하는지를 모른다. 따라서 “건드리면 안 되는 계약 목록” — 공개 인터페이스의 시그니처, 반환 타입, 예외 계약 — 을 노트로 명시해둘 필요가 있다.
닫는 말
1편은 세션 사이를 artifact로 이었고, 이 글은 세션 안을 curation으로 다듬었다. 두 글이 공유하는 전제가 하나 있다 — 에이전트는 하나라는 것이다. 하나의 에이전트가 하나의 컨텍스트 윈도우를 안고, 그 안에서 무엇을 넣고 무엇을 뺄지를 고민하는 구조다.
하지만 4장의 표를 다시 보면, 네 시나리오 중 셋에서 sub-agent가 등장한다. 버그 추적에서 원인 후보를 나눠 파고, 리팩토링에서 모듈 단위로 변경을 위임하고, 마이그레이션에서 파일 단위 변환을 넘기는 — 이 패턴들은 단일 에이전트의 컨텍스트 한계를 우회하기 위해 자연스럽게 에이전트를 쪼개는 방향으로 흐른다. 이 글에서 sub-agent는 세 기법 중 하나로만 다뤘지만, Anthropic은 이 기법을 대규모로 확장했을 때 어떤 일이 벌어지는지를 별도의 글에서 다룬다.
다음 글에서는 Anthropic이 같은 블로그에서 공개한 장시간 앱을 위한 하네스 설계를 같은 방식으로 정리한다. 1편이 파일 네 개로 세션 사이를 이었고, 2편이 curation으로 세션 안을 다듬었다면, 3편은 플래너·생성자·평가자로 에이전트를 쪼갰을 때 무엇이 달라지는지에 대한 이야기다.