프롬프트는 대화가 아닙니다. 그것은 컴포넌트 계약입니다.
요약
LLM 프롬프트를 단순한 대화가 아닌 구조화된 컴포넌트 계약으로 정의합니다. 프로덕션 환경에서 재사용 가능한 프롬프트를 작성하기 위한 구성 요소와 설계 원칙을 제시합니다.
핵심 포인트
- 프롬프트는 지시 사항, 문맥 등을 포함한 구조적 설계가 필요함
- 구체적인 제약 조건은 모델의 출력 범위를 좁히는 조준 역할을 함
- 출력 대상이 코드일 경우 프롬프트는 API 스키마와 같은 계약이 됨
- 명확성을 위해 단순하고 직접적인 언어를 사용하는 것이 중요함
우리 대부분은 시행착오를 통해 LLM (Large Language Model)을 사용합니다. 이 포스트는 여러분에게 구조를 제공합니다: LLM의 구성 요소와 프로덕션(production)용 프롬프트를 작성하기 위한 재사용 가능한 템플릿입니다.
LLM이란 무엇인가?
파운데이션 모델 (Foundation models)은 인터넷 데이터로 사전 학습된(pretrained) 매우 큰 모델이며, 이것이 생성형 AI (Generative AI)를 구축합니다. 파운데이션 모델을 사용하면 하나의 사전 학습된 모델을 다양한 작업에 적응시킬 수 있습니다.
LLM (Large Language Model)은 텍스트를 위한 파운데이션 모델이며, 그 핵심적으로 동일한 FM (Foundation Model)이 요약 (summarisation), 분류 (classification), 번역 (translation), 코드 생성 (code generation) 등 많은 작업에 사용될 수 있습니다.
따라서 LLM이 하는 일은 시퀀스(sequence)에서 다음 단어(다음 토큰 (next token))를 예측하는 것입니다. 각 단계에서 모델은 지금까지 본 주변 문맥(context)을 확인한 다음, 가능한 다음 토큰들에 대한 확률 분포(probability distribution)를 생성합니다. 이를 루프(loop) 내에서 실행하면 유창하고 일관된 새로운 콘텐츠가 생성됩니다. 따라서 기본적으로 LLM은 텍스트를 입력받아 텍스트를 반환합니다. 입력은 프롬프트 (prompt)라고 불리며, 출력은 완성 (completion)이라고 불립니다.
프롬프트에 들어가는 것
프롬프트는 원하는 출력을 생성하기 위해 생성 모델 (generative model)에 주어지는 모든 입력을 의미합니다. 프롬프트 엔지니어링 (Prompt engineering)은 모델로부터 가능한 최고의 결과를 얻기 위해 이러한 프롬프트를 설계하고 개선하는 관행입니다. 프롬프트를 개선한다는 것은 모델의 출력에 영향을 미치는 요소들을 실험하는 것을 의미합니다. 모호한 프롬프트는 많은 합리적인 응답을 유도할 수 있지만, 제약 조건(constraint)을 추가할 때마다 가능한 응답의 수는 줄어듭니다.
잘 구조화된 프롬프트는 보통 네 가지 부분으로 구성됩니다.
| 구성 요소 (Building block) | 역할 (Role) |
|---|---|
| 지시 사항 (Instruction) | 수행하기를 원하는 작업. |
| ... |
여기서 지시 사항(instruction)과 문맥(context)은 마지막에 함께 살펴볼 템플릿 슬롯인 작업 (Task) 과 문맥 (Context) 의 두 가지 요소입니다.
그리고 프롬프트 작성을 위한 모범 사례는 네 가지 차원으로 조직될 수 있습니다. 강력한 프롬프트는 이 네 가지 모두를 고려합니다.
| 차원 (Dimension) | 실행 방법 (Practice) |
|---|---|
| 명확성 (Clarity) | 단순하고 직접적인 언어를 사용하세요. 프롬프트가 쉽게 이해될 수 있도록 모호하거나 지나치게 복잡한 용어는 피하십시오. |
| ... |
프롬프트를 탐색 광선(search beam)이라고 생각하십시오. 모호함은 넓은 광선과 같아서, 모델이 넓고 유효한 영역 중 어딘가에 착륙하게 만듭니다. 각 제약 조건은 광선을 좁힙니다. 구체성은 모델에 대한 예의가 아니라, 조준(aiming)입니다.
출력을 읽는 대상: 코드인가 사람인가
출력은 우리에게 더 흥미로운 부분입니다. LLM의 출력에는 파서 (parser)와 사람 (person)이라는 두 가지 가능한 대상이 있으며, 당신은 각각을 위한 계약 (contract)을 작성하는 것입니다. 사람이 출력을 읽을 때조차, "괜찮아 보인다"는 것이 "내가 필요로 했던 것과 일치한다"는 것과 같지는 않습니다. LLM의 출력이 눈(eyes) 대신 코드 (code)에 의해 읽힐 때, 출력은 API 응답이 되고 프롬프트는 그 스키마 (schema)가 됩니다. 사람은 지저분한 답변을 용서하지만, json.loads()는 그렇지 않습니다. 성공하거나 오류를 던질 뿐입니다. 명시적인 사양 (spec)이 없으면, 모델 (model)이 형식, 길이, 어조, 깊이를 결정하며, 그럴듯하지만 일반적인 무언가를 선택합니다. 여기서 출력을 제어한다는 것은 그 결정권을 모델에서 당신에게로 가져오는 것을 의미합니다.
두 가지 종류의 제어:
- 형식 제어 (Format control): 출력의 형태 (JSON 키와 타입, 또는 산문 vs 불렛 포인트 vs 표, 제목, 섹션).
- 행동 제어 (Behavior control): 모델이 할 수 있는 것과 해서는 안 되는 것 (예: "주석 없이 유효한 Terraform만 작성"; 또는 "기술적 전문 용어 없이 간결한 경영진 어조로 작성").
각 대상은 서로 다르게 실패하며, 그중 하나는 조용히 실패합니다:
| 파서 (Parser) (코드가 읽는 출력) | 사람 (Person) (사람이 읽는 출력) | |
|---|---|---|
| 무엇이 깨지는가 | 마크다운 구분선 (Markdown fences), 수다스러운 서문, 임의로 생성/변경된 키, 문자열로 처리된 숫자 | 너무 긴 길이, 잘못된 어조, 누락된 섹션, 부적절한 수준의 설명 |
| 어떻게 깨지는가 | 명시적 (Loud): json.loads()가 오류를 던지고 파이프라인이 중단됩니다. 즉시 인지할 수 있습니다. | 암묵적 (Quiet): 출력이 유창하고 완전해 보입니다. 격차는 두 번째 읽을 때나 드러나며, 아무도 문제를 제기하지 않습니다. |
명백한 실패(Loud failure)는 짜증을 유발하지만, 암묵적 실패(Quiet failure)는 더 위험합니다. 미묘하게 목표에서 벗어난 답변은 눈에 띄지 않고 지나갈 수 있기 때문입니다.
더 나은 결과를 위해 적용할 수 있는 몇 가지 완화 방법(Mitigations)이 있습니다:
파서(Parser)를 위한 방법:
- 정확한 스키마(Schema)를 지정하세요.
- 노이즈(Noise)를 금지하세요.
- 정확한 형태의 예시를 하나 제공하세요.
temperature: 0으로 설정하세요.- 네이티브 **구조화된 출력 (Structured outputs) / 도구 호출 (Tool-calling)**을 사용하세요.
사람을 위한 방법:
- 구조를 명시적으로 기술하세요.
- 길이 제한(Length budget)을 부여하세요.
- 대상 독자(Audience)를 지정하세요.
- 필수 요소들을 나열하세요.
- 반복되는 형식의 경우, 원하는 출력의 예시를 하나 보여주세요.
따라서 여기서 핵심은 프롬프트만으로 제어하는 형식(Format)은 '요청'일 뿐이며, 디코딩 시점의 제약(Decode-time constraints)이 '보장'이라는 점입니다. 모델의 출력을 API 응답으로 취급하고, 프롬프트를 그 스키마로 취급하십시오.
**제약 사항 (Constraints)**과 출력 (Output) 또한 템플릿 슬롯입니다. 즉, 행동을 가지치기하는 규칙이자 당신이 계약한 정확한 형태입니다.
시스템(System)과 사용자(User)의 분리
우리는 이미 효과적인 프롬프트의 네 가지 차원 중 하나로 역할을 살펴보았습니다. 이는 많은 사람들이 잊어버리는 부분입니다.
A 프롬프트는 세 가지 유형의 메시지를 포함할 수 있습니다: 시스템(System), 사용자(User), 어시스턴트(Assistant).
- 시스템 메시지 (System message): 모델의 행동, 규칙 및 전반적인 역할을 정의합니다.
- 사용자 메시지 (User message): 현재 작업에 대한 요청이나 입력을 포함합니다.
- 어시스턴트 메시지 (Assistant message): 대화 문맥이나 예시로 사용되는 모델의 이전 응답입니다.
"유형(Type)"은 보통 각 메시지의 role 필드를 통해 설정됩니다.
시스템 메시지는 컴포넌트의 설정(Configuration)인 반면, 사용자 메시지는 특정 호출(Call)을 위한 입력입니다.
시스템 프롬프트는 지속적인 행동과 규칙을 정의합니다. 사용자 메시지는 해당 특정 요청을 위한 가변적인 데이터를 제공합니다. 시스템은 컴포넌트가 일반적으로 어떻게 행동할지를 설정하고, 사용자 메시지는 지금 당장 무엇을 할지를 알려줍니다.
역할(Role)이 작동하는 이유는 무엇일까요? 그것은 마법이 아닙니다.
당신이 "시니어 보안 엔지니어처럼 행동하라"고 말하면, 모델은 학습 데이터 내에서 해당 종류의 글쓰기와 통계적으로 연관된 패턴을 향해 출력을 전환합니다.
마찬가지로, "이것을 주니어 개발자에게 설명해 주세요"라고 하면 모델은 더 단순하고, 교육적이며, 더 상세한 설명이 포함된 응답을 생성하도록 유도됩니다.
역할(Role)을 부여한다고 해서 모델에게 실제 인격이 생기는 것은 아닙니다. 이는 모델이 생성할 가능성이 높은 텍스트의 확률 분포(probability distribution)를 변화시키는 것입니다. 이것이 템플릿의 첫 번째 줄인 역할 (Role) 슬롯입니다.
프로덕션 환경에서 시스템/사용자(system/user) 분리가 중요한 이유:
- 캐싱 (Caching). 시스템 메시지는 보통 안정적이며 여러 요청에 걸쳐 재사용되므로, 프롬프트 캐싱 (prompt caching)에 이상적입니다. 시스템 프롬프트를 일관되게 유지하고, 변경되는 데이터의 대부분은 사용자 메시지에 배치하세요.
- 테스트 가능성 (Testability). 시스템 프롬프트는 AI 애플리케이션에서 가장 영향력이 큰 부분 중 하나입니다. 이를 코드처럼 취급하세요. 버전을 관리하고, 변경 사항을 비교하며, 주의 깊게 테스트해야 합니다.
- 보안 (Security). 신뢰할 수 있는 지침은 시스템 메시지에 있어야 합니다. 신뢰할 수 없는 콘텐츠 (사용자 입력, 검색된 문서, 도구 출력)는 사용자 메시지에 머물러야 합니다. 신뢰할 수 없는 텍스트가 모델이 지침으로 처리하는 곳에 도달하면, **프롬프트 인젝션 (prompt injection)**이 발생합니다. 명확한 분리가 첫 번째 방어선입니다.
모델에게 예시 보여주기
좋은 프롬프트 하나가 큰 차이를 만듭니다. 몇 가지 기술을 사용하면 그 차이는 더 커집니다. 가장 유용한 기술은 바로 모델에게 예시를 보여주는 것입니다.
퓨샷 프롬프팅 (few-shot prompting)에서는 프롬프트에 몇 가지 작업 시연(demonstrations)이 포함됩니다. 모델은 인컨텍스트 러닝 (in-context learning)을 사용하여 패턴을 추론하고 이를 새로운 입력에 적용합니다.
퓨샷 예시 (few-shot examples)는 명세서(spec) 역할을 겸하는 유닛 테스트 (unit tests)입니다.
작동 원리: 모델은 패턴 연속기 (pattern continuator)입니다. 몇 개의 입력→출력 쌍은 강력하고 모호함이 낮은 패턴을 구축하며, 모델은 이를 이어갑니다.
언제 무엇을 사용할 것인가:
-
Zero-shot: 모델이 명확하게 여러 번 접해본 단순하고 일반적인 작업들.
[ { "role": "user",
...
```
-
One-shot: 주로 _형식 (format)_을 고정해야 할 때.
[ { "role": "user",
...
```
- Few-shot: 분류 (classification), 구조화된 출력 (structured output), 코드 스타일, 또는 모델이 추측하기 어려운 특정 스키마 (schema)나 예외 케이스 (edge cases)가 있는 모든 경우.
이러한 작동 증명들이 바로 예시 (Examples) 슬롯입니다.
아래 예시에서 마지막 메시지는 새로운 입력이며, 모델은 assistant 차례를 생성합니다.
[
{"role": "user", "content": "Today the weather is fantastic"},
{"role": "assistant", "content": "positive"},
...
토큰 (Tokens)은 비용의 단위입니다
LLM (Large Language Models)은 무료가 아니며, 토큰은 계량기 역할을 합니다. LLM에게 토큰은 비용과 지연 시간 (latency) 모두의 단위입니다. 우리 대부분에게 이 멘탈 모델 (mental model)은 익숙합니다. 토큰은 네트워크 페이로드 (payload)이며, 각 LLM 호출은 비용이 발생하고 지연 시간이 수반되는 API 요청입니다.
출력 토큰 (Output tokens)이 지연 시간의 대부분을 차지합니다. 모델은 한 번에 하나의 토큰씩 텍스트를 생성하며, 각 새로운 토큰은 이전의 모든 토큰에 의존하기 때문에 생성은 순차적으로 이루어져야 합니다.
입력 (Input)은 다르게 작동합니다. 프롬프트는 단일 병렬 "프리필 (prefill)" 패스를 통해 처리되며, 이는 상대적으로 빠릅니다 (비록 해당 토큰들에 대한 비용을 지불하더라도 말입니다).
네 가지 레버 (levers)가 대부분의 작업을 수행합니다:
-
입력 압축 (Compress the input)
전체 문서를 보내는 대신, 먼저 요약하거나 관련 부분만 추출하세요. 대부분의 프롬프트는 모델이 전혀 읽지 않는 컨텍스트 (context)를 함께 보냅니다.
-
출력 제한 및 구조화 (Limit and structure the output)
max_tokens를 설정하고, 긴 산문 대신 JSON이나 배열과 같은 구조화된 형식 (structured formats)을 선호하며, "120 토큰 이내로 유지"와 같이 간결한 요약을 요청하세요. -
작업에 적합한 모델 사용 (Use the right model for the task)
분류 (classification), 라우팅 (routing), 또는 추출 (extraction)과 같은 단순한 작업에는 작은 모델이 더 빠르고 저렴합니다. 추론 (reasoning)이나 종합 (synthesis)이 실제로 필요한 작업에는 더 강력한 모델을 아껴두세요.
-
캐싱 사용 (Use caching)
두 가지 종류가 있습니다:
- **프롬프트/접두사 캐싱 (Prompt/prefix caching)**
시스템 프롬프트 (system prompts), 예시 (examples), 또는 참조 문서와 같이 변하지 않는 프롬프트 섹션을 재사용합니다. 제공업체가 이를 서버 측에서 캐싱하므로, 비용이 많이 드는 입력 처리 단계를 다시 계산하는 것을 피할 수 있습니다.
...
재사용 가능한 프롬프트 템플릿 (A Reusable Prompt Template)
여기서 살펴본 모든 내용은 프로덕션 프롬프트 (production prompt)를 작업할 때 사용할 템플릿을 구축할 수 있는 구조를 제공합니다.
R-T-C-C-E-O 템플릿:
[ROLE] 모델이 수행하는 역할. 출력 분포 (output distribution)를 설정합니다.
[TASK] 모호하지 않게 명시된, 수행해야 할 단 한 가지 작업.
[CONTEXT] 입력값, 배경, 데이터; 지침 (instructions)과 명확하게 구분되어야 합니다.
...
모든 프롬프트에 이 여섯 가지가 모두 필요하지는 않지만, 모든 프롬프트는 우연이 아닌 의도적인 부분 집합이어야 합니다.
마지막으로, 프롬프트가 배포되기 전의 사전 점검 단계인 프로덕션 체크리스트 (production checklist)를 만들어 보겠습니다:
- 역할 (Role): 설정되었으며 작업과 일치하는가?
- 작업 (Task): 유능한 타인이 읽었을 때 오해할 소지가 있는가?
- 제약 사항 (Constraints): 각 제약 사항이 실제로 목격한 실패 모드 (failure mode)와 매칭되는가? 장식적인 제약 사항은 제거하라.
- 출력 계약 (Output contract): 명시적인가? 코드가 소비하는 경우 스키마 (schema)와 예시가 포함되었는가?
- 형식 보장 (Format guarantee): 단순한 말로 된 요청이 아니라, 디코딩 시점의 제약 (structured output / tool call)인가?
- 예시 (Examples): 분류/구조화 작업에 대해 예시가 존재하는가? 다양하고, 일관되며, 최소한인가?
- 샘플링 (Sampling):
temperature가 작업에 맞게 설정되었는가? - 토큰 예산 (Token budget):
max_tokens가 제한되었는가? 출력 형식이 압축적인가? - 모델 (Model): 단순히 "가장 강력한 모델"이 아니라, 적절한 크기 (right-sized)인가?
- 캐시 친화성 (Cache-friendliness): 안정적인 콘텐츠를 앞에, 가변적인 콘텐츠를 뒤에 배치했는가?
- 주입 안전성 (Injection safety): 지침은 시스템 메시지 (system message)에, 데이터는 사용자 메시지 (user message)에 있는가?
- 버전 관리 및 테스트 (Versioned & tested): 소스 제어 (source control)에 포함되어 있으며, 몇 가지 회귀 테스트 (regression cases)를 거쳤는가?
이렇게 우리는 역할 (Role), 작업 (Task), 문맥 (Context), 제약 사항 (Constraints), 예시 (Examples), 출력 (Output)이라는 여섯 가지 슬롯과 사전 점검 체크리스트를 다루었습니다. 모든 프로덕션 프롬프트를 이 두 가지를 통해 실행한다면, 당신은 막연한 희망을 담은 요청을 검증된 컴포넌트로 탈바꿈시킨 것입니다. 프롬프트는 대화가 아닙니다. 그것은 컴포넌트 계약입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기