- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 프롬프트 엔지니어링이 여전히 중요한 이유
- 기법 1: System Prompt 설계 원칙
- 기법 2: Few-Shot Prompting
- 기법 3: Chain-of-Thought (CoT) 프롬프팅
- 기법 4: Structured Output (구조화된 출력)
- 기법 5: XML 태그로 구조화 (Claude 특효)
- 기법 6: Role + Context + Constraint 패턴
- 프롬프트 버전 관리: 프롬프트를 코드처럼 관리하라
- 모델별 프롬프트 최적화 팁
- 프롬프트 디버깅 방법론
- 결론: 프롬프트 엔지니어링은 기술이다
프롬프트 엔지니어링이 여전히 중요한 이유
"LLM이 너무 똑똑해져서 프롬프트 엔지니어링은 곧 필요 없어지지 않을까요?"
이 질문을 자주 받습니다. 답은 "아니오"입니다. 이유가 있습니다.
모델 능력 × 프롬프트 품질 = 출력 품질
최고의 모델에 나쁜 프롬프트를 주면 나쁜 결과가 나옵니다. 좋은 프롬프트는 같은 모델에서 20-40% 이상의 성능 차이를 만들어낼 수 있습니다. GPT-4o가 GPT-3.5보다 능력이 좋지만, GPT-3.5에 최적화된 프롬프트를 넣은 결과가 GPT-4o에 허접한 프롬프트를 넣은 결과보다 나을 수 있습니다.
2025년에는 새로운 모델이 계속 나오고 있지만, 좋은 프롬프트 작성 능력은 모델과 무관하게 이식 가능한 기술입니다.
기법 1: System Prompt 설계 원칙
System Prompt는 LLM의 "설정값"입니다. 대부분의 개발자들은 이걸 소홀히 합니다.
# 나쁜 시스템 프롬프트 (대부분의 사람들이 이렇게 시작)
bad_prompt = "You are a helpful assistant."
# 문제: 너무 모호해서 모델이 무엇을 해야 하는지 모름
# COSTAR 프레임워크를 적용한 좋은 시스템 프롬프트
good_prompt = """
## Role (역할)
당신은 TechCorp의 시니어 한국어 고객 서비스 전문가입니다.
## Context (맥락)
TechCorp는 HR 관리를 위한 B2B SaaS 소프트웨어를 판매합니다.
고객은 주로 100-5,000명 규모 회사의 HR 매니저입니다.
## Objective (목표)
고객의 문제를 해결하고 제품 관련 질문에 답합니다.
결제 관련 문제는 finance@techcorp.com으로 에스컬레이션하세요.
## Style (스타일)
- 격식 있지만 따뜻한 한국어 (존댓말 사용)
- 답변은 200자 이내로 유지
- 항상 후속 질문으로 마무리
## Tone (톤)
전문적, 공감적, 해결 지향적
## Audience (대상)
한국의 HR 매니저, 보통 비기술적 사용자
## Response Format (형식)
1. 문제 인지 ("네, [문제]에 대해 도움드리겠습니다")
2. 해결책 제시 (필요시 단계별로)
3. 마무리: "이 외에 도움이 필요한 부분이 있으신가요?"
"""
COSTAR 프레임워크: Context, Objective, Style, Tone, Audience, Response Format. 이 여섯 가지를 명시하면 예측 가능한 일관된 결과를 얻을 수 있습니다.
기법 2: Few-Shot Prompting
"말로 설명하는 것보다 예시를 보여주는 게 빠르다"는 원칙입니다.
# Zero-shot (예시 없음)
zero_shot = """
리뷰 감정을 분류하세요: '배송이 너무 느려요'
"""
# 결과: 분류는 하지만 일관성이 없을 수 있음
# Few-shot (예시 포함)
few_shot = """
리뷰 감정을 긍정적 / 부정적 / 중립으로 분류하세요.
리뷰: "정말 좋은 제품이에요!" → 긍정적
리뷰: "배송이 조금 늦었지만 제품은 만족해요" → 중립
리뷰: "완전 불량품이에요 환불하고 싶어요" → 부정적
리뷰: "가격 대비 그냥 그래요" → 중립
리뷰: "배송이 너무 느려요" → """
# 결과: 부정적 (훨씬 일관성 높음)
Few-shot의 핵심:
- 예시의 질: 전형적인 케이스를 포함할 것
- 예시의 다양성: 엣지 케이스도 보여줄 것
- 예시의 수: 보통 3-8개가 최적 (더 많다고 꼭 좋지 않음)
- 예시의 순서: 가장 최근 예시가 답에 가장 큰 영향 (Recency Bias)
기법 3: Chain-of-Thought (CoT) 프롬프팅
LLM에게 "생각하는 과정"을 보여달라고 하면 복잡한 추론 능력이 극적으로 향상됩니다.
# CoT 없음 (나쁜 결과)
no_cot = """
기차가 2시간 동안 120km를 이동하고, 이후 3시간 동안 180km를 이동했습니다.
평균 속도는 얼마인가요?
"""
# LLM이 (120/2 + 180/3) / 2 = 60으로 잘못 계산할 수 있음 (틀린 방법)
# CoT 적용 (올바른 결과)
with_cot = """
기차가 2시간 동안 120km를 이동하고, 이후 3시간 동안 180km를 이동했습니다.
평균 속도는 얼마인가요?
단계별로 생각해봅시다:
"""
# LLM: "총 거리 = 120 + 180 = 300km
# 총 시간 = 2 + 3 = 5시간
# 평균 속도 = 300 / 5 = 60 km/h"
# 정확한 답!
# Zero-shot CoT (가장 단순한 형태)
zero_shot_cot = """
문제: [복잡한 수학 문제]
풀이: 단계별로 생각해봅시다.
"""
CoT가 효과적인 이유: LLM은 자기 회귀적(autoregressive)으로 생성합니다. 중간 추론 단계를 토큰으로 생성하면, 이후 토큰들이 그 추론을 "참고"할 수 있어 최종 답의 정확도가 높아집니다.
주의: GPT-4o1, o3 같은 reasoning 모델들은 내부적으로 CoT를 이미 수행합니다. 이런 모델에는 "단계별로 생각해" 대신 문제를 명확하게 기술하는 데 집중하세요.
기법 4: Structured Output (구조화된 출력)
프로덕션 시스템에서 LLM 출력을 파싱해야 한다면, 반드시 구조화된 출력을 사용하세요.
import json
from openai import OpenAI
from pydantic import BaseModel
client = OpenAI()
# 방법 1: JSON Mode (간단한 경우)
response = client.chat.completions.create(
model="gpt-4o",
response_format={"type": "json_object"},
messages=[{
"role": "user",
"content": """
다음 텍스트에서 제품 정보를 추출하고 JSON으로 반환하세요:
"아이폰 15 프로 맥스, 256GB, 스페이스 블랙, 정가 1,850,000원"
반환 형식: {"name": ..., "storage": ..., "color": ..., "price_krw": ...}
"""
}]
)
data = json.loads(response.choices[0].message.content)
# 방법 2: Structured Outputs (타입 안전, 더 안정적)
class ProductInfo(BaseModel):
name: str
storage: str
color: str
price_krw: int
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[{
"role": "user",
"content": "아이폰 15 프로 맥스, 256GB, 스페이스 블랙, 정가 1,850,000원"
}],
response_format=ProductInfo
)
product = response.choices[0].message.parsed
print(product.price_krw) # 1850000 (int 타입으로 보장)
OpenAI의 Structured Outputs는 JSON Schema를 100% 준수하는 출력을 보장합니다. 파싱 에러가 사라집니다.
기법 5: XML 태그로 구조화 (Claude 특효)
Claude는 XML 태그로 구조화된 프롬프트에 특히 잘 반응합니다. 이는 Anthropic이 Claude를 훈련할 때 XML 구조를 많이 사용했기 때문입니다.
# Claude API 사용 예시
import anthropic
client = anthropic.Anthropic()
prompt = """
<task>
다음 문서를 요약하세요.
</task>
<document>
[긴 문서 내용 여기에]
</document>
<constraints>
- 최대 3개의 불릿 포인트
- 각 불릿: 1-2문장
- 실행 가능한 항목에 집중
- 전문 용어는 쉬운 말로 바꿀 것
</constraints>
<output_format>
다음 JSON 형식으로 반환하세요:
{
"summary_bullets": ["...", "...", "..."],
"key_action": "가장 중요한 실행 항목 1개"
}
</output_format>
"""
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}]
)
XML 태그를 사용하면 LLM이 각 섹션의 역할을 명확히 이해하고, 지시사항을 더 정확하게 따릅니다.
기법 6: Role + Context + Constraint 패턴
가장 범용적이고 신뢰할 수 있는 프로덕션 프롬프트 패턴입니다.
def create_production_prompt(
role: str,
context: str,
task: str,
constraints: list[str],
output_format: str
) -> str:
constraints_text = "\n".join(f"- {c}" for c in constraints)
return f"""# Role
{role}
# Context
{context}
# Task
{task}
# Constraints
{constraints_text}
# Output Format
{output_format}"""
# 실제 사용 예시
prompt = create_production_prompt(
role="당신은 10년 경력의 한국어 카피라이터입니다. MZ 세대를 타겟으로 합니다.",
context="클라이언트: 스타트업 커피 브랜드 'BREW'. 신제품 콜드브루 출시.",
task="Instagram 캡션 3가지 버전을 작성하세요.",
constraints=[
"각 캡션은 50자 이내",
"이모지 1-2개 포함",
"가격이나 프로모션 언급 금지",
"해시태그 포함하지 말 것"
],
output_format="JSON 배열로 반환: ['캡션1', '캡션2', '캡션3']"
)
이 패턴의 장점:
- 코드로 프롬프트를 동적으로 생성 가능
- 각 구성요소를 독립적으로 수정 가능
- A/B 테스트 하기 쉬움
프롬프트 버전 관리: 프롬프트를 코드처럼 관리하라
프로덕션에서 프롬프트는 코드입니다. 버전 관리가 필요합니다.
# 나쁜 방법: 코드에 하드코딩
def summarize(text):
prompt = f"Summarize: {text}" # 버전 관리 불가
return call_llm(prompt)
# 좋은 방법: 프롬프트를 별도 파일/DB로 관리
# prompts/summarize_v2.txt
"""
당신은 전문 비즈니스 분석가입니다.
다음 텍스트를 3줄 이내로 요약하세요.
핵심 수치와 의사결정 사항을 강조하세요.
텍스트: {text}
"""
class PromptManager:
def __init__(self):
self.prompts = {}
self.active_versions = {}
def register(self, name: str, version: str, template: str):
key = f"{name}_v{version}"
self.prompts[key] = template
def get(self, name: str, version: str = None) -> str:
version = version or self.active_versions.get(name, "1")
return self.prompts[f"{name}_v{version}"]
def set_active(self, name: str, version: str):
self.active_versions[name] = version
# A/B 테스트: 50% 확률로 새 버전 사용
# self.active_versions[name] = random.choice(["1", "2"])
pm = PromptManager()
pm.register("summarize", "1", "Summarize: {text}")
pm.register("summarize", "2", "당신은 전문 분석가... {text}")
pm.set_active("summarize", "2")
더 체계적인 관리를 원한다면 LangSmith나 PromptLayer 같은 도구를 고려하세요. 이들은 프롬프트 버전 관리, A/B 테스트, 성능 모니터링을 통합 제공합니다.
모델별 프롬프트 최적화 팁
같은 프롬프트가 모든 모델에서 동일하게 작동하지 않습니다:
GPT-4o:
- 명확한 지시사항 선호
- Markdown 형식을 잘 따름
- "당신은 [역할]입니다" 스타일에 잘 반응
Claude 3.5/3.7:
- XML 태그 구조에 특히 잘 반응
- 긴 시스템 프롬프트를 잘 소화
- "반드시 ~해야 합니다" 같은 강한 제약에 순응적
Gemini:
- 멀티모달 컨텍스트에 강함
- 검색 증강(grounding)을 내장 지원
오픈소스 (Llama, Mistral 등):
- 각 모델의 특정 채팅 템플릿을 반드시 사용
- 상업적 모델보다 지시사항 따르기가 약할 수 있음
- 더 명시적이고 구체적인 지시가 필요
프롬프트 디버깅 방법론
프롬프트가 기대한 대로 작동하지 않을 때:
1. 온도(Temperature)를 0으로 설정
→ 재현 가능한 결과 확보
2. 지시사항을 더 구체적으로
→ "좋게 써줘" → "전문적이고 직접적인 어조로, 능동태 사용, 50자 이내로 써줘"
3. 원하는 출력 예시 추가 (Few-shot)
→ 말로 설명하는 것보다 예시가 효과적
4. 제약사항 명시
→ "하지 말아야 할 것"을 명시하면 네거티브 케이스 감소
5. Chain-of-thought 추가
→ "단계별로 생각해봅시다" 한 마디가 복잡한 추론 정확도를 크게 높임
6. 프롬프트 길이 최적화
→ 길다고 꼭 좋은 게 아님. 필요 없는 내용 제거
결론: 프롬프트 엔지니어링은 기술이다
프롬프트 엔지니어링은 마법이 아닙니다. 테스트하고, 측정하고, 반복하는 공학적 기술입니다.
실전 조언:
- 작게 시작해서 점진적으로 개선: 기본 프롬프트부터 시작해 문제 발생 시 개선
- 프롬프트를 코드로 관리: 버전 관리, 테스트, 배포 파이프라인 구축
- 정량적으로 평가: "더 좋아진 것 같아"가 아니라 측정 가능한 지표로 판단
- 모델 업데이트를 모니터링: 새 모델 버전에서 기존 프롬프트가 다르게 작동할 수 있음
좋은 프롬프트는 좋은 코드처럼 시간이 지나도 유지보수 가능해야 합니다.