프롬프팅 (Prompting)¶
LLM에 명령과 맥락을 제공하여 원하는 출력을 유도하는 기법. 모델 가중치를 수정하지 않고 입력만으로 행동을 조절함.
프롬프팅이 중요한 이유¶
| 관점 | 파인튜닝 | 프롬프팅 |
|---|---|---|
| 비용 | 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개를 먹었습니다. 남은 사과는?")