프롬프트 모드 설계 — Full / Task / Minimal / None으로 토큰 조절하기

🌐 English

잘 만든 에이전트의 시스템 프롬프트는 대개 큽니다. 페르소나, 응답 스타일, 툴 사용 규약, 스킬 목록, 메모리 사용법, 팀 협업 규칙, 보안 경계... 사용자와 대화하는 메인 에이전트에게는 이게 다 필요합니다.

그런데 같은 에이전트가 다른 상황에서도 도는 게 문제입니다. 5분마다 "새 메일 있나?"만 확인하는 하트비트 작업에 2만 토큰짜리 페르소나가 필요할까요? "이 파일 요약해"만 하고 죽는 서브에이전트에게 팀 협업 규칙이 필요할까요?

필요 없습니다. 그런데 대부분의 구현은 프롬프트가 하나뿐이라 그걸 매번 보냅니다. 이 글은 프롬프트를 상황별 크기 단계로 나누는 설계입니다.

네 단계로 나누는 이유

실전에서 수렴한 모드는 넷입니다.

  • Full — 사용자와 직접 대화하는 메인 에이전트. 모든 섹션.
  • Task — 자동화 작업. 성격은 덜 필요하지만 일은 제대로 해야 하므로 툴·스킬 관련은 남긴다.
  • Minimal — 주기 점검, 단순 반복 작업. 최소한만.
  • None — 정체성 한 줄. 순수 변환 작업(번역, 분류)에서 프롬프트가 오히려 방해될 때.

두 단계(full/minimal)로 시작했다가 Task가 나중에 끼어드는 경우가 많습니다. minimal은 성격을 없애려고 만들었는데, 그러다 보니 스킬 검색과 실행 지향 지시까지 같이 사라져 작업 에이전트가 "하겠습니다"만 하고 아무것도 안 하는 문제가 생깁니다. Task는 그 사이를 메웁니다.

모드를 순서가 있는 값으로 다루기

모드를 문자열로만 두면 "이 둘 중 더 제한적인 건?"을 물을 수 없습니다. 순위를 부여하세요.

type PromptMode string

const (

    PromptFull    PromptMode = "full"    // 메인 에이전트 — 전 섹션

    PromptTask    PromptMode = "task"    // 자동화 — 가볍지만 유능하게

    PromptMinimal PromptMode = "minimal" // 서브에이전트·주기작업 — 축소

    PromptNone    PromptMode = "none"    // 정체성 한 줄만

)

var modeRank = map[PromptMode]int{

    PromptFull: 3, PromptTask: 2, PromptMinimal: 1, PromptNone: 0,

}

// 둘 중 더 제한적인 모드를 고른다

func minMode(a, b PromptMode) PromptMode {

    if modeRank[a] <= modeRank[b] {

        return a

    }

    return b

}

minMode가 왜 필요하냐면, 모드를 정하는 주체가 여럿이기 때문입니다. 에이전트 설정이 minimal인데 실행 종류가 task를 요구할 때, 둘 중 더 제한적인 쪽을 따라야 합니다. 설정이 "가볍게 써라"라고 했으면 실행 종류가 그걸 뒤집어 더 무겁게 만들면 안 되니까요.

모드 결정: 4계층 해석

실제 결정은 여러 소스를 우선순위대로 훑어 이뤄집니다.

// 우선순위: 런타임 지정 > 실행 종류 자동판별 > 에이전트 설정 > 기본값

func resolvePromptMode(runtimeOverride PromptMode, sessionKey string,

    configMode PromptMode) PromptMode {

    // 1계층: 호출자가 명시했으면 무조건 그것

    if runtimeOverride != "" {

        return runtimeOverride

    }

    // 2a계층: 주기 점검 — 단순 확인이니 minimal로 묶는다

    if isHeartbeatSession(sessionKey) {

        if configMode != "" {

            return minMode(configMode, PromptMinimal)

        }

        return PromptMinimal

    }

    // 2b계층: 서브에이전트·예약작업 — task를 상한으로 둔다.

    //   메모리는 슬림하게, 스킬 검색과 실행 지향 지시는 남겨야 일을 한다.

    if isSubagentSession(sessionKey) || isCronSession(sessionKey) {

        if configMode != "" {

            return minMode(configMode, PromptTask)

        }

        return PromptTask

    }

    // 3계층: 에이전트별 설정

    if configMode != "" {

        return configMode

    }

    // 4계층: 기본값

    return PromptFull

}

핵심은 2계층이 "강제"가 아니라 "상한"이라는 점입니다. minMode를 쓰기 때문에, 설정이 이미 더 제한적이면 그걸 존중합니다. 자동 판별이 설정을 덮어써서 더 무겁게 만드는 일은 절대 없습니다. 이 방향성이 중요합니다 — 자동 규칙은 항상 줄이는 쪽으로만 작동해야 예측 가능합니다.

섹션 게이팅 — 플래그 네 개로 조립

프롬프트 조립부는 모드를 불리언 넷으로 펼쳐 두고, 각 섹션이 자기 조건을 선언합니다.

func BuildSystemPrompt(cfg Config) string {

    isFull    := cfg.Mode == PromptFull || cfg.Mode == ""

    isTask    := cfg.Mode == PromptTask

    isMinimal := cfg.Mode == PromptMinimal

    var lines []string

    // 정체성 — 모든 모드 (None 포함)

    lines = append(lines, identityLine(cfg))

    // 페르소나 — full/task만. 성격은 자동화에도 일부 필요하다

    if (isFull || isTask) && len(cfg.PersonaFiles) > 0 {

        lines = append(lines, buildPersonaSection(cfg.PersonaFiles)...)

    }

    // 실행 지향 지시 — full/task.

    // "계획만 말하지 말고 이번 턴에 실제로 도구를 써라"

    if (isFull || isTask) && cfg.HasTools {

        lines = append(lines, buildExecutionBiasSection()...)

    }

    // 툴 호출 스타일(내부 도구명 비노출 등) — full만.

    // 사용자에게 보이지 않는 실행에는 불필요하다

    if isFull && cfg.HasTools {

        lines = append(lines, buildToolCallStyleSection()...)

    }

    // 스킬 — full/task. 핀 고정된 것만 인라인, 나머지는 검색으로

    if (isFull || isTask) && cfg.HasSkills {

        lines = append(lines, buildSkillsHybridSection(cfg.PinnedSkills, cfg.HasSkillSearch)...)

    } else if isMinimal && cfg.PinnedSkills != "" {

        // minimal은 핀 고정 스킬 요약만 (검색 안내 없이)

        lines = append(lines, buildPinnedSkillsMinimalSection(cfg.PinnedSkills)...)

    }

    return strings.Join(lines, "\n")

}

이 방식이 좋은 건 새 섹션을 추가할 때 그 자리에서 조건을 선언한다는 점입니다. 모드별 프롬프트 템플릿을 네 개 따로 두면 섹션 하나 추가할 때마다 네 곳을 고쳐야 하고, 반드시 어긋납니다.

무엇을 자를 것인가 — 판단 기준

섹션마다 물어볼 질문은 이겁니다. "이게 없으면 이 실행이 실패하는가, 아니면 덜 우아해지는가?"

  • 실패하는 것 → 남긴다: 툴 사용법, 실행 지향 지시, 안전 경계, 작업 완료 기준.
  • 덜 우아해지는 것 → 자른다: 말투 가이드, 유머 수위, 이모지 정책, 대화 스타일. 크론 작업이 위트 있게 실패할 필요는 없습니다.
  • 사용자에게 안 보이면 → 자른다: "도구 이름을 사용자에게 노출하지 마라" 같은 지시는 사람이 읽지 않는 실행에서 무의미합니다.

실전에서 가장 값진 게 세 번째입니다. 프롬프트의 상당 부분이 "사용자 앞에서 어떻게 보일까"에 대한 지시인데, 백그라운드 작업에는 사용자가 없습니다.

주의 1: 너무 자르면 일을 안 한다

토큰을 아끼려다 흔히 빠지는 함정입니다. 프롬프트를 극단적으로 줄이면 모델이 대화 모드로 돌아갑니다. "파일을 정리해줘"라는 작업 지시에 툴을 쓰지 않고 "네, 정리하겠습니다. 어떤 기준으로 할까요?"라고 답하고 끝나는 겁니다.

이걸 막는 게 실행 지향 섹션이고, 몇 줄밖에 안 되지만 자동화 모드에서 반드시 살려야 합니다.

## Execution Bias

사용자가 작업을 요청하면 같은 턴에 시작하라.

실행 가능한 작업이면 실제 도구 호출을 사용하라 — 계획이나 하겠다는 답변에서 멈추지 마라.

도구가 있고 다음 행동이 명확한데 설명만 하는 턴은 미완성이다.

Task 모드가 Minimal이 아니라 별도로 존재하는 이유가 바로 이겁니다. 자동화는 성격은 필요 없지만 실행력은 필요합니다.

주의 2: 캐싱과 충돌한다

프롬프트 캐싱을 함께 쓴다면 반드시 알아야 할 상호작용이 있습니다. 캐싱은 프롬프트 앞부분의 완전 일치로 동작하는데, 모드가 바뀌면 정적 구간의 내용 자체가 달라집니다. 즉 모드가 바뀔 때마다 캐시가 새로 만들어집니다.

실무 규칙 두 가지입니다.

  • 같은 세션 안에서 모드를 흔들지 마라. 턴마다 모드를 재계산하는 구조라면, 입력에 따라 결과가 달라지지 않도록 세션 키 기반으로 고정하세요.
  • 모드별로 캐시가 따로 생긴다고 가정하라. 세 모드를 섞어 쓰는 에이전트는 캐시 항목도 셋입니다. 각각이 재사용될 만큼 호출량이 있는지 확인하세요. 적으면 캐시 생성 비용만 세 번 내는 셈입니다.

주의 3: 미리보기 없이 조절하지 마라

섹션 게이팅을 도입하면 실제로 나가는 프롬프트를 보기 어려워집니다. 조건이 십수 개 얽히면 "이 에이전트의 task 모드 프롬프트가 정확히 뭐지?"를 코드만 읽고 답하기 힘듭니다.

그래서 모드를 인자로 받아 완성된 프롬프트를 반환하는 미리보기 함수를 처음부터 만들어 두세요. 관리 화면이나 CLI에 붙여 두면 디버깅 시간이 크게 줄고, 모드별 토큰 수를 실제 값으로 비교할 수 있습니다.

// 에이전트와 모드를 받아 최종 프롬프트를 반환한다.

// 관리 UI·CLI·테스트에서 공용으로 쓴다.

func BuildPreviewPrompt(ctx context.Context, agent *AgentData,

    mode PromptMode, userID string, deps PreviewDeps) PreviewResult

테스트도 이 함수 위에 쌓입니다. "minimal 모드에 팀 섹션이 들어가면 안 된다" 같은 규칙을 문자열 포함 검사로 못 박아 두면, 나중에 섹션을 추가하다 실수로 새어 나가는 걸 막을 수 있습니다.

실제 효과

절감폭은 프롬프트 구성에 따라 다르지만 경향은 분명합니다. 페르소나·스타일 가이드·팀 규칙이 큰 에이전트일수록 하위 모드의 절감이 큽니다. 그리고 이런 배경 실행은 대개 호출 빈도가 높습니다 — 5분마다 도는 하트비트는 하루 288번입니다. 사용자 대화보다 훨씬 자주 돕니다.

즉, 토큰 절감 효과가 가장 큰 곳은 사람이 보지 않는 실행이고, 정확히 그곳이 프롬프트를 가장 많이 잘라도 되는 곳입니다. 운이 좋은 구조입니다.

정리

  • 프롬프트는 Full / Task / Minimal / None 네 단계로 — 둘은 부족하다
  • 모드에 순위를 주고 minMode로 합성하라
  • 자동 판별은 상한으로만 작동 — 설정을 덮어써 무겁게 만들지 않는다
  • 섹션마다 조건을 선언하는 게이팅 방식, 모드별 템플릿 복사는 금물
  • 기준: "없으면 실패하는가, 덜 우아해지는가" — 사용자에게 안 보이는 실행은 스타일 지시 불필요
  • 실행 지향 지시는 자동화에서 반드시 유지 — 자르면 일을 안 한다
  • 캐싱과 상호작용 주의 — 세션 안에서 모드 고정
  • 미리보기 함수를 처음부터 만들어 두라

프롬프트 모드는 결국 "이 실행에 누가 있는가"라는 질문입니다. 사람이 있으면 어떻게 보일지가 중요하고, 없으면 무엇을 해내는지만 중요합니다. 그 차이만큼 프롬프트를 줄이면 됩니다.

댓글

이 블로그의 인기 게시물

한국투자증권 KIS API로 실시간 시세 받기 (WebSocket 실전)

파이썬으로 업비트 API 연동하기 — 시세 조회부터 주문까지 기초

Go로 자동매매 신호봇 프레임워크 설계하기