서론: 실제 발생했던 통합 실패 사례
저는去年 서울의 한 게임 스튜디오에서 MMORPG NPC 대화 시스템 구축 프로젝트를 진행했습니다. 팀은 행동 트리로 기본 NPC 논리를 구현했지만, 플레이어의 자연어 입력에 유연하게 반응해야 하는 요구사항이 생겼습니다. LLM API를 연동한 후想像대로 동작하지 않았습니다:
# 실제 발생했던 오류 - 연결 타임아웃
import openai
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "user", "content": "마을의 blacksmith에게 가서 검을 만들어달라고 해줘"}],
timeout=5
)
결과: openai.APITimeoutError: Request timed out after 5.1 seconds
두 번째 오류 - Rate Limit 초과
연속 요청 후 발생한 오류
RateLimitError: 429 Client Error: Too Many Requests
Tier 2 limit reached. Retry-After: 45 seconds
세 번째 오류 - 잘못된 API 엔드포인트
openai.APIConnectionError: Error communicating with OpenAI
Could not connect to api.openai.com:443
(중국 서버에서 접속 시 자주 발생)
이 튜토리얼에서는 이러한 문제들을 해결하면서 행동 트리와 LLM을 효과적으로 통합하는 시스템을 구축하는 방법을 설명드리겠습니다.
행동 트리(Behavior Tree) 기초 개념
행동 트리는 NPC의 행동을 계층적으로 구성하는 기법입니다. 각 노드는 다음과 같이 분류됩니다:
- 실행 노드(Execution Node): 실제 행동 수행
- 조건 노드(Condition Node): 상태 확인
- 선택 노드(Selector): 자식 중 성공한 첫 번째 노드 실행
- 시퀀스 노드(Sequence): 자식을 순서대로 실행
- 병렬 노드(Parallel): 여러 자식 동시 실행
아키텍처 설계: 행동 트리 + LLM 하이브리드 시스템
완전한 NPC 시스템은 세 가지 레이어로 구성됩니다:
┌─────────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ (대화 UI, 애니메이션, 음성 출력) │
├─────────────────────────────────────────────────────────────┤
│ Behavior Tree Layer │
│ (전투, 이동, 탐색, 대화管理等 구조화된 행동) │
├─────────────────────────────────────────────────────────────┤
│ LLM Layer │
│ (자연어 이해, 응답 생성, 감정 분석, 스토리 진행) │
└─────────────────────────────────────────────────────────────┘
핵심 구현: Python 기반 NPC 시스템
1단계: 기본 행동 트리 프레임워크
# npc_behavior_tree.py
from abc import ABC, abstractmethod
from enum import Enum
from typing import List, Optional
import time
class NodeState(Enum):
"""노드 실행 상태"""
RUNNING = "running"
SUCCESS = "success"
FAILURE = "failure"
IDLE = "idle"
class BehaviorNode(ABC):
"""행동 트리 노드 기본 클래스"""
def __init__(self, name: str):
self.name = name
self.state = NodeState.IDLE
self.parent: Optional['CompositeNode'] = None
@abstractmethod
def execute(self, context: dict) -> NodeState:
"""노드 실행 로직"""
pass
def tick(self, context: dict) -> NodeState:
"""틱마다 호출되는 실행 메소드"""
if self.state == NodeState.IDLE:
self.on_enter(context)
self.state = self.execute(context)
if self.state != NodeState.RUNNING:
self.on_exit(context)
return self.state
def on_enter(self, context: dict):
"""상태 진입 시 호출"""
pass
def on_exit(self, context: dict):
"""상태 종료 시 호출"""
pass
class SequenceNode(BehaviorNode):
"""시퀀스 노드: 모든 자식 성공 시 성공, 하나라도 실패 시 실패"""
def __init__(self, name: str, children: List[BehaviorNode]):
super().__init__(name)
self.children = children
self.current_child_index = 0
def execute(self, context: dict) -> NodeState:
while self.current_child_index < len(self.children):
child = self.children[self.current_child_index]
result = child.tick(context)
if result == NodeState.FAILURE:
self.current_child_index = 0
return NodeState.FAILURE
elif result == NodeState.RUNNING:
return NodeState.RUNNING
self.current_child_index += 1
self.current_child_index = 0
return NodeState.SUCCESS
class SelectorNode(BehaviorNode):
"""선택 노드: 첫 번째 성공 노드 반환, 모두 실패 시 실패"""
def __init__(self, name: str, children: List[BehaviorNode]):
super().__init__(name)
self.children = children
self.current_child_index = 0
def execute(self, context: dict) -> NodeState:
while self.current_child_index < len(self.children):
child = self.children[self.current_child_index]
result = child.tick(context)
if result == NodeState.SUCCESS:
self.current_child_index = 0
return NodeState.SUCCESS
elif result == NodeState.RUNNING:
return NodeState.RUNNING
self.current_child_index += 1
self.current_child_index = 0
return NodeState.FAILURE
class ConditionNode(BehaviorNode):
"""조건 노드: 조건 만족 시 성공"""
def __init__(self, name: str, condition_func):
super().__init__(name)
self.condition_func = condition_func
def execute(self, context: dict) -> NodeState:
return NodeState.SUCCESS if self.condition_func(context) else NodeState.FAILURE
class ActionNode(BehaviorNode):
"""행동 노드: 실제 행동 수행"""
def __init__(self, name: str, action_func):
super().__init__(name)
self.action_func = action_func
self.elapsed_time = 0
def execute(self, context: dict) -> NodeState:
result = self.action_func(context, self.elapsed_time)
self.elapsed_time += context.get('delta_time', 0.1)
return result
class LLMDecisionNode(BehaviorNode):
"""LLM 기반 의사결정 노드 - 핵심 통합 부분"""
def __init__(self, name: str, llm_client):
super().__init__(name)
self.llm_client = llm_client
self.last_decision = None
self.decision_cache = {}
def execute(self, context: dict) -> NodeState:
player_input = context.get('player_input', '')
npc_state = context.get('npc_state', {})
# 캐시 키 생성