Long-Horizon 에이전트 비용 60~90% 줄이기: 캐싱·압축·라우팅 전략
AI 에이전트를 프로덕션에 올리고 나서 처음 청구서를 받았을 때의 충격을 아직도 기억한다. 단순 챗봇이라면 예측 가능했을 텐데, 에이전트는 달랐다. 버그 하나 고치는 데 컨텍스트가 수만 토큰씩 쌓이고, 실패하면 그 비용 그대로 날리고 처음부터 다시. 단일 실행이 예상의 열 배를 넘어가는 건 예사였다.
처음엔 나도 "싼 모델로 갈아타면 되는 거 아냐?"라고 생각했는데, 실제로 해보니 성능이 뚝 떨어지는 게 먼저 왔다. 진짜 문제는 모델 선택이 아니라 설계였다. 이 글에서는 비용이 어디서 터지는지 구조부터 파악하고, 프롬프트 캐싱·컨텍스트 압축·모델 라우팅·체크포인팅이라는 네 가지 전략을 실제 코드와 함께 살펴본다. 성능을 유지하면서 60~90% 절감이 가능한데, 그 근거와 구현 방법은 각 전략 섹션에서 구체적으로 다루겠다.
읽고 나면 오늘 당장 적용해볼 것들이 눈에 들어올 것이다. 특히 코드 한 줄로 즉각적인 효과를 내는 캐싱부터, 장기 실행 에이전트의 실패 비용을 원천 차단하는 체크포인팅까지 순서대로 짚는다.
비용이 터지는 구조
세 가지 비용 차원
Long-Horizon 에이전트는 수 분에서 수 시간, 심지어 수 일에 걸쳐 계획·추론·도구 호출·자기 수정을 반복하는 자율 시스템이다. 이 특성 때문에 비용 구조가 일반 LLM API 호출과 완전히 다르게 움직인다.
에이전트 비용을 이야기할 때 "토큰 비용"만 떠올리는 경우가 많은데, 실제로는 세 가지 차원이 얽혀 있다.
| 비용 유형 | 발생 원인 | 증가 패턴 |
|---|---|---|
| 토큰 비용 | 입력·출력 토큰 API 과금 | 선형 |
| 컨텍스트 비용 | 대화 이력·도구 출력 누적 | 비선형 증가¹ |
| 실패 비용 | 중간 실패 시 전체 재시도 | 기하급수적 |
¹ API 과금 자체는 토큰 수에 비례해 선형이다. 하지만 컨텍스트가 길어질수록 모델 내부의 어텐션 연산 복잡도가 O(n²)로 증가하고, 더 중요하게는 컨텍스트 드리프트로 인한 재시도·실패 확률이 함께 올라가 간접 비용이 비선형적으로 증가한다.
컨텍스트 비용이 특히 위험한 건, 방치하면 컨텍스트 드리프트(context drift) 가 발생하기 때문이다.
컨텍스트 드리프트 vs. 컨텍스트 소진
- 컨텍스트 소진: 컨텍스트 창이 물리적으로 꽉 차는 것. 200K 창도 결국 채울 수 있다.
- 컨텍스트 드리프트: 창이 남아 있어도 오래된 정보나 무관한 내용이 쌓이면서 모델의 추론 품질이 조용히 떨어지는 것.
드리프트가 훨씬 조용히 오고, 더 위험하다.
실패 비용도 간과하기 쉬운 함정이다. Trajectory Reduction 연구에 따르면 작업 시간이 두 배로 늘어나면 실패율이 네 배로 치솟는다. 에이전트 성공률이 약 35분 이후부터 눈에 띄게 하락하기 시작한다는 데이터도 있어, 단순히 컨텍스트 창을 크게 잡는 것만으로는 이 문제를 해결할 수 없다.
4가지 핵심 절감 전략
전략 1: 프롬프트 캐싱 — 가장 쉽게 얻는 절감
KV 캐시(Key-Value Cache)란 트랜스포머 모델이 GPU 메모리에 중간 어텐션 연산 결과를 저장해 재계산을 피하는 구조다. 주요 LLM 제공사들이 이 KV 캐시를 API 레벨에서 공식 지원하기 시작했다. 솔직히 이걸 안 쓰고 있다면 그냥 돈을 버리는 것이나 다름없다.
Anthropic Claude의 경우, 캐시 히트 시 입력 토큰 비용이 일반 가격 대비 90% 저렴하다($0.30/M vs. $3.00/M). OpenAI는 별도 설정 없이 자동으로 프리픽스를 캐싱하며, 캐시 히트 토큰은 일반 가격의 50%로 청구된다(평균 절감이 50%라는 의미가 아니라, 히트된 토큰의 단가가 반값이라는 뜻이다).
우리 팀에서 이걸 처음 적용했을 때, 코드 리뷰 에이전트의 일일 비용이 하루 만에 절반 이하로 떨어졌다. 구현도 놀랍도록 간단했다.
import anthropic
client = anthropic.Anthropic()
LONG_CODING_GUIDELINES = "..." # 수천 토큰짜리 코딩 가이드라인
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=[
{
"type": "text",
"text": "당신은 코드 리뷰 전문 에이전트입니다.\n" + LONG_CODING_GUIDELINES,
"cache_control": {"type": "ephemeral"} # 이 블록을 캐시로 지정
}
],
messages=[
{"role": "user", "content": f"다음 코드를 리뷰해줘:\n{user_code}"}
]
)
# 캐시 히트 여부 확인
usage = response.usage
print(f"캐시 히트 토큰: {usage.cache_read_input_tokens}")
print(f"캐시 생성 토큰: {usage.cache_creation_input_tokens}")핵심 설계 포인트: 캐시가 효과를 내려면 프롬프트를 정적 프리픽스(시스템 프롬프트, 도구 정의, 긴 문서) + 동적 파트(사용자 입력, 대화 이력) 구조로 분리해야 한다. 동적 파트가 앞에 오면 캐시가 전혀 작동하지 않는다.
캐싱으로 정적 비용을 잡았다면, 이번엔 동적으로 쌓이는 컨텍스트를 다뤄볼 차례다.
전략 2: 컨텍스트 압축 — 드리프트를 막는 핵심
장기 실행 에이전트에서 컨텍스트 창이 팽창하는 걸 막으려면 주기적인 압축이 필요하다. AgentDiet(에이전트 실행 궤적에서 불필요한 스텝을 제거하는 연구 프레임워크)는 입력 토큰 3960% 절감을 보여줬고, ACON(성공/실패 궤적을 비교해 압축 전략을 반복 개선하는 연구)도 피크 토큰 2654% 절감을 달성했다.
실무에서 가장 많이 쓰는 패턴은 롤링 요약(rolling summary) 이다.
from typing import List, Dict
import anthropic
class ContextManager:
def __init__(self, max_tokens: int = 50_000, summary_threshold: int = 40_000):
self.max_tokens = max_tokens
self.summary_threshold = summary_threshold
self.messages: List[Dict] = []
self.summary: str = ""
self.client = anthropic.Anthropic()
async def add_message(self, role: str, content: str, token_count: int):
self.messages.append({
"role": role,
"content": content,
"tokens": token_count
})
current_total = sum(m["tokens"] for m in self.messages)
if current_total > self.summary_threshold:
await self._compress_old_messages()
async def _compress_old_messages(self):
cutoff = len(self.messages) // 2
to_summarize = self.messages[:cutoff]
conversation_text = "\n".join(
f"[{m['role']}]: {m['content']}" for m in to_summarize
)
# 압축 자체는 저렴한 소형 모델로 처리 — 이게 핵심 절감 포인트
response = self.client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{
"role": "user",
"content": f"다음 대화의 핵심 정보를 간결하게 요약해줘:\n\n{conversation_text}"
}]
)
new_summary = response.content[0].text
self.summary = f"{self.summary}\n\n{new_summary}".strip()
self.messages = self.messages[cutoff:]
def get_context(self) -> List[Dict]:
context = []
if self.summary:
context.append({
"role": "system",
"content": f"[이전 대화 요약]\n{self.summary}"
})
context.extend(
{"role": m["role"], "content": m["content"]}
for m in self.messages
)
return context압축 작업 자체에도 LLM 호출 비용이 발생하기 때문에, 요약에는 비싼 프론티어 모델 대신 저렴한 소형 모델을 쓰는 게 핵심이다. 이 패턴 하나로 컨텍스트 관련 비용이 눈에 띄게 줄어든다.
컨텍스트 압축으로 쌓이는 양을 잡았다면, 이번엔 각 작업에 맞는 모델을 골라 쓰는 방법으로 넘어가보자.
전략 3: 모델 라우팅 — 가장 큰 폭의 비용 절감
처음엔 "모든 작업에 Claude Opus나 GPT-4 쓰면 되는 거 아냐?"라고 생각했는데, 이게 생각보다 꽤 비싼 착각이다. 프로덕션 운영 경험을 보면 기업 쿼리의 약 85%는 저가 모델로 충분히 처리 가능하다. 단순 요약, 번역, 형식 변환에 Opus를 쓰는 건 버스를 택시 요금으로 타는 격이다.
from enum import Enum
class TaskComplexity(Enum):
SIMPLE = "simple" # Haiku급 — 번역, 요약, 형식 변환
MODERATE = "moderate" # Sonnet급 — 일반 분석, 코드 설명
COMPLEX = "complex" # Opus급 — 아키텍처 설계, 보안 분석
class ModelRouter:
ROUTING_RULES = {
TaskComplexity.SIMPLE: "claude-haiku-4-5-20251001",
TaskComplexity.MODERATE: "claude-sonnet-4-6",
TaskComplexity.COMPLEX: "claude-opus-4-8",
}
def classify_task(self, task: str, context_tokens: int) -> TaskComplexity:
# 컨텍스트가 크면 복잡한 작업일 가능성 높음
if context_tokens > 30_000:
return TaskComplexity.COMPLEX
# 키워드 기반 복잡도 판단 (규모가 크면 별도 분류 모델 권장)
complex_signals = ["아키텍처 설계", "보안 취약점", "성능 최적화", "리팩토링"]
simple_signals = ["번역", "요약", "형식 변환", "단순 질문"]
if any(signal in task for signal in complex_signals):
return TaskComplexity.COMPLEX
if any(signal in task for signal in simple_signals):
return TaskComplexity.SIMPLE
return TaskComplexity.MODERATE
def get_model(self, task: str, context_tokens: int) -> str:
complexity = self.classify_task(task, context_tokens)
return self.ROUTING_RULES[complexity]계층적 멀티에이전트 구조로 확장하면 더 강력하다. 오케스트레이터(전략 결정)는 대형 모델, 워커(실제 실행)는 소형 모델을 쓰는 방식이다. OPTIMA 연구(멀티에이전트 대화 궤적 최적화 훈련 프레임워크)에서 97.7% 성능을 유지하면서 대규모 비용 절감을 달성한 결과가 보고됐다.
라우팅 로직 구현이 번거롭게 느껴지더라도, 규모가 커질수록 ROI가 압도적이다. 모델 선택이 정리됐다면, 마지막으로 실패 비용을 원천 차단하는 전략으로 넘어가보자.
전략 4: 체크포인팅 — 실패 비용을 막는 안전망
장기 실행 에이전트에서 가장 가슴 아픈 상황이 있다. 40분 돌린 작업이 막판에 실패해서 처음부터 다시 시작해야 할 때다. 체크포인팅은 이 상황을 방지하는 필수 인프라다.
interface AgentCheckpoint {
taskId: string;
step: number;
state: Record<string, unknown>;
completedActions: string[];
timestamp: number;
}
const MAX_STEPS = 100;
class CheckpointedAgent {
private checkpointInterval = 5; // 5스텝마다 저장
async execute(task: string, taskId: string): Promise<void> {
const checkpoint = await this.loadCheckpoint(taskId);
const startStep = checkpoint?.step ?? 0;
let state: Record<string, unknown> = checkpoint?.state ?? this.initState(task);
console.log(`스텝 ${startStep}부터 재개`);
for (let step = startStep; step < MAX_STEPS; step++) {
state = await this.executeStep(step, state);
if (step % this.checkpointInterval === 0) {
await this.saveCheckpoint({
taskId,
step: step + 1,
state,
completedActions: (state.actions as string[]) ?? [],
timestamp: Date.now()
});
}
if (this.isComplete(state)) break;
}
}
private async saveCheckpoint(cp: AgentCheckpoint): Promise<void> {
await redis.setex(`checkpoint:${cp.taskId}`, 3600, JSON.stringify(cp));
}
private async loadCheckpoint(taskId: string): Promise<AgentCheckpoint | null> {
const data = await redis.get(`checkpoint:${taskId}`);
return data ? JSON.parse(data) : null;
}
private initState(task: string): Record<string, unknown> {
return { task, actions: [], status: "started" };
}
private async executeStep(
step: number,
state: Record<string, unknown>
): Promise<Record<string, unknown>> {
// 실제 스텝 실행 로직 구현
return state;
}
private isComplete(state: Record<string, unknown>): boolean {
return state.status === "done";
}
}실전 적용
예시 1: 소프트웨어 엔지니어링 자동화 에이전트
코드베이스를 읽고 → 버그를 진단하고 → 수정하고 → 테스트 실행 → 실패 시 반복 수정하는 워크플로는 Long-Horizon 에이전트의 대표 사례다. 컴파일 로그, 테스트 결과가 계속 쌓이는 구조라 컨텍스트 관리가 핵심이다. 실무에서 자주 맞닥뜨리는 상황인데, 아래 패턴으로 세 가지 최적화를 동시에 적용할 수 있다.
import anthropic
from dataclasses import dataclass, field
@dataclass
class BugFixAgent:
client: anthropic.Anthropic = field(default_factory=anthropic.Anthropic)
context_manager: ContextManager = field(default_factory=ContextManager)
router: ModelRouter = field(default_factory=ModelRouter)
SYSTEM_PROMPT = """당신은 시니어 소프트웨어 엔지니어입니다.
버그를 진단하고 수정하는 전문가입니다.
항상 테스트를 작성하고 코드 품질을 유지합니다."""
async def fix_bug(self, bug_report: str, codebase_context: str) -> str:
# [최적화 1] 작업 복잡도에 따라 모델 선택 — 모델 라우팅
analysis_model = self.router.get_model("버그 분석", len(codebase_context))
analysis = await self.client.messages.create(
model=analysis_model,
max_tokens=2048,
system=[{
"type": "text",
"text": self.SYSTEM_PROMPT + "\n\n" + codebase_context,
"cache_control": {"type": "ephemeral"} # [최적화 2] 코드베이스 캐싱
}],
messages=[{
"role": "user",
"content": f"버그 리포트: {bug_report}\n\n근본 원인을 분석해줘."
}]
)
# [최적화 3] 복잡한 수정은 대형 모델로 전환
# 동일한 codebase_context → 캐시 히트 발생 (캐싱 효과 재활용)
fix = await self.client.messages.create(
model="claude-opus-4-8",
max_tokens=4096,
system=[{
"type": "text",
"text": self.SYSTEM_PROMPT + "\n\n" + codebase_context,
"cache_control": {"type": "ephemeral"}
}],
messages=[
{"role": "user", "content": f"버그 리포트: {bug_report}"},
{"role": "assistant", "content": analysis.content[0].text},
{"role": "user", "content": "위 분석을 바탕으로 수정 코드를 작성해줘."}
]
)
return fix.content[0].text세 가지 최적화가 어떻게 맞물리는지 정리하면:
| 최적화 포인트 | 코드 위치 | 절감 메커니즘 |
|---|---|---|
| 시스템 프롬프트 캐싱 | cache_control: ephemeral |
반복 호출 시 90% 비용 절감 |
| 코드베이스 컨텍스트 캐싱 | 분석→수정 단계 동일 시스템 | 두 번째 호출도 캐시 히트 |
| 단계별 모델 라우팅 | analysis_model vs claude-opus-4-8 |
분석은 소형, 수정은 대형 모델 |
예시 2: 딥 리서치 에이전트
웹 검색 → 문서 읽기 → 합성 보고서 작성 워크플로에서 반복되는 검색 결과를 어떻게 관리하느냐가 비용 차이를 만든다. 1,000회/일 규모의 자동화 작업에서 컨텍스트 최적화만으로 일일 비용을 $150에서 $45로 절감($38,000/년 절약)한 사례가 있다.
from typing import AsyncGenerator
import anthropic
class ResearchAgent:
def __init__(self):
self.client = anthropic.Anthropic() # 명시적으로 클래스 레벨에서 초기화
self.context_manager = ContextManager(
max_tokens=80_000,
summary_threshold=60_000
)
async def research(self, query: str) -> AsyncGenerator[str, None]:
search_results = []
async for result in self._search(query):
# 검색 결과 즉시 요약 — 원문 대신 요약본만 컨텍스트에 저장
summary = await self._summarize_result(result, query)
search_results.append(summary)
# 토큰 수 추정 — 영어 기준 근사값
# 한국어는 공백 기준 단어 수와 실제 토큰 수 차이가 크므로
# tiktoken 또는 API 토큰 카운터 사용을 권장
estimated_tokens = int(len(summary.split()) * 1.5)
await self.context_manager.add_message(
role="tool",
content=f"[검색 결과 요약]\n{summary}",
token_count=estimated_tokens
)
yield f"진행 중: {len(search_results)}개 결과 수집"
# 최종 보고서는 대형 모델로
report = await self._synthesize(
query=query,
context=self.context_manager.get_context()
)
yield report
async def _summarize_result(self, result: str, query: str) -> str:
# self.client 사용 — 전역 변수 아님
response = self.client.messages.create(
model="claude-haiku-4-5-20251001", # 저렴한 소형 모델로 요약
max_tokens=256,
messages=[{
"role": "user",
"content": f"쿼리 '{query}'와 관련된 핵심만 3문장으로 요약:\n{result[:2000]}"
}]
)
return response.content[0].text
async def _search(self, query: str) -> AsyncGenerator[str, None]:
# 실제 검색 로직 구현
pass
async def _synthesize(self, query: str, context: list) -> str:
# 최종 보고서 생성 로직 구현
pass장단점 분석
각 전략은 구현 난이도가 다르고, 효과가 나타나는 시점도 다르다. 어디서부터 시작할지 결정하는 데 이 표가 도움이 될 것이다.
전략별 ROI 비교
| 전략 | 구현 난이도 | 즉시 효과 | 절감 규모 |
|---|---|---|---|
| 프롬프트 캐싱 | 낮음 (코드 한 줄) | 즉각적 | 캐시 히트 토큰의 50~90% |
| 컨텍스트 압축 | 중간 | 누적될수록 효과 증가 | 입력 토큰 39~60% |
| 모델 라우팅 | 중간~높음 | 즉각적 | 전체 비용 60~90% |
| 체크포인팅 | 높음 | 장기 실행 시 효과 | 재시도 비용 전체 |
구현 시 트레이드오프
| 항목 | 리스크 | 대응 방안 |
|---|---|---|
| 캐시 미스 | 동적 파트가 정적 부분 앞에 오면 캐시가 전혀 작동 안 함 | 프롬프트 구조를 정적→동적 순서로 강제 설계 |
| 압축 시 정보 손실 | 압축률 높일수록 중요한 컨텍스트 유실 위험 | ACON 방식처럼 성공/실패 궤적 비교로 압축 품질 검증 |
| 라우팅 오류 | 복잡한 작업을 소형 모델로 보내면 품질 급락 | 분류 신뢰도 낮은 경우 상위 모델로 폴백 처리 |
| 비용 비예측성 | 분기 로직·재시도로 토큰 사용량이 비선형 증가 | 토큰 예산 상한선과 자동 중단 안전장치 필수 |
| 상태 직렬화 어려움 | 복잡한 에이전트 상태를 체크포인트로 저장하기 어려운 경우 존재 | 처음부터 직렬화 가능한 상태 구조로 설계 |
이 네 가지 전략은 독립적으로도 효과가 있지만, 함께 적용하면 효과가 곱으로 커진다. 다만 동시에 다 구현하려다 보면 복잡도가 올라가니, 캐싱 → 압축 → 라우팅 → 체크포인팅 순서로 하나씩 적용해보는 게 현실적이다.
실무에서 가장 흔한 실수
-
캐시 구조를 고려하지 않은 프롬프트 설계: 사용자 입력을 시스템 프롬프트 앞에 붙이는 구조는 캐시를 완전히 무력화한다. 캐시 히트율이 0%라면 거의 항상 이 문제다. 프롬프트 설계 단계에서 정적/동적 분리를 처음부터 고려하는 것이 좋다.
-
압축 없이 컨텍스트를 무한 확장하는 전략: "어차피 컨텍스트 창 200K니까 괜찮겠지"는 드리프트와 비용 폭탄 모두를 부른다. 35분 임계점 이후 성능 저하가 시작된다는 점을 기억할 필요가 있다.
-
모든 작업에 프론티어 모델 적용: 단순 요약, 번역, 형식 변환에 Opus를 쓰는 건 버스를 택시 요금으로 타는 격이다. 라우팅 로직 구현이 번거롭게 느껴지더라도, 규모가 커질수록 ROI가 압도적이다.
마치며
싼 모델이 아니라, 언제 비싼 모델을 안 쓸지를 설계하는 것이 Long-Horizon 에이전트 비용 최적화의 핵심이다.
지금 바로 시작해볼 수 있는 3단계:
-
현재 캐시 히트율부터 측정: Anthropic이라면
response.usage.cache_read_input_tokens를, OpenAI라면usage.prompt_tokens_details.cached_tokens를 로깅해볼 수 있다. 히트율이 0%라면 프롬프트 구조 문제일 가능성이 높다. -
cache_control한 줄 추가: 시스템 프롬프트와 정적 컨텍스트(문서, 코드베이스)를cache_control: {"type": "ephemeral"}로 감싸보는 것을 권장한다. 코드 한 줄로 반복 호출 비용을 최대 90% 줄일 수 있는, 투자 대비 효과가 가장 좋은 첫 번째 단계다. -
롤링 요약 붙이기: 컨텍스트가 특정 임계값(예: 40K 토큰)을 넘으면 오래된 메시지를 소형 모델로 압축하는
ContextManager를 붙여볼 수 있다. 위에서 소개한 패턴을 기반으로, 우선 가장 긴 실행 세션 하나에 적용해 비용 변화를 확인해보는 게 좋다.
참고 자료
- ACON: Optimizing Context Compression for Long-horizon LLM Agents | arXiv
- Don't Break the Cache: An Evaluation of Prompt Caching for Long-Horizon Agentic Tasks | arXiv
- Reducing Cost of LLM Agents with Trajectory Reduction | arXiv
- Long-Horizon Agents Are Here. Full Autopilot Isn't | DEV Community
- Long-horizon agents explained: Hype, reality, engineering lessons | EPAM Insights
- Context Engineering in 2025: The Complete Guide to AI Agent Optimization | Mem0 Blog
- AI Agent Context Compression: Strategies for Long-Running Sessions | Zylos Research
- Prompt Caching for Anthropic and OpenAI Models | DigitalOcean Blog
- LangChain Cost Optimization: Agent Execution Cost Analysis and Reduction
- Long Horizon Document Agents | LlamaIndex Blog
- Context Window Management Strategies for Long-Context AI Agents | Maxim AI
- Long-Running AI Agents and Task Decomposition 2026 | Zylos Research