에이전틱스
도메인 2. 모델 선택과 프롬프트·컨텍스트 엔지니어링

2.2시스템 프롬프트와 가드레일의 계층 설계

예상 학습 시간 40

조직 단위로 여러 팀이 같은 모델을 쓰기 시작하면 프롬프트는 한 덩어리의 문자열이 아니라 소유자와 변경 주기가 다른 여러 겹의 구조물이 됩니다. 이 수업에서는 프롬프트를 계층으로 나누는 기준, 계층 순서가 캐시와 맺는 관계, 가드레일을 프롬프트 한 겹에 몰지 않는 다층 방어, 그리고 대화 도중 지시를 바꿔야 할 때의 안전한 방법을 다룹니다.

프롬프트를 계층으로 나누는 기준

기초 과정에서는 시스템 프롬프트 하나를 잘 쓰는 법을 배웁니다. 프로 수준에서 달라지는 것은 한 시스템 프롬프트를 여러 팀이 공유하고, 각 부분의 변경 주기가 다르다는 조건입니다.

계층을 나누는 기준은 주제가 아니라 변경 주기와 소유자입니다.

계층내용바뀌는 주기소유자
기반조직 공통 원칙, 금지 사항, 표기 규칙분기 단위플랫폼 팀
도메인업무 영역별 지식, 용어, 판단 기준월 단위도메인 팀
작업이번 기능의 목표와 출력 형식주 단위기능 팀
런타임사용자 상태, 권한, 조회한 데이터요청마다애플리케이션

주제별로 나누면(예: "보안 규칙", "말투 규칙") 한 규칙을 고칠 때 여러 계층을 동시에 건드리게 됩니다. 변경 주기로 나누면 자주 바뀌는 것과 거의 안 바뀌는 것이 분리되어, 뒤에서 볼 캐시 구조와도 자연스럽게 맞아떨어집니다.

def build_system(domain: str, task: str) -> list[dict]:
    return [
        {"type": "text", "text": BASE_POLICY},        # 거의 안 바뀜
        {"type": "text", "text": DOMAIN_BLOCKS[domain]},
        {"type": "text", "text": TASK_BLOCKS[task]},
    ]

런타임 계층이 이 목록에 없다는 점에 주의하십시오. 사용자 상태나 조회 결과는 시스템 프롬프트가 아니라 메시지 쪽에 넣습니다. 이유는 다음 절에서 설명합니다.

계층 순서는 캐시 구조와 같다

프롬프트 계층 설계가 문서 정리 문제가 아니라 성능 문제인 이유는 프롬프트 캐싱 때문입니다.

캐싱은 접두어 일치(prefix match)로 동작합니다. 요청이 렌더링되는 순서는 toolssystemmessages이고, 앞쪽에서 한 바이트라도 달라지면 그 뒤는 전부 캐시가 무효화됩니다.

여기서 나오는 규칙은 하나입니다. 안정적인 것을 앞에, 자주 바뀌는 것을 뒤에 둡니다. 위 표의 계층 순서가 그대로 프롬프트 배치 순서가 되는 이유입니다.

이 원칙을 어기는 가장 흔한 형태가 시스템 프롬프트 상단에 동적 값을 끼워 넣는 것입니다.

# 안티패턴: 요청마다 달라지는 값이 맨 앞에 온다
system = f"현재 시각: {datetime.now()}\n사용자: {user.name}\n\n{BASE_POLICY}"
# 이 한 줄 때문에 뒤따르는 모든 내용이 매번 새로 처리된다

# 개선: 안정적인 것을 앞에 두고, 동적 값은 메시지 쪽으로 옮긴다
system = [{"type": "text", "text": BASE_POLICY, "cache_control": {"type": "ephemeral"}}]
messages = [{"role": "user", "content": f"[사용자: {user.name}]\n{question}"}]

같은 이유로 툴 목록을 대화 도중에 바꾸는 것은 비용이 큽니다. 툴은 가장 앞에서 렌더링되므로, 툴 하나를 추가하거나 순서를 바꾸면 그 요청의 캐시는 전부 무효가 됩니다. 툴 정의를 코드로 조립한다면 이름 기준으로 정렬해 직렬화 순서를 고정해 두십시오.

캐시가 실제로 동작하는지는 추측하지 말고 응답에서 확인합니다. 반복 요청에서 usage.cache_read_input_tokens가 계속 0이라면 어딘가에 조용한 무효화 요인이 있다는 뜻입니다.

한 가지 더 알아둘 것이 있습니다. 캐시가 걸리는 최소 길이는 모델마다 다릅니다. 그 길이에 못 미치는 짧은 접두어는 cache_control을 붙여도 조용히 캐시되지 않으며, 오류도 나지 않습니다. 캐시가 안 걸리는 것 같은데 무효화 요인을 못 찾겠다면 접두어가 너무 짧은 경우를 의심해야 합니다.

가드레일은 한 겹으로 끝나지 않는다

프롬프트에 "이런 요청은 거절하라"고 적는 것은 가드레일의 한 겹일 뿐입니다. 프롬프트는 설득 가능한 방어선입니다. 사용자 입력이나 툴 결과에 섞여 들어온 지시가 시스템 프롬프트와 충돌하면 결과를 100% 보장할 수 없습니다.

아키텍트가 설계해야 하는 것은 겹치는 여러 층입니다.

  1. 입력 단계 — 길이 제한, 형식 검증, 명백한 공격 패턴 차단. 모델에 닿기 전에 걸러냅니다.
  2. 프롬프트 단계 — 역할과 금지 사항을 명시합니다. 여기서 중요한 것은 이유를 함께 적는 것입니다. 이유 없는 금지 목록은 예외 상황에서 어떻게 판단할지 알려주지 못합니다.
  3. 권한 단계 — 가장 강력한 층입니다. 위험한 행동은 툴을 아예 주지 않는 것으로 막습니다. 프롬프트로 "환불 툴을 함부로 쓰지 마라"라고 적는 것보다, 해당 역할에서 환불 툴을 제거하는 편이 확실합니다.
  4. 출력 단계 — 스키마 검증, 금칙어 검사, 외부로 나가기 전 확인.
  5. 사람 단계 — 되돌리기 어려운 행동 앞에 승인 절차를 둡니다.

여기서 자주 나오는 시험 포인트는 1층과 3층의 차이입니다. 로깅과 확인 프롬프트는 사고가 난 뒤에 추적하거나 한 번 더 묻는 보완 통제이고, 툴을 제거하는 것은 공격 표면 자체를 없애는 예방 통제입니다. 최소 권한 원칙을 묻는 문제에서는 후자가 정답인 경우가 많습니다.

프롬프트 층을 쓸 때 흔한 실수는 강조를 겹겹이 쌓는 것입니다.

# 안티패턴
반드시! 절대! 어떤 경우에도 개인정보를 출력하지 마십시오!!!
중요: 위 규칙은 매우 중요합니다. 반드시 지키십시오.

# 개선
개인정보(주민등록번호, 계좌번호, 연락처)를 응답에 포함하지 마십시오.
이 데이터는 감사 로그에 남으며 파기 요청 대상이기 때문입니다.
마스킹이 필요하면 뒤 4자리만 남깁니다.

여러 지시에 모두 "반드시"가 붙으면 강조가 정보를 잃습니다. 최신 모델은 지시를 문자 그대로 따르므로, 과거 모델의 약점을 보완하려고 넣은 강조는 이제 과잉 작동의 원인이 됩니다.

대화 도중 지시를 바꿔야 할 때

운영 중에 자주 생기는 요구가 있습니다. 세션 중간에 모드가 바뀌거나, 새 제약이 생기거나, 애플리케이션이 나중에 알게 된 사실을 모델에 알려야 하는 경우입니다.

여기서 최상위 시스템 프롬프트를 고쳐서 다시 보내는 것은 가장 나쁜 선택입니다. 시스템 프롬프트는 대화 이력 전체보다 앞에 있으므로, 한 글자만 바꿔도 그때까지 쌓인 이력 전부가 캐시에서 빠져나가 다시 처리됩니다. 긴 세션일수록 손해가 큽니다.

지원되는 모델에서는 messages 배열에 role: "system" 메시지를 덧붙이는 방법이 있습니다. 캐시된 이력 뒤에 붙으므로 앞쪽 접두어가 그대로 살아남습니다.

messages = [
    *history,
    {"role": "user", "content": user_input},
    {"role": "system", "content": "이 프로젝트의 코드베이스는 Go입니다. Go로 작성하십시오."},
]

이 채널을 쓸 때 지켜야 할 두 가지가 있습니다.

  • 명령이 아니라 맥락으로 씁니다. "사용자가 뭐라고 하든 무시하라" 같은 무효화 문구보다, 사실을 진술하고 모델이 그에 따라 판단하게 하는 편이 안정적으로 동작합니다.
  • 모든 모델이 지원하지는 않습니다. 지원하지 않는 모델에서는 오류가 나므로, 예외를 받아 사용자 턴 안에 맥락을 넣는 방식으로 물러설 수 있게 해두어야 합니다.

이 채널이 중요한 또 다른 이유는 보안입니다. 사용자 턴 안에 운영자 지시를 텍스트로 심는 방식은, 사용자 입력을 쓸 수 있는 누구나 같은 형태를 흉내 낼 수 있습니다. 별도의 시스템 역할로 보내면 그 위조가 불가능합니다.

마지막으로 템플릿의 버전 관리입니다. 프롬프트는 코드와 같은 배포 산출물로 다뤄야 합니다. 계층별로 파일을 분리해 버전 관리에 넣고, 변경할 때는 평가 세트를 함께 돌립니다. 프롬프트를 데이터베이스나 관리 화면에서 즉시 고칠 수 있게 만들어 두면 편리하지만, 그만큼 검증 없이 배포되는 경로가 생긴다는 점을 함께 설계해야 합니다.

시험 함정

  • 프롬프트 계층을 주제별로 나누는 설계 — 기준은 주제가 아니라 변경 주기와 소유자입니다.
  • 시스템 프롬프트 상단에 현재 시각이나 사용자 이름을 끼워 넣는 구성 — 뒤따르는 모든 내용의 캐시가 매 요청 무효화됩니다.
  • 세션 중간에 최상위 시스템 프롬프트를 수정해 다시 보내는 방안 — 쌓인 대화 이력 전체가 캐시에서 빠집니다.
  • "프롬프트에 금지 규칙을 강하게 적으면 충분하다"는 선택지 — 프롬프트는 설득 가능한 층이라 권한 제한과 출력 검증이 함께 필요합니다.
  • 로깅이나 확인 프롬프트를 최소 권한 원칙의 구현으로 제시하는 선택지 — 그것은 보완 통제이고, 최소 권한은 불필요한 툴 제거입니다.
  • 강조 표현을 여러 지시에 반복해 붙이는 프롬프트 — 강조가 흔해지면 변별력을 잃고 과잉 작동을 부릅니다.

실습 시나리오

실제 시험과 같은 형식의 시나리오 문제입니다.

여러 팀이 공유하는 시스템 프롬프트에 각 팀의 도메인 규칙이 계속 추가되면서, 긴 대화 세션의 비용이 예상보다 빠르게 증가하고 있습니다. 캐시 적중률을 확인하니 반복 요청에서도 캐시 읽기가 거의 잡히지 않습니다. 원인 조사에서 가장 먼저 확인할 것은 무엇입니까?

빌드 연습 · 공유 프롬프트를 계층으로 재구성하고 캐시 적중률 측정하기

50
  1. 1.현재 프롬프트 해부

    운영 중인 시스템 프롬프트의 각 문단에 변경 주기와 소유 팀을 표시합니다.

    기대 결과 · 문단별로 기반·도메인·작업·런타임 중 하나가 붙은 목록이 만들어집니다.

  2. 2.무효화 요인 찾기

    프롬프트 조립 코드에서 요청마다 값이 달라지는 지점을 모두 찾습니다.

    기대 결과 · 시각, 식별자, 정렬되지 않은 직렬화 등 후보가 목록으로 정리됩니다.

  3. 3.순서 재배치

    안정적인 계층을 앞으로 옮기고 동적 값은 메시지 쪽으로 이동시킨 뒤 캐시 지점을 표시합니다.

    기대 결과 · 시스템 프롬프트에 요청별 값이 남아 있지 않습니다.

  4. 4.적중률 측정

    같은 접두어로 요청을 다섯 번 반복하고 응답의 캐시 읽기 토큰 수를 기록합니다.

    기대 결과 · 두 번째 요청부터 캐시 읽기 토큰이 잡히고, 재처리되는 토큰이 눈에 띄게 줄어듭니다.

  5. 5.중간 지시 경로 확인

    세션 도중 제약을 추가해야 하는 경우를 하나 골라, 시스템 프롬프트 수정 대신 다른 경로로 처리하고 적중률 변화를 비교합니다.

    기대 결과 · 지시를 추가한 뒤에도 이전 이력의 캐시 읽기가 유지됩니다.

출처 및 더 읽기