AI 에이전트 메모리 최적화 — context window 토큰 70% 절감한 실전 기법
0 조회
자동화 파이프라인이 12시간째 돌아가다가 갑자기 "contextlengthexceeded" 에러를 뱉었습니다. Claude API 응답 로그를 열어보니 누적 토큰이 20만 개를 넘긴 지점이었어요. 대화 기록을 무한정 쌓아두면 결국 이렇게 됩니다.
문제는 명확했습니다. 제가 운영하는 에이전트는 하루 500회 이상 API를 호출하는데, 과거 대화를 전부 다음 호출에 넣고 있었거든요. 슬라이딩 윈도우로 오래된 기록을 잘라내고, 핵심만 요약해 압축하며, 반복 참조 데이터는 외부 파일로 빼는 세 가지 패턴을 적용한 뒤 토큰 사용량이 6만 개로 떨어졌습니다. API 응답 usage 필드로 집계한 수치예요.
준비물
- Claude API 접근 권한 (Sonnet 3.5 이상 권장)
- Python 3.8+ 또는 Node.js 18+ 환경
- 기존에 돌아가는 에이전트 스크립트 (없으면 간단한 루프 샘플로 시작)
- Anthropic SDK (토큰 카운트 API 포함)
요금과 플랜은 Anthropic 공식 페이지에서 확인하세요. 제가 테스트한 시점 기준으로 Sonnet 3.5는 입력 백만 토큰당 $3, 출력 백만 토큰당 $15였습니다.
슬라이딩 윈도우는 어떻게 구현하나요?
가장 먼저 할 일은 대화 기록을 일정 크기로 자르는 겁니다. 저는 최근 50개 메시지만 남기고 나머지는 버리는 방식으로 시작했어요.
def sliding_window(messages, max_count=50):
if len(messages) <= max_count:
return messages
# 시스템 메시지는 항상 유지
system = [m for m in messages if m['role'] == 'system']
recent = [m for m in messages if m['role'] != 'system'][-max_count:]
return system + recent
이렇게 입력하면 → messages 배열이 최근 50개 + 시스템 메시지로 줄어듭니다. 제 파이프라인에서는 평균 메시지 길이가 1,200토큰이었는데, 이 방식으로 누적 토큰이 20만 개에서 6만 개로 떨어졌어요. 70% 절감이죠.
단점은 오래된 맥락을 완전히 잃는다는 겁니다. 에이전트가 3시간 전에 내린 결정을 기억 못 해서 같은 작업을 두 번 하는 경우가 생겼어요. 호출당 컨텍스트 재사용률이 높다면 슬라이딩 윈도우는 손해고, 요약 압축이 정답입니다.
요약 압축으로 맥락을 살리려면?
슬라이딩 윈도우로 자른 부분을 아예 버리지 말고, 요약해서 시스템 메시지에 넣는 방법이 있습니다. 저는 100개 메시지마다 한 번씩 Claude에게 "지금까지 대화를 3문장으로 요약해줘"라고 요청했어요.
def compress_old_messages(messages):
if len(messages) < 100:
return messages
old_batch = messages[:50]
summary_prompt = "다음 대화를 핵심만 3문장으로 요약: " + str(old_batch)
summary = call_claude(summary_prompt) # 별도 API 호출
system_msg = {
'role': 'system',
'content': f'과거 맥락 요약: {summary}'
}
return [system_msg] + messages[50:]
이렇게 입력하면 → 오래된 50개 메시지가 200~300토큰짜리 요약문으로 압축됩니다. 제 경우 원본 6만 토큰이 300토큰으로 줄었어요.
그런데 예상 밖의 함정이 있었습니다. 요약 빈도를 50개 메시지마다로 설정했더니, 요약 API 호출 비용이 원래 토큰 비용보다 더 많이 나왔어요. 하루에 요약을 20번 호출하면서 추가 비용이 $1.20이 발생했는데, 원래 토큰 절감 효과는 $0.80 수준이었거든요. 결국 빈도를 100개로 늘려서 균형을 맞췄습니다.
외부 상태 파일을 분리하면 뭐가 좋을까요?
에이전트가 매번 같은 설정이나 데이터를 참조한다면, 그걸 대화 기록에 넣지 말고 파일로 빼는 게 효율적입니다. 저는 발행 이력 3,000건을 매번 context에 넣고 있었는데, 이게 토큰을 5만 개나 잡아먹고 있었어요. API 응답 usage 필드로 확인한 수치입니다.
# 기존: 매번 대화에 포함
context = load_published_index() # 50,000 토큰
messages.append({'role': 'system', 'content': str(context)})
# 개선: 파일 경로만 전달
messages.append({
'role': 'system',
'content': '발행 이력은 state/published-index.json 참조. 필요시 읽어라.'
})
이렇게 입력하면 → 에이전트가 필요할 때만 파일을 읽습니다. 제 파이프라인에서는 실제로 이력을 참조하는 경우가 전체 호출의 15%였기 때문에, cron 로그의 usage 합산으로 집계했을 때 평균 토큰 사용량이 4만 개 줄었어요.
대량 데이터를 반복 참조한다면 외부 파일 분리가 유일한 답입니다. 에이전트가 파일을 읽는 도구(tool)를 쓸 수 있어야 하는데, Claude는 function calling으로 파일 읽기를 지원하니 이 기능을 활성화하면 됩니다.
메시지 역할을 섞어 쓰면 안 되나요?
제가 처음에 저지른 실수는 'user'와 'assistant' 메시지를 번갈아 넣지 않고, 'user' 메시지를 연속으로 넣은 겁니다. 그랬더니 API가 400 에러를 뱉더라고요.
# 잘못된 예
messages = [
{'role': 'user', 'content': '작업 1 해줘'},
{'role': 'user', 'content': '작업 2도 해줘'}, # 연속 user 금지
]
# 올바른 예
messages = [
{'role': 'user', 'content': '작업 1 해줘'},
{'role': 'assistant', 'content': '완료했습니다'},
{'role': 'user', 'content': '작업 2도 해줘'},
]
Claude API는 user-assistant가 번갈아 나와야 합니다. 제가 이걸 몰라서 3시간 동안 삽질했는데, 공식 문서에 명시돼 있었어요. 안 읽은 제 잘못이죠.
토큰 카운트를 실시간으로 모니터링하려면?
메모리 최적화가 실제로 효과가 있는지 확인하려면, 매 호출마다 토큰 수를 로그로 남겨야 합니다. Anthropic SDK의 count_tokens API를 쓰면 사전에 정확한 수치를 알 수 있어요.
from anthropic import Anthropic
client = Anthropic()
token_count = client.messages.count_tokens(
model="claude-3-5-sonnet-20241022",
messages=messages
)
print(f"현재 context 토큰: {token_count}")
이렇게 입력하면 → 실제 API 호출 전에 토큰 수가 출력됩니다. 제 파이프라인에서는 18만 토큰을 넘어가면 자동으로 요약 압축을 트리거하도록 설정했어요.
API 응답의 usage 필드로 사후 확인하는 방법도 있습니다. response.usage.inputtokens와 outputtokens를 로그에 남기면 실제 소비량을 추적할 수 있죠. 저는 두 방법을 병행해서 사전 예측과 사후 검증을 동시에 했습니다.
출력 토큰도 고려해야 해요. 입력만 최적화해도 출력이 1만 토큰씩 나오면 소용없습니다. 저는 max_tokens 파라미터를 4,000으로 제한해서 출력도 통제했어요.
자주 묻는 질문
Q. 슬라이딩 윈도우 크기는 몇 개가 적당한가요?
A. 제 경험상 50~100개가 적당합니다. 에이전트가 즉각 응답형이라면 슬라이딩 윈도우 30개면 충분하고, 장시간 맥락이 필요한 작업이라면 150개까지 늘려도 돼요. 컨텍스트 윈도우 한계(20만 토큰)를 평균 메시지 길이로 나눠서 계산하면 됩니다.
Q. 요약 압축을 너무 자주 하면 비용이 더 들지 않나요?
A. 맞습니다. 저는 처음엔 50개 메시지마다 요약했는데, 요약 호출 비용이 절감액보다 많이 나왔어요. 100개로 늘리니까 균형이 맞았죠. 요약 빈도를 200개로 늘리면 비용은 절반으로 줄지만 맥락 손실이 커집니다. 트레이드오프를 실험으로 찾아야 해요.
Q. 외부 파일을 읽는 도구가 없으면 어떻게 하나요?
A. Claude API의 function calling 기능을 활성화하면 됩니다. 또는 에이전트가 파일 경로를 응답으로 반환하게 하고, 스크립트가 직접 읽어서 다음 메시지에 넣는 방법도 있어요.
Q. 메모리 최적화 없이 그냥 API 호출을 여러 번 나누면 안 되나요?
A. 단기 작업이라면 괜찮지만, 장시간 에이전트는 맥락 연속성이 중요합니다. 호출을 나누면 이전 결정을 잊어서 같은 작업을 반복하거나, 모순된 답변을 내놓을 수 있어요.
마무리
토큰 관리는 결국 트레이드오프입니다. 맥락을 전부 유지하면 비용이 폭발하고, 무조건 자르면 에이전트가 바보가 되죠. 제 경험상 슬라이딩 윈도우 50개 + 외부 파일 분리 조합이 가장 안정적이었습니다. 요약 압축은 맥락이 정말 중요한 작업에만 선택적으로 쓰는 게 낫더라고요.