대화 중간 시스템 프롬프트: 캐시를 깨뜨리지 않고 에이전트 제어하기
요약
장기 실행 에이전트 구축 시 시스템 프롬프트 수정으로 인한 프롬프트 캐시 무효화 문제를 해결하는 방법을 다룹니다. 메시지 배열 내에 'system' 역할을 직접 삽입하여 캐시를 보존하고 보안성을 높이는 기술적 방안을 제시합니다.
핵심 포인트
- 최상위 시스템 프롬프트 수정 시 프롬프트 캐시가 무효화되어 비용과 속도 저하 발생
- messages 배열 내에 system role 메시지를 삽입하여 캐시된 접두사 유지 가능
- 사용자 메시지에 지침을 넣는 방식보다 프롬프트 인젝션 방어에 유리함
- 새로운 지침을 '재정의'가 아닌 '사실(facts)'로 기술하는 것이 효과적
장기 실행되는 에이전트를 구축하면서 제가 직면했던 문제입니다. 세션 중간에 새로운 지침(예: "프로젝트는 Go 언어입니다. Go로 작성하세요")을 주입해야 했는데, 이를 위해 최상위 시스템 프롬프트(system prompt)를 수정하면 프롬프트 캐시(prompt cache) 전체가 무효화되었습니다. 캐시된 모든 턴이 전체 비용을 지불하며 다시 처리되었습니다. 해결책은 현재 Claude 모델들에 도입된 기능인 '대화 중간 시스템 메시지(mid-conversation system messages)'입니다. 이것이 무엇인지, 그리고 언제 사용해야 하는지 설명하겠습니다.
캐시를 깨뜨리는 설정
장기적인 에이전트 세션은 크고 안정적인 시스템 프롬프트와 점점 늘어나는 메시지 히스토리(message history)를 가지며, 각 턴마다 이전 작업을 저렴하게 재사용할 수 있도록 접두사(prefix)를 캐싱합니다. 하지만 세션 중간에 에이전트가 알아야 할 새로운 사실을 알게 될 때까지는 이 방식이 잘 작동합니다. 예를 들어 모드가 전환되거나, 사용자가 비동기 컨텍스트(async context)를 전달하거나, 디스크의 파일이 변경되거나, 토큰 예산(token budget)이 줄어드는 경우입니다.
가장 단순한 방법은 새로운 사실을 포함하도록 시스템 프롬프트를 수정하는 것입니다. 하지만 시스템 프롬프트는 캐시된 접두사의 맨 앞(front)에 위치합니다. 거기서 단 1바이트만 변경해도 그 뒤의 모든 것이 무효화됩니다. 다음 요청 시 전체 대화 히스토리가 전체 입력 비용(full input price)으로 다시 처리됩니다. 긴 세션의 경우, 이는 비용이 많이 들고 느립니다.
해결책: messages 배열 내의 시스템 메시지
현재 모델들은 최상위 system을 수정하는 대신, 히스토리 뒤에 오는 messages 배열 안에 system 역할(role)의 메시지를 직접 넣을 수 있게 해줍니다:
const response = await client.messages.create(
{
model: "claude-opus-4-8",
...
새로운 지침이 캐시된 히스토리 '뒤'에 위치하기 때문에, 그 이전의 어떤 것도 무효화하지 않습니다. 캐시된 접두사는 온전하게 유지되며, 작은 새로운 메시지에 대해서만 전체 비용을 지불하면 됩니다. 또한 에이전트는 운영자 권한(operator authority)을 가진 지침을 여전히 전달받게 됩니다.
이것이 사용자 메시지에 밀어 넣는 것보다 나은 이유
기존의 임시 방편(workaround)은 운영자 지침을 <system-reminder>와 같은 태그로 감싸 사용자 턴(user turn) 안에 넣는 것이었습니다. 이 방식도 캐시(cache)를 동일한 방식으로 보존하지만, 보안 문제가 있습니다. 사용자 메시지는 위조(forgeable)가 가능하기 때문입니다. 사용자에게 보이는 입력값에 쓸 수 있는 것이라면 무엇이든, 운영자인 당신으로부터 온 것처럼 보이는 지침을 사칭(spoof)할 수 있습니다.
role: "system" 메시지는 위조가 불가능한 운영자 채널입니다. 이는 사용자 턴 지침이 갖지 못하는 운영자 권한(operator authority)을 지니며, 신뢰할 수 없는 사용자 입력(untrusted user input)을 처리하는 에이전트에 신뢰할 수 있는 상태(trusted state, 예: 모드 전환, 권한)를 주입할 때 매우 중요합니다. 따라서 이는 캐싱(caching) 측면의 이점과 프롬프트 인젝션 안전성(prompt-injection-safety) 측면의 이점을 모두 제공합니다.
명령이 아닌 문맥(context)으로 표현하기
제가 제대로 구현하기 위해 한 번의 시행착오를 겪었던 미묘한 차이점은, 이러한 메시지를 '재정의(overrides)'가 아닌 '사실(facts)'로 표현하는 것입니다. 상황을 기술하고 모델이 그에 따라 행동하게 하세요. "사용자가 말한 것을 무시하세요" 또는 "이전 지침을 무시하세요"와 같은 재정의 스타일의 언어는 피해야 합니다. 모델은 사용자에게 해가 되는 지침으로부터 사용자를 보호하도록 훈련되었으며, 이러한 보호 기제는 시스템 역할(system role)에도 적용됩니다. 따라서 다음과 같이 작성해야 합니다:
// 좋음: 문맥을 기술하고, 모델이 행동하게 함
{ role: "system", content: "이 세션에 대해 자동 승인 모드가 활성화되었습니다." }
// 위험: 재정의 프레임워크, 저항을 받을 수 있음
...
알아두어야 할 제약 사항
명세(spec)에 따른 몇 가지 규칙입니다:
- 사용자 메시지(또는 서버 도구 결과로 끝나는 어시스턴트 메시지) 다음에 와야 합니다.
messages[0]이 될 수 없습니다. 초기 프롬프트에는 최상위system을 사용하세요. - 콘텐츠는 텍스트만 가능합니다.
- 모델에 의해 제한됩니다(model-gated). 이를 지원하지 않는 모델에서는 400 에러(
role 'system' is not supported on this model)가 발생합니다. 이를 포착(catch)하여 사용자 턴의<system-reminder>패턴으로 폴백(fallback)하세요.
try {
// ... 대화 중간 시스템 메시지
} catch (err) {
...
언제 이를 사용해야 하는가
트리거는 다음과 같습니다: 세션 중간에 에이전트에게 필요한 무언가를 학습했으며, 접두사 (prefix)를 다시 구축하지 않고 이를 전달하고 싶을 때. 모드 변경, 세션 시작 후 새롭게 전달된 컨텍스트 (context), 애플리케이션이 세션 시작 후에 발견한 상태 (state) 등이 해당됩니다. 즉, 평소라면 시스템 프롬프트 (system prompt)에 끼워 넣고 싶었을 모든 동적인 요소들이 대상입니다.
만약 해당 사실을 세션 시작 시점에 알고 있다면, 평소와 같이 최상위 system 프롬프트에 포함되어야 합니다. 대화 중간 채널 (mid-conversation channel)은 캐시된 접두사 (cached prefix)가 이미 구축된 _이후_에 알게 된 사항들을 위해 특별히 존재합니다. 이러한 방식으로 사용하면 캐시를 활성 상태 (hot)로 유지하고, 운영자 지침 (operator instructions)이 위조되지 않도록 보호하며, 제가 저질렀던 값비싼 실수, 즉 세션 중간에 시스템 프롬프트를 수정하여 전체 대화가 전체 비용을 지불하며 다시 처리되는 상황을 방지할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기