콘텐츠로 이동
Data Prep
상세

프롬프팅 (Prompting)

LLM에 명령과 맥락을 제공하여 원하는 출력을 유도하는 기법. 모델 가중치를 수정하지 않고 입력만으로 행동을 조절함.

프롬프팅이 중요한 이유

전통적 ML:    데이터 수집 → 학습 → 배포 (수주~수개월)
프롬프팅:     프롬프트 작성 → 테스트 → 배포 (수분~수시간)
관점 파인튜닝 프롬프팅
비용 GPU 학습 비용 API 호출 비용
시간 수시간~수일 즉시
유연성 태스크 고정 태스크 자유롭게 변경
전문성 ML 지식 필요 도메인 지식 필요
성능 한계 높음 (도메인 특화) 중간 (범용)

기본 프롬프트 구조

[System Instruction]     - 역할, 제약조건
[Context / Examples]     - 배경 정보, 예시
[Task Description]       - 수행할 작업
[Input Data]             - 처리할 데이터
[Output Format]          - 원하는 출력 형식

실제 예시

System: 당신은 금융 분야 전문가입니다. 정확한 정보만 제공하고, 불확실한 내용은 명시하세요.

Context: 아래는 2024년 1분기 실적 보고서입니다.
---
매출: 1,234억원 (전년 대비 +15%)
영업이익: 234억원 (전년 대비 +8%)
---

Task: 위 실적을 분석하고 투자자 관점에서 핵심 포인트를 3가지로 요약하세요.

Output Format:
1. [포인트 제목]: [설명]
2. [포인트 제목]: [설명]
3. [포인트 제목]: [설명]

Zero-Shot Prompting

예시 없이 지시만으로 태스크 수행.

def zero_shot_classification(text, categories):
    prompt = f"""다음 텍스트를 아래 카테고리 중 하나로 분류하세요.

카테고리: {', '.join(categories)}

텍스트: {text}

분류 결과:"""

    return llm.generate(prompt)

# 사용
result = zero_shot_classification(
    "아이폰 15 프로 맥스 배터리 너무 좋아요",
    ["긍정", "부정", "중립"]
)
# 출력: "긍정"

장점: 빠른 프로토타이핑, 토큰 절약 단점: 복잡한 태스크에서 정확도 낮음

Few-Shot Prompting

소수의 예시를 제공하여 패턴 학습 유도.

기본 Few-Shot

def few_shot_sentiment(text):
    prompt = """텍스트의 감성을 분석하세요.

텍스트: "이 영화 정말 재미있었어요!"
감성: 긍정

텍스트: "배송이 너무 느려서 화가 났습니다"
감성: 부정

텍스트: "보통이에요. 그냥 무난합니다."
감성: 중립

텍스트: "{}"
감성:""".format(text)

    return llm.generate(prompt)

예시 선택 전략

from sentence_transformers import SentenceTransformer
import numpy as np

class DynamicFewShotSelector:
    """입력과 유사한 예시를 동적으로 선택"""

    def __init__(self, examples, k=3):
        self.examples = examples
        self.k = k
        self.encoder = SentenceTransformer('all-MiniLM-L6-v2')
        self.example_embeddings = self.encoder.encode(
            [ex['input'] for ex in examples]
        )

    def select(self, query):
        query_embedding = self.encoder.encode([query])
        similarities = np.dot(self.example_embeddings, query_embedding.T).flatten()
        top_k_idx = np.argsort(similarities)[-self.k:][::-1]
        return [self.examples[i] for i in top_k_idx]

# 사용
selector = DynamicFewShotSelector(example_pool, k=3)
relevant_examples = selector.select(user_input)

Few-Shot 최적화 팁

요소 권장사항
예시 수 3-5개 (너무 많으면 토큰 낭비, 적으면 패턴 학습 부족)
예시 순서 가장 관련 있는 예시를 마지막에 배치
예시 다양성 다양한 케이스 포함 (edge case 포함)
포맷 일관성 모든 예시가 동일한 형식 유지

Chain-of-Thought (CoT)

추론 과정을 단계별로 명시하도록 유도.

Zero-Shot CoT

def zero_shot_cot(question):
    prompt = f"""{question}

단계별로 생각해봅시다."""

    return llm.generate(prompt)

# 예시
question = "철수는 사과 5개를 가지고 있었습니다. 영희에게 2개를 주고, 가게에서 3개를 더 샀습니다. 철수는 지금 사과가 몇 개 있나요?"

# Without CoT: "6개" (종종 틀림)
# With CoT: "5 - 2 + 3 = 6개" (추론 과정 포함)

Few-Shot CoT

def few_shot_cot(question):
    prompt = """Q: 바구니에 사과 23개가 있습니다. 20개를 더 넣으면 몇 개가 되나요?
A: 바구니에 처음 23개가 있었습니다.
   20개를 더 넣으면 23 + 20 = 43개가 됩니다.
   정답: 43개

Q: 레스토랑에 손님이 15명 있었습니다. 8명이 떠나고 5명이 왔습니다. 몇 명이 있나요?
A: 처음에 15명이 있었습니다.
   8명이 떠나면 15 - 8 = 7명이 됩니다.
   5명이 오면 7 + 5 = 12명이 됩니다.
   정답: 12명

Q: {}
A:""".format(question)

    return llm.generate(prompt)

Self-Consistency

여러 추론 경로로 답을 생성하고 다수결.

def self_consistency(question, n_samples=5):
    answers = []

    for _ in range(n_samples):
        response = few_shot_cot(question)
        answer = extract_final_answer(response)
        answers.append(answer)

    # 다수결
    from collections import Counter
    most_common = Counter(answers).most_common(1)[0][0]
    return most_common

Tree-of-Thoughts (ToT)

여러 추론 경로를 트리 구조로 탐색.

def tree_of_thoughts(problem, depth=3, breadth=3):
    """
    각 단계에서 여러 가능한 다음 단계를 생성하고,
    가장 유망한 경로를 선택
    """

    def generate_thoughts(state, n=3):
        prompt = f"""현재 상태: {state}

다음 단계로 가능한 접근법 {n}가지를 제시하세요."""

        response = llm.generate(prompt)
        return parse_thoughts(response)

    def evaluate_thought(thought):
        prompt = f"""다음 접근법이 문제 해결에 얼마나 유망한지 
1-10점으로 평가하세요.

접근법: {thought}

점수:"""

        score = int(llm.generate(prompt))
        return score

    # BFS/DFS로 탐색
    current_states = [problem]

    for d in range(depth):
        all_thoughts = []
        for state in current_states:
            thoughts = generate_thoughts(state, breadth)
            for t in thoughts:
                score = evaluate_thought(t)
                all_thoughts.append((t, score))

        # 상위 k개만 유지
        all_thoughts.sort(key=lambda x: x[1], reverse=True)
        current_states = [t[0] for t in all_thoughts[:breadth]]

    return current_states[0]

ReAct (Reasoning + Acting)

추론과 도구 사용을 번갈아 수행.

def react_agent(question, tools):
    prompt = f"""질문에 답하기 위해 추론(Thought)과 행동(Action)을 번갈아 수행하세요.

사용 가능한 도구:
- search[query]: 웹 검색
- calculator[expression]: 수식 계산
- lookup[term]: 위키피디아 검색

질문: {question}

Thought 1:"""

    max_steps = 5
    context = prompt

    for step in range(max_steps):
        response = llm.generate(context, stop=["Observation:"])
        context += response

        # Action 파싱
        action_match = re.search(r"Action: (\w+)\[(.+?)\]", response)
        if action_match:
            tool_name, tool_input = action_match.groups()
            observation = tools[tool_name](tool_input)
            context += f"\nObservation: {observation}\nThought {step+2}:"

        # 최종 답변 확인
        if "Final Answer:" in response:
            return extract_final_answer(response)

    return "답변을 찾지 못했습니다."

ReAct 프롬프트 예시

질문: 2024년 올림픽 개최 도시의 인구는 몇 명인가요?

Thought 1: 2024년 올림픽 개최 도시를 먼저 알아야 합니다.
Action: search[2024 올림픽 개최 도시]
Observation: 2024년 하계 올림픽은 파리에서 개최됩니다.

Thought 2: 파리의 인구를 검색해야 합니다.
Action: search[파리 인구 2024]
Observation: 파리의 인구는 약 210만 명입니다.

Thought 3: 정보를 모두 얻었습니다.
Final Answer: 2024년 올림픽 개최 도시 파리의 인구는 약 210만 명입니다.

Prompt Chaining

복잡한 태스크를 여러 단계로 분리.

def document_qa_chain(document, question):
    # Step 1: 문서 요약
    summary_prompt = f"""다음 문서를 핵심 내용 위주로 요약하세요.

문서:
{document}

요약:"""
    summary = llm.generate(summary_prompt)

    # Step 2: 관련 정보 추출
    extract_prompt = f"""다음 요약에서 질문과 관련된 정보를 추출하세요.

요약: {summary}
질문: {question}

관련 정보:"""
    relevant_info = llm.generate(extract_prompt)

    # Step 3: 최종 답변 생성
    answer_prompt = f"""추출된 정보를 바탕으로 질문에 답하세요.

정보: {relevant_info}
질문: {question}

답변:"""
    answer = llm.generate(answer_prompt)

    return answer

System Prompt 설계

역할 정의

system_prompts = {
    "코드_리뷰어": """당신은 시니어 소프트웨어 엔지니어입니다.
- 코드의 버그, 보안 취약점, 성능 이슈를 찾아냅니다
- 개선 방안을 구체적인 코드와 함께 제시합니다
- 코딩 컨벤션과 베스트 프랙티스를 적용합니다""",

    "기술_문서_작성자": """당신은 기술 문서 전문가입니다.
- 복잡한 개념을 명확하고 간결하게 설명합니다
- 예시와 다이어그램을 적극 활용합니다
- 독자의 기술 수준에 맞춰 설명 수준을 조절합니다""",

    "데이터_분석가": """당신은 데이터 분석 전문가입니다.
- 데이터의 패턴과 인사이트를 발견합니다
- 통계적 근거를 바탕으로 결론을 도출합니다
- 시각화와 함께 분석 결과를 전달합니다"""
}

출력 형식 제어

def structured_output(task, schema):
    prompt = f"""{task}

출력은 반드시 다음 JSON 스키마를 따라야 합니다:
```json
{json.dumps(schema, indent=2, ensure_ascii=False)}

JSON만 출력하세요. 다른 설명은 포함하지 마세요."""

response = llm.generate(prompt)
return json.loads(response)

사용

schema = { "sentiment": "string (긍정/부정/중립)", "confidence": "float (0.0-1.0)", "keywords": ["string"] } result = structured_output("이 리뷰를 분석하세요: 배송 빠르고 품질 좋아요", schema)

## 프롬프트 최적화

### 반복적 개선

```python
def optimize_prompt(initial_prompt, test_cases, metric_fn, iterations=5):
    """프롬프트를 반복적으로 개선"""

    current_prompt = initial_prompt
    best_score = 0

    for i in range(iterations):
        # 현재 프롬프트 평가
        scores = []
        failures = []

        for test in test_cases:
            output = llm.generate(current_prompt.format(input=test['input']))
            score = metric_fn(output, test['expected'])
            scores.append(score)
            if score < 1.0:
                failures.append({
                    'input': test['input'],
                    'output': output,
                    'expected': test['expected']
                })

        avg_score = sum(scores) / len(scores)

        if avg_score > best_score:
            best_score = avg_score
            best_prompt = current_prompt

        # 실패 케이스로 프롬프트 개선
        improve_prompt = f"""현재 프롬프트:
{current_prompt}

실패 케이스:
{json.dumps(failures[:3], ensure_ascii=False)}

실패 케이스를 해결할 수 있도록 프롬프트를 개선하세요.
개선된 프롬프트:"""

        current_prompt = llm.generate(improve_prompt)

    return best_prompt, best_score

프롬프트 압축

토큰 수를 줄이면서 성능 유지.

def compress_prompt(prompt, target_tokens):
    """프롬프트를 압축"""

    compression_prompt = f"""다음 프롬프트를 {target_tokens} 토큰 이내로 압축하세요.
핵심 지시사항과 예시는 유지하면서 불필요한 설명을 제거하세요.

원본 프롬프트:
{prompt}

압축된 프롬프트:"""

    return llm.generate(compression_prompt)

실무 트레이드오프

비용 vs 성능

기법 토큰 사용량 정확도 적용 케이스
Zero-Shot 낮음 보통 단순 분류, 빠른 프로토타입
Few-Shot (3개) 중간 높음 대부분의 태스크
CoT 중간 높음 (추론) 수학, 논리 문제
Self-Consistency 높음 (n배) 매우 높음 중요한 의사결정
ReAct 가변 높음 도구 사용 필요 시

지연시간 고려

단일 호출 < Few-Shot < CoT < Self-Consistency < ToT

실시간 응답 필요: Zero-Shot, Few-Shot
배치 처리 가능: CoT, Self-Consistency

흔한 실수와 해결책

문제 원인 해결책
지시 무시 프롬프트가 너무 김 핵심 지시를 상단에 배치
환각 (Hallucination) 과도한 창의성 "모르면 '모르겠'라고 답하세요" 추가
형식 불일치 출력 형식 불명확 명시적 예시와 스키마 제공
일관성 부족 temperature 높음 temperature=0 또는 낮게 설정

HuggingFace 구현 예시

from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

class PromptingPipeline:
    def __init__(self, model_name="meta-llama/Llama-2-7b-chat-hf"):
        self.tokenizer = AutoTokenizer.from_pretrained(model_name)
        self.model = AutoModelForCausalLM.from_pretrained(
            model_name,
            torch_dtype=torch.float16,
            device_map="auto"
        )

    def generate(self, prompt, max_new_tokens=256, temperature=0.7):
        inputs = self.tokenizer(prompt, return_tensors="pt").to(self.model.device)

        with torch.no_grad():
            outputs = self.model.generate(
                **inputs,
                max_new_tokens=max_new_tokens,
                temperature=temperature,
                do_sample=temperature > 0,
                pad_token_id=self.tokenizer.eos_token_id
            )

        response = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
        return response[len(prompt):]  # 프롬프트 제외

    def few_shot(self, examples, query, instruction=""):
        prompt = instruction + "\n\n" if instruction else ""

        for ex in examples:
            prompt += f"Input: {ex['input']}\nOutput: {ex['output']}\n\n"

        prompt += f"Input: {query}\nOutput:"

        return self.generate(prompt)

    def chain_of_thought(self, question):
        prompt = f"""{question}

단계별로 생각해봅시다:

1단계:"""

        return self.generate(prompt, max_new_tokens=512)

# 사용
pipeline = PromptingPipeline()

# Few-shot
examples = [
    {"input": "I love this!", "output": "positive"},
    {"input": "This is terrible.", "output": "negative"}
]
result = pipeline.few_shot(examples, "Not bad, actually.")

# CoT
result = pipeline.chain_of_thought("철수가 사과 5개 중 2개를 먹었습니다. 남은 사과는?")

참고 자료