Split View: LLM 프롬프트 엔지니어링 고급 기법: Chain-of-Thought·ReAct·Tree of Thoughts 실전 적용
LLM 프롬프트 엔지니어링 고급 기법: Chain-of-Thought·ReAct·Tree of Thoughts 실전 적용
- 들어가며
- 프롬프트 엔지니어링 기초 원칙
- Few-shot 프롬프팅 전략
- Chain-of-Thought (CoT) 심층 분석
- ReAct 프레임워크 구현
- Self-Consistency 샘플링
- Tree of Thoughts (ToT)
- 구조화된 출력 (JSON Mode, Function Calling)
- 프로덕션 프롬프트 관리 전략
- 프롬프트 평가 방법론
- 기법 비교표
- 실전 체크리스트
- 마치며

들어가며
프롬프트 엔지니어링은 LLM의 성능을 극대화하기 위한 핵심 기술이다. 단순히 질문을 던지는 것을 넘어서, 모델이 어떻게 추론하고 응답할지를 체계적으로 설계하는 엔지니어링 분야로 발전했다. 2022년 Google의 Chain-of-Thought 논문을 시작으로, ReAct, Self-Consistency, Tree of Thoughts 등 다양한 고급 프롬프팅 기법이 등장하며 LLM의 추론 능력을 비약적으로 향상시켰다.
이 글에서는 각 기법의 이론적 배경과 동작 원리를 분석하고, Python 구현 코드와 함께 프로덕션 환경에서 실제로 적용하는 전략을 다룬다. 특히 구조화된 출력(JSON Mode, Function Calling), 프롬프트 템플릿 관리, 평가 방법론까지 포괄적으로 살펴본다.
프롬프트 엔지니어링 기초 원칙
효과적인 프롬프트를 설계하기 위해서는 다음 네 가지 원칙을 이해해야 한다.
1. 명확성 (Clarity)
모호한 지시를 피하고 구체적인 요구사항을 명시한다. "좋은 글을 써줘" 대신 "500자 이내의 기술 블로그 도입부를 작성해줘. 대상 독자는 주니어 개발자이며, 비유를 활용해 개념을 설명해라"처럼 작성한다.
2. 구조화 (Structure)
역할(Role), 맥락(Context), 지시(Instruction), 출력 형식(Format)을 명확히 분리하여 프롬프트를 구성한다.
3. 제약 조건 (Constraints)
출력의 길이, 형식, 톤앤매너, 금지사항 등을 명시하여 원하는 방향으로 응답을 유도한다.
4. 반복 개선 (Iteration)
프롬프트는 한 번에 완성되지 않는다. 출력 결과를 분석하고 지속적으로 수정하며 최적화한다.
Few-shot 프롬프팅 전략
Few-shot 프롬프팅은 프롬프트에 소수의 입출력 예시를 포함하여 모델이 패턴을 학습하도록 유도하는 기법이다. Zero-shot 대비 15~25%의 정확도 향상을 기대할 수 있으며, 특히 일관된 형식의 출력이 필요한 작업에서 효과적이다.
from openai import OpenAI
client = OpenAI()
def few_shot_sentiment_analysis(text: str) -> str:
"""Few-shot 프롬프팅을 활용한 감성 분석"""
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": "주어진 텍스트의 감성을 분석하여 긍정/부정/중립으로 분류하세요."
},
# Few-shot 예시 1
{"role": "user", "content": "이 제품 정말 최고입니다! 배송도 빠르고 품질도 좋아요."},
{"role": "assistant", "content": "감성: 긍정\n근거: '최고', '빠르고', '좋아요' 등 긍정적 표현이 다수 포함"},
# Few-shot 예시 2
{"role": "user", "content": "배송이 너무 늦었고 제품에 흠집이 있었습니다."},
{"role": "assistant", "content": "감성: 부정\n근거: '너무 늦었고', '흠집이 있었습니다' 등 부정적 경험 서술"},
# Few-shot 예시 3
{"role": "user", "content": "보통 수준의 제품입니다. 가격 대비 적당합니다."},
{"role": "assistant", "content": "감성: 중립\n근거: '보통 수준', '적당합니다' 등 균형적 평가"},
# 실제 분석 대상
{"role": "user", "content": text}
],
temperature=0.0
)
return response.choices[0].message.content
Few-shot 설계 팁:
- 예시는 3~5개가 최적이다. 너무 많으면 컨텍스트 낭비, 너무 적으면 패턴 학습 부족
- 예시의 다양성을 확보하라. 긍정/부정/중립, 짧은 문장/긴 문장 등
- 경계 사례(edge case)를 예시에 포함하라
- 예시의 순서가 결과에 영향을 미칠 수 있으므로, 랜덤하게 배치하는 것을 고려하라
Chain-of-Thought (CoT) 심층 분석
Chain-of-Thought는 2022년 Google의 Wei et al. 논문에서 제안된 기법으로, LLM이 최종 답변에 도달하기 전에 중간 추론 단계를 명시적으로 생성하도록 유도한다. 복잡한 추론 작업에서 정확도를 40~60% 향상시키는 것으로 알려져 있다.
Zero-shot CoT
가장 간단한 형태로, "단계별로 생각해보세요(Let's think step by step)"라는 한 문장만 추가하면 된다.
Few-shot CoT
추론 과정을 포함한 예시를 제공하여 더 정교한 단계적 사고를 유도한다.
def chain_of_thought_math(problem: str) -> str:
"""Chain-of-Thought를 활용한 수학 문제 해결"""
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": (
"당신은 수학 문제를 단계별로 풀어주는 튜터입니다. "
"각 단계를 명확히 설명하고, 최종 답을 제시하세요."
)
},
# CoT 예시: 추론 과정 포함
{
"role": "user",
"content": "카페에 손님이 23명 있었다. 오전에 12명이 더 왔고, 오후에 8명이 나갔다. 현재 카페에 몇 명이 있는가?"
},
{
"role": "assistant",
"content": (
"단계별로 풀어보겠습니다.\n\n"
"1단계: 초기 인원 파악\n"
"- 처음 카페에 있던 손님: 23명\n\n"
"2단계: 오전 변동 계산\n"
"- 12명이 추가로 입장: 23 + 12 = 35명\n\n"
"3단계: 오후 변동 계산\n"
"- 8명이 퇴장: 35 - 8 = 27명\n\n"
"최종 답: 현재 카페에는 27명이 있습니다."
)
},
{"role": "user", "content": problem}
],
temperature=0.0
)
return response.choices[0].message.content
고급 CoT 변형
- Auto-CoT: 다양한 질문을 자동으로 샘플링하고 추론 체인을 생성하여 데모를 구성
- Layered CoT: 추론을 여러 패스(pass)로 나누어 각 단계에서 검증 및 수정 기회를 제공
- Trace-of-Thought: 소규모 모델(7B 파라미터)에 최적화된 CoT로, 하위 문제를 생성하여 산술 추론 향상
ReAct 프레임워크 구현
ReAct (Reasoning + Acting)는 2022년 Yao et al.이 제안한 프레임워크로, LLM이 사고(Thought), 행동(Action), 관찰(Observation) 루프를 반복하며 외부 도구와 상호작용하여 문제를 해결하는 패러다임이다. HotpotQA에서 환각(hallucination)을 크게 줄이고, ALFWorld 벤치마크에서 기존 방법 대비 34% 높은 성공률을 달성했다.
import json
import requests
from openai import OpenAI
client = OpenAI()
# 도구 정의
def search_wikipedia(query: str) -> str:
"""위키피디아 검색 도구"""
url = "https://ko.wikipedia.org/w/api.php"
params = {
"action": "query",
"list": "search",
"srsearch": query,
"format": "json",
"srlimit": 3
}
resp = requests.get(url, params=params)
results = resp.json().get("query", {}).get("search", [])
if not results:
return "검색 결과가 없습니다."
return "\n".join([r["title"] + ": " + r["snippet"] for r in results])
def calculator(expression: str) -> str:
"""안전한 수식 계산기"""
allowed_chars = set("0123456789+-*/.(). ")
if all(c in allowed_chars for c in expression):
try:
result = eval(expression)
return str(result)
except Exception as e:
return f"계산 오류: {e}"
return "허용되지 않는 문자가 포함되어 있습니다."
TOOLS = {
"search_wikipedia": search_wikipedia,
"calculator": calculator,
}
def react_agent(question: str, max_steps: int = 5) -> str:
"""ReAct 패턴을 구현한 에이전트"""
system_prompt = """당신은 ReAct 에이전트입니다. 다음 형식으로 응답하세요:
Thought: 현재 상황을 분석하고, 다음에 무엇을 해야 할지 추론합니다.
Action: 사용할 도구와 입력을 JSON 형식으로 제공합니다.
Observation: 도구 실행 결과를 확인합니다.
... (필요한 만큼 반복)
Final Answer: 최종 답변을 제시합니다.
사용 가능한 도구:
- search_wikipedia: 위키피디아 검색 (입력: 검색어)
- calculator: 수식 계산 (입력: 수학 표현식)"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": question}
]
for step in range(max_steps):
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
temperature=0.0
)
assistant_msg = response.choices[0].message.content
# Final Answer가 포함되면 종료
if "Final Answer:" in assistant_msg:
return assistant_msg.split("Final Answer:")[-1].strip()
# Action 파싱 및 실행
if "Action:" in assistant_msg:
action_line = assistant_msg.split("Action:")[-1].strip()
try:
action_data = json.loads(action_line.split("\n")[0])
tool_name = action_data.get("tool", "")
tool_input = action_data.get("input", "")
if tool_name in TOOLS:
observation = TOOLS[tool_name](tool_input)
else:
observation = f"알 수 없는 도구: {tool_name}"
except json.JSONDecodeError:
observation = "Action 파싱 실패"
messages.append({"role": "assistant", "content": assistant_msg})
messages.append({
"role": "user",
"content": f"Observation: {observation}"
})
return "최대 단계 수에 도달하여 답변을 생성하지 못했습니다."
ReAct의 장점:
- 추론 과정이 투명하여 디버깅과 감사(audit)가 용이
- 외부 도구 연동으로 환각 문제를 크게 완화
- 에이전트 시스템의 기반 아키텍처로 널리 채택
Self-Consistency 샘플링
Self-Consistency는 Wang et al.(2022)이 제안한 디코딩 전략으로, 동일한 프롬프트에 대해 여러 개의 추론 경로를 생성한 뒤 다수결(majority voting)로 최종 답변을 선택한다. CoT와 결합하면 GSM8K에서 17.9%, AQuA에서 12.2%의 정확도 향상을 달성했다.
핵심 아이디어는 간단하다. 복잡한 문제에는 여러 가지 유효한 풀이 경로가 존재하며, 이들이 동일한 정답으로 수렴한다면 그 답이 정확할 확률이 높다는 것이다.
from collections import Counter
from openai import OpenAI
client = OpenAI()
def self_consistency_solve(
question: str,
num_samples: int = 5,
temperature: float = 0.7
) -> dict:
"""Self-Consistency 샘플링으로 정확도를 높이는 추론"""
system_prompt = (
"수학 문제를 단계별로 풀어주세요. "
"마지막 줄에 '정답: [숫자]' 형식으로 답을 표기하세요."
)
answers = []
reasoning_paths = []
for i in range(num_samples):
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": question}
],
temperature=temperature # 다양한 추론 경로 생성
)
content = response.choices[0].message.content
reasoning_paths.append(content)
# 정답 추출
for line in content.split("\n"):
if line.strip().startswith("정답:"):
answer = line.split("정답:")[-1].strip()
answers.append(answer)
break
# 다수결 투표
if not answers:
return {"final_answer": "답변 추출 실패", "confidence": 0.0}
counter = Counter(answers)
most_common = counter.most_common(1)[0]
confidence = most_common[1] / len(answers)
return {
"final_answer": most_common[0],
"confidence": confidence,
"vote_distribution": dict(counter),
"num_samples": num_samples,
"reasoning_paths": reasoning_paths
}
# 사용 예시
result = self_consistency_solve(
"학교에 학생이 450명 있다. 남학생이 전체의 60%이고, "
"남학생 중 25%가 안경을 착용한다. 안경을 착용한 남학생은 몇 명인가?",
num_samples=7
)
print(f"최종 답: {result['final_answer']}")
print(f"신뢰도: {result['confidence']:.1%}")
print(f"투표 분포: {result['vote_distribution']}")
Self-Consistency 최적화 팁:
- temperature를 0.5~0.9 범위로 설정하여 다양성과 품질의 균형을 맞춘다
- 샘플 수는 5~10개가 비용 대비 효과적이다
- Universal Self-Consistency(USC)는 자유 형식 텍스트 생성에도 적용 가능
Tree of Thoughts (ToT)
Tree of Thoughts는 Yao et al.(2023)이 제안한 기법으로, CoT를 확장하여 여러 추론 경로를 트리 구조로 탐색하고 평가한다. Game of 24 과제에서 GPT-4의 CoT 정확도가 4%였던 것이 ToT 적용 후 74%로 급격히 향상되었다.
ToT의 핵심 구성 요소:
- Thought Decomposition: 문제를 중간 사고 단계로 분해
- Thought Generator: 각 노드에서 후보 사고를 생성 (샘플링 또는 제안)
- State Evaluator: 각 사고 상태의 유망성을 평가
- Search Algorithm: BFS(너비 우선) 또는 DFS(깊이 우선) 탐색
from openai import OpenAI
from typing import List
client = OpenAI()
def generate_thoughts(problem: str, current_state: str, n: int = 3) -> List[str]:
"""현재 상태에서 가능한 다음 사고를 생성"""
prompt = f"""문제: {problem}
현재까지의 추론: {current_state}
이 상태에서 가능한 다음 추론 단계를 {n}개 제안하세요.
각 단계는 서로 다른 접근법이어야 합니다.
형식:
1. [첫 번째 접근]
2. [두 번째 접근]
3. [세 번째 접근]"""
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
temperature=0.8
)
content = response.choices[0].message.content
thoughts = []
for line in content.split("\n"):
line = line.strip()
if line and line[0].isdigit() and "." in line:
thoughts.append(line.split(".", 1)[1].strip())
return thoughts[:n]
def evaluate_thought(problem: str, thought_path: str) -> float:
"""사고 경로의 유망성을 0~1 사이로 평가"""
prompt = f"""문제: {problem}
추론 경로: {thought_path}
이 추론 경로가 정답에 도달할 가능성을 평가하세요.
0.0 (전혀 유망하지 않음) ~ 1.0 (매우 유망함) 사이의 점수를 제시하세요.
점수만 숫자로 응답하세요."""
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
temperature=0.1
)
try:
score = float(response.choices[0].message.content.strip())
return min(max(score, 0.0), 1.0)
except ValueError:
return 0.5
def tree_of_thoughts_bfs(
problem: str,
max_depth: int = 3,
beam_width: int = 2
) -> str:
"""BFS 기반 Tree of Thoughts 구현"""
# 초기 상태
current_states = [("", 1.0)] # (추론 경로, 점수)
for depth in range(max_depth):
all_candidates = []
for state, _ in current_states:
# 각 상태에서 후보 사고 생성
thoughts = generate_thoughts(problem, state)
for thought in thoughts:
new_path = f"{state}\n단계 {depth+1}: {thought}" if state else f"단계 1: {thought}"
score = evaluate_thought(problem, new_path)
all_candidates.append((new_path, score))
# beam_width만큼 상위 후보 선택
all_candidates.sort(key=lambda x: x[1], reverse=True)
current_states = all_candidates[:beam_width]
print(f"깊이 {depth+1}: 최고 점수 = {current_states[0][1]:.2f}")
# 최종 답변 생성
best_path = current_states[0][0]
final_prompt = f"""문제: {problem}
추론 경로: {best_path}
위 추론을 바탕으로 최종 답변을 작성하세요."""
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": final_prompt}],
temperature=0.0
)
return response.choices[0].message.content
ToT 적용 시 주의사항:
- API 호출 횟수가 기하급수적으로 증가하므로 비용을 면밀히 관리해야 한다
- 단순한 문제에는 오히려 과도한 기법이다. 복잡한 계획, 창의적 글쓰기, 퍼즐 풀이에 적합
- beam_width와 max_depth를 문제의 복잡도에 맞게 조절한다
구조화된 출력 (JSON Mode, Function Calling)
프로덕션 환경에서는 LLM 출력을 프로그래밍적으로 처리해야 한다. 2026년 현재 주요 LLM 제공자들이 네이티브 구조화 출력을 지원하며, Pydantic과 같은 검증 라이브러리와 결합하면 안정적인 파이프라인을 구축할 수 있다.
JSON Mode 활용
from openai import OpenAI
from pydantic import BaseModel, Field
from typing import List, Optional
client = OpenAI()
class CodeReview(BaseModel):
"""코드 리뷰 결과 스키마"""
file_path: str = Field(description="리뷰 대상 파일 경로")
severity: str = Field(description="심각도: critical, warning, info")
category: str = Field(description="카테고리: security, performance, style, logic")
line_number: Optional[int] = Field(description="해당 라인 번호")
message: str = Field(description="리뷰 코멘트")
suggestion: str = Field(description="개선 제안 코드")
class CodeReviewResult(BaseModel):
"""전체 코드 리뷰 결과"""
reviews: List[CodeReview]
summary: str = Field(description="리뷰 요약")
overall_score: int = Field(description="전체 점수 (1-10)")
def structured_code_review(code: str, language: str = "python") -> CodeReviewResult:
"""구조화된 출력을 활용한 코드 리뷰"""
response = client.beta.chat.completions.parse(
model="gpt-4o",
messages=[
{
"role": "system",
"content": (
f"당신은 시니어 {language} 개발자입니다. "
"주어진 코드를 리뷰하고 구조화된 형식으로 피드백을 제공하세요."
)
},
{"role": "user", "content": f"다음 코드를 리뷰해주세요:\n\n{code}"}
],
response_format=CodeReviewResult
)
return response.choices[0].message.parsed
Function Calling 패턴
Function Calling은 LLM이 사전 정의된 함수를 호출하도록 하여, 외부 시스템과의 안정적인 통합을 가능하게 한다.
import json
from openai import OpenAI
client = OpenAI()
# 도구 스키마 정의
tools = [
{
"type": "function",
"function": {
"name": "get_stock_price",
"description": "주식의 현재 가격을 조회합니다",
"parameters": {
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "주식 심볼 (예: AAPL, GOOGL)"
},
"currency": {
"type": "string",
"enum": ["USD", "KRW", "JPY"],
"description": "표시 통화"
}
},
"required": ["symbol"]
}
}
},
{
"type": "function",
"function": {
"name": "create_alert",
"description": "주가 알림을 설정합니다",
"parameters": {
"type": "object",
"properties": {
"symbol": {"type": "string"},
"target_price": {"type": "number"},
"direction": {
"type": "string",
"enum": ["above", "below"]
}
},
"required": ["symbol", "target_price", "direction"]
}
}
}
]
def function_calling_agent(user_message: str) -> str:
"""Function Calling을 활용한 에이전트"""
messages = [
{
"role": "system",
"content": "당신은 주식 정보를 제공하는 금융 어시스턴트입니다."
},
{"role": "user", "content": user_message}
]
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
tool_choice="auto"
)
msg = response.choices[0].message
# Function Call 처리
if msg.tool_calls:
for tool_call in msg.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
# 실제 함수 실행 (여기서는 모의 데이터)
if func_name == "get_stock_price":
result = json.dumps({
"symbol": func_args["symbol"],
"price": 185.50,
"change": "+2.3%"
})
elif func_name == "create_alert":
result = json.dumps({
"status": "success",
"alert_id": "alert_12345"
})
else:
result = json.dumps({"error": "Unknown function"})
messages.append(msg)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
# 최종 응답 생성
final_response = client.chat.completions.create(
model="gpt-4o",
messages=messages
)
return final_response.choices[0].message.content
return msg.content
프로덕션 프롬프트 관리 전략
프로덕션 환경에서 프롬프트를 효과적으로 관리하려면 코드와 동일한 수준의 엔지니어링 프랙티스가 필요하다.
버전 관리
프롬프트를 별도 파일로 분리하고 Git으로 관리한다. 각 변경에 대한 이유와 성능 영향을 기록한다.
A/B 테스트
새로운 프롬프트를 배포할 때 일부 트래픽에만 적용하여 성능을 비교한다. 정확도, 지연시간, 비용 세 가지 지표를 동시에 모니터링한다.
가드레일
프롬프트 인젝션 방어, 출력 검증, 유해 콘텐츠 필터링 등의 안전장치를 반드시 구현한다.
프롬프트 템플릿 시스템
from dataclasses import dataclass, field
from typing import Dict, Any
from datetime import datetime
@dataclass
class PromptTemplate:
"""프롬프트 템플릿 관리 클래스"""
name: str
version: str
template: str
metadata: Dict[str, Any] = field(default_factory=dict)
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
def render(self, **kwargs) -> str:
"""변수를 치환하여 최종 프롬프트 생성"""
result = self.template
for key, value in kwargs.items():
placeholder = f"__{key.upper()}__"
result = result.replace(placeholder, str(value))
return result
# 프롬프트 레지스트리
class PromptRegistry:
def __init__(self):
self._templates: Dict[str, PromptTemplate] = {}
def register(self, template: PromptTemplate):
key = f"{template.name}:{template.version}"
self._templates[key] = template
def get(self, name: str, version: str = "latest") -> PromptTemplate:
if version == "latest":
candidates = [
v for k, v in self._templates.items()
if k.startswith(f"{name}:")
]
return max(candidates, key=lambda t: t.version)
return self._templates[f"{name}:{version}"]
# 사용 예시
registry = PromptRegistry()
registry.register(PromptTemplate(
name="code_review",
version="2.1",
template=(
"당신은 시니어 __LANGUAGE__ 개발자입니다.\n"
"다음 코드를 리뷰하세요.\n\n"
"리뷰 기준:\n"
"- 보안 취약점\n"
"- 성능 이슈\n"
"- 코드 스타일\n\n"
"코드:\n__CODE__"
),
metadata={"model": "gpt-4o", "temperature": 0.0}
))
프롬프트 평가 방법론
프롬프트의 효과를 객관적으로 측정하기 위해 체계적인 평가 파이프라인이 필요하다.
평가 지표:
| 지표 | 설명 | 측정 방법 |
|---|---|---|
| 정확도 | 정답과의 일치 비율 | 자동 비교 또는 LLM-as-Judge |
| 일관성 | 동일 입력에 대한 출력 변동성 | 여러 번 실행 후 표준편차 |
| 지연시간 | 응답까지의 소요 시간 | API 호출 시간 측정 |
| 비용 | 토큰 소비량 기반 비용 | 입출력 토큰 수 추적 |
| 안전성 | 유해 콘텐츠 생성 비율 | 안전성 분류기 적용 |
| 형식 준수율 | 지정된 출력 형식 준수 비율 | 스키마 검증 |
LLM-as-Judge 패턴: 자동 평가가 어려운 자유 형식 텍스트에 대해, 별도의 LLM을 평가자로 활용하여 품질을 점수화한다. 평가 기준을 루브릭으로 명시하고, 평가자 LLM도 CoT를 활용하도록 유도하면 평가 정확도가 향상된다.
기법 비교표
| 기법 | 핵심 원리 | 정확도 향상 | API 호출 수 | 적합한 작업 | 복잡도 |
|---|---|---|---|---|---|
| Zero-shot | 직접 질문 | 기준선 | 1회 | 단순 작업 | 낮음 |
| Few-shot | 예시 기반 학습 | +15~25% | 1회 | 형식 일관성 | 낮음 |
| Zero-shot CoT | 단계별 추론 유도 | +20~40% | 1회 | 수학, 논리 | 낮음 |
| Few-shot CoT | 추론 예시 제공 | +40~60% | 1회 | 복잡 추론 | 중간 |
| ReAct | 추론+행동 루프 | 작업 의존 | 다수 | 도구 활용 | 중간 |
| Self-Consistency | 다수결 투표 | +10~18% | 5~10회 | 추론 정확도 | 중간 |
| Tree of Thoughts | 트리 탐색 | +70% (특정 과제) | 다수 | 계획, 퍼즐 | 높음 |
| Structured Output | 스키마 강제 | 형식 100% | 1회 | 데이터 추출 | 낮음 |
실전 체크리스트
프로덕션에 프롬프트를 배포하기 전 다음 항목을 점검하라.
- 기법 선택: 작업의 복잡도에 맞는 기법을 선택했는가? 단순 작업에 ToT를 쓰는 것은 낭비다
- 예시 품질: Few-shot 예시가 다양하고 정확한가? 경계 사례를 포함하는가?
- CoT 활용: 추론이 필요한 작업에 단계별 사고를 유도하고 있는가?
- 출력 형식: 구조화된 출력이 필요한 경우 JSON Mode 또는 Function Calling을 활용하는가?
- 비용 관리: Self-Consistency나 ToT 적용 시 API 비용을 추정하고 예산을 설정했는가?
- 평가 파이프라인: 프롬프트 변경의 효과를 측정할 자동화된 평가 시스템이 있는가?
- 가드레일: 프롬프트 인젝션 방어와 출력 검증 로직이 구현되어 있는가?
- 버전 관리: 프롬프트가 Git으로 관리되고 변경 이력이 추적되는가?
- 모니터링: 정확도, 지연시간, 비용을 실시간으로 모니터링하는가?
- 폴백 전략: 프롬프트 실패 시 대체 전략(더 간단한 프롬프트, 기본값 반환 등)이 준비되어 있는가?
마치며
프롬프트 엔지니어링은 단순한 "질문 잘하기"가 아니라, LLM의 추론 능력을 체계적으로 끌어내는 엔지니어링 분야이다. Chain-of-Thought로 단계적 추론을 유도하고, ReAct로 외부 도구와 연동하며, Self-Consistency로 신뢰도를 높이고, Tree of Thoughts로 복잡한 문제를 탐색하는 것, 이 기법들의 원리를 이해하고 적재적소에 활용하는 것이 핵심이다.
특히 o1, o3 같은 추론 네이티브 모델이 등장하면서 일부 기법은 모델 자체에 내재화되는 추세이지만, 프롬프트 설계의 기본 원칙과 프로덕션 관리 전략은 여전히 유효하다. 프롬프트를 코드처럼 관리하고, 평가 파이프라인으로 품질을 보장하며, 지속적으로 개선하는 문화를 구축하는 것이 프로덕션 LLM 시스템의 성공 열쇠이다.
Advanced LLM Prompt Engineering: Chain-of-Thought, ReAct, and Tree of Thoughts in Practice
- Introduction
- Fundamental Principles of Prompt Engineering
- Few-shot Prompting Strategies
- Chain-of-Thought (CoT) Deep Dive
- ReAct Framework Implementation
- Self-Consistency Sampling
- Tree of Thoughts (ToT)
- Structured Output (JSON Mode, Function Calling)
- Production Prompt Management Strategies
- Prompt Evaluation Methodology
- Technique Comparison Table
- Production Checklist
- Conclusion

Introduction
Prompt engineering is a core discipline for maximizing LLM performance. It has evolved beyond simply asking questions into a systematic engineering field that designs how models reason and respond. Starting with Google's Chain-of-Thought paper in 2022, a series of advanced prompting techniques -- ReAct, Self-Consistency, Tree of Thoughts -- have emerged to dramatically improve LLM reasoning capabilities.
This article analyzes the theoretical foundations and mechanics of each technique, provides Python implementation code, and covers practical strategies for applying them in production environments. We also address structured output (JSON Mode, Function Calling), prompt template management, and evaluation methodologies.
Fundamental Principles of Prompt Engineering
Effective prompt design requires understanding four core principles.
1. Clarity
Avoid ambiguous instructions and specify concrete requirements. Instead of "Write a good article," use "Write a 500-word introduction for a technical blog post. The target audience is junior developers, and use analogies to explain concepts."
2. Structure
Clearly separate the Role, Context, Instruction, and Output Format within your prompt.
3. Constraints
Specify output length, format, tone, and restrictions to guide responses in the desired direction.
4. Iteration
Prompts are never perfect on the first try. Analyze outputs and continuously refine and optimize.
Few-shot Prompting Strategies
Few-shot prompting includes a small number of input-output examples in the prompt so the model learns the desired pattern. It can achieve 15-25% accuracy improvement over zero-shot prompting and is especially effective for tasks requiring consistent output formatting.
from openai import OpenAI
client = OpenAI()
def few_shot_sentiment_analysis(text: str) -> str:
"""Sentiment analysis using few-shot prompting"""
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": "Analyze the sentiment of the given text and classify it as positive, negative, or neutral."
},
# Few-shot example 1
{"role": "user", "content": "This product is amazing! Fast shipping and great quality."},
{"role": "assistant", "content": "Sentiment: Positive\nReason: Contains positive expressions like 'amazing', 'fast', 'great quality'"},
# Few-shot example 2
{"role": "user", "content": "The delivery was extremely late and the product had scratches."},
{"role": "assistant", "content": "Sentiment: Negative\nReason: Describes negative experiences with 'extremely late', 'had scratches'"},
# Few-shot example 3
{"role": "user", "content": "It's an average product. Fair for the price."},
{"role": "assistant", "content": "Sentiment: Neutral\nReason: Balanced evaluation with 'average', 'fair'"},
# Actual analysis target
{"role": "user", "content": text}
],
temperature=0.0
)
return response.choices[0].message.content
Few-shot Design Tips:
- 3-5 examples is optimal. Too many waste context; too few fail to establish the pattern
- Ensure diversity in examples: positive/negative/neutral, short/long sentences
- Include edge cases in your examples
- Example ordering can affect results, so consider randomizing placement
Chain-of-Thought (CoT) Deep Dive
Chain-of-Thought was proposed in 2022 by Wei et al. at Google. It guides LLMs to explicitly generate intermediate reasoning steps before arriving at a final answer. It has been shown to improve accuracy by 40-60% on complex reasoning tasks.
Zero-shot CoT
The simplest form -- just append "Let's think step by step" to your prompt.
Few-shot CoT
Provides examples that include the reasoning process to induce more sophisticated step-by-step thinking.
def chain_of_thought_math(problem: str) -> str:
"""Math problem solving using Chain-of-Thought"""
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": (
"You are a math tutor who solves problems step by step. "
"Explain each step clearly and present the final answer."
)
},
# CoT example with reasoning process
{
"role": "user",
"content": "A cafe has 23 customers. 12 more arrived in the morning, and 8 left in the afternoon. How many customers are in the cafe now?"
},
{
"role": "assistant",
"content": (
"Let me solve this step by step.\n\n"
"Step 1: Identify the initial count\n"
"- Customers initially in the cafe: 23\n\n"
"Step 2: Calculate morning change\n"
"- 12 more arrived: 23 + 12 = 35\n\n"
"Step 3: Calculate afternoon change\n"
"- 8 customers left: 35 - 8 = 27\n\n"
"Final Answer: There are 27 customers in the cafe now."
)
},
{"role": "user", "content": problem}
],
temperature=0.0
)
return response.choices[0].message.content
Advanced CoT Variants
- Auto-CoT: Automatically samples diverse questions and generates reasoning chains to construct demonstrations
- Layered CoT: Splits reasoning into multiple passes, allowing review and correction at each stage
- Trace-of-Thought: Optimized for smaller models (~7B parameters), creates subproblems to improve arithmetic reasoning
ReAct Framework Implementation
ReAct (Reasoning + Acting), proposed by Yao et al. in 2022, is a framework where LLMs iterate through Thought, Action, and Observation loops while interacting with external tools to solve problems. It significantly reduces hallucinations on HotpotQA and achieved a 34% higher success rate than prior methods on the ALFWorld benchmark.
import json
import requests
from openai import OpenAI
client = OpenAI()
# Tool definitions
def search_wikipedia(query: str) -> str:
"""Wikipedia search tool"""
url = "https://en.wikipedia.org/w/api.php"
params = {
"action": "query",
"list": "search",
"srsearch": query,
"format": "json",
"srlimit": 3
}
resp = requests.get(url, params=params)
results = resp.json().get("query", {}).get("search", [])
if not results:
return "No search results found."
return "\n".join([r["title"] + ": " + r["snippet"] for r in results])
def calculator(expression: str) -> str:
"""Safe arithmetic calculator"""
allowed_chars = set("0123456789+-*/.(). ")
if all(c in allowed_chars for c in expression):
try:
result = eval(expression)
return str(result)
except Exception as e:
return f"Calculation error: {e}"
return "Contains disallowed characters."
TOOLS = {
"search_wikipedia": search_wikipedia,
"calculator": calculator,
}
def react_agent(question: str, max_steps: int = 5) -> str:
"""Agent implementing the ReAct pattern"""
system_prompt = """You are a ReAct agent. Respond in the following format:
Thought: Analyze the current situation and reason about what to do next.
Action: Provide the tool and input in JSON format.
Observation: Review the tool execution result.
... (repeat as needed)
Final Answer: Present the final answer.
Available tools:
- search_wikipedia: Search Wikipedia (input: search query)
- calculator: Arithmetic calculation (input: math expression)"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": question}
]
for step in range(max_steps):
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
temperature=0.0
)
assistant_msg = response.choices[0].message.content
# Exit if Final Answer is present
if "Final Answer:" in assistant_msg:
return assistant_msg.split("Final Answer:")[-1].strip()
# Parse and execute Action
if "Action:" in assistant_msg:
action_line = assistant_msg.split("Action:")[-1].strip()
try:
action_data = json.loads(action_line.split("\n")[0])
tool_name = action_data.get("tool", "")
tool_input = action_data.get("input", "")
if tool_name in TOOLS:
observation = TOOLS[tool_name](tool_input)
else:
observation = f"Unknown tool: {tool_name}"
except json.JSONDecodeError:
observation = "Failed to parse Action"
messages.append({"role": "assistant", "content": assistant_msg})
messages.append({
"role": "user",
"content": f"Observation: {observation}"
})
return "Reached maximum steps without generating an answer."
Benefits of ReAct:
- Transparent reasoning process makes debugging and auditing straightforward
- External tool integration significantly mitigates hallucination issues
- Widely adopted as the foundational architecture for agent systems
Self-Consistency Sampling
Self-Consistency, proposed by Wang et al. (2022), is a decoding strategy that generates multiple reasoning paths for the same prompt and selects the final answer through majority voting. When combined with CoT, it achieved accuracy improvements of 17.9% on GSM8K and 12.2% on AQuA.
The core idea is simple: complex problems typically have multiple valid solution paths, and if these converge on the same answer, that answer is likely correct.
from collections import Counter
from openai import OpenAI
client = OpenAI()
def self_consistency_solve(
question: str,
num_samples: int = 5,
temperature: float = 0.7
) -> dict:
"""Improved reasoning accuracy through Self-Consistency sampling"""
system_prompt = (
"Solve the math problem step by step. "
"On the last line, write the answer in the format 'Answer: [number]'."
)
answers = []
reasoning_paths = []
for i in range(num_samples):
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": question}
],
temperature=temperature # Generate diverse reasoning paths
)
content = response.choices[0].message.content
reasoning_paths.append(content)
# Extract answer
for line in content.split("\n"):
if line.strip().startswith("Answer:"):
answer = line.split("Answer:")[-1].strip()
answers.append(answer)
break
# Majority voting
if not answers:
return {"final_answer": "Failed to extract answer", "confidence": 0.0}
counter = Counter(answers)
most_common = counter.most_common(1)[0]
confidence = most_common[1] / len(answers)
return {
"final_answer": most_common[0],
"confidence": confidence,
"vote_distribution": dict(counter),
"num_samples": num_samples,
"reasoning_paths": reasoning_paths
}
# Usage example
result = self_consistency_solve(
"A school has 450 students. 60% are boys, and 25% of boys wear glasses. "
"How many boys wear glasses?",
num_samples=7
)
print(f"Final answer: {result['final_answer']}")
print(f"Confidence: {result['confidence']:.1%}")
print(f"Vote distribution: {result['vote_distribution']}")
Self-Consistency Optimization Tips:
- Set temperature in the 0.5-0.9 range to balance diversity and quality
- 5-10 samples is cost-effective for most applications
- Universal Self-Consistency (USC) extends the technique to free-form text generation
Tree of Thoughts (ToT)
Tree of Thoughts, proposed by Yao et al. (2023), extends CoT by exploring and evaluating multiple reasoning paths in a tree structure. On the Game of 24 task, GPT-4's CoT accuracy was 4%, which jumped to 74% with ToT.
Core components of ToT:
- Thought Decomposition: Breaking the problem into intermediate thought steps
- Thought Generator: Generating candidate thoughts at each node (sampling or proposing)
- State Evaluator: Assessing the promise of each thought state
- Search Algorithm: BFS (breadth-first) or DFS (depth-first) traversal
from openai import OpenAI
from typing import List
client = OpenAI()
def generate_thoughts(problem: str, current_state: str, n: int = 3) -> List[str]:
"""Generate possible next thoughts from the current state"""
prompt = f"""Problem: {problem}
Reasoning so far: {current_state}
Propose {n} possible next reasoning steps from this state.
Each step should represent a different approach.
Format:
1. [First approach]
2. [Second approach]
3. [Third approach]"""
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
temperature=0.8
)
content = response.choices[0].message.content
thoughts = []
for line in content.split("\n"):
line = line.strip()
if line and line[0].isdigit() and "." in line:
thoughts.append(line.split(".", 1)[1].strip())
return thoughts[:n]
def evaluate_thought(problem: str, thought_path: str) -> float:
"""Evaluate the promise of a reasoning path on a 0-1 scale"""
prompt = f"""Problem: {problem}
Reasoning path: {thought_path}
Evaluate how likely this reasoning path is to reach the correct answer.
Provide a score between 0.0 (not promising at all) and 1.0 (very promising).
Respond with only the numeric score."""
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
temperature=0.1
)
try:
score = float(response.choices[0].message.content.strip())
return min(max(score, 0.0), 1.0)
except ValueError:
return 0.5
def tree_of_thoughts_bfs(
problem: str,
max_depth: int = 3,
beam_width: int = 2
) -> str:
"""BFS-based Tree of Thoughts implementation"""
# Initial state
current_states = [("", 1.0)] # (reasoning path, score)
for depth in range(max_depth):
all_candidates = []
for state, _ in current_states:
# Generate candidate thoughts from each state
thoughts = generate_thoughts(problem, state)
for thought in thoughts:
new_path = f"{state}\nStep {depth+1}: {thought}" if state else f"Step 1: {thought}"
score = evaluate_thought(problem, new_path)
all_candidates.append((new_path, score))
# Select top candidates by beam_width
all_candidates.sort(key=lambda x: x[1], reverse=True)
current_states = all_candidates[:beam_width]
print(f"Depth {depth+1}: Best score = {current_states[0][1]:.2f}")
# Generate final answer
best_path = current_states[0][0]
final_prompt = f"""Problem: {problem}
Reasoning path: {best_path}
Based on the reasoning above, write the final answer."""
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": final_prompt}],
temperature=0.0
)
return response.choices[0].message.content
Considerations When Using ToT:
- API call count grows exponentially, so costs must be carefully managed
- Overkill for simple problems. Best suited for complex planning, creative writing, and puzzle-solving
- Tune beam_width and max_depth according to problem complexity
Structured Output (JSON Mode, Function Calling)
In production environments, LLM outputs must be processed programmatically. As of 2026, major LLM providers support native structured output, and combining them with validation libraries like Pydantic enables robust pipelines.
JSON Mode Usage
from openai import OpenAI
from pydantic import BaseModel, Field
from typing import List, Optional
client = OpenAI()
class CodeReview(BaseModel):
"""Code review result schema"""
file_path: str = Field(description="File path under review")
severity: str = Field(description="Severity: critical, warning, info")
category: str = Field(description="Category: security, performance, style, logic")
line_number: Optional[int] = Field(description="Line number")
message: str = Field(description="Review comment")
suggestion: str = Field(description="Suggested improvement code")
class CodeReviewResult(BaseModel):
"""Overall code review result"""
reviews: List[CodeReview]
summary: str = Field(description="Review summary")
overall_score: int = Field(description="Overall score (1-10)")
def structured_code_review(code: str, language: str = "python") -> CodeReviewResult:
"""Code review with structured output"""
response = client.beta.chat.completions.parse(
model="gpt-4o",
messages=[
{
"role": "system",
"content": (
f"You are a senior {language} developer. "
"Review the given code and provide feedback in a structured format."
)
},
{"role": "user", "content": f"Please review the following code:\n\n{code}"}
],
response_format=CodeReviewResult
)
return response.choices[0].message.parsed
Function Calling Pattern
Function Calling enables LLMs to invoke predefined functions, allowing reliable integration with external systems.
import json
from openai import OpenAI
client = OpenAI()
# Tool schema definition
tools = [
{
"type": "function",
"function": {
"name": "get_stock_price",
"description": "Retrieves the current price of a stock",
"parameters": {
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "Stock symbol (e.g., AAPL, GOOGL)"
},
"currency": {
"type": "string",
"enum": ["USD", "KRW", "JPY"],
"description": "Display currency"
}
},
"required": ["symbol"]
}
}
},
{
"type": "function",
"function": {
"name": "create_alert",
"description": "Sets up a stock price alert",
"parameters": {
"type": "object",
"properties": {
"symbol": {"type": "string"},
"target_price": {"type": "number"},
"direction": {
"type": "string",
"enum": ["above", "below"]
}
},
"required": ["symbol", "target_price", "direction"]
}
}
}
]
def function_calling_agent(user_message: str) -> str:
"""Agent using Function Calling"""
messages = [
{
"role": "system",
"content": "You are a financial assistant that provides stock information."
},
{"role": "user", "content": user_message}
]
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
tool_choice="auto"
)
msg = response.choices[0].message
# Handle Function Calls
if msg.tool_calls:
for tool_call in msg.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
# Execute function (mock data here)
if func_name == "get_stock_price":
result = json.dumps({
"symbol": func_args["symbol"],
"price": 185.50,
"change": "+2.3%"
})
elif func_name == "create_alert":
result = json.dumps({
"status": "success",
"alert_id": "alert_12345"
})
else:
result = json.dumps({"error": "Unknown function"})
messages.append(msg)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
# Generate final response
final_response = client.chat.completions.create(
model="gpt-4o",
messages=messages
)
return final_response.choices[0].message.content
return msg.content
Production Prompt Management Strategies
Managing prompts effectively in production requires the same level of engineering practices as managing code.
Version Control
Separate prompts into dedicated files and manage them with Git. Record the rationale and performance impact for each change.
A/B Testing
When deploying a new prompt, apply it to a subset of traffic and compare performance. Monitor accuracy, latency, and cost simultaneously.
Guardrails
Implement safeguards such as prompt injection defense, output validation, and harmful content filtering.
Prompt Template System
from dataclasses import dataclass, field
from typing import Dict, Any
from datetime import datetime
@dataclass
class PromptTemplate:
"""Prompt template management class"""
name: str
version: str
template: str
metadata: Dict[str, Any] = field(default_factory=dict)
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
def render(self, **kwargs) -> str:
"""Render final prompt with variable substitution"""
result = self.template
for key, value in kwargs.items():
placeholder = f"__{key.upper()}__"
result = result.replace(placeholder, str(value))
return result
# Prompt registry
class PromptRegistry:
def __init__(self):
self._templates: Dict[str, PromptTemplate] = {}
def register(self, template: PromptTemplate):
key = f"{template.name}:{template.version}"
self._templates[key] = template
def get(self, name: str, version: str = "latest") -> PromptTemplate:
if version == "latest":
candidates = [
v for k, v in self._templates.items()
if k.startswith(f"{name}:")
]
return max(candidates, key=lambda t: t.version)
return self._templates[f"{name}:{version}"]
# Usage example
registry = PromptRegistry()
registry.register(PromptTemplate(
name="code_review",
version="2.1",
template=(
"You are a senior __LANGUAGE__ developer.\n"
"Review the following code.\n\n"
"Review criteria:\n"
"- Security vulnerabilities\n"
"- Performance issues\n"
"- Code style\n\n"
"Code:\n__CODE__"
),
metadata={"model": "gpt-4o", "temperature": 0.0}
))
Prompt Evaluation Methodology
A systematic evaluation pipeline is needed to objectively measure prompt effectiveness.
Evaluation Metrics:
| Metric | Description | Measurement Method |
|---|---|---|
| Accuracy | Match rate with correct answers | Automated comparison or LLM-as-Judge |
| Consistency | Output variability for same input | Standard deviation across multiple runs |
| Latency | Time to response | API call time measurement |
| Cost | Token consumption-based cost | Input/output token count tracking |
| Safety | Rate of harmful content generation | Safety classifier application |
| Format Compliance | Adherence to specified output format | Schema validation |
LLM-as-Judge Pattern: For free-form text where automated evaluation is difficult, use a separate LLM as an evaluator to score quality. Specifying evaluation criteria as a rubric and having the evaluator LLM use CoT improves evaluation accuracy.
Technique Comparison Table
| Technique | Core Principle | Accuracy Gain | API Calls | Best For | Complexity |
|---|---|---|---|---|---|
| Zero-shot | Direct question | Baseline | 1 | Simple tasks | Low |
| Few-shot | Example-based learning | +15-25% | 1 | Format consistency | Low |
| Zero-shot CoT | Step-by-step reasoning | +20-40% | 1 | Math, logic | Low |
| Few-shot CoT | Reasoning examples | +40-60% | 1 | Complex reasoning | Medium |
| ReAct | Reasoning+action loop | Task-dependent | Multiple | Tool usage | Medium |
| Self-Consistency | Majority voting | +10-18% | 5-10 | Reasoning accuracy | Medium |
| Tree of Thoughts | Tree search | +70% (specific tasks) | Multiple | Planning, puzzles | High |
| Structured Output | Schema enforcement | 100% format | 1 | Data extraction | Low |
Production Checklist
Review the following items before deploying prompts to production.
- Technique Selection: Have you chosen a technique matching the task complexity? Using ToT for simple tasks is wasteful
- Example Quality: Are Few-shot examples diverse and accurate? Do they include edge cases?
- CoT Usage: Are you inducing step-by-step thinking for tasks that require reasoning?
- Output Format: Are you using JSON Mode or Function Calling for tasks needing structured output?
- Cost Management: Have you estimated API costs and set budgets when using Self-Consistency or ToT?
- Evaluation Pipeline: Do you have an automated evaluation system to measure the effect of prompt changes?
- Guardrails: Are prompt injection defense and output validation logic implemented?
- Version Control: Are prompts managed with Git with tracked change history?
- Monitoring: Are you monitoring accuracy, latency, and cost in real-time?
- Fallback Strategy: Do you have fallback strategies (simpler prompts, default values, etc.) for prompt failures?
Conclusion
Prompt engineering is not just about "asking good questions" -- it is an engineering discipline for systematically eliciting reasoning capabilities from LLMs. Inducing step-by-step reasoning with Chain-of-Thought, integrating external tools with ReAct, boosting reliability with Self-Consistency, and exploring complex problems with Tree of Thoughts -- understanding the principles of these techniques and applying them where they fit is the key.
While reasoning-native models like o1 and o3 are internalizing some of these techniques, the fundamental principles of prompt design and production management strategies remain essential. Managing prompts like code, ensuring quality with evaluation pipelines, and building a culture of continuous improvement are the keys to successful production LLM systems.