LLM을 쓰다 보면 이런 상황이 생겨요.
"분명히 풀 수 있는 문제인데 틀린 답을 내놓네. 어떻게 하면 더 정확하게 추론하게 만들지?"
모델을 바꾸거나 파인튜닝하지 않아도 추론 방식을 바꾸는 것만으로 정확도를 크게 올릴 수 있어요. 이번 글에서는 세 가지 핵심 추론 기법 — Chain-of-Thought, Tree-of-Thought, Self-Consistency — 을 원리부터 실전 적용까지 비교해 드릴게요.
왜 추론 기법이 필요한가
LLM은 기본적으로 다음 토큰을 예측하는 모델이에요. 바로 답을 내놓으라고 하면 중간 과정 없이 확률적으로 그럴듯한 답을 생성해요. 복잡한 문제에서는 이게 틀릴 확률이 높아요.
# 바로 답하기 — 틀리기 쉬움
질문: "농부가 닭 17마리와 양 10마리를 키운다. 다리는 총 몇 개인가?"
LLM: "54개" (틀림)
# 단계별 추론 — 정확함
질문: (동일)
LLM: "닭은 다리가 2개이므로 17 × 2 = 34개.
양은 다리가 4개이므로 10 × 4 = 40개.
총 34 + 40 = 74개"
중간 추론 과정을 거치게 하면 오류를 스스로 잡을 수 있어요.
기법 1: Chain-of-Thought (CoT)
개념
LLM이 최종 답을 내놓기 전에 중간 추론 단계를 순서대로 생성하게 해요. 사람이 수학 문제를 풀 때 풀이 과정을 쓰는 것과 같아요.
단계 1 → 단계 2 → 단계 3 → ... → 최종 답
두 가지 방식
Few-shot CoT — 예시를 보여줘서 같은 방식으로 풀게 해요.
prompt = """
다음 예시를 참고해서 문제를 풀어줘.
예시:
Q: 사과 5개가 있고 3개를 더 샀다. 그 중 2개를 먹었다. 남은 사과는?
A: 처음에 5개 있었다.
3개를 더 사면 5 + 3 = 8개.
2개를 먹으면 8 - 2 = 6개.
정답: 6개
Q: 책이 12권 있고 7권을 빌려줬다. 그 후 4권을 더 샀다. 남은 책은?
A:
"""
Zero-shot CoT — 예시 없이 "단계별로 생각해봐"라는 말만으로 추론을 유도해요.
prompt = """
책이 12권 있고 7권을 빌려줬다. 그 후 4권을 더 샀다. 남은 책은?
단계별로 생각해봐.
"""
# 또는 영어로
prompt += "\nLet's think step by step."
"Let's think step by step" 한 줄만 추가해도 성능이 크게 올라요. PaLM 540B 기준으로 GSM8K 수학 벤치마크에서 58%를 기록했어요.
장단점
장점 — 구현이 간단해요. 프롬프트 수정만으로 적용 가능해요. 추론 과정이 보여서 디버깅이 쉬워요.
단점 — 한 줄기 추론만 하기 때문에 첫 단계에서 방향이 틀리면 그대로 틀린 답으로 가요. 백트래킹이 없어요.
언제 써야 하나
수학 문제, 논리 추론, 단계가 명확한 문제. 대부분의 복잡한 질문에 기본으로 적용해도 돼요.
기법 2: Tree-of-Thought (ToT)
개념
CoT가 일직선으로 추론한다면, ToT는 여러 가능성을 동시에 탐색하고 유망한 방향을 선택하면서 나아가요. 체스 플레이어가 여러 수를 미리 생각해보는 것과 같아요.
[시작]
/ \
[경로 A] [경로 B]
/ \ \
[A-1] [A-2] [B-1]
↑ 가장 유망
|
[A-2-1]
|
[최종 답]
동작 방식
세 단계로 구성돼요.
1. 생각 생성 — 현재 상태에서 가능한 다음 단계를 여러 개 생성해요.
2. 상태 평가 — 각 후보 단계가 목표에 얼마나 유망한지 LLM이 평가해요.
3. 탐색 전략 — BFS(너비 우선)나 DFS(깊이 우선)로 트리를 탐색해요.
def tree_of_thought(problem: str, n_branches: int = 3, depth: int = 3):
current_states = [problem]
for step in range(depth):
next_states = []
for state in current_states:
# 각 상태에서 여러 후보 생성
candidates = generate_thoughts(state, n=n_branches)
# 각 후보 평가
scored = []
for candidate in candidates:
score = evaluate_thought(candidate, problem)
scored.append((score, candidate))
# 상위 후보만 유지
scored.sort(reverse=True)
next_states.extend([s[1] for s in scored[:2]])
current_states = next_states
# 최종 상태 중 가장 좋은 것으로 답 생성
best_state = max(current_states, key=lambda s: evaluate_thought(s, problem))
return generate_final_answer(best_state)
def generate_thoughts(state: str, n: int) -> list[str]:
prompt = f"""
현재 상태: {state}
다음으로 취할 수 있는 {n}가지 서로 다른 접근 방식을 제안해줘.
각 접근 방식은 서로 달라야 해.
"""
return llm.invoke(prompt, n=n)
def evaluate_thought(thought: str, original_problem: str) -> float:
prompt = f"""
원래 문제: {original_problem}
현재 추론 상태: {thought}
이 추론이 올바른 답에 얼마나 가까운지 1~10점으로 평가해줘.
JSON으로만 답해: {{"score": 점수, "reason": "이유"}}
"""
result = json.loads(llm.invoke(prompt).content)
return result["score"]
장단점
장점 — 막다른 길에 들어서도 백트래킹 가능해요. 창의적인 문제나 탐색이 필요한 문제에 강해요. CoT보다 훨씬 높은 정확도를 냅니다.
단점 — LLM 호출이 많아서 비용과 시간이 크게 늘어나요. 구현이 복잡해요. 간단한 문제에는 오버엔지니어링이에요.
언제 써야 하나
창의적 글쓰기, 코드 설계, 전략적 계획처럼 여러 접근법을 탐색해야 하는 문제. 단순 계산보다는 복잡한 의사결정 문제에 적합해요.
기법 3: Self-Consistency
개념
같은 문제에 대해 CoT를 여러 번 실행하고, 가장 많이 나온 답을 최종 답으로 선택해요. 다수결 투표예요.
질문 (동일)
│
├─ CoT 실행 1 → 답: 74
├─ CoT 실행 2 → 답: 74
├─ CoT 실행 3 → 답: 72 (틀린 추론)
├─ CoT 실행 4 → 답: 74
└─ CoT 실행 5 → 답: 74
다수결: 74가 4표 → 최종 답: 74
구현
from collections import Counter
def self_consistency(question: str, n_samples: int = 5, temperature: float = 0.7) -> str:
answers = []
for _ in range(n_samples):
# 매번 다른 추론 경로로 답 생성 (temperature > 0 으로 다양성 확보)
response = llm.invoke(
f"{question}\n\n단계별로 생각해봐.",
temperature=temperature
)
# 최종 답만 추출
answer = extract_final_answer(response.content)
answers.append(answer)
# 다수결로 최종 답 선택
most_common = Counter(answers).most_common(1)[0][0]
return most_common
def extract_final_answer(response: str) -> str:
# "정답: X" 또는 "답: X" 패턴 추출
import re
patterns = [r"정답[:\s]+(.+)", r"답[:\s]+(.+)", r"따라서\s+(.+)"]
for pattern in patterns:
match = re.search(pattern, response)
if match:
return match.group(1).strip()
return response.split("\n")[-1].strip()
Self-Consistency는 GSM8K에서 CoT 단독 대비 +17.9% 성능 향상을 보였어요. SVAMP에서 +11.0%, AQuA에서 +12.2%로 수학 추론 벤치마크에서 특히 효과가 커요.
장단점
장점 — 구현이 간단해요. CoT에서 발생하는 우연한 오류를 걸러낼 수 있어요. 정확도가 크게 올라가요.
단점 — N번 실행하면 비용도 N배예요. 시간도 N배 걸려요. 답이 수치가 아닌 주관적 내용이면 다수결이 어려워요.
언제 써야 하나
정확도가 중요한 수학, 논리, 사실 확인 문제. 비용보다 정확도가 중요한 상황.
세 기법 비교
구분 CoT ToT Self-Consistency
| 추론 구조 | 선형 | 트리 탐색 | 병렬 선형 |
| LLM 호출 수 | 1회 | 많음 (N × depth) | N회 |
| 비용 | 낮음 | 높음 | 중간 |
| 구현 복잡도 | 낮음 | 높음 | 낮음 |
| 백트래킹 | 없음 | 있음 | 없음 |
| 적합한 문제 | 순차적 추론 | 탐색/창의적 | 수학/사실 확인 |
| 성능 향상 | 중간 | 높음 | 높음 |
실전 조합 전략
세 기법을 상황에 맞게 조합하는 게 효과적이에요.
단순한 추론 문제
# CoT만으로 충분
response = llm.invoke(f"{question}\n\n단계별로 생각해봐.")
정확도가 중요한 수학/사실 문제
# Self-Consistency 적용
answer = self_consistency(question, n_samples=5)
탐색이 필요한 복잡한 문제
# ToT 적용
answer = tree_of_thought(problem, n_branches=3, depth=3)
정확도 최대화가 필요할 때
# CoT + Self-Consistency 조합
answers = []
for _ in range(5):
cot_response = llm.invoke(f"{question}\n\nLet's think step by step.")
answers.append(extract_answer(cot_response))
final = Counter(answers).most_common(1)[0][0]
2025년 이후 트렌드 — 추론 모델의 등장
CoT, ToT, Self-Consistency는 프롬프트 레벨에서 추론을 강화하는 기법이에요. 2024년 이후에는 추론 자체를 모델 내부에 내재화한 추론 모델이 등장했어요.
OpenAI o1/o3, DeepSeek-R1, Claude 3.7 Sonnet 같은 모델들은 응답 전에 긴 내부 CoT를 자동으로 실행해요. 개발자가 별도로 CoT 프롬프트를 구성하지 않아도 돼요.
근데 여전히 세 기법이 유효한 이유가 있어요.
첫째, 추론 모델은 비용이 비싸요. 간단한 문제에 추론 모델을 쓰는 건 오버킬이에요.
둘째, 오픈소스 모델(Qwen, LLaMA 등)을 직접 서빙할 때는 여전히 CoT와 Self-Consistency가 필요해요.
셋째, Self-Consistency는 추론 모델에도 적용하면 추가 성능 향상이 가능해요.
마무리
정리하면 이렇습니다.
CoT — 가장 기본. 복잡한 질문에 항상 적용해도 손해 없어요.
Self-Consistency — 정확도가 중요할 때. 비용 N배지만 효과도 확실해요.
ToT — 창의적이고 탐색이 필요한 문제에. 비용이 높아서 꼭 필요할 때만 써요.
세 기법 모두 모델을 바꾸거나 파인튜닝 없이 프롬프트와 호출 방식만 바꿔서 성능을 올릴 수 있다는 게 핵심이에요. 😄
'LLM' 카테고리의 다른 글
| 구글 Gemma 4 완전 분석 — 오픈소스 AI의 판을 바꾼 모델 (0) | 2026.04.08 |
|---|---|
| 구글의 딥시크: 터보퀀트(TurboQuant) 완전 분석 — 메모리 6배 절감이 반도체 주가를 흔든 이유 (0) | 2026.03.27 |
| [기초] LLM이 도구를 직접 호출한다 — Function Calling 원리와 구현 완전 정리 (0) | 2026.03.25 |
| sglang vs vLLM — 오픈소스 LLM 서빙 프레임워크 실전 비교 (0) | 2026.03.24 |
| LLM 성능 평가는 어떻게 할까? MT-Bench부터 HELM까지 (0) | 2026.03.24 |