콘텐츠로 이동

프롬프트 조합과 KV-Cache 최적화

에이전트 시스템에서 프롬프트는 단일 문자열이 아닙니다. 여러 소스에서 동적으로 조립됩니다. 잘 설계된 프롬프트 조합 시스템은 다음 특성을 가집니다.

  • 모듈성: 각 섹션이 독립적으로 교체 가능
  • 우선순위: 컨텍스트 한계에 가까워질 때 무엇을 유지할지 결정
  • 캐시 친화성: KV-Cache 히트율을 최대화하는 순서 배치

OpenDev 연구에서 제안된 방식은 프롬프트 섹션에 **우선순위 번호(10~95)**를 부여하여 조합 순서와 컨텍스트 압박 시 생략 순서를 결정합니다.

interface PromptSection {
priority: number; // 10(필수) ~ 95(선택)
content: string;
cacheable: boolean; // KV-Cache 대상 여부
provider?: 'openai' | 'anthropic' | 'google'; // 특정 제공자 전용
}
function assemblePrompt(
sections: PromptSection[],
maxTokens: number
): string {
// 우선순위 오름차순 정렬 (낮을수록 중요)
const sorted = [...sections].sort((a, b) => a.priority - b.priority);
let result = '';
let tokenCount = 0;
for (const section of sorted) {
const sectionTokens = estimateTokens(section.content);
if (tokenCount + sectionTokens > maxTokens) {
// 우선순위 높은(숫자 큰) 섹션부터 생략
continue;
}
result += section.content + '\n\n';
tokenCount += sectionTokens;
}
return result;
}
우선순위섹션 종류설명
10시스템 역할 정의절대 생략 불가
20안전 가이드라인필수 제약 사항
30현재 태스크 설명핵심 지시
50관련 도구 목록태스크 관련 도구만
70최근 대화 히스토리압박 시 요약으로 대체
85참고 예시공간 있을 때만 포함
95상세 배경 지식선택적 보강

**KV-Cache(Key-Value Cache)**는 트랜스포머 어텐션 메커니즘에서 이미 계산한 키-값 쌍을 재사용하는 기술입니다. 같은 프롬프트 prefix가 반복될 때 재계산을 건너뜁니다.

다음은 일반 입력이 $3/100만 토큰, 캐시 읽기가 $0.30인 경우의 단가 예시입니다. 전체 요청 비용에는 출력과 최초 캐시 쓰기도 포함됩니다.

캐시 미스 토큰 비용: $3.00 / 1M tokens
캐시 히트 토큰 비용: $0.30 / 1M tokens
──────────────────────────────────────
비용 절감: 10배 (90% 감소)
// 나쁜 예: 자주 변하는 내용이 앞에 있으면 캐시 무효화
const unstablePrompt = `
현재 시각: ${new Date().toISOString()} // 매번 변함 → 캐시 미스
당신은 코드 리뷰 전문가입니다.
[안전 가이드라인 500 토큰...]
[코드베이스 컨텍스트 2000 토큰...]
코드: ${userCode}
`;
// 좋은 예: 안정적인 내용을 앞에, 동적 내용을 뒤에
const cacheFriendlyPrompt = `
당신은 코드 리뷰 전문가입니다. // 캐시 가능 ✓
[안전 가이드라인 500 토큰...] // 캐시 가능 ✓
[코드베이스 컨텍스트 2000 토큰...] // 캐시 가능 ✓
현재 시각: ${new Date().toISOString()} // 동적
코드: ${userCode} // 동적
`;

대화 히스토리를 수정하지 않고 항상 끝에 추가합니다. 중간 메시지를 편집하거나 삭제하면 그 이후의 모든 캐시가 무효화됩니다.

class AppendOnlyContext {
private messages: Message[] = [];
// 메시지 추가만 허용, 수정/삭제 없음
append(message: Message): void {
this.messages = [...this.messages, message];
}
// 압축이 필요할 때: 오래된 메시지를 요약으로 대체
compactOldMessages(keepRecent: number): void {
if (this.messages.length <= keepRecent) return;
const toSummarize = this.messages.slice(0, -keepRecent);
const summary = summarize(toSummarize);
// 앞부분을 요약으로 바꾸면 이후 prefix 캐시는 다시 구성됨
this.messages = [
{ role: 'system', content: `[이전 대화 요약]: ${summary}` },
...this.messages.slice(-keepRecent),
];
}
}

Anthropic API는 cache_control 파라미터로 캐시 구간을 명시적으로 지정합니다.

const response = await anthropic.messages.create({
model: 'claude-sonnet-5-5',
max_tokens: 4096,
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: systemPrompt,
cache_control: { type: 'ephemeral' }, // 캐시 브레이크포인트
},
{
type: 'text',
text: userMessage, // 이 이후는 캐시 미적용
},
],
},
],
});

압축은 캐시 재계산 비용을 발생시키지만 긴 컨텍스트와 지연을 줄일 수 있습니다. 캐시 유지를 위해 오래된 오류나 잘못된 결론을 계속 남겨 두지 않습니다. 입력 길이·캐시 가능 최소 길이·실제 cache hit를 함께 측정합니다.

도구 목록이 바뀌면 시스템 프롬프트 캐시가 깨집니다. Manus가 사용하는 해결책은 **도구 제거 대신 로짓 마스킹(logit masking)**입니다.

모든 도구를 프롬프트에 유지하되, 디코딩 단계에서 특정 도구 호출에 해당하는 토큰의 로짓을 -∞으로 설정하여 선택 불가능하게 만듭니다. 이렇게 하면 시스템 프롬프트는 변경 없이 유지됩니다. 이는 추론 엔진 수준의 접근이 필요한 기법이며, 일반 API에서 임의 로짓 마스킹을 제공한다고 가정하지 않습니다. API가 제공하는 도구 선택·허용 기능을 쓰더라도 실제 실행 권한은 서버에서 검사합니다.

프롬프트 조합은 우선순위를 부여한 모듈 단위로 설계하고, KV-Cache를 최대화하려면 안정적인 내용을 앞에, 동적 내용을 뒤에 배치해야 합니다. 캐시 읽기 단가와 전체 비용 절감을 구분하고, 쓰기·출력·압축 비용까지 계산해야 합니다. 캐시 정책은 모델·제공자별로 다릅니다.