Skip to content

Split View: Prompt Engineering 2025 실전 가이드: GPT-4o와 Claude 3.5를 최대한 활용하는 법

|

Prompt Engineering 2025 실전 가이드: GPT-4o와 Claude 3.5를 최대한 활용하는 법

프롬프트 엔지니어링이 여전히 중요한 이유

"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. 프롬프트 길이 최적화
   → 길다고 꼭 좋은 게 아님. 필요 없는 내용 제거

결론: 프롬프트 엔지니어링은 기술이다

프롬프트 엔지니어링은 마법이 아닙니다. 테스트하고, 측정하고, 반복하는 공학적 기술입니다.

실전 조언:

  1. 작게 시작해서 점진적으로 개선: 기본 프롬프트부터 시작해 문제 발생 시 개선
  2. 프롬프트를 코드로 관리: 버전 관리, 테스트, 배포 파이프라인 구축
  3. 정량적으로 평가: "더 좋아진 것 같아"가 아니라 측정 가능한 지표로 판단
  4. 모델 업데이트를 모니터링: 새 모델 버전에서 기존 프롬프트가 다르게 작동할 수 있음

좋은 프롬프트는 좋은 코드처럼 시간이 지나도 유지보수 가능해야 합니다.

Prompt Engineering 2025: Getting Maximum Performance from Modern LLMs

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:

  1. Start simple, improve incrementally: begin with a basic prompt and refine when you see failures
  2. Manage prompts as code: version control, testing, deployment pipelines
  3. Evaluate quantitatively: measure with concrete metrics, not "feels better"
  4. 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.