Split View: Prompt Engineering 2025 실전 가이드: GPT-4o와 Claude 3.5를 최대한 활용하는 법
Prompt Engineering 2025 실전 가이드: GPT-4o와 Claude 3.5를 최대한 활용하는 법
- 프롬프트 엔지니어링이 여전히 중요한 이유
- 기법 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. 프롬프트 길이 최적화
→ 길다고 꼭 좋은 게 아님. 필요 없는 내용 제거
결론: 프롬프트 엔지니어링은 기술이다
프롬프트 엔지니어링은 마법이 아닙니다. 테스트하고, 측정하고, 반복하는 공학적 기술입니다.
실전 조언:
- 작게 시작해서 점진적으로 개선: 기본 프롬프트부터 시작해 문제 발생 시 개선
- 프롬프트를 코드로 관리: 버전 관리, 테스트, 배포 파이프라인 구축
- 정량적으로 평가: "더 좋아진 것 같아"가 아니라 측정 가능한 지표로 판단
- 모델 업데이트를 모니터링: 새 모델 버전에서 기존 프롬프트가 다르게 작동할 수 있음
좋은 프롬프트는 좋은 코드처럼 시간이 지나도 유지보수 가능해야 합니다.
Prompt Engineering 2025: Getting Maximum Performance from Modern LLMs
- Why Prompt Engineering Still Matters
- Technique 1: System Prompt Design Principles
- Technique 2: Few-Shot Prompting
- Technique 3: Chain-of-Thought (CoT) Prompting
- Technique 4: Structured Output
- Technique 5: XML Tags for Structure (Claude-specific but broadly useful)
- Technique 6: Role + Context + Constraint Pattern
- Prompt Version Control: Treat Prompts Like Code
- Model-Specific Optimization Tips
- Prompt Debugging Methodology
- Conclusion: Prompt Engineering Is an Engineering Discipline
Why Prompt Engineering Still Matters
"With LLMs getting so capable, won't prompt engineering become obsolete soon?"
I get this question often. The answer is no. Here's why:
model capability x prompt quality = output quality
The best model with a bad prompt gives bad results. A well-crafted prompt can produce 20-40% better performance from the same model. In some task-specific cases, a small model with an excellent prompt beats a large model with a sloppy one.
As better models keep arriving in 2025, good prompting remains a portable skill that transfers across model generations.
Technique 1: System Prompt Design Principles
The system prompt is the LLM's "configuration." Most developers treat it as an afterthought.
# Bad system prompt (how most people start)
bad_prompt = "You are a helpful assistant."
# Problem: so vague the model doesn't know what to do
# Good system prompt using the COSTAR framework
good_prompt = """
## Role
You are a senior customer service specialist at TechCorp.
## Context
TechCorp sells B2B SaaS software for HR management.
Customers are typically HR managers at companies with 100-5,000 employees.
## Objective
Help customers resolve issues and answer product questions.
Escalate billing issues to finance@techcorp.com.
## Style
- Professional but warm English
- Keep responses under 150 words
- Always end with a follow-up question
## Tone
Professional, empathetic, solution-oriented
## Audience
HR managers, often non-technical
## Response Format
1. Acknowledge the issue
2. Provide solution (step-by-step if needed)
3. Close with: "Is there anything else I can help you with?"
"""
The COSTAR framework: Context, Objective, Style, Tone, Audience, Response Format. Specifying all six produces predictable, consistent results.
Technique 2: Few-Shot Prompting
Showing examples is faster than describing behavior in words.
# Zero-shot (no examples)
zero_shot = """
Classify the sentiment of this review: 'Delivery took way too long'
"""
# Works, but output format and consistency may vary
# Few-shot (with examples)
few_shot = """
Classify review sentiment as: positive / negative / neutral
Review: "Absolutely love this product!" -> positive
Review: "Shipping was slow but the product is great" -> neutral
Review: "Complete junk, want a refund" -> negative
Review: "Nothing special for the price" -> neutral
Review: "Delivery took way too long" -> """
# Output: negative (much more reliably structured)
Few-shot principles:
- Example quality: include representative cases
- Diversity: cover edge cases too
- Count: 3-8 examples is typically optimal (more isn't always better)
- Order: the last example has the most influence on output (recency bias)
Technique 3: Chain-of-Thought (CoT) Prompting
Asking the LLM to show its reasoning dramatically improves complex inference.
# Without CoT (unreliable)
no_cot = """
A train travels 120km in 2 hours, then 180km in 3 hours.
What is the average speed?
"""
# LLM might compute (120/2 + 180/3) / 2 = 60 incorrectly using the wrong formula
# With CoT (reliable)
with_cot = """
A train travels 120km in 2 hours, then 180km in 3 hours.
What is the average speed?
Let me think through this step by step:
"""
# LLM: "Total distance = 120 + 180 = 300km.
# Total time = 2 + 3 = 5 hours.
# Average speed = 300 / 5 = 60 km/h"
# Correct!
# Zero-shot CoT (simplest form — often works just as well)
zero_shot_cot = """
Problem: [complex reasoning problem]
Solution: Let's think step by step.
"""
Why it works: LLMs generate tokens autoregressively. When intermediate reasoning steps are generated as tokens, subsequent tokens can "reference" that reasoning — improving final answer accuracy significantly.
Note: Reasoning models like o1 and o3 perform CoT internally. For these models, focus on clearly stating the problem rather than prompting them to reason step by step.
Technique 4: Structured Output
If your production system parses LLM output, always enforce structured output.
import json
from openai import OpenAI
from pydantic import BaseModel
client = OpenAI()
# Method 1: JSON Mode (simple cases)
response = client.chat.completions.create(
model="gpt-4o",
response_format={"type": "json_object"},
messages=[{
"role": "user",
"content": """
Extract product info from this text and return JSON:
"iPhone 15 Pro Max, 256GB, Space Black, retail price $1,199"
Return: {"name": ..., "storage": ..., "color": ..., "price_usd": ...}
"""
}]
)
data = json.loads(response.choices[0].message.content)
# Method 2: Structured Outputs (type-safe, more reliable)
class ProductInfo(BaseModel):
name: str
storage: str
color: str
price_usd: float
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[{
"role": "user",
"content": "iPhone 15 Pro Max, 256GB, Space Black, retail price $1,199"
}],
response_format=ProductInfo
)
product = response.choices[0].message.parsed
print(product.price_usd) # 1199.0, guaranteed float type
OpenAI's Structured Outputs guarantee 100% JSON Schema compliance. Parsing errors disappear.
Technique 5: XML Tags for Structure (Claude-specific but broadly useful)
Claude responds especially well to XML-structured prompts. This is likely because Anthropic used extensive XML structure during training.
import anthropic
client = anthropic.Anthropic()
prompt = """
<task>
Summarize the following document.
</task>
<document>
[long document content here]
</document>
<constraints>
- Maximum 3 bullet points
- Each bullet: 1-2 sentences
- Focus on actionable items only
- Replace jargon with plain language
</constraints>
<output_format>
Return as JSON:
{
"summary_bullets": ["...", "...", "..."],
"key_action": "Single most important action item"
}
</output_format>
"""
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}]
)
XML tags help the LLM understand the role of each section and follow instructions more precisely. This pattern works well on other models too, not just Claude.
Technique 6: Role + Context + Constraint Pattern
The most broadly applicable and reliable production prompting pattern.
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}"""
# Example usage
prompt = create_production_prompt(
role="You are a senior copywriter with 10 years of experience targeting Gen Z audiences.",
context="Client: startup coffee brand 'BREW'. New cold brew product launch.",
task="Write 3 versions of an Instagram caption.",
constraints=[
"Each caption under 50 characters",
"Include 1-2 emojis",
"No mention of price or promotions",
"Do not include hashtags"
],
output_format="Return as JSON array: ['caption1', 'caption2', 'caption3']"
)
Benefits of this pattern:
- Prompts can be dynamically generated in code
- Each component can be modified independently
- Easy to A/B test variations
Prompt Version Control: Treat Prompts Like Code
In production, prompts are code. They need version control.
# Bad: hardcoded in business logic
def summarize(text):
prompt = f"Summarize: {text}" # untracked, untestable
return call_llm(prompt)
# Good: manage prompts as separate versioned artifacts
class PromptManager:
def __init__(self):
self.prompts = {}
self.active_versions = {}
def register(self, name: str, version: str, template: str):
self.prompts[f"{name}_v{version}"] = template
def get(self, name: str, version: str = None) -> str:
v = version or self.active_versions.get(name, "1")
return self.prompts[f"{name}_v{v}"]
def set_active(self, name: str, version: str):
self.active_versions[name] = version
pm = PromptManager()
pm.register("summarize", "1", "Summarize: {text}")
pm.register("summarize", "2", "You are a business analyst. Summarize in 3 lines: {text}")
pm.set_active("summarize", "2")
For more sophisticated management, consider LangSmith or PromptLayer. They provide prompt versioning, A/B testing, and performance monitoring as an integrated platform.
Model-Specific Optimization Tips
The same prompt doesn't work equally well across all models:
GPT-4o:
- Prefers clear, direct instructions
- Follows Markdown formatting well
- Responds well to "You are a [role]" framing
Claude 3.5/3.7:
- Especially responsive to XML tag structure
- Handles long system prompts well
- Compliant with strong constraints ("you must...", "never...")
Gemini:
- Stronger at multimodal contexts
- Built-in search grounding support
Open-source (Llama, Mistral, etc.):
- Always use the model's specific chat template
- Instruction-following may be weaker than commercial models
- Requires more explicit and specific instructions
Prompt Debugging Methodology
When a prompt isn't working as expected:
1. Set temperature to 0
-> Get reproducible results for diagnosis
2. Make instructions more specific
-> "Write it well" -> "Professional tone, active voice, under 50 words"
3. Add output examples (few-shot)
-> Showing is more effective than describing
4. Specify what NOT to do
-> Negative constraints reduce failure cases
5. Add chain-of-thought
-> "Let's think step by step" meaningfully improves complex reasoning
6. Optimize prompt length
-> Longer isn't always better. Remove what isn't earning its place
Conclusion: Prompt Engineering Is an Engineering Discipline
Prompt engineering isn't magic. It's an engineering practice built on testing, measuring, and iterating.
Practical advice:
- Start simple, improve incrementally: begin with a basic prompt and refine when you see failures
- Manage prompts as code: version control, testing, deployment pipelines
- Evaluate quantitatively: measure with concrete metrics, not "feels better"
- Monitor model updates: prompt behavior can change when a model is upgraded
A good prompt is like good code: maintainable, testable, and designed to be changed.