본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 25. · 2 Views
Tree of Thoughts 완벽 가이드
LLM이 복잡한 문제를 해결할 때 사람처럼 여러 사고 경로를 탐색하고 평가하며 최적의 답을 찾아가는 Tree of Thoughts(ToT) 기법을 배웁니다. BFS/DFS 탐색부터 백트래킹, 프루닝까지 실전 예제와 함께 알아봅니다.
목차
- ToT 개념과 BFS/DFS 탐색
- 사고 단계별 평가
- Backtracking과 Pruning
- 복잡한 문제 해결 프로세스
- 법적 준수 확인
- 실습: 게임 AI에 ToT 적용
- 실습: 창의적 글쓰기 ToT
- 일관성 (1~10): 앞 내용과 자연스럽게 이어지는가?
1. ToT 개념과 BFS/DFS 탐색
어느 날 AI 스타트업에 입사한 지 3개월 차인 김개발 씨는 CEO로부터 흥미로운 요청을 받았습니다. "우리 챗봇이 복잡한 수학 문제를 잘 못 풀더라고요.
GPT-4도 가끔 틀리던데, 더 나은 방법이 없을까요?" 김개발 씨는 고민에 빠졌습니다.
**Tree of Thoughts(ToT)**는 LLM이 한 번에 답을 내는 대신, 마치 사람이 생각하듯 여러 사고 경로를 탐색하며 문제를 해결하는 기법입니다. 마치 체스 선수가 수십 가지 수를 머릿속으로 시뮬레이션하듯이, AI도 여러 중간 단계를 생성하고 평가하며 최적의 답을 찾아갑니다.
이를 통해 복잡한 추론 문제에서 획기적인 성능 향상을 얻을 수 있습니다.
다음 코드를 살펴봅시다.
# Tree of Thoughts - BFS 탐색 예제
from collections import deque
def tot_bfs(problem, max_depth=3, beam_width=3):
# 초기 상태를 큐에 추가
queue = deque([{"thought": "", "depth": 0, "score": 0}])
best_solution = None
while queue:
current = queue.popleft()
# 최대 깊이 도달 시 종료
if current["depth"] >= max_depth:
if best_solution is None or current["score"] > best_solution["score"]:
best_solution = current
continue
# 현재 상태에서 가능한 다음 사고들을 생성
next_thoughts = generate_thoughts(problem, current["thought"])
# 각 사고를 평가하고 상위 beam_width개만 선택
evaluated = [(t, evaluate_thought(t)) for t in next_thoughts]
evaluated.sort(key=lambda x: x[1], reverse=True)
# 상위 후보들을 큐에 추가
for thought, score in evaluated[:beam_width]:
queue.append({
"thought": current["thought"] + " -> " + thought,
"depth": current["depth"] + 1,
"score": score
})
return best_solution
김개발 씨는 선배 개발자 박시니어 씨를 찾아갔습니다. "시니어님, LLM이 복잡한 문제를 더 잘 풀게 할 방법이 없을까요?" 박시니어 씨는 화이트보드에 나무 그림을 그리며 설명을 시작했습니다.
"김개발 씨, 사람이 어려운 문제를 풀 때 어떻게 하죠? 한 번에 답을 맞히나요?" 김개발 씨는 고개를 저었습니다.
"아니요, 여러 가지 방법을 시도해보고, 막히면 다른 방법을 찾아보죠." Tree of Thoughts란 정확히 그런 것입니다. 쉽게 비유하자면, ToT는 마치 미로 찾기 게임과 같습니다.
출구를 찾기 위해 한 길만 고집하지 않고, 여러 갈림길을 탐색하고, 막다른 길을 만나면 돌아와서 다른 길을 시도합니다. LLM도 마찬가지로 여러 사고의 경로를 만들어가며 가장 유망한 경로를 선택해 나갑니다.
기존 LLM의 한계는 무엇이었을까요? 일반적인 Chain of Thought(CoT) 방식에서는 LLM이 한 방향으로만 생각을 이어갑니다.
"A → B → C → D" 이런 식으로 일직선입니다. 만약 B 단계에서 잘못된 판단을 하면 그대로 틀린 답으로 이어집니다.
되돌아갈 방법이 없습니다. 더 큰 문제는 복잡한 문제일수록 중간 단계가 여러 갈래로 나뉜다는 점입니다.
수학 문제를 풀 때도 방법 1, 방법 2, 방법 3이 있을 수 있는데, 처음부터 하나만 선택하면 최적의 해법을 놓칠 수 있습니다. 바로 이런 문제를 해결하기 위해 Tree of Thoughts가 등장했습니다.
ToT를 사용하면 여러 사고 경로를 동시에 탐색할 수 있습니다. 각 단계마다 여러 가능성을 생성하고, 각각을 평가하여 가장 유망한 경로들만 계속 확장해나갑니다.
마치 나무가 가지를 뻗듯이 사고가 확장되는 것입니다. ToT를 탐색하는 방법은 크게 두 가지입니다.
첫 번째는 BFS(너비 우선 탐색) 방식입니다. 이는 각 레벨을 완전히 탐색한 후 다음 레벨로 넘어갑니다.
위의 코드를 보면 queue를 사용하여 구현되어 있습니다. 먼저 들어간 것이 먼저 나오는 FIFO 구조입니다.
코드의 핵심을 살펴보겠습니다. generate_thoughts 함수는 현재 사고 상태에서 가능한 다음 단계들을 생성합니다.
예를 들어 수학 문제라면 "방정식을 정리한다", "인수분해를 시도한다", "양변을 나눈다" 같은 여러 선택지가 나옵니다. evaluate_thought 함수는 각 사고가 얼마나 유망한지 점수를 매깁니다.
이게 ToT의 핵심입니다. 단순히 많은 경로를 만드는 게 아니라, 좋은 경로를 선별해야 합니다.
beam_width 매개변수는 각 단계에서 몇 개의 후보를 유지할지 결정합니다. 이 값이 크면 더 많은 가능성을 탐색하지만 계산 비용이 늘어납니다.
보통 3~5 정도가 적당합니다. DFS(깊이 우선 탐색) 방식도 있습니다.
이는 한 경로를 끝까지 탐색한 후 다른 경로로 넘어갑니다. 특정 문제 유형에서는 DFS가 더 효율적일 수 있습니다.
예를 들어 논리 퍼즐처럼 한 번 잘못된 가정을 하면 빨리 포기하고 다른 가정을 시도해야 하는 경우입니다. 실제 현업에서는 어떻게 활용할까요?
한 AI 스타트업은 법률 자문 챗봇을 개발할 때 ToT를 활용했습니다. 복잡한 법률 질문에 답할 때, 여러 법률 조항을 검토하고, 각 해석의 타당성을 평가하며, 가장 합리적인 결론을 도출했습니다.
기존 방식보다 정확도가 40% 향상되었습니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수는 무작정 깊이와 너비를 크게 설정하는 것입니다. max_depth=10, beam_width=10처럼 설정하면 탐색 공간이 기하급수적으로 늘어나 API 비용이 폭발합니다.
문제의 복잡도에 맞게 적절히 조절해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 들은 김개발 씨는 눈이 반짝였습니다. "아, 그래서 복잡한 문제는 트리로 탐색하는 거군요!" ToT의 BFS/DFS 탐색을 이해하면 LLM이 단순한 대화 모델을 넘어 진짜 추론 엔진으로 발전할 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - BFS는 최단 경로를 보장하지만 메모리를 많이 사용하고, DFS는 메모리 효율적이지만 최적해를 놓칠 수 있습니다
- beam_width는 3~5가 적당하며, 복잡한 문제일수록 크게 설정합니다
- 탐색 초기에는 다양성을 위해 temperature를 높이고, 후반에는 정확성을 위해 낮추는 것이 효과적입니다
2. 사고 단계별 평가
김개발 씨가 ToT를 처음 구현했을 때, 모든 사고 경로가 똑같은 점수를 받았습니다. "이상하네요.
분명히 어떤 경로는 더 좋아 보이는데..." 박시니어 씨가 화면을 보더니 웃으며 말했습니다. "평가 함수를 제대로 안 만들었군요.
ToT의 핵심은 평가입니다."
**사고 평가(Thought Evaluation)**는 생성된 각 사고 단계의 품질과 유망성을 측정하는 과정입니다. 마치 체스에서 포지션 평가 함수가 현재 국면의 우세를 판단하듯이, ToT에서도 각 중간 사고가 최종 목표에 얼마나 가까운지 판단해야 합니다.
좋은 평가 함수가 있어야 수많은 경로 중 진짜 유망한 것만 선별할 수 있습니다.
다음 코드를 살펴봅시다.
# 사고 평가 - Value 함수와 Vote 방식
def evaluate_thought_value(thought, problem, method="value"):
if method == "value":
# LLM에게 1~10 점수로 평가 요청
prompt = f"""
문제: {problem}
현재 사고 단계: {thought}
이 사고가 문제 해결에 얼마나 도움이 될까요?
1~10 점수로 평가하고, 이유를 설명하세요.
"""
response = llm_call(prompt)
score = extract_score(response) # "8점" -> 8
return score
elif method == "vote":
# 여러 번 평가하여 투표
votes = {"good": 0, "bad": 0, "neutral": 0}
for _ in range(3): # 3번 평가
prompt = f"""
문제: {problem}
사고: {thought}
이 사고는 good/bad/neutral 중 어디에 해당하나요?
"""
response = llm_call(prompt)
category = extract_category(response)
votes[category] += 1
# 다수결로 최종 판정
return max(votes, key=votes.get)
김개발 씨는 첫 번째 ToT 구현에서 큰 문제에 부딪혔습니다. 수백 개의 사고 경로가 생성되었지만, 어느 것이 좋은지 구분할 수 없었습니다.
모두 그럴듯해 보였기 때문입니다. 박시니어 씨가 커피를 한 모금 마시고 설명을 시작했습니다.
"김개발 씨, 사람이 문제를 풀 때도 매 단계마다 '이게 맞는 방향인가?'를 판단하잖아요. AI도 마찬가지예요." 사고 평가란 바로 그것입니다.
쉽게 비유하자면, 사고 평가는 마치 등산할 때 GPS로 현재 위치를 확인하는 것과 같습니다. 정상까지 가는 길은 여러 갈래이지만, 현재 길이 정상으로 가는 올바른 방향인지 중간중간 확인해야 합니다.
잘못된 길로 한참 걸었다가 되돌아오면 시간 낭비이기 때문입니다. 평가 함수가 없던 시절에는 어땠을까요?
초기 ToT 연구에서는 단순히 많은 경로를 생성하고 마지막에만 검증했습니다. 문제는 대부분의 경로가 중간에 이미 잘못되어 있다는 점입니다.
예를 들어 수학 문제에서 첫 단계부터 잘못된 공식을 적용했다면, 그 이후 아무리 논리적으로 전개해도 답은 틀립니다. 더 큰 문제는 계산 비용이었습니다.
모든 경로를 끝까지 탐색하려면 LLM API 호출이 수천 번 필요할 수 있습니다. 비용이 기하급수적으로 늘어나는 것입니다.
바로 이런 문제를 해결하기 위해 단계별 평가가 필수적입니다. 사고를 평가하는 방법은 크게 두 가지입니다.
첫 번째는 Value 방식입니다. LLM에게 직접 "이 사고가 문제 해결에 얼마나 도움이 되는가?"를 물어보고 1~10 점수를 받습니다.
이 방법의 장점은 세밀한 비교가 가능하다는 것입니다. 7점짜리와 8점짜리를 명확히 구분할 수 있습니다.
위의 코드에서 evaluate_thought_value 함수를 보면, 현재 사고를 프롬프트에 포함시켜 LLM에게 평가를 요청합니다. LLM은 문맥을 고려하여 "이 단계는 방정식을 올바르게 정리했으므로 8점입니다"처럼 평가합니다.
두 번째는 Vote 방식입니다. 여러 번 평가를 요청하고 "good/bad/neutral" 중 하나로 분류한 뒤, 다수결로 최종 판정합니다.
이 방법은 LLM의 불확실성을 완화합니다. 한 번의 평가는 운이 작용할 수 있지만, 세 번 모두 "good"이 나왔다면 신뢰도가 높습니다.
코드를 자세히 살펴보겠습니다. method="value" 부분에서는 프롬프트에 현재 문제와 사고를 모두 제공합니다.
LLM은 문제의 목표를 이해하고 현재 사고가 그 목표에 얼마나 가까운지 판단합니다. extract_score 함수는 "이 사고는 8점입니다"에서 숫자 8을 추출합니다.
method="vote" 부분에서는 같은 평가를 3번 반복합니다. LLM의 출력은 약간의 무작위성이 있기 때문에, 매번 결과가 다를 수 있습니다.
그래서 투표를 통해 안정적인 판단을 얻습니다. max(votes, key=votes.get)은 가장 많은 표를 받은 카테고리를 반환합니다.
실제 현업에서는 어떻게 활용할까요? 한 EdTech 스타트업은 수학 문제 자동 채점 시스템을 만들 때 ToT 평가를 활용했습니다.
학생의 풀이 과정을 단계별로 평가하여, 어느 단계에서 실수했는지 정확히 찾아냈습니다. 단순히 "틀렸습니다" 대신 "3단계에서 분배법칙을 잘못 적용했습니다"처럼 구체적인 피드백을 제공할 수 있었습니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수는 평가를 너무 자주 하는 것입니다.
모든 후보 사고를 3번씩 Vote 방식으로 평가하면 API 호출이 3배로 늘어납니다. 실무에서는 일단 Value로 빠르게 필터링하고, 최종 상위 후보들만 Vote로 재평가하는 하이브리드 방식을 사용합니다.
또 다른 실수는 평가 프롬프트가 애매한 것입니다. "이 사고가 좋은가요?"처럼 모호하게 물으면 LLM도 일관성 없는 답을 합니다.
"문제의 목표인 X를 달성하는 데 이 사고가 얼마나 기여하는가?"처럼 구체적으로 물어야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 조언대로 평가 함수를 개선한 김개발 씨는 놀라운 결과를 얻었습니다. API 호출은 70% 줄었는데, 정확도는 오히려 20% 올랐습니다.
"좋은 경로만 선별하니까 효율이 확 올랐네요!" 사고 평가를 제대로 구현하면 ToT의 성능과 비용 효율을 동시에 잡을 수 있습니다. 여러분도 문제 유형에 맞는 평가 방식을 실험해 보세요.
실전 팁
💡 - Value 방식은 세밀한 비교가 필요할 때, Vote 방식은 안정적인 판단이 필요할 때 사용합니다
- 평가 프롬프트에 구체적인 평가 기준을 명시하면 일관성이 높아집니다
- 비용 절감을 위해 초기 필터링은 Value, 최종 검증은 Vote로 하는 2단계 평가를 권장합니다
3. Backtracking과 Pruning
김개발 씨가 ToT를 돌렸더니 10분이 지나도 결과가 안 나왔습니다. "왜 이렇게 느리죠?" 로그를 보니 똑같은 잘못된 경로를 계속 탐색하고 있었습니다.
박시니어 씨가 한숨을 쉬며 말했습니다. "백트래킹과 프루닝을 안 했네요.
이러면 평생 걸려요."
**Backtracking(백트래킹)**은 막다른 길에 도달했을 때 이전 분기점으로 돌아가는 것이고, **Pruning(가지치기)**은 명백히 나쁜 경로를 조기에 제거하는 것입니다. 마치 정원사가 죽은 가지를 잘라내듯이, ToT도 희망 없는 사고 경로를 일찍 포기하고 다른 경로에 집중해야 효율적입니다.
이 두 기법이 없으면 ToT는 실용성이 없습니다.
다음 코드를 살펴봅시다.
# Backtracking과 Pruning을 포함한 DFS 탐색
def tot_dfs_with_pruning(problem, max_depth=5, threshold=5):
best_solution = {"thought": "", "score": 0}
def dfs(current_thought, depth, path):
nonlocal best_solution
# 최대 깊이 도달
if depth >= max_depth:
score = final_evaluate(current_thought)
if score > best_solution["score"]:
best_solution = {"thought": current_thought, "score": score}
return
# 다음 사고들 생성
next_thoughts = generate_thoughts(problem, current_thought)
for thought in next_thoughts:
# Pruning: 점수가 임계값 미만이면 탐색 중단
score = evaluate_thought_value(thought, problem)
if score < threshold:
print(f"Pruned: {thought} (score: {score})")
continue
# 순환 방지: 이미 탐색한 사고는 스킵
if thought in path:
print(f"Cycle detected: {thought}")
continue
# Backtracking: 재귀 호출 후 자동으로 이전 상태로 복귀
dfs(current_thought + " -> " + thought, depth + 1, path + [thought])
dfs("", 0, [])
return best_solution
김개발 씨의 첫 ToT 구현은 심각한 문제가 있었습니다. 한번 잘못된 방향으로 가면 계속 그 방향만 파고들었습니다.
마치 미로에서 막다른 골목에 들어갔는데도 계속 앞으로만 가려는 것과 같았습니다. 박시니어 씨가 화이트보드에 미로 그림을 그렸습니다.
"김개발 씨, 미로를 탈출할 때 막히면 어떻게 하죠?" "되돌아가서 다른 길을 찾아요." "바로 그겁니다. 그게 백트래킹이에요." Backtracking과 Pruning이란 바로 그것입니다.
쉽게 비유하자면, Backtracking은 등산할 때 잘못된 길로 갔다는 걸 깨닫고 갈림길로 되돌아가는 것입니다. Pruning은 "이 길은 절벽이니 아예 가지 말자"고 표지판을 세우는 것입니다.
둘 다 시간과 체력을 아끼기 위한 전략입니다. 이런 기법이 없던 시절에는 어땠을까요?
초기 트리 탐색 알고리즘은 모든 경로를 무식하게 탐색했습니다. 깊이가 5이고 각 노드가 3개 자식을 가지면 총 3^5 = 243개 경로를 확인해야 합니다.
깊이가 10이면 59,049개입니다. LLM API 호출이 이만큼 필요하다면 비용이 수백만 원까지 나올 수 있습니다.
더 큰 문제는 명백히 틀린 경로도 끝까지 탐색한다는 것입니다. 예를 들어 수학 문제에서 첫 단계에 "0으로 나눈다"는 사고가 나왔다면, 이건 100% 틀렸습니다.
그런데도 계속 탐색하는 건 낭비입니다. 바로 이런 문제를 해결하기 위해 Backtracking과 Pruning이 필수적입니다.
Backtracking은 DFS에서 자연스럽게 일어납니다. 재귀 함수가 반환되면 자동으로 이전 상태로 돌아가기 때문입니다.
위의 코드에서 dfs 함수가 재귀 호출되고, for 루프가 다음 thought로 넘어가는 부분을 보세요. 이게 Backtracking입니다.
Pruning은 명시적으로 구현해야 합니다. 코드의 핵심 부분을 살펴보겠습니다.
threshold=5 매개변수는 프루닝 기준입니다. 어떤 사고의 점수가 5점 미만이면 "이건 희망 없다"고 판단하고 그 경로 전체를 포기합니다.
continue로 다음 반복으로 넘어가므로, 그 아래 자식 노드들은 아예 탐색하지 않습니다. if thought in path 부분은 순환을 방지합니다.
만약 "A → B → C → B"처럼 B가 다시 나오면, 이건 무한 루프가 될 수 있습니다. 이미 탐색한 사고가 다시 나오면 스킵합니다.
path + [thought] 부분이 중요합니다. 파이썬 리스트의 + 연산은 새 리스트를 만듭니다.
즉, 재귀 호출마다 독립적인 경로를 유지합니다. 이래야 Backtracking이 제대로 작동합니다.
실제로 프루닝 기준을 어떻게 정할까요? 한 연구팀은 24 Game(네 숫자로 24 만들기)을 ToT로 풀 때, 각 단계에서 "24에 도달할 가능성"을 평가했습니다.
예를 들어 "8 × 3"은 24이므로 최고 점수, "2 + 3"은 5이므로 24와 거리가 멀어 낮은 점수를 받습니다. 점수 6 미만은 프루닝하도록 설정했더니, 탐색 시간이 80% 줄었습니다.
동적 프루닝도 가능합니다. 탐색 초기에는 threshold를 낮게(예: 3) 설정해서 다양한 가능성을 열어두고, 좋은 후보를 몇 개 찾은 후에는 threshold를 높게(예: 7) 설정해서 더 엄격하게 필터링합니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수는 threshold를 너무 높게 설정하는 것입니다.
예를 들어 threshold=9로 하면 거의 완벽한 사고만 통과합니다. 문제는 초기 단계에서는 완벽한 사고를 만들기 어렵다는 점입니다.
결과적으로 모든 경로가 프루닝되어 해를 찾지 못합니다. 또 다른 실수는 순환 감지를 하지 않는 것입니다.
특히 창의적인 문제에서는 LLM이 비슷한 사고를 반복 생성할 수 있습니다. "주인공이 집을 떠난다" → "여행을 시작한다" → "집을 떠난다"처럼 순환하면 무한 루프에 빠집니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 프루닝과 백트래킹을 적용한 후, 10분 걸리던 작업이 30초로 줄었습니다.
"와, 이게 진짜 실용적이네요!" 김개발 씨는 감탄했습니다. Backtracking과 Pruning을 제대로 구현하면 ToT는 실험실 기술에서 실전 무기로 바뀝니다.
여러분도 문제 특성에 맞는 프루닝 기준을 찾아보세요.
실전 팁
💡 - Threshold는 문제 난이도에 따라 3~7 사이에서 조절하며, 너무 높으면 해를 못 찾습니다
- 순환 감지는 반드시 구현해야 하며,
set을 사용하면 O(1) 시간에 확인 가능합니다 - 동적 프루닝(초기 완화, 후기 강화)은 탐색 품질과 효율의 균형을 잡는 좋은 전략입니다
4. 복잡한 문제 해결 프로세스
김개발 씨가 CEO에게 ToT 데모를 보여줬습니다. "오, 신기하네요!
그런데 우리 실제 문제에는 어떻게 적용하죠? 법률 자문은 수학 문제랑 다르잖아요." 김개발 씨는 당황했습니다.
박시니어 씨가 끼어들었습니다. "문제 유형마다 ToT 설계가 달라야 해요."
복잡한 문제 해결 프로세스는 문제 유형을 분석하고, 적절한 ToT 전략을 설계하며, 평가 함수를 커스터마이징하는 전체 워크플로우입니다. 마치 건축가가 건물 용도에 따라 설계도를 다르게 그리듯이, 수학 문제, 창의적 글쓰기, 전략 게임 등 각 도메인마다 최적의 ToT 구조가 다릅니다.
이 프로세스를 이해해야 실무에 적용할 수 있습니다.
다음 코드를 살펴봅시다.
# 범용 ToT 프레임워크 - 문제 유형별 커스터마이징
class ToTSolver:
def __init__(self, problem_type):
self.problem_type = problem_type
self.config = self.get_config(problem_type)
def get_config(self, problem_type):
# 문제 유형별 설정
configs = {
"math": {
"max_depth": 5,
"beam_width": 3,
"search": "dfs",
"eval_method": "value",
"threshold": 6
},
"creative_writing": {
"max_depth": 8,
"beam_width": 5,
"search": "bfs",
"eval_method": "vote",
"threshold": None # 프루닝 없음
},
"game_strategy": {
"max_depth": 10,
"beam_width": 2,
"search": "dfs",
"eval_method": "value",
"threshold": 7
}
}
return configs.get(problem_type, configs["math"])
def solve(self, problem):
# 1단계: 문제 분해
sub_problems = self.decompose_problem(problem)
# 2단계: 각 하위 문제에 ToT 적용
results = []
for sub in sub_problems:
if self.config["search"] == "bfs":
result = tot_bfs(sub, self.config["max_depth"], self.config["beam_width"])
else:
result = tot_dfs_with_pruning(sub, self.config["max_depth"], self.config["threshold"])
results.append(result)
# 3단계: 결과 통합
final_answer = self.synthesize_results(results)
return final_answer
def decompose_problem(self, problem):
# 문제를 하위 단계로 분해
prompt = f"다음 문제를 3~5개의 하위 단계로 나누세요: {problem}"
return llm_call(prompt).split("\n")
def synthesize_results(self, results):
# 여러 하위 결과를 하나로 통합
combined = " ".join([r["thought"] for r in results])
return f"최종 답안: {combined}"
김개발 씨는 고민에 빠졌습니다. 수학 문제로 테스트했을 때는 ToT가 잘 작동했는데, 법률 자문 챗봇에 적용하려니 어디서부터 시작해야 할지 막막했습니다.
"문제가 너무 복잡해요..." 박시니어 씨가 노트북을 열며 말했습니다. "복잡한 문제는 단순한 문제들로 쪼개야 해요.
그게 바로 문제 분해입니다." 복잡한 문제 해결이란 바로 그것입니다. 쉽게 비유하자면, 복잡한 문제 해결은 마치 큰 프로젝트를 관리하는 것과 같습니다.
"1년 안에 앱 출시"라는 큰 목표를 바로 달성할 수 없습니다. 먼저 "기획 → 디자인 → 개발 → 테스트"로 단계를 나누고, 각 단계를 다시 세부 작업으로 쪼갭니다.
ToT도 마찬가지입니다. 단순한 ToT로는 해결 못하는 문제가 뭘까요?
예를 들어 "기후 변화 대응 정책 제안"이라는 문제를 생각해봅시다. 이건 한두 단계로 답할 수 있는 게 아닙니다.
경제적 측면, 기술적 측면, 사회적 측면을 모두 고려해야 하고, 각 측면마다 여러 선택지가 있습니다. 트리가 너무 복잡해져서 탐색이 불가능합니다.
또 다른 문제는 문제 유형마다 최적의 전략이 다르다는 점입니다. 수학 문제는 정답이 하나이므로 DFS로 깊게 파고들면 됩니다.
하지만 창의적 글쓰기는 여러 스타일을 탐색해야 하므로 BFS가 좋습니다. 바로 이런 문제를 해결하기 위해 체계적인 프로세스가 필요합니다.
첫 번째 단계는 문제 분해입니다. 큰 문제를 작은 하위 문제들로 나눕니다.
위의 코드에서 decompose_problem 함수가 이 역할을 합니다. LLM에게 "이 문제를 단계별로 나눠줘"라고 요청하면, 놀랍게도 꽤 합리적으로 분해합니다.
예를 들어 "새로운 전자상거래 사이트 기획"이라는 문제를 분해하면:
5. 법적 준수 확인
실전 팁
💡 - 문제 분해는 3~5개 단계가 적당하며, 너무 세밀하면 통합이 어렵습니다
- 문제 유형(수학/창의/게임 등)에 따라 max_depth, beam_width, 탐색 방식을 다르게 설정합니다
- 하위 문제 간 의존성이 있다면 순차적으로, 독립적이면 병렬로 처리하면 효율적입니다
5. 실습: 게임 AI에 ToT 적용
김개발 씨가 회사 게임 개발팀과 협업하게 되었습니다. "우리 게임 AI가 너무 멍청해요.
플레이어가 쉽게 이겨버려서 재미가 없어요." 게임 디자이너가 한숨을 쉬었습니다. 김개발 씨는 번뜩이는 아이디어가 떠올랐습니다.
"ToT로 AI가 여러 수를 미리 읽게 하면 어떨까요?"
게임 AI에 ToT 적용은 체스, 바둑, 전략 게임 등에서 AI가 여러 수 앞을 내다보고 최적의 행동을 선택하게 하는 것입니다. 마치 프로 바둑기사가 머릿속으로 수십 수를 시뮬레이션하듯이, ToT도 가능한 행동들을 트리로 탐색하고 각 결과를 평가합니다.
이를 통해 단순 휴리스틱보다 훨씬 강력한 AI를 만들 수 있습니다.
다음 코드를 살펴봅시다.
# 턴제 전략 게임에 ToT 적용 (예: Tic-Tac-Toe 변형)
class GameToT:
def __init__(self, game_state):
self.game_state = game_state
self.max_depth = 4 # 4수 앞까지 내다봄
def find_best_move(self):
"""최적의 수를 찾기 위한 ToT 탐색"""
possible_moves = self.get_possible_moves(self.game_state)
best_move = None
best_score = float('-inf')
for move in possible_moves:
# 이 수를 두었을 때의 상태
new_state = self.apply_move(self.game_state, move)
# ToT로 이후 전개 탐색
score = self.evaluate_move_tree(new_state, depth=1, is_ai_turn=False)
print(f"수 {move}: 평가 점수 {score}")
if score > best_score:
best_score = score
best_move = move
return best_move
def evaluate_move_tree(self, state, depth, is_ai_turn):
"""재귀적으로 게임 트리 탐색"""
# 종료 조건: 게임 끝 또는 최대 깊이
if self.is_game_over(state) or depth >= self.max_depth:
return self.evaluate_state(state)
possible_moves = self.get_possible_moves(state)
if is_ai_turn:
# AI 차례: 최대값 선택 (Maximizing)
max_score = float('-inf')
for move in possible_moves:
new_state = self.apply_move(state, move)
score = self.evaluate_move_tree(new_state, depth + 1, False)
max_score = max(max_score, score)
return max_score
else:
# 플레이어 차례: 최소값 선택 (Minimizing)
min_score = float('inf')
for move in possible_moves:
new_state = self.apply_move(state, move)
score = self.evaluate_move_tree(new_state, depth + 1, True)
min_score = min(min_score, score)
return min_score
def evaluate_state(self, state):
"""현재 상태의 AI 유리함 평가 (-100 ~ 100)"""
# LLM을 사용한 상태 평가
prompt = f"""
게임 상태: {state}
AI 입장에서 이 상태가 얼마나 유리한가요?
-100(매우 불리) ~ 100(매우 유리) 점수로 평가하세요.
"""
response = llm_call(prompt)
return extract_score(response)
김개발 씨는 게임 개발팀의 요청에 흥분했습니다. ToT를 실제 게임에 적용할 수 있다니!
하지만 어디서부터 시작해야 할까요? 박시니어 씨가 체스판을 꺼내 들었습니다.
"게임 AI의 핵심은 뭘까요? 바로 읽기 깊이입니다.
얼마나 많은 수를 미리 내다볼 수 있느냐가 실력을 결정하죠." 게임 AI에 ToT를 적용한다는 것은 바로 그것입니다. 쉽게 비유하자면, 게임 AI는 마치 프로 체스 선수와 같습니다.
체스 그랜드마스터는 한 수를 두기 전에 머릿속으로 10수 이상을 시뮬레이션합니다. "내가 이렇게 두면, 상대는 저렇게 두고, 그러면 나는 이렇게 대응하고..." ToT는 AI도 똑같이 할 수 있게 합니다.
전통적인 게임 AI는 어떻게 작동했을까요? 초기 게임 AI는 단순한 규칙 기반이었습니다.
"적이 가까우면 공격, 멀면 이동"처럼 if-else로 가득했습니다. 문제는 예측 불가능한 상황에서 멍청하게 행동한다는 것입니다.
플레이어가 함정을 파면 그대로 빠집니다. 조금 더 발전한 AI는 Minimax 알고리즘을 사용했습니다.
게임 트리를 탐색하면서 AI는 점수를 최대화하고, 플레이어는 최소화한다고 가정합니다. 하지만 상태 평가 함수가 수작업으로 만들어진 휴리스틱이라 한계가 있었습니다.
바로 이런 한계를 극복하기 위해 ToT + LLM을 결합합니다. ToT의 트리 탐색 구조는 Minimax와 완벽하게 맞아떨어집니다.
위의 코드에서 evaluate_move_tree 함수를 보세요. is_ai_turn에 따라 최대값 또는 최소값을 선택합니다.
이게 Minimax의 핵심입니다. 차이점은 평가 함수입니다.
전통적인 AI는 "말의 가치 합계 = 폰 1점, 나이트 3점, 룩 5점..."처럼 고정된 공식을 사용했습니다. 하지만 ToT는 evaluate_state에서 LLM에게 물어봅니다.
"이 상태가 유리한가?" LLM은 단순히 말의 개수만 세는 게 아니라, 포지션의 전략적 가치까지 이해합니다. "폰은 하나 덜 있지만, 킹을 위협하는 위치에 있으므로 유리하다"처럼 복잡한 판단이 가능합니다.
코드를 자세히 살펴보겠습니다. find_best_move 함수는 현재 가능한 모든 수를 평가합니다.
각 수를 두었을 때 evaluate_move_tree로 이후 전개를 시뮬레이션하고, 가장 높은 점수를 받은 수를 선택합니다. max_depth=4는 4수 앞까지 내다본다는 뜻입니다.
깊이 1은 AI의 수, 깊이 2는 플레이어의 대응, 깊이 3은 AI의 재대응, 깊이 4는 플레이어의 재재대응입니다. 4수 앞이면 웬만한 함정은 미리 보입니다.
재귀 구조가 중요합니다. is_ai_turn=True일 때는 max()로 최고 점수를 찾고, False일 때는 min()으로 최악의 경우를 가정합니다.
이게 Minimax의 정의입니다. 실제 게임에 적용한 사례를 볼까요?
한 인디 게임 스튜디오는 턴제 전략 게임 "배틀 택틱스"에 ToT AI를 적용했습니다. 기존 AI는 단순히 가장 가까운 적을 공격했지만, ToT AI는 3수 앞을 내다보며 전략적으로 움직였습니다.
플레이어들이 "AI가 너무 똑똑해서 긴장감 있다"는 리뷰를 남겼습니다. 난이도 조절도 쉽습니다.
max_depth=2는 초급, max_depth=4는 중급, max_depth=6은 고급으로 설정하면 됩니다. 깊이가 깊을수록 AI가 강해지지만, 턴당 계산 시간도 늘어납니다.
프루닝으로 효율을 높일 수도 있습니다. 알파-베타 프루닝을 적용하면, "이미 더 좋은 수를 찾았으니 이 가지는 볼 필요 없다"고 판단하여 탐색을 건너뜁니다.
같은 깊이를 훨씬 빠르게 탐색할 수 있습니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수는 깊이를 너무 깊게 설정하는 것입니다. max_depth=10으로 하면 이론상 10수 앞을 보지만, 가능한 경로가 기하급수적으로 늘어나 한 턴에 10분 걸릴 수 있습니다.
실시간 게임에서는 쓸 수 없습니다. 또 다른 실수는 모든 가능한 수를 탐색하는 것입니다.
체스는 한 턴에 평균 35가지 수가 가능합니다. 4수 앞까지 보려면 35^4 = 150만 가지입니다.
상위 5개 후보만 탐색하도록 빔 서치를 적용하면 5^4 = 625가지로 줄어듭니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
ToT AI를 적용한 게임을 테스트한 게임 디자이너가 감탄했습니다. "이제 AI가 진짜 사람처럼 생각하네요!
함정을 파도 안 걸려들어요." 게임 AI에 ToT를 적용하면 단순한 봇이 전략적 사고를 하는 적수로 바뀝니다. 여러분도 자신의 게임에 맞는 평가 함수를 설계해 보세요.
실전 팁
💡 - 깊이는 게임 복잡도와 실시간성에 따라 2~6이 적당하며, 턴제는 깊게, 실시간은 얕게 설정합니다
- 알파-베타 프루닝을 적용하면 같은 깊이를 훨씬 빠르게 탐색할 수 있습니다
- LLM 평가는 비용이 높으므로, 리프 노드(최대 깊이)에서만 사용하고 중간 노드는 캐싱된 휴리스틱을 사용하면 효율적입니다
6. 실습: 창의적 글쓰기 ToT
김개발 씨가 마케팅팀으로부터 새로운 요청을 받았습니다. "블로그 글을 AI가 자동으로 써주면 좋겠어요.
근데 너무 틀에 박힌 글은 싫고, 창의적이었으면 좋겠어요." 김개발 씨는 고민에 빠졌습니다. "창의성을 어떻게 프로그래밍하지?" 박시니어 씨가 미소를 지었습니다.
"ToT로 여러 아이디어를 탐색하면 돼요."
창의적 글쓰기 ToT는 글의 전개 방향을 트리로 탐색하며 다양한 아이디어를 생성하고, 가장 흥미로운 경로를 선택하여 최종 글을 완성하는 기법입니다. 마치 작가가 여러 개요를 스케치하고, 각각을 조금씩 써보고, 가장 매력적인 것을 선택하듯이, ToT도 여러 문장/문단을 생성하고 평가합니다.
이를 통해 단조로운 AI 글쓰기를 탈피할 수 있습니다.
다음 코드를 살펴봅시다.
# 창의적 글쓰기를 위한 ToT - 다양성 중심 탐색
class CreativeWritingToT:
def __init__(self, topic, style="blog"):
self.topic = topic
self.style = style
self.max_depth = 6 # 6개 문단
self.beam_width = 5 # 다양성을 위해 넓게
def generate_article(self):
"""BFS로 다양한 전개 탐색"""
# 초기 도입부 생성
intros = self.generate_intros(self.topic)
# 각 도입부에서 전개
candidates = []
for intro in intros:
article = self.expand_story_tree(intro)
candidates.append(article)
# 최종 선택
best = self.select_best_article(candidates)
return best
def generate_intros(self, topic):
"""다양한 스타일의 도입부 생성"""
styles = [
"질문으로 시작",
"놀라운 사실로 시작",
"스토리텔링으로 시작",
"통계로 시작",
"인용구로 시작"
]
intros = []
for style in styles:
prompt = f"""
주제: {topic}
스타일: {style}
이 스타일로 매력적인 도입부를 2-3문장으로 작성하세요.
"""
intro = llm_call(prompt, temperature=0.9) # 창의성 높임
intros.append({"text": intro, "style": style})
return intros
def expand_story_tree(self, intro):
"""트리 탐색으로 글 전개"""
current = intro["text"]
for depth in range(self.max_depth):
# 다음 문단 후보들 생성
next_paragraphs = self.generate_next_paragraphs(current)
# 평가하여 상위 후보 선택 (Vote 방식)
scored = []
for para in next_paragraphs:
score = self.evaluate_creativity(para)
scored.append({"text": para, "score": score})
# 상위 beam_width개 중 무작위 선택 (다양성)
scored.sort(key=lambda x: x["score"], reverse=True)
top = scored[:self.beam_width]
selected = random.choice(top) # 무작위 선택으로 다양성
current += "\n\n" + selected["text"]
return current
def evaluate_creativity(self, text):
"""창의성 평가 - 독창성, 흥미, 일관성"""
prompt = f"""
다음 문단을 평가하세요:
{text}
3. 일관성 (1~10): 앞 내용과 자연스럽게 이어지는가?
김개발 씨는 처음에는 자신이 없었습니다. "창의성이라는 게 알고리즘으로 가능한가요?" 하지만 박시니어 씨는 확신에 차 있었습니다.
"김개발 씨, 창의성의 비밀을 아세요? 바로 다양성입니다.
많은 아이디어 중에서 좋은 걸 골라내는 거예요." 박시니어 씨가 노트에 여러 문장을 빠르게 적어 내려갔습니다. 창의적 글쓰기에 ToT를 적용한다는 것은 바로 그것입니다.
쉽게 비유하자면, 창의적 글쓰기는 마치 정원사가 여러 종자를 뿌리고 가장 잘 자란 것을 키우는 것과 같습니다. 처음부터 완벽한 글을 쓸 수 없습니다.
여러 방향을 시도해보고, 가장 매력적인 것을 선택하고, 그걸 더 발전시킵니다. 일반적인 AI 글쓰기의 문제는 뭘까요?
기존 LLM에게 "블로그 글 써줘"라고 하면 한 방향으로만 글을 씁니다. 도입-본론-결론 구조로 안전하게 가지만, 그래서 재미가 없습니다.
모든 AI 블로그 글이 비슷비슷한 이유입니다. 더 큰 문제는 중간에 실수하면 되돌릴 방법이 없다는 것입니다.
도입부가 지루하게 시작됐는데 계속 그 톤으로 이어집니다. "아, 질문으로 시작할 걸..." 하고 후회해도 이미 늦었습니다.
바로 이런 문제를 해결하기 위해 창의적 ToT가 필요합니다. 창의적 글쓰기 ToT는 수학 문제 ToT와 전략이 다릅니다.
첫 번째 차이는 다양성입니다. 수학은 정답이 하나지만, 글쓰기는 정답이 여러 개입니다.
그래서 beam_width=5로 넓게 탐색합니다. 코드의 generate_intros 함수를 보면 5가지 다른 스타일로 도입부를 만듭니다.
두 번째 차이는 무작위성입니다. selected = random.choice(top) 부분을 보세요.
최고 점수만 선택하지 않고, 상위 5개 중 무작위로 선택합니다. 왜일까요?
항상 최고만 선택하면 안전한 길로만 가서 지루해지기 때문입니다. 약간의 모험이 창의성을 만듭니다.
세 번째 차이는 평가 기준입니다. evaluate_creativity 함수는 독창성, 흥미, 일관성 세 가지를 평가합니다.
수학처럼 "맞다/틀리다"가 아니라 주관적 품질을 측정합니다. 코드를 단계별로 살펴보겠습니다.
generate_intros는 5가지 스타일로 도입부를 만듭니다. "질문으로 시작"은 "혹시 이런 경험 있으신가요?"처럼 독자와 대화하고, "놀라운 사실로 시작"은 "90%의 사람들이 모르는 사실..."처럼 호기심을 자극합니다.
temperature=0.9로 설정하여 창의적인 출력을 유도합니다. expand_story_tree는 각 도입부를 6개 문단으로 확장합니다.
매 문단마다 5가지 전개 방향을 시도하고, 각각을 평가하고, 상위 후보 중 하나를 선택합니다. 이 과정이 6번 반복됩니다.
generate_next_paragraphs는 다음 문단의 여러 가능성을 만듭니다. "구체적인 예시"는 실제 사례를 들고, "반대 의견 고려"는 비판적 관점을 제시하고, "비유와 은유"는 이미지로 설명합니다.
5가지 방향이 모두 다른 느낌을 줍니다. 실제 현업에서는 어떻게 활용할까요?
한 콘텐츠 마케팅 에이전시는 블로그 글 초안 생성에 ToT를 활용했습니다. 한 주제에 대해 10개의 다른 글을 생성하고, 마케터가 가장 매력적인 2~3개를 골라 다듬었습니다.
"AI가 아이디어 브레인스토밍을 해주는 것 같다"는 평가를 받았습니다. 소설 작가도 활용할 수 있습니다.
플롯의 전환점에서 여러 가능성을 탐색하여 가장 극적인 전개를 찾습니다. "주인공이 진실을 알게 된다"는 한 가지가 아니라, "우연히 듣는다", "친구가 고백한다", "증거를 발견한다" 등 여러 방법을 시도해봅니다.
평가 함수를 커스터마이징할 수도 있습니다. 기술 블로그라면 "정확성"을 추가하고, 감성 에세이라면 "감동"을 추가합니다.
타겟 독자에 따라 평가 기준을 바꾸면 됩니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수는 temperature를 너무 높게 설정하는 것입니다. temperature=1.5처럼 하면 완전히 무작위가 되어 횡설수설합니다.
창의성과 일관성의 균형이 중요하며, 0.7~0.9가 적당합니다. 또 다른 실수는 모든 경로를 끝까지 전개하는 것입니다.
5개 도입부 × 6개 문단이면 계산량이 어마어마합니다. 실무에서는 처음 23문단까지만 탐색하고, 가장 유망한 12개만 끝까지 완성하는 것이 효율적입니다.
일관성 체크도 중요합니다. 여러 경로를 탐색하다 보면 앞뒤가 안 맞을 수 있습니다.
도입부에서 "비용 절감"을 강조했는데 결론에서 "품질 향상"을 말하면 이상합니다. evaluate_creativity의 일관성 점수가 이를 방지합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 창의적 ToT로 생성한 블로그 글을 마케팅팀에 보여주자, 팀장이 깜짝 놀랐습니다.
"이거 진짜 AI가 쓴 거예요? 사람이 쓴 것 같은데요!" 창의적 글쓰기 ToT는 AI 콘텐츠를 '틀에 박힌 글'에서 '읽을 만한 글'로 업그레이드합니다.
여러분도 자신의 콘텐츠 스타일에 맞게 평가 함수를 조정해 보세요.
실전 팁
💡 - Temperature는 0.7~0.9가 창의성과 일관성의 균형점이며, 장르에 따라 조절합니다
- 모든 경로를 끝까지 전개하지 말고, 초반만 탐색 후 상위 후보만 완성하면 효율적입니다
- 평가 기준에 타겟 독자의 선호도를 반영하면(예: 기술적 깊이, 감성적 몰입) 더 효과적인 글을 생성합니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
ReAct 패턴 마스터 완벽 가이드
LLM이 생각하고 행동하는 ReAct 패턴을 처음부터 끝까지 배웁니다. Thought-Action-Observation 루프로 똑똑한 에이전트를 만들고, 실전 예제로 웹 검색과 계산을 결합한 강력한 AI 시스템을 구축합니다.
AI 에이전트의 모든 것 - 개념부터 실습까지
AI 에이전트란 무엇일까요? 단순한 LLM 호출과 어떻게 다를까요? 초급 개발자를 위해 에이전트의 핵심 개념부터 실제 구현까지 이북처럼 술술 읽히는 스타일로 설명합니다.
프로덕션 RAG 시스템 완벽 가이드
검색 증강 생성(RAG) 시스템을 실제 서비스로 배포하기 위한 확장성, 비용 최적화, 모니터링 전략을 다룹니다. AWS/GCP 배포 실습과 대시보드 구축까지 프로덕션 환경의 모든 것을 담았습니다.
RAG 캐싱 전략 완벽 가이드
RAG 시스템의 성능을 획기적으로 개선하는 캐싱 전략을 배웁니다. 쿼리 캐싱부터 임베딩 캐싱, Redis 통합까지 실무에서 바로 적용할 수 있는 최적화 기법을 다룹니다.
실시간으로 답변하는 RAG 시스템 만들기
사용자가 질문하면 즉시 답변이 스트리밍되는 RAG 시스템을 구축하는 방법을 배웁니다. 실시간 응답 생성부터 청크별 스트리밍, 사용자 경험 최적화까지 실무에서 바로 적용할 수 있는 완전한 가이드입니다.