
장기 실행 에이전트의 컨텍스트 비대화(Context Bloat): 무엇을 유지하고, 요약하고, 버릴 것인가
요약
장기 실행 에이전트 운영 시 발생하는 컨텍스트 비대화(Context Bloat) 문제와 그 해결책을 다룹니다. 컨텍스트가 커짐에 따라 발생하는 지연 시간 증가와 모델 성능 저하를 방지하기 위해 효율적인 데이터 관리 전략이 필요함을 강조합니다.
핵심 포인트
- 컨텍스트 비대화는 지연 시간 증가와 모델의 추론 품질 저하를 유발함
- 도구 호출 결과(tool results)와 같은 대규모 데이터가 컨텍스트를 빠르게 점유함
- 컨텍스트 창의 약 60% 지점에서 압축 및 관리 전략을 시작해야 함
- 단순히 대화 턴(turn) 수가 아닌 실제 토큰(token) 양을 기준으로 관리해야 함
- 도서: Agents in Production — Building, Tracing, and Shipping Multi-Step AI You Can Trust
- 저자의 다른 저서: Observability for LLM Applications — The AI Engineer's Library (2권 시리즈)의 동반 도서
- 내 프로젝트: Hermes IDE | GitHub — Claude Code 및 기타 AI 코딩 도구를 사용하여 작업하는 개발자를 위한 IDE
- 나에 대하여: xgabriel.com | GitHub
당신은 에이전트에게 긴 작업을 부여합니다. 처음 6단계까지는 잘 작동합니다. 9단계에서는 지연 시간(latency)이 두 배로 늘어납니다. 14단계에서는 답변이 모호해지기 시작하고, 이미 수행했던 도구 호출(tool call)을 반복하기 시작합니다. 20단계에 이르면 요청은 컨텍스트 길이(context-length) 오류와 함께 중단되며, 당신의 온콜(on-call) 엔지니어는 새벽 2시에 읽어야 할 300KB 크기의 메시지 배열을 떠안게 됩니다.
무언가 고장 난 것이 아닙니다. 컨텍스트 창(context window)이 가득 찬 것입니다. 이것이 바로 컨텍스트 비대화(context bloat)이며, 첫 에이전트 루프(agent loop)를 연결할 때 아무도 경고해주지 않는 실패 모드입니다. 컨텍스트 창은 유한합니다. 무엇을 유지하고, 무엇을 요약하며, 무엇을 버릴지를 사전에 결정하지 않는 한, 장기 실행되는 에이전트는 매번 그 한계에 도달하게 될 것입니다.
컨텍스트가 무한히 커지는 이유
에이전트 루프의 매 턴(turn)마다 데이터가 추가됩니다. 모델이 도구를 요청하면, 당신은 이를 실행하고 그 결과를 다시 입력합니다. 그 결과는 messages 리스트에 붙여져 작업이 끝날 때까지 모든 향후 프롬프트(prompt)에 함께 실려 갑니다.
문제는 도구 결과(tool results)입니다. 검색 API나 데이터베이스 쿼리에서 오는 단일 JSON 블롭(blob) 하나만으로도 1,000 토큰이 넘을 수 있습니다. 그런 것이 10개만 쌓여도 당신의 버퍼는 어제의 40턴 대화보다 더 무거워집니다. 모델의 컨텍스트 창은 크지만, 크다고 해서 무한한 것은 아닙니다. Claude Opus 4.6은 1M 토큰을 제공합니다. 하지만 모든 단계에서 전체 이력을 그대로 끌고 간다면, 당신은 여전히 그 한계에 부딪힐 것입니다.
물리적인 한계(hard wall)에 부딪히기 전에 두 가지 문제가 먼저 발생합니다. 첫째, 지연 시간(Latency)은 입력 토큰 수에 따라 대략 선형적으로 증가하므로, 비대해진 버퍼는 매 턴(turn)을 더 느리게 만듭니다. 둘째, 긴 입력값에서는 "중간에서 길을 잃는(lost in the middle)" 효과 때문에 품질이 저하됩니다. 이는 모델이 긴 프롬프트의 중간에 위치한 정보에 제대로 주의(attention)를 기울이지 못하는 현상을 말합니다. 첫 번째 단계에서 부여했던 목표는 이제 19개의 도구 결과(tool results) 아래에 파묻혀 버리고, 모델은 그 목표보다 노이즈에 더 많은 주의를 기울이게 됩니다.
경험 법칙(rule of thumb): 컨텍스트 창(window)의 약 60% 지점부터 압축을 시작하세요. 에러가 발생할 때까지 기다리지 마십시오.
턴(turn)이 아니라 토큰을 측정하라
보통 턴 횟수에 따라 가지치기(prune)를 하려는 본능이 작용합니다. 마지막 20개의 메시지만 유지하고 나머지는 버리는 식입니다. 하지만 이러한 본능은 틀렸습니다. 왜냐하면 턴마다 크기가 다르기 때문입니다. 커다란 도구 결과(tool result)를 포함한 한 번의 턴은 짧은 채팅 10번의 턴보다 더 큰 비중을 차지합니다.
대신 토큰을 세십시오. Anthropic은 이를 위한 엔드포인트(endpoint)를 제공하며, 도구 결과(tool results)와 이미지 블록(image blocks)은 글자 수(character length)와 깔끔하게 매칭되지 않으므로 이 API를 호출할 가치가 충분합니다.
# budget.py -- pip install "anthropic==0.94.1"
from anthropic import Anthropic
...
매 모델 호출 전에 over_budget()을 호출하세요. 임계치를 넘으면 가지치기(prune)를 수행합니다. 예산(budget)은 당신의 임계값이며, 일반적인 부하 상황에서 매 턴마다 발생하는 것이 아니라 대략 10번의 턴 중 한 번 정도 가지치기가 실행되도록 튜닝해야 합니다.
제거 정책(Eviction policy) 1: 슬라이딩 윈도우 (the sliding window)
가장 단순하면서도 효과적인 정책은 마지막 N개의 메시지만 유지하고 그 이전의 모든 것은 버리는 것입니다.
KEEP_LAST = 12
def slide(messages: list[dict]) -> list[dict]:
...
이것은 단 한 줄의 실제 로직이며, 마지막 몇 번의 동작을 제외한 컨텍스트가 오히려 혼란을 주는 상태 비저장(stateless) 도구 사용(tool-use) 작업에 적합합니다. 디프(diff)를 따라가며 검토하는 코드 리뷰 에이전트(code-review agent)는 15번째 단계에 도달했을 때 3번째 단계의 정보가 필요하지 않습니다. 풀 리퀘스트(pull request)가 상태(state)이며, 마지막 몇 번의 동작만이 중요할 뿐입니다.
이러한 트레이드오프(trade-off)는 완전합니다. 20턴 전에 사용자가 말한 내용은 무엇이든 사라집니다. 대화형 어시스턴트(conversational assistant)에게 이는 버그와 같습니다. 사용자가 처음에 언급한 식단 선호도가 화면 밖으로 스크롤되어 사라지는 순간 소멸하기 때문입니다. 워크로드(workload)에 맞춰 정책을 매칭하세요. 상태가 없는 궤적(stateless trajectories)에는 슬라이딩 윈도우(sliding window)를, 대화에는 메모리(memory)가 있는 방식을 사용하십시오.
목표를 고정하여 절대 제거되지 않도록 하기
여기 두 정책 모두를 위험하게 만드는 실수가 있습니다. 만약 버퍼(buffer)의 앞부분(head)을 가지치기(prune)한다면, 원래의 지시 사항이 제거될 수 있습니다. 그러면 에이전트(agent)는 자신의 컨텍스트(context) 내에 무엇을 해야 하는지에 대한 정보가 없기 때문에 방황하게 됩니다.
목표를 고정(Pin)하십시오. 작업 명세(task statement)를 가지치기 가능한 버퍼 외부에 유지하고 매 턴마다 다시 주입(re-inject)하여, 어떤 제거 정책(eviction policy)도 목표에 손을 댈 수 없게 만드십시오.
def build_prompt(goal: str, buffer: list[dict]) -> list[dict]:
pinned = {
"role": "user",
...
목표를 유지하는 데는 수백 개의 토큰(tokens)이 비용으로 들지만, 30번째 단계에서도 자신의 목표를 여전히 알고 있는 에이전트를 얻을 수 있습니다. 하드 제약 조건(hard constraints)(예: 예산 상한선, 출력 스키마(output schema), "고객에게 절대 이메일을 보내지 말 것"이라는 규칙)에 대해서도 동일하게 적용하십시오. 작업이 끝날 때까지 살아남아야 하는 제약 조건은 가지치기 가능한 히스토리(history)에 포함되어서는 안 됩니다. 그것들은 고정(pinned)되어야 합니다.
제거 정책 2: 요약 후 삭제
대화 및 장기 연구 작업의 경우, 단순히 삭제하는 것은 정보 손실이 너무 큽니다. 대신 요약(summarize)하십시오. 마지막 몇 턴은 그대로(verbatim) 유지하고, 그보다 오래된 모든 내용은 저렴한 모델(cheap model)에 전달한 뒤, 요약본을 합성 메시지(synthetic message)로 다시 붙이십시오.
# compress.py
KEEP_VERBATIM = 6
SUMMARIZER = "claude-sonnet-4-6"
...
요약기(summarizer)는 별도의 모델 호출(model call)을 수행합니다. 압축기(compressor)는 요약기를 호출할 시점을 결정하고, 최근의 뒷부분(tail)은 그대로 유지하며, 요약본을 결합합니다.
# compress.py (continued)
def compress(messages: list[dict]) -> list[dict]:
if not over_budget(messages):
...
세 가지 요소가 자격을 얻습니다. 요약기(Summarizer)는 Claude Sonnet 4.6으로, 호출 비용이 노이즈에 묻힐 만큼 저렴하면서도 맥락을 놓치지 않을 만큼 똑똑합니다. 프롬프트는 보존해야 할 내용을 정확히 지정하므로, 요약본이 단순히 "사용자가 몇 가지 질문을 했습니다"와 같이 뭉뚱그려지는 대신 결정 사항과 선호도를 유지합니다. 그리고 재귀(Recursion) 방식은 원문 그대로의 뒷부분(verbatim tail)조차 너무 클 경우, 적합한 크기가 될 때까지 절반씩 줄여나가는 처리를 수행합니다. 이는 메인 호출이 실패하여 전체 루프를 다시 시도하는 것보다 비용이 저렴합니다.
주의할 점: 요약은 설계상 정보 손실(Lossy)이 발생합니다. 에이전트는 이전에 제공한 사실을 기억하지 못한다고 주장할 때가 있습니다. 만약 특정 사실이 압축 과정에서 반드시 살아남아야 한다면, 요약본이 이를 전달할 것이라고 믿지 마세요. 해당 정보를 격상(Promote)시키십시오. 다음 주에 필요할 정보를 기록하는 방식과 마찬가지로, 키-값 저장소(Key-value store)나 영구 메모리(Durable memory)에 기록하고 결정론적(Deterministically)으로 다시 읽어오십시오. 단기 버퍼(Short-term buffer)는 작업의 흐름을 위한 것입니다. 인덱스 카드에 적어둘 만한 내용이라면, 요약기가 지울 수 없는 어딘가에 보관해야 합니다.
항목별 결정 기준
세 가지 동사는 버퍼 내의 모든 메시지에 대해 던질 수 있는 세 가지 질문과 매칭됩니다.
- 유지(Keep): 목표(Goal), 엄격한 제약 조건(Hard constraint), 또는 마지막 몇 차례의 대화라면 원문 그대로 유지합니다. 첫 두 개의 메시지는 고정(Pin)하여 제거(Eviction) 대상이 되지 않도록 합니다.
- 요약(Summarize): 에이전트에게 여전히 필요하지만 토씨 하나 틀리지 않고 유지할 필요는 없는 맥락을 담고 있다면 요약합니다. 오래된 대화나 결정에 영향을 미친 초기 도구 실행 결과(Tool results)가 이에 해당합니다.
- 삭제(Drop): 노이즈라면 삭제합니다. 확인 메시지(Acknowledgement), 재시도된 도구 호출(Retried tool call), 아무것도 바꾸지 못한 사족 등이 해당됩니다.
이 결정은 대화 횟수(Turn count)가 아닌 토큰 예산(Token budget)을 기준으로 실행하십시오. 에러가 발생했을 때가 아니라, 컨텍스트 창(Window)의 60% 지점에서 실행하십시오. 에이전트가 왜 실행되고 있는지 결코 잊지 않도록 목표를 고정하십시오. 그렇게 하면, 장기 실행 에이전트가 컨텍스트의 한계에 부딪혀 무너지는 일을 방지할 수 있습니다.
이 내용이 유용했다면
컨텍스트 비대화 (Context bloat)는 에이전트가 데모 수준 이상의 기간 동안 실행될 때 비로소 나타나는 실패 모드 (failure modes) 중 하나이며, 그 해결책은 루프 (loop)를 어떻게 구축하고 어떻게 모니터링하는지에 달려 있습니다. _Agents in Production_은 스크래치패드 위생 (scratchpad hygiene)부터 지속 가능한 회상 (durable recall)에 이르기까지, 이러한 정책들의 이면에 있는 메모리 아키텍처 (memory architecture)를 다룹니다. _The AI Engineer's Library_의 동반서인 _Observability for LLM Applications_는 트레이싱 (tracing) 및 평가 (evals) 측면을 다루므로, 사용자가 인지하기 전에 요약 과정에서 중요한 사실 하나가 누락되는 상황을 미리 확인할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기