컨텍스트 엔지니어링 4전략 (Context Engineering) — 멀티에이전트 시스템에 Write·Select·Compress·Isolate 적용하기
LLM 에이전트를 며칠 동안 돌리다 보면 반드시 한 번은 마주하는 순간이 있다. 에이전트가 방금 전 자신이 한 일을 기억하지 못하거나, 서브에이전트가 오케스트레이터의 대화 히스토리를 오염시키거나, 200k 토큰 윈도우가 꽉 차서 태스크가 강제 종료되는 상황이다. 이 문제들은 모두 같은 뿌리를 공유한다 — 컨텍스트를 어떻게 관리하느냐.
프롬프트 엔지니어링이 "무엇을 말할 것인가"의 문제였다면, 컨텍스트 엔지니어링(Context Engineering)은 "어떤 정보를 언제, 어디에 두어야 모델이 최적으로 행동하는가"의 문제다. Anthropic은 2025년 9월 공식 엔지니어링 블로그를 통해 에이전트의 컨텍스트 관리를 Write·Select·Compress·Isolate 4가지 전략(버킷)으로 체계화했다. 이 글은 LLM API 호출 경험이 있는 백엔드·풀스택 개발자를 대상으로, 각 전략이 무엇인지, 언제 써야 하는지, 실제 TypeScript 코드로 어떻게 구현하는지를 하나의 일관된 시나리오(뉴스 요약 멀티에이전트)를 통해 다룬다.
이 글을 끝까지 읽으면, 자신의 에이전트 코드에서 컨텍스트 병목이 어디서 발생하는지 진단하고, 4전략을 독립적으로 또는 조합하여 프로덕션 수준 에이전트를 구현할 수 있다.
핵심 개념
컨텍스트 윈도우는 에이전트의 "작업 기억"이다
인간의 단기 기억처럼, LLM의 컨텍스트 윈도우는 한 번에 처리할 수 있는 정보의 총량이다. Claude 3.7 기준 200k 토큰이지만, 토큰 수가 늘어날수록 Context Rot 현상이 발생한다. 모든 토큰이 모든 토큰을 어텐딩하는 O(n²) 관계 때문에, 컨텍스트가 클수록 모델의 정확한 정보 회상 능력이 저하된다. 단순히 "더 긴 컨텍스트를 쓰면 해결된다"는 발상이 왜 틀렸는지가 여기서 드러난다.
핵심 질문: 어떤 컨텍스트 구성이 모델의 원하는 행동을 가장 잘 이끌어내는가?
Anthropic은 이 질문에 답하기 위해 4가지 버킷을 정의했다.
| 전략 | 핵심 동작 | 해결하는 문제 |
|---|---|---|
| Write | 컨텍스트 외부 저장소에 정보 기록 | 세션 경계를 넘는 상태 유지 불가 |
| Select | 관련 정보만 동적으로 컨텍스트에 로드 | 불필요한 컨텍스트 팽창 및 Context Rot |
| Compress | 대화 히스토리를 요약 후 새 컨텍스트로 재시작 | 컨텍스트 윈도우 한계 도달 시 강제 종료 |
| Isolate | 서브에이전트에 독립된 컨텍스트 윈도우 배분 | 서브에이전트의 세부 내용이 오케스트레이터를 오염 |
이 4전략은 서로 배타적이지 않다. 실제 프로덕션 에이전트는 이 중 둘 이상을 조합해서 사용한다.
Write — 컨텍스트 바깥에 기억을 쌓아라
Write 전략은 에이전트가 컨텍스트 윈도우 외부(파일, DB, 런타임 상태 객체)에 정보를 능동적으로 저장하는 패턴이다. 컨텍스트가 리셋되거나 세션이 끊겨도, 에이전트는 외부에 기록된 스크래치패드를 다시 읽어 이어갈 수 있다.
적용 시점:
- 수십~수백 스텝에 걸친 장기 태스크 (코드베이스 분석, 게임 에이전트)
- 여러 에이전트가 공유해야 하는 상태를 외부화할 때
- 오류 발생 시 처음부터가 아닌 마지막 체크포인트부터 재시도해야 할 때
트레이드오프: 외부 저장소 I/O 지연이 추가되고, 파일 시스템 의존성이 생긴다. 비동기 쓰기와 로컬 캐시 계층으로 완화할 수 있다.
Select — 필요한 정보만 정확하게 가져와라
Select 전략은 에이전트가 현재 태스크와 관련 있는 정보만 동적으로 컨텍스트에 주입하는 패턴이다. 모든 문서를 통째로 컨텍스트에 넣는 대신, 관련성 높은 청크만 검색해서 가져온다.
Select 전략의 세 가지 메모리 유형
- 에피소딕: 원하는 행동의 예시 (few-shot examples)
- 절차적: 에이전트 행동 지침 —
CLAUDE.md같은 규칙 파일- 의미적: 태스크 관련 사실·지식 (RAG, 벡터 DB)
Anthropic이 제안한 Contextual Retrieval은 기존 RAG의 핵심 한계를 개선한다. 기존 RAG는 청크를 문서에서 잘라낼 때 문맥 정보가 소실된다. Contextual Retrieval은 각 청크에 그 청크가 문서 내에서 어떤 맥락에 있는지 설명하는 문구를 prepend하고 임베딩한다. BM25 키워드 검색과 의미 임베딩 검색을 하이브리드로 결합하면 기존 대비 검색 오류를 49% 감소시킬 수 있다.
트레이드오프: 검색 품질에 에이전트 전체 성능이 좌우된다. 관련 없는 청크가 주입되면 오히려 역효과가 난다. 평가 파이프라인 구축이 필수다.
Compress — 컨텍스트 한계를 넘어 계속 달려라
Compress 전략은 컨텍스트 윈도우가 임계값에 근접했을 때 대화 히스토리를 요약(컴팩션)하고 새 컨텍스트로 재시작하는 패턴이다. 세션을 강제 종료하는 대신, 지금까지의 핵심 내용을 압축해서 이어갈 수 있다.
압축 과정에서는 정보 손실이 불가피하다. 무엇을 보존하고 무엇을 버릴 것인지를 명확히 정해야 한다.
| 보존 대상 | 버릴 수 있는 것 |
|---|---|
| 태스크 목표 및 완료된 단계 | 중간 추론 과정의 세부 내용 |
| 발견된 핵심 사실과 데이터 | 반복된 시도와 실패 경로 |
| 현재 상태 및 미완료 항목 | 성공한 도구 호출의 원본 응답 전체 |
| 주의해야 할 제약·오류 이력 | 이미 요약된 이전 컴팩션 전의 원본 |
Claude Agent SDK는
compaction_control파라미터로 자동 컴팩션을 지원한다. Claude Code는 컨텍스트 윈도우 95% 도달 시 전체 히스토리를 자동으로 요약한다.
트레이드오프: 요약 품질이 낮으면 핵심 정보가 영구적으로 손실된다. 압축 결과를 별도로 검증하거나, 압축 전 반드시 Write로 원본을 백업하는 것을 권장한다.
Isolate — 서브에이전트에게 컨텍스트를 나눠줘라
Isolate 전략은 복잡한 태스크를 여러 서브에이전트에 분배하여 각자 독립된 컨텍스트 윈도우를 갖도록 하는 패턴이다. 오케스트레이터는 전략과 조율만 담당하고, 실제 작업의 세부 내용은 서브에이전트 컨텍스트 안에만 머문다.
컨텍스트 오염(Context Pollution): 서브에이전트의 긴 검색 결과나 오류 로그가 오케스트레이터 컨텍스트로 직접 유입되면, 오케스트레이터가 높은 레벨의 의사결정에 집중하지 못하게 된다. Isolate 전략은 서브에이전트의 결과 요약만 오케스트레이터에게 반환하게 함으로써 이를 차단한다.
트레이드오프: 서브에이전트가 많아질수록 토큰 비용이 급증한다. Anthropic의 멀티에이전트 리서처 사례에서 단일 에이전트 대비 최대 15배 많은 토큰을 소비했다. 서브에이전트 수와 태스크 분해 기준을 신중히 설계해야 한다.
실전 적용
예시 1: 뉴스 요약 멀티에이전트 — 4전략 전후 비교
매일 수백 개의 뉴스 기사를 수집·분류·요약하는 에이전트 파이프라인을 구현하는 시나리오다.
Before — 전략 없는 나이브한 구현:
// 모든 문제가 발생하는 순진한 구현
interface Message {
role: "user" | "assistant";
content: string;
}
async function naiveNewsAgent(articles: string[]): Promise<string> {
let messages: Message[] = [];
for (const article of articles) {
// 문제 1: 모든 기사를 순차적으로 컨텍스트에 쌓는다 → Context Rot
messages.push({ role: "user", content: `이 기사를 요약해줘: ${article}` });
const summary = await callClaudeWithHistory(messages);
messages.push({ role: "assistant", content: summary });
// 200기사 처리 시 컨텍스트 윈도우 초과 → 100번째 기사에서 강제 종료
// 문제 2: 중단되면 처음부터 재시작 — 진행 상황 없음
// 문제 3: 단일 스레드 순차 처리 — 처리 속도 느림
}
return messages[messages.length - 1].content;
}After — Write + Select + Compress + Isolate 조합:
먼저 Write 전략의 스크래치패드를 정의한다.
// [Write] 에이전트 스크래치패드 — 세션 간 상태 영속화
import fs from "fs/promises";
import path from "path";
interface ScratchpadEntry {
step: number;
timestamp: string;
action: string;
result: string;
notes: string;
}
class AgentScratchpad {
private filePath: string;
private entries: ScratchpadEntry[] = [];
constructor(taskId: string) {
this.filePath = path.join("./scratchpad", `${taskId}.json`);
}
async load(): Promise<void> {
try {
const raw = await fs.readFile(this.filePath, "utf-8");
this.entries = JSON.parse(raw);
console.log(`[Write] 이전 체크포인트 ${this.entries.length}개 로드 완료`);
} catch {
this.entries = []; // 첫 실행이면 빈 상태로 시작
}
}
async write(entry: Omit<ScratchpadEntry, "step" | "timestamp">): Promise<void> {
const newEntry: ScratchpadEntry = {
step: this.entries.length + 1,
timestamp: new Date().toISOString(),
...entry,
};
this.entries.push(newEntry);
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
await fs.writeFile(this.filePath, JSON.stringify(this.entries, null, 2));
}
getSummary(): string {
return this.entries
.map((e) => `[Step ${e.step}] ${e.action}: ${e.notes}`)
.join("\n");
}
}다음은 Select 전략의 Contextual Retrieval 기반 검색이다.
// [Select] Contextual Retrieval — 관련 청크만 컨텍스트에 주입
interface NewsChunk {
id: string;
content: string;
contextualDescription: string; // 청크의 문서 내 위치 설명
}
class NewsKnowledgeBase {
private chunks: NewsChunk[] = [];
// 인덱싱 시: 각 청크에 컨텍스트 설명을 prepend (Contextual Retrieval 핵심)
async indexArticle(articleId: string, fullText: string): Promise<void> {
const rawChunks = this.splitIntoChunks(fullText, 400);
for (const [i, chunk] of rawChunks.entries()) {
// 각 청크가 전체 기사에서 어떤 역할을 하는지 설명 생성
const contextualDescription = await callClaude(
`다음 기사 "${articleId}" 전체 내용:\n${fullText}\n\n` +
`아래 청크가 이 기사 내에서 어떤 맥락에 위치하는지 2문장으로 설명하라.\n\n청크: ${chunk}`
);
this.chunks.push({
id: `${articleId}-${i}`,
content: chunk,
contextualDescription,
});
}
}
// 검색 시: BM25 + 임베딩 하이브리드 검색으로 관련 청크만 선별
async buildContext(query: string, topK = 5): Promise<string> {
// 실제 구현에서는 벡터 DB(Pinecone 등)와 BM25 점수를 결합
const relevant = this.hybridSearch(query, topK);
// 전체 문서 대신 관련 청크만 주입 → 토큰 절약 + 관련성 집중
return relevant
.map((c) => `[배경 정보]\n${c.contextualDescription}\n${c.content}`)
.join("\n\n");
}
private hybridSearch(query: string, topK: number): NewsChunk[] {
// 0.4 * BM25 + 0.6 * 임베딩 유사도 하이브리드 가중치
return this.chunks.slice(0, topK); // 간략화된 예시
}
private splitIntoChunks(text: string, maxWords: number): string[] {
const words = text.split(" ");
const chunks: string[] = [];
for (let i = 0; i < words.length; i += maxWords) {
chunks.push(words.slice(i, i + maxWords).join(" "));
}
return chunks;
}
}Isolate 전략으로 카테고리별 서브에이전트를 병렬 실행한다.
// [Isolate] 서브에이전트 — 독립 컨텍스트에서 좁은 태스크 수행 후 요약만 반환
interface SubTaskResult {
taskId: string;
summary: string;
keyFindings: string[];
}
async function runSubAgent(
taskId: string,
specificTask: string
): Promise<SubTaskResult> {
// 서브에이전트는 자신의 태스크 범위만 알고 있다
// 오케스트레이터의 전체 히스토리는 전혀 전달되지 않는다 (격리)
const response = await callClaude(
`[서브태스크 #${taskId}]\n${specificTask}\n\n` +
`완료 후 반드시 JSON 형식으로 응답하라:\n` +
`{ "summary": "...", "keyFindings": ["...", "..."] }`
);
// 오케스트레이터에게는 요약된 결과만 전달 — 세부 컨텍스트는 격리
const parsed = JSON.parse(extractJson(response));
return { taskId, ...parsed };
}Compress 전략으로 컨텍스트 한계 도달 시 자동 압축한다.
// [Compress] 컴팩션 트리거 — 80% 도달 시 히스토리를 요약하고 재시작
const CONTEXT_WINDOW_TOKENS = 200_000;
const COMPACTION_THRESHOLD = 0.80;
function estimateTokenCount(messages: Message[]): number {
return messages.reduce((acc, m) => acc + m.content.length / 4, 0);
}
async function compactIfNeeded(
messages: Message[],
taskGoal: string
): Promise<Message[]> {
const usage = estimateTokenCount(messages) / CONTEXT_WINDOW_TOKENS;
if (usage < COMPACTION_THRESHOLD) return messages;
console.log(`[Compress] 컨텍스트 ${Math.round(usage * 100)}% 사용 → 컴팩션 실행`);
const historyText = messages
.map((m) => `[${m.role}]: ${m.content}`)
.join("\n");
const summary = await callClaude(
`태스크 "${taskGoal}"의 대화 히스토리를 다음 형식으로 요약하라:\n` +
`1. 완료된 작업 (구체적)\n2. 현재 상태 및 미완료 항목\n` +
`3. 수집된 핵심 사실\n4. 주의해야 할 제약·오류\n\n` +
`히스토리:\n${historyText}`
);
// 압축된 단일 메시지로 새 컨텍스트 시작
return [
{
role: "user",
content: `[이전 작업 요약]\n${summary}\n\n태스크를 이어서 진행하라.`,
},
];
}마지막으로 4전략을 조합한 프로덕션 수준의 오케스트레이터다.
// 4전략 통합 오케스트레이터
async function productionNewsAgent(
taskId: string,
articleUrls: string[]
): Promise<string> {
// [Write] 이전 실행 체크포인트 로드
const scratchpad = new AgentScratchpad(taskId);
await scratchpad.load();
const knowledgeBase = new NewsKnowledgeBase();
let messages: Message[] = [];
// [Isolate] 카테고리별 서브에이전트 병렬 실행
const categories = ["정치", "경제", "기술", "사회"];
const subResults = await Promise.all(
categories.map((category) =>
runSubAgent(
`${taskId}-${category}`,
`다음 URL 목록에서 "${category}" 관련 기사만 선별해 핵심 내용을 요약하라:\n` +
articleUrls.join("\n")
)
)
);
// [Write] 서브에이전트 결과를 스크래치패드에 영속화
for (const result of subResults) {
await scratchpad.write({
action: `카테고리 요약 완료: ${result.taskId}`,
result: result.summary,
notes: result.keyFindings.join("; "),
});
}
// [Compress] 오케스트레이터 컨텍스트가 임계값 도달 시 자동 압축
messages.push({
role: "user",
content: `카테고리별 요약:\n${scratchpad.getSummary()}`,
});
messages = await compactIfNeeded(messages, "일간 뉴스 브리핑 생성");
// [Select] 과거 관련 기사를 지식베이스에서 검색해 컨텍스트 보강
const currentSummary = subResults.map((r) => r.summary).join(" ");
const backgroundContext = await knowledgeBase.buildContext(currentSummary);
return await callClaude(
`[오늘 카테고리 요약]\n${scratchpad.getSummary()}\n\n` +
`[관련 배경 정보]\n${backgroundContext}\n\n` +
`위를 통합해 500자 이내의 독자용 데일리 뉴스레터를 작성하라.`
);
}Before vs After 정량 비교:
| 항목 | Before (전략 없음) | After (4전략 조합) |
|---|---|---|
| 200기사 처리 가능 여부 | 불가 — 100번째에서 초과 종료 | 가능 — Compress + Write 조합 |
| 중간 오류 시 재시작 | 처음부터 전체 재시작 | 마지막 체크포인트부터 재개 |
| 토큰 사용량 (200기사) | ~160k 누적 (모두 컨텍스트에 쌓임) | ~40k — 관련 청크만 선택적 로드 |
| 처리 속도 | 순차 처리 (200기사 × 평균 3초) | 카테고리별 병렬 (4 서브에이전트) |
| 카테고리 간 정보 오염 | 정치 기사가 기술 요약에 영향 | Isolate로 완전 격리 |
장단점 분석
장점
| 전략 | 주요 장점 | 대표 사용 사례 |
|---|---|---|
| Write | 세션 경계를 초월한 장기 태스크 영속성 | 코드베이스 분석, 포켓몬 게임 에이전트 |
| Select | 토큰 비용 절감 + 관련 정보 집중으로 정확도 향상 | RAG 시스템, 도메인 지식 에이전트 |
| Compress | 사실상 무제한 컨텍스트 길이로 장기 작업 가능 | 장기 대화 에이전트, 반복 실행 파이프라인 |
| Isolate | 병렬 처리로 처리량 증가 + 컨텍스트 오염 방지 | 리서치 에이전트, 멀티스텝 분석 시스템 |
단점 및 주의사항
| 전략 | 단점 | 대응 방안 |
|---|---|---|
| Write | 외부 저장소 I/O 지연, 파일 시스템 의존성 | 비동기 쓰기 + 인메모리 캐시 계층 추가 |
| Select | 검색 품질에 전체 성능이 좌우됨 | Contextual Retrieval 방식 도입 + 검색 평가 파이프라인 구축 |
| Compress | 요약 품질 낮으면 핵심 정보 영구 손실 | 압축 전 Write로 원본 백업, 보존 항목 명시적 지정 |
| Isolate | 서브에이전트 수만큼 토큰 비용 증가 (최대 15배) | 서브에이전트 수 제한, 태스크 분해 기준 명확화 |
실무에서 가장 흔한 실수
- Select 없이 Isolate만 쓰는 패턴: 서브에이전트마다 전체 문서를 컨텍스트에 통째로 주입한다. 에이전트 수만큼 토큰 비용이 배수로 증가한다. Isolate와 Select는 항상 세트로 적용해야 한다.
- Write를 로그로만 쓰는 패턴: 스크래치패드에 기록은 하지만, 다음 에이전트 실행 시 그 내용을 컨텍스트에 다시 읽어오지 않는다. Write는 반드시 "쓰기 → 읽기" 사이클로 완성되어야 의미가 있다.
- Compress를 너무 늦게 트리거하는 패턴: 컨텍스트 윈도우가 꽉 찬 뒤 에러로 종료되고 처음부터 재시작한다. 80% 시점에 선제적으로 컴팩션을 트리거하고, 이전에 Write로 체크포인트를 남겨야 안전하다.
마치며
컨텍스트 엔지니어링의 핵심은 "올바른 정보를 올바른 시점에 올바른 위치에 두는 것"이다. Write로 상태를 외부화하고, Select로 관련 정보만 선별하고, Compress로 윈도우 한계를 돌파하고, Isolate로 오염을 막는 — 이 4전략은 서로를 보완하며 프로덕션급 에이전트를 만드는 설계 기반이 된다.
지금 바로 시작할 수 있는 3단계:
- 진단: 현재 에이전트가 대화 히스토리를 전부 컨텍스트에 누적하고 있다면 Write + Compress 조합부터 적용하라. 스크래치패드 파일 하나만 추가해도 장기 태스크 재개 능력이 생긴다.
- 개선: RAG를 사용 중이라면 Anthropic Contextual Retrieval 방식으로 각 청크에 문서 내 위치 설명을 prepend하여 검색 오류를 줄여라. 기존 코드를 크게 바꾸지 않고도 검색 정확도가 유의미하게 올라간다.
- 확장: 단일 에이전트가 너무 많은 역할을 하고 있다면 태스크를 서브에이전트로 분리하되, 오케스트레이터에게는 요약만 반환하도록 Isolate 전략을 적용하라. 병렬 처리 효과와 컨텍스트 오염 방지 효과를 동시에 얻는다.
다음 글: MCP(Model Context Protocol) 서버 직접 구축하기 — 에이전트에 도메인 특화 컨텍스트를 실시간으로 주입하는 실전 패턴
참고 자료
- Effective context engineering for AI agents | Anthropic Engineering
- Context Engineering for Agents | LangChain Blog
- Contextual Retrieval | Anthropic
- Anthropic Multi-Agent Research System | Anthropic
- Context Management and Compaction | Claude Cookbooks DeepWiki
- Agentic Context Engineering: The Complete 2025 Guide | Sundeep Teki
- Context Engineering 101 — What We Can Learn from Anthropic | omnigeorgio
- Claude's Context Engineering Secrets | Bojie Li