
AI Agent의 컨텍스트 엔지니어링: 유한한 토큰 윈도우를 다루는 네 가지 전략
- 1AI Agent의 구조: 모델, 도구, 루프가 만드는 자율적 시스템
- 2AI Agent 워크플로우 패턴: 단순한 Chaining에서 동적 Orchestration까지
- 3AI Agent의 도구 설계: ACI 원칙부터 프로덕션 스키마까지
- 4AI Agent의 컨텍스트 엔지니어링: 유한한 토큰 윈도우를 다루는 네 가지 전략읽는 중
- 5AI Agent 루프: 한 턴의 요청이 처리되는 6단계
Claude Code에 수십 개 파일에 걸친 대규모 리팩터링을 요청하면 흥미로운 현상이 관찰됩니다. 처음에는 네이밍 컨벤션도 정확히 지키고, 의존성도 빠짐없이 업데이트합니다. 그런데 대화가 길어지면서 이미 수정한 파일을 다시 수정하거나, 초반에 합의한 규칙을 슬쩍 잊어버리는 순간이 옵니다.
이것은 모델이 “나빠진” 게 아닙니다. 도구 호출 결과, 파일 내용, 에러 메시지가 쌓이면서 컨텍스트 윈도우가 포화 상태에 도달한 것입니다. 초반에 확인했던 컨벤션 정보가 수백 개의 도구 결과 사이에 묻히고, 모델의 주의(attention)가 분산되는 현상. 벤치마크 연구에서 밝혀진 context rot이라는 개념이 이 현상을 설명합니다.
이전 글에서 도구 설계 원칙을 살펴봤는데, 완벽하게 설계된 도구라도 그 결과가 쌓이는 공간인 컨텍스트가 관리되지 않으면 성능이 무너집니다. 이 글에서는 “어떤 정보를, 언제, 어떤 형태로 모델에게 보여줄 것인가”를 설계하는 컨텍스트 엔지니어링을 살펴봅니다.
프롬프트 엔지니어링에서 컨텍스트 엔지니어링으로
프롬프트의 한계
프롬프트 엔지니어링은 모델에게 전달하는 지시문을 다듬는 기술입니다. “이렇게 말하면 더 좋은 답이 나온다”는 접근으로, 단일 턴 질의응답에서는 충분히 효과적입니다.
그런데 에이전트는 루프를 돕니다. 첫 번째 글에서 살펴본 것처럼, 에이전트는 도구를 호출하고 결과를 확인하고 다시 판단하는 과정을 반복합니다. 한 턴의 도구 호출이 5,000 토큰의 결과를 반환한다면, 50턴 후에는 도구 결과만 250,000 토큰에 달합니다. 처음에 정성 들여 작성한 시스템 프롬프트가 전체 컨텍스트의 1~2%에 불과해지는 셈입니다.
이 상황에서 “프롬프트를 더 잘 쓰자”는 접근으로는 부족합니다. 문제는 프롬프트의 품질이 아니라, 모델이 매 턴마다 보게 되는 전체 정보의 구성이기 때문입니다. Andrej Karpathy는 이 관점의 전환을 이렇게 표현했습니다.
“+1 for ‘context engineering’ over ‘prompt engineering’. People associate prompts with short task descriptions you’d give an LLM in your day-to-day use. When in every industrial-strength LLM app, context engineering is the delicate art and science of filling the context window with just the right information for the next step.”
컨텍스트 윈도우는 작업 기억이다
Karpathy는 별도의 발표에서 LLM을 CPU에, 컨텍스트 윈도우를 RAM(작업 기억)에 비유하기도 했습니다. CPU가 아무리 강력해도 RAM에 올라와 있지 않은 데이터는 처리할 수 없는 것처럼, 모델이 아무리 똑똑해도 컨텍스트 윈도우에 없는 정보는 활용할 수 없습니다.
그런데 이 RAM에는 근본적인 제약이 있습니다. Anthropic이 어텐션 예산(attention budget)이라 부르는 개념입니다.
“Like humans, who have limited working memory capacity, LLMs have an ‘attention budget’ that they draw on when parsing large volumes of context. Every new token introduced depletes this budget by some amount.”
트랜스포머 아키텍처에서 모든 토큰은 다른 모든 토큰과 상호 참조합니다. n개 토큰이 있으면 n²개의 쌍별 관계가 생기는 구조입니다. 컨텍스트가 길어질수록 이 관계망이 기하급수적으로 복잡해지고, 모델의 주의력이 분산됩니다.
토큰 수 쌍별 관계 상대적 복잡도
10,000 → 100M → 1x
50,000 → 2.5B → 25x
200,000 → 40B → 400x결과는 성능의 급격한 절벽이 아니라 점진적 기울기입니다. 모델은 긴 컨텍스트에서도 작동하지만, 정보 검색과 장거리 추론의 정밀도가 떨어지게 됩니다. 이러한 복잡도 증가가 context rot의 한 가지 원인으로 작용합니다.
Anthropic은 이 원칙을 한 문장으로 요약합니다. "원하는 결과의 가능성을 최대화하는, 가장 작은 고신호(high-signal) 토큰 집합을 찾아라." 컨텍스트 엔지니어링은 무엇을 포함할지만큼 무엇을 제외할지가 중요합니다.
정리하면, 컨텍스트 엔지니어링이란 유한한 컨텍스트 윈도우 안에서 올바른 정보를 올바른 시점에 올바른 형태로 제공하는 설계 규율입니다.
컨텍스트의 해부학
컨텍스트 윈도우에 무엇이 들어가는지를 먼저 이해해야 관리 전략을 세울 수 있습니다. 에이전트의 컨텍스트는 크게 네 가지 구성 요소로 나뉩니다. 50턴 에이전트 세션의 전형적인 컨텍스트 구성을 보면 이렇습니다.
[200K 토큰 윈도우의 구성 비율 예시: 50턴 시점]
시스템 프롬프트 + CLAUDE.md ██ ~5% (10K)
도구 정의 (스키마) ████ ~10% (20K)
대화 메시지 (user/assistant) ██████ ~15% (30K)
도구 반환값 (누적) ████████████████████ ~70% (140K)도구 반환값이 전체의 70%를 차지합니다. 컨텍스트 엔지니어링의 주된 전장이 어디인지를 보여주는 수치입니다.
시스템 프롬프트: 올바른 고도에서 작성하라
시스템 프롬프트는 컨텍스트 윈도우에서 가장 가치 있는 부동산입니다. 위치상 모델의 주의를 가장 많이 받고, 에이전트의 전체 행동 방향을 결정하기 때문입니다.
Anthropic은 시스템 프롬프트에 “올바른 고도(right altitude)”라는 개념을 적용합니다.
“The right altitude is the Goldilocks zone between two common failure modes.”
너무 낮은 고도는 if-else를 하드코딩한 것과 같습니다. “Python 파일을 보면 먼저 import를 확인하고, 그 다음 함수 정의를 찾고…” 식의 미시적 지시는 취약하고 유지보수가 어렵습니다. 반대로 너무 높은 고도는 “좋은 코드를 작성하세요” 같은 모호한 지시로, 모델에게 충분한 신호를 주지 못합니다.
올바른 고도는 그 사이에 있습니다. “당신은 시니어 엔지니어입니다. 정확성을 속도보다 우선하세요. 코드 수정 후에는 반드시 테스트를 실행하세요.” 이런 수준의 행동 지침이 효과적입니다.
Claude Code는 이 원칙을 시스템 프롬프트와 CLAUDE.md의 분리로 구현합니다. 에이전트의 핵심 행동 규칙은 시스템 프롬프트에 넣고, 프로젝트별 컨벤션과 설정은 CLAUDE.md 파일로 분리합니다. 루트 CLAUDE.md는 세션 시작 시 로딩되지만, 하위 디렉터리의 CLAUDE.md 파일은 에이전트가 해당 디렉터리에 접근할 때만 로딩되는 lazy loading 방식입니다. 관련 없는 디렉터리의 설정이 컨텍스트를 소비하지 않도록 하는 것입니다.
[시스템 프롬프트] ← 핵심 행동 규칙 (항상 로드)
└─ CLAUDE.md (루트) ← 프로젝트 전역 설정 (세션 시작 시 로드)
├─ src/CLAUDE.md ← 소스 코드 컨벤션 (src/ 접근 시 로드)
└─ tests/CLAUDE.md ← 테스트 설정 (tests/ 접근 시 로드)이 계층 구조가 바로 시스템 프롬프트에 적용된 컨텍스트 엔지니어링입니다. 모든 설정을 한꺼번에 올리지 않고, 에이전트가 실제로 해당 디렉터리의 파일을 읽을 때 관련 설정만 로딩합니다.
도구 정의는 컨텍스트를 소비한다
이전 글에서 도구 설계를 다뤘는데, 여기서 간과하기 쉬운 사실이 있습니다. 도구의 이름, 설명, 파라미터 스키마 자체가 컨텍스트 윈도우의 토큰을 소비한다는 점입니다.
도구 하나의 스키마는 수백에서 수천 토큰을 소비합니다. 도구가 20개면 수만 토큰, MCP 서버를 여러 개 연결해서 도구가 50개를 넘기면 수만에서 10만 토큰 가까이 소비될 수 있습니다. 200,000 토큰 윈도우에서 도구 정의만으로 상당 부분이 사라지는 셈입니다.
Claude Code는 이 문제를 두 가지 메커니즘으로 해결합니다. 첫째, 19개의 핵심 도구만 항상 전체 스키마를 유지하고 나머지는 피처 플래그와 사용자 유형에 따라 조건부로 등록합니다. 둘째, ToolSearch가 활성화된 환경에서는 일부 도구의 이름만 초기 컨텍스트에 포함하고, 모델이 실제로 필요한 도구를 검색할 때 전체 스키마를 로딩하는 지연 로딩(deferred schemas) 방식을 사용합니다. 이것이 도구에 적용된 컨텍스트 엔지니어링입니다.
도구 반환값과 누적 비용
도구 정의보다 더 빠르게 컨텍스트를 소비하는 것은 도구 반환값입니다. 파일을 읽으면 파일 내용이, grep을 실행하면 검색 결과가, 테스트를 돌리면 테스트 출력이 컨텍스트에 쌓입니다.
이전 글에서 다룬 25,000 토큰 상한이 개별 반환값의 크기를 제한하지만, 누적 효과는 막을 수 없습니다. 도구 호출 10번에 각 5,000 토큰이면 50,000 토큰, 30번이면 150,000 토큰입니다. 200,000 토큰 윈도우가 도구 결과만으로 가득 차는 것은 시간문제에 불과하게 됩니다.
Few-shot 예시에도 같은 관점이 적용됩니다. Anthropic은 예시의 효과를 강조하지만, 컨텍스트 엔지니어링 관점에서 보면 예시도 토큰을 소비합니다. 모든 엣지 케이스를 예시로 나열하는 것보다, 핵심 패턴을 보여주는 소수의 대표적 예시가 효과적입니다. Anthropic의 표현대로 “예시는 천 마디 말의 가치가 있는 그림”이기 때문입니다.
네 가지 컨텍스트 전략
컨텍스트가 유한한 자원이라면, 이를 관리하는 전략이 필요합니다. Anthropic의 가이드와 프로덕션 에이전트의 실전 사례에서 다루는 전략들을 네 가지로 정리할 수 있습니다. 이 전략들은 상호 배타적이 아니라 상호 보완적입니다. 프로덕션 에이전트는 대부분 네 가지를 조합해서 사용합니다.
| 전략 | 핵심 아이디어 | 적용 시점 | trade-off |
|---|---|---|---|
| JIT 검색 | 필요할 때 가져온다 | 외부 정보 필요 시 | 검색 품질 vs 지연 |
| 컴팩션 | 압축해서 공간을 확보한다 | 윈도우 한계 접근 시 | 정보 손실 vs 용량 |
| 구조화된 노트 | 핵심을 기록해 둔다 | 다단계 추론 필요 시 | 토큰 비용 vs 연속성 |
| 서브에이전트 격리 | 작업을 분리한다 | 복잡한 하위 작업 시 | 오버헤드 vs 보호 |
전략 1: Just-in-Time 컨텍스트 검색
전통적인 RAG 파이프라인은 사전에 문서를 임베딩하고, 질의 시 유사도 검색으로 관련 문서를 가져옵니다. 정적인 지식 베이스에는 효과적이지만, 에이전트에는 한계가 있습니다. 에이전트가 어떤 정보를 언제 필요로 할지 미리 알 수 없기 때문이죠.
에이전틱 검색(agentic search)은 다른 접근을 취합니다. Anthropic이 “just in time” 전략이라 부르는 방식으로, 모든 정보를 미리 컨텍스트에 올리는 대신 경량 식별자(파일 경로, 저장된 쿼리, 웹 링크)만 유지하고 런타임에 도구를 통해 동적으로 데이터를 로딩합니다.
Claude Code가 대규모 데이터베이스 분석을 할 때의 패턴이 좋은 예시입니다. 전체 데이터를 컨텍스트에 올리는 대신, 타겟 쿼리를 작성하고, 결과를 저장한 뒤, head나 tail 같은 Bash 명령어로 필요한 부분만 확인합니다. 전체 데이터 객체가 컨텍스트에 올라갈 필요가 없는 것입니다.
이것은 인간의 인지 방식과 유사합니다. 우리는 정보의 전체 본문을 암기하지 않습니다. 대신 파일 시스템, 북마크, 검색 엔진 같은 외부 인덱싱 시스템을 만들어서 필요할 때 정보를 가져옵니다.
에이전틱 검색의 강점은 프로그레시브 디스클로저(progressive disclosure)에 있습니다. 에이전트가 환경을 탐색하면서 점진적으로 관련 컨텍스트를 발견하는 패턴입니다. 파일 크기는 복잡도를 암시하고, 네이밍 컨벤션은 목적을 알려주고, 타임스탬프는 관련성의 프록시가 됩니다. tests/ 폴더 안의 test_utils.py와 src/core_logic/ 안의 같은 이름 파일은 완전히 다른 맥락을 내포합니다. 에이전트는 이런 메타데이터 신호를 레이어별로 조립하면서, 필요한 것만 작업 기억에 유지합니다.
실전에서 가장 효과적인 접근은 하이브리드 전략입니다. Claude Code가 이 모델을 사용합니다. CLAUDE.md 파일은 세션 시작 시 즉시 로딩되어 프로젝트 컨텍스트를 제공하고, grep과 glob 같은 도구는 필요한 파일을 just-in-time으로 검색합니다. 사전 로딩의 속도와 에이전틱 검색의 유연성을 결합한 셈입니다.
어떤 정보를 사전 로딩하고 어떤 정보를 JIT로 가져올지 결정하는 기준은 간단합니다. 변하지 않는 프로젝트 설정(코딩 컨벤션, 빌드 설정)은 사전 로딩. 매번 달라지는 파일 내용과 검색 결과는 JIT. Anthropic의 조언대로 "가장 단순하게 작동하는 것"이 최선입니다.
전략 2: 컴팩션
에이전트가 충분히 오래 작동하면, 어떤 JIT 전략을 쓰더라도 컨텍스트 윈도우가 한계에 도달합니다. 이때 필요한 것이 컴팩션(compaction)입니다. 대화 내용을 요약해서 압축하고, 압축된 요약으로 새로운 컨텍스트 윈도우를 시작하는 기법입니다.
“Compaction distills the contents of a context window in a high-fidelity manner, enabling the agent to continue with minimal performance degradation.”
비유하면, 긴 디버깅 세션 동안 모든 명령어를 기억하려 하지 않고 핵심 발견사항을 노트에 정리하는 것과 같습니다. 핵심은 무엇을 유지하고 무엇을 버릴지의 결정에 있습니다.
두 가지 접근이 존재합니다. Claude Code는 모델이 대화를 요약해서 사람이 읽을 수 있는 텍스트를 생성하는 방식을 사용합니다. 아키텍처 결정, 해결하지 못한 버그, 구현 세부사항을 보존하고, 중복된 도구 출력을 버립니다. Codex는 /responses/compact API 엔드포인트를 통해 서버 측에서 대화를 압축하고, 그 결과를 암호화된 불투명 blob으로 반환합니다.
컴팩션의 근본적 trade-off는 정보 손실입니다. 너무 공격적으로 압축하면 나중에 중요해질 미묘한 컨텍스트가 사라집니다. Anthropic은 컴팩션 프롬프트를 복잡한 에이전트 트레이스에서 튜닝하되, 먼저 재현율(recall)을 최대화한 뒤 정밀도를 높이라고 권장합니다.
가장 안전한 초기 단계는 도구 호출과 결과를 정리하는 것입니다. 대화 깊은 곳에 있는 도구 결과를 에이전트가 다시 볼 필요가 있을까요? 대부분의 경우 그렇지 않습니다. Claude Code의 컴팩션 과정을 예시로 보면 이렇습니다.
[컴팩션 전: 180K 토큰]
시스템 프롬프트 → 사용자 요청 → Read(파일 A) → 결과(3K) →
Edit(파일 A) → 결과(1K) → Bash(테스트) → 결과(5K) →
Read(파일 B) → 결과(4K) → ... (150K 도구 결과)
[컴팩션 후: 20K 토큰]
시스템 프롬프트 → [요약: 파일 A를 수정하고 테스트 통과.
파일 B의 import 구조 확인 완료. 남은 작업: 파일 C, D 수정]
→ 최근 접근 파일 5개 내용150K 토큰의 도구 결과가 핵심 결정을 보존한 수천 토큰의 요약으로 교체됩니다.
Claude Code의 5단계 컴팩션 파이프라인(Budget Reduction, Snip, Microcompact, Context Collapse, Auto-Compact)과 Codex의 암호화 blob 방식의 내부 구조는 이 시리즈의 후속 글에서 다룹니다. 여기서는 컴팩션이 왜 필요하고 어떤 trade-off가 있는지에 집중합니다.
전략 3: 구조화된 노트 (에이전틱 메모리)
컴팩션은 컨텍스트 윈도우를 관리하지만, 압축 과정에서 정보가 손실됩니다. 이 손실을 보완하는 전략이 구조화된 노트(structured note-taking)입니다. 에이전트가 컨텍스트 윈도우 바깥에 정보를 기록하고, 필요할 때 다시 가져오는 패턴입니다.
“Like Claude Code creating a to-do list, or your custom agent maintaining a NOTES.md file, this simple pattern allows the agent to track progress across complex tasks, maintaining critical context and dependencies that would otherwise be lost.”
Claude가 포켓몬 게임을 플레이한 사례가 이 전략의 위력을 보여줍니다. 에이전트는 수천 게임 스텝에 걸쳐 정확한 수치를 유지합니다. “지난 1,234 스텝 동안 루트 1에서 포켓몬을 훈련 중. 피카츄가 목표 10레벨 중 8레벨 상승.” 메모리 구조에 대한 별도의 지시 없이도, 탐험한 지역의 지도를 만들고, 어떤 공격이 어떤 상대에게 효과적인지 전략 노트를 관리합니다.
컨텍스트가 리셋된 후에도 에이전트는 자신의 노트를 읽고 수 시간에 걸친 작업을 이어갑니다. 모든 정보를 컨텍스트 윈도우 안에 유지하는 것만으로는 불가능한 장기 전략이 가능해집니다.
Claude Code에서 이 패턴은 CLAUDE.md의 읽기/쓰기와 자동 메모리 시스템으로 구현됩니다. 에이전트가 대화 중에 프로젝트에 대해 학습한 내용을 메모리 파일에 기록하고, 다음 세션에서 이를 참조합니다. 컨텍스트 윈도우의 크기 제한을 넘어서는 영속적 지식 축적이 가능해집니다.
구조화된 노트의 핵심은 컴팩션과의 관계입니다. 컴팩션이 실행되기 전에 핵심 사실을 노트로 추출하면, 요약 과정에서 정보가 손실되더라도 노트가 이를 보완합니다. 일종의 이중 안전장치입니다.
[컴팩션 전] 대화 기록 → 핵심 사실 추출 → 노트에 기록
[컴팩션 실행] 대화 기록 → 요약으로 교체 (일부 정보 손실)
[컴팩션 후] 요약 + 노트 → 모델이 핵심 사실에 접근 가능전략 4: 서브에이전트 격리
네 가지 전략 중 가장 강력한 것은 서브에이전트 격리입니다. 하나의 컨텍스트 윈도우에 모든 것을 담는 대신, 전문화된 서브에이전트에게 하위 작업을 위임하고 결과 요약만 받는 방식입니다.
“Each subagent might explore extensively, using tens of thousands of tokens or more, but returns only a condensed, distilled summary of its work (often 1,000-2,000 tokens).”
두 번째 글에서 Orchestrator-Workers 패턴을 작업 분배의 관점에서 살펴봤습니다. 여기서 한 발 더 나아가면, 이 패턴은 컨텍스트 관리 전략이기도 합니다. 서브에이전트가 50번의 도구 호출로 25만 토큰을 소비하더라도, 부모 에이전트는 2,000 토큰의 요약만 받습니다. 서브에이전트의 상세한 탐색 과정이 부모의 컨텍스트를 오염시키지 않게 됩니다.
Claude Code는 이를 사이드체인(sidechain) 아키텍처로 구현합니다. 서브에이전트의 대화 기록은 부모 에이전트의 메시지 배열이 아니라 별도의 사이드체인 파일에 저장됩니다. 서브에이전트가 완료되면 요약 텍스트만 부모에게 반환되고, 전체 대화 기록은 감사(audit) 목적으로 사이드체인에 남습니다. 부모 에이전트는 대규모 작업을 위임하고도 컨텍스트 풋프린트를 가볍게 유지할 수 있습니다.
핵심 통찰은 이것입니다. 서브에이전트는 단순히 병렬성을 위한 것이 아니라, 컨텍스트 보호를 위한 아키텍처적 결정이라는 점입니다.
프로덕션 비교: 컨텍스트 전략 현황
Claude Code와 Codex가 컨텍스트 엔지니어링을 어떻게 구현했는지 비교합니다.
| 관점 | Claude Code | Codex |
|---|---|---|
| 컨텍스트 윈도우 | 200K~1M 토큰 (모델별) | 192K 토큰 (codex-1) |
| 지속 컨텍스트 | CLAUDE.md (lazy load) | AGENTS.md (세션 시작 시 로드) |
| 검색 전략 | 하이브리드 (CLAUDE.md + grep/glob) | 세션 스코프 (로컬 작업 디렉터리 내 파일) |
| 도구 정의 관리 | 지연 로딩 (deferred schemas) | 정적 등록 (캐시 프리픽스 보존) |
| 컴팩션 트리거 | 다단계 (budget/threshold/reactive) | auto_compact_limit 임계값 |
| 컴팩션 결과 | LLM 요약 (사람이 읽을 수 있음) | 암호화 blob (불투명) |
| 서브에이전트 결과 | 요약만 반환 (sidechain 격리) | 세션 상태 반환 |
| 컴팩션 후 복구 | 파이프라인 내장 (5단계 순차) | 최근 편집 5개 파일 자동 재읽기 |
두 가지 관찰이 눈에 띕니다.
투명성 vs 불투명성. Claude Code의 컴팩션은 사람이 읽을 수 있는 LLM 요약입니다. 개발자가 무엇이 보존되고 무엇이 버려졌는지 확인할 수 있습니다. Codex의 암호화 blob은 모델의 “잠재적 이해(latent understanding)“를 보존하지만, 클라이언트가 내용을 확인하거나 수정할 수 없습니다. 투명성과 변조 방지 사이의 설계 결정인 셈입니다.
애플리케이션 레벨 vs API 레벨. Claude Code는 애플리케이션 레벨에서 컨텍스트를 관리합니다. 하니스(harness) 코드 안에 다단계 파이프라인을 구현해서, 매 모델 호출 전에 컨텍스트를 정제합니다. Codex는 API 레벨에서 관리합니다. /responses/compact 엔드포인트에 컴팩션을 위임하고, 프롬프트 캐싱으로 중복 비용을 제거합니다. Codex의 모든 프롬프트는 이전 프롬프트의 정확한 프리픽스가 되도록 구성되어, 캐시 히트 시 사실상 선형 비용으로 작동합니다.
이것은 본질적으로 에이전트 루프의 2차(quadratic) 비용 문제를 다루는 것이기도 합니다. 에이전트 루프에서 매번 전체 대화 기록을 API에 보내면 n번째 턴에 보내는 JSON의 총량은 n²에 비례합니다. Codex는 프롬프트 캐싱으로, Claude Code는 점진적 컴팩션으로 이 비용을 선형에 가깝게 줄입니다.
직접 구현: 컨텍스트 매니저를 갖춘 에이전트 루프
네 가지 전략의 핵심을 순수 Python으로 구현합니다. 프로덕션 수준이 아니라 개념을 보여주기 위한 코드입니다.
import json
from dataclasses import dataclass, field
@dataclass
class ContextManager:
max_tokens: int = 200_000
compact_threshold: float = 0.7
messages: list = field(default_factory=list)
notes: dict = field(default_factory=lambda: {
"plan": "",
"completed": [],
"key_facts": [],
})
def estimate_tokens(self) -> int:
return sum(len(json.dumps(m, ensure_ascii=False)) // 4
for m in self.messages)
def usage_ratio(self) -> float:
return self.estimate_tokens() / self.max_tokens
def should_compact(self) -> bool:
return self.usage_ratio() > self.compact_threshold
async def compact(self, llm_call):
history = json.dumps(self.messages, ensure_ascii=False, indent=2)
# 1단계: 핵심 사실을 노트로 추출 (컴팩션에서 살아남는다)
extraction = await llm_call(
"다음 대화에서 핵심 결정, 발견한 사실, 현재 계획을 "
"JSON으로 추출하라. 키: plan, completed, key_facts.\n\n"
+ history
)
self.notes = json.loads(extraction)
# 2단계: 대화를 요약으로 교체
summary = await llm_call(
"다음 대화를 500 토큰 이내로 요약하라. "
"모든 아키텍처 결정과 미해결 이슈를 보존하라.\n\n"
+ history
)
self.messages = [
{"role": "system", "content": f"[컴팩션 완료]\n{summary}"}
]
def build_prompt(self) -> str:
prompt_parts = [json.dumps(self.messages, ensure_ascii=False)]
# 노트가 있으면 주입 (컴팩션 후에도 유지된다)
if any(self.notes.values()):
prompt_parts.append(
f"\n[작업 노트]\n"
f"계획: {self.notes['plan']}\n"
f"완료: {', '.join(self.notes['completed'])}\n"
f"핵심 사실: {'; '.join(self.notes['key_facts'])}"
)
return "\n".join(prompt_parts)이 ContextManager를 사용하는 에이전트 루프입니다.
async def agent_loop(task: str, tools: dict, llm_call,
max_turns: int = 100):
ctx = ContextManager()
ctx.messages.append({"role": "user", "content": task})
for turn in range(max_turns):
# 컨텍스트 압력 확인
if ctx.should_compact():
print(f"[turn {turn}] 컴팩션 실행 "
f"(사용률 {ctx.usage_ratio():.0%})")
await ctx.compact(llm_call)
# 프롬프트 구성 (노트 포함)
prompt = ctx.build_prompt()
response = await llm_call(prompt, tools=tools)
# 도구 호출이 있으면 실행 (JIT 검색)
if response.tool_calls:
for call in response.tool_calls:
result = await tools[call.name](**call.args)
ctx.messages.append({
"role": "tool",
"name": call.name,
"content": str(result)[:25_000], # 반환값 상한
})
else:
# 최종 응답
return response.text
return "최대 턴 수 도달"이 코드에서 각 부분이 어떤 컨텍스트 전략을 구현하는지 정리합니다.
| 코드 | 전략 | 설명 |
|---|---|---|
should_compact() + compact() |
컴팩션 | 임계값 기반 자동 압축 |
notes + build_prompt() |
구조화된 노트 | 컴팩션 후에도 살아남는 핵심 정보 |
tools[call.name](**call.args) |
JIT 검색 | 모델이 필요할 때 도구로 정보 획득 |
str(result)[:25_000] |
반환값 제한 | 개별 도구 결과의 토큰 상한 |
서브에이전트 격리는 이 루프 안에서 agent_loop을 재귀 호출하는 형태로 확장할 수 있습니다. 개념적으로는 이런 모습입니다.
async def delegate_to_subagent(task: str, tools: dict, llm_call):
"""서브에이전트에게 작업을 위임하고 요약만 반환한다."""
# 서브에이전트는 자체 ContextManager를 갖는다 (격리)
result = await agent_loop(task, tools, llm_call, max_turns=30)
# 부모 에이전트에는 요약만 전달 (컨텍스트 보호)
summary = await llm_call(
f"다음 작업 결과를 2,000 토큰 이내로 요약하라:\n{result}"
)
return summary이 함수를 tools 딕셔너리에 도구로 등록하면, 모델이 스스로 서브에이전트 위임을 결정할 수 있습니다. 전체 구현은 에이전트 루프를 다루는 다음 글에서 살펴봅니다.
이 코드는 단일
compact() 호출로 전체 대화를 한 번에 압축합니다. 프로덕션 시스템은 상황에 따라 다른 전략을 선택하는 다단계 파이프라인을 사용합니다. Claude Code의 5단계 파이프라인에서 가벼운 단계(도구 결과 정리)가 먼저 실행되고, 무거운 단계(LLM 요약)는 마지막에만 실행됩니다.
한계와 열린 문제
컴팩션의 정보 손실
컴팩션은 본질적으로 비가역적입니다. 100턴 전에 언급된 제약 조건이 200턴 후에 중요해질 수 있지만, 이미 요약 과정에서 버려졌을 수 있습니다. 구조화된 노트가 이를 완화하지만, 무엇을 기록해야 하는지를 에이전트가 미리 완벽하게 판단할 수는 없습니다. 무엇을 잃어버렸는지는 에이전트가 그로 인한 실수를 할 때까지 알 수 없습니다. 모든 것을 유지하자니 컨텍스트가 초과되고, 압축하자니 정보가 손실되는 근본적 긴장입니다.
어텐션 예산의 비선형성
컨텍스트를 두 배로 늘리면 성능이 절반이 된다는 단순한 관계가 아닙니다. n² 쌍별 관계 때문에 인지 부하는 선형 이상으로 증가합니다. 그런데 “최적 컨텍스트 크기”에 대한 공식은 없습니다. 작업의 복잡도, 정보의 밀도, 모델의 능력에 따라 달라집니다. Karpathy의 표현대로 컨텍스트 엔지니어링이 “예술이자 과학”인 이유가 여기에 있습니다.
평가의 어려움
컨텍스트 엔지니어링이 잘 작동하는지 어떻게 측정할까요? 토큰 사용률은 쉽게 측정할 수 있지만, “모델이 올바른 시점에 올바른 정보를 가지고 있었는가?”는 측정하기 어렵습니다. 프롬프트 엔지니어링은 출력을 A/B 테스트할 수 있지만, 컨텍스트 엔지니어링의 실패는 종종 침묵합니다. 모델은 그럴듯하지만 틀린 답을 내놓고, 그 원인이 압축 과정에서 사라진 사실이었다는 것을 사후에야 알게 됩니다.
측정 가능한 지표(토큰 사용률, 컴팩션 횟수, 도구 호출 수)와 실제로 중요한 지표(정보 적시성, 결정 품질) 사이의 간극이 큽니다. 더 큰 컨텍스트 윈도우가 등장하더라도, context rot과 정보 관련성 문제는 윈도우 크기에 관계없이 존재합니다. 컨텍스트 엔지니어링의 평가 프레임워크는 아직 초기 단계이며, 이 시리즈의 후반부에서 에이전트 평가 체계를 다룰 때 이 문제를 다시 짚어봅니다.
전략 선택의 어려움
네 가지 전략 중 어떤 것을 어떤 비율로 조합할지에 대한 정답은 없습니다. Anthropic은 작업 특성에 따른 가이드를 제시합니다. 장시간의 대화형 작업에는 컴팩션이, 명확한 마일스톤이 있는 반복 개발에는 구조화된 노트가, 병렬 탐색이 효과적인 연구 작업에는 서브에이전트 격리가 적합합니다. 그러나 실전에서는 이런 특성이 하나의 작업 안에 혼재하는 경우가 많고, 최적의 조합은 실험으로 찾아야 합니다. Anthropic의 조언은 간결합니다. “Do the simplest thing that works.”
마치며
컨텍스트 엔지니어링은 프롬프트 엔지니어링의 자연스러운 확장입니다. 단일 프롬프트를 다듬는 것에서, 에이전트의 전체 수명 동안 정보 흐름을 설계하는 것으로 관점이 이동합니다. Karpathy의 비유대로 컨텍스트 윈도우가 RAM이라면, 컨텍스트 엔지니어링은 그 RAM을 효율적으로 관리하는 운영체제를 설계하는 것입니다. 모델이 강력해질수록 덜 처방적인 엔지니어링이 필요해지겠지만, 컨텍스트를 유한하고 소중한 자원으로 다루는 원칙은 변하지 않을 것입니다.
다음 글에서는 이 컨텍스트가 실제로 소비되는 공간, 즉 에이전트 루프의 내부를 해부합니다. Claude Code의 queryLoop()과 Codex의 Responses API 기반 루프가 한 턴을 어떻게 처리하는지 살펴보겠습니다.