서론: 실제 발생했던 통합 실패 사례

저는去年 서울의 한 게임 스튜디오에서 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의 행동을 계층적으로 구성하는 기법입니다. 각 노드는 다음과 같이 분류됩니다:

아키텍처 설계: 행동 트리 + 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', {})
        
        # 캐시 키 생성