Behavior Tree와 LLM의 결합: 예측 불가능하지만 통제된 NPC

Behavior Tree와 LLM의 결합: 예측 불가능하지만 통제된 NPC

LLM의 대화·계획 능력은 살리되 게임 규칙과 실행 권한은 Behavior Tree가 유지하는 NPC 설계 방법을 정리한다. Unity와 Unreal 모두에 적용할 수 있는 인터페이스, 검증, 평가 구조를 예제로 설명한다.

왜 LLM만으로 NPC를 움직이면 위험한가

LLM은 상황을 해석하고 자연스러운 대사를 만들며 여러 행동 후보를 제안하는 데 강하다. 반면 게임 플레이를 직접 제어하게 두면 목표가 모호해지거나 존재하지 않는 행동을 제안하거나 같은 상황에서도 결과가 달라지는 문제가 생긴다.

예를 들어 경비병 NPC에게 침입자를 막아라라고만 지시하면 LLM은 추격, 경고, 지원 요청, 문 잠금 같은 그럴듯한 선택지를 만들 수 있다. 하지만 현재 맵에 없는 문을 잠그려 하거나 전투 규칙을 무시한 채 공격을 반복하면 게임 시스템과 충돌한다.

핵심은 역할 분리다. LLM은 의도와 후보 행동을 제안하고 Behavior Tree(BT)는 현재 상태에서 허용된 행동만 선택하고 실행한다. 이렇게 하면 대사와 전술적 변주는 얻으면서도 게임의 규칙, 성능, 디버깅 가능성은 유지할 수 있다.

권한을 분리하는 구조

BT는 이미 검증된 제어 흐름을 제공한다. Selector는 우선순위가 높은 행동을 먼저 시도하고 Sequence는 조건과 실행 단계를 순서대로 묶는다. 여기에 LLM을 새 실행 계층으로 넣기보다 제한된 정보를 내놓는 판단 서비스로 배치하는 편이 안전하다.

flowchart LR
    A[게임 월드 상태] --> B[상태 요약기]
    B --> C[LLM 판단 서비스]
    C --> D[구조화된 행동 제안]
    D --> E[검증기]
    E -->|허용됨| F[Behavior Tree 블랙보드]
    E -->|거부됨| G[안전한 기본값]
    G --> F
    F --> H[Behavior Tree]
    H --> I[이동·전투·대화 시스템]

LLM에 원본 월드 상태 전체를 전달할 필요는 없다. NPC가 알 수 있는 정보와 행동 판단에 필요한 정보만 요약한다. 이 구분은 프롬프트 길이를 줄이고 플레이어가 보지 못한 정보를 NPC가 아는 문제도 막는다.

경비병 NPC가 순찰 중 소리와 침입자 정보를 받아 제한된 행동 후보를 선택하는 게임 AI 개념도

LLM이 맡을 일과 맡기지 않을 일

LLM에 맡기기 좋은 일은 다음과 같다.

  • 현재 상황에 어울리는 말투와 경고 대사 생성
  • 사전에 정의한 전술 중 우선순위 제안
  • 장기 기억을 짧은 성격·관계 요약으로 정리
  • 퀘스트나 사건을 설명하는 자연어 표현 생성

반대로 다음은 게임 코드와 BT가 맡아야 한다.

  • 피해량, 명중 판정, 쿨다운, 인벤토리 변경
  • 이동 경로 탐색과 애니메이션 재생
  • 퀘스트 완료, 저장 데이터 수정, 보상 지급
  • 허용되지 않은 명령의 실행 여부 결정

LLM의 출력은 명령이 아니라 제안이다. 실제 실행 권한은 언제나 게임 런타임에 있어야 한다.

행동 공간을 작게 정의한다

무엇이든 할 수 있다는 프롬프트보다 NPC가 실행할 수 있는 행동 목록을 명시하는 방식이 훨씬 안정적이다. 행동마다 필요한 인자와 범위를 정의하고 BT는 그 값을 블랙보드에 저장해 기존 태스크를 실행한다.

경비병의 행동 공간을 다음처럼 제한할 수 있다.

{
  "allowed_actions": [
    "continue_patrol",
    "investigate_noise",
    "warn_target",
    "call_backup",
    "engage_target",
    "retreat_to_guard_post"
  ],
  "constraints": {
    "can_call_backup": true,
    "max_investigate_distance": 18,
    "target_must_be_visible_for_engage": true
  }
}

LLM에는 자유 형식 문장 대신 JSON 스키마에 맞는 결과를 요구한다. 예시는 단순하지만 실제 서비스에서는 JSON Schema 또는 함수 호출 형식으로 필드와 열거형을 강제하는 편이 좋다.

{
  "action": "investigate_noise",
  "target_id": "noise_42",
  "urgency": 0.72,
  "reason": "순찰 구역 안에서 반복된 소리가 감지되었다."
}

reason은 플레이어에게 그대로 보여 줄 문장이 아니라 개발용 설명으로 취급하는 편이 안전하다. 플레이어 대사는 별도의 생성 요청으로 만들거나 검수된 대사 템플릿과 결합한다.

BT에서 LLM 제안을 소비하는 방법

LLM 응답을 매 프레임 기다리면 안 된다. 네트워크 지연과 비용 때문에 게임의 핵심 루프가 흔들릴 수 있다. 대신 특정 사건이 발생했을 때 비동기로 요청하고 결과가 도착할 때까지는 BT가 기존 규칙으로 행동하게 한다.

아래 C++ 예제는 엔진에 독립적인 검증기의 형태다. 실제 Unity에서는 C# 서비스와 Blackboard 컴포넌트로 Unreal에서는 BT Service 또는 EQS 결과와 함께 비슷한 구조로 구현할 수 있다.

#include <optional>
#include <string>

struct ActionProposal {
    std::string action;
    std::string targetId;
    float urgency;
};

struct GuardContext {
    bool seesTarget;
    bool backupAvailable;
    float noiseDistance;
};

std::optional<ActionProposal> ValidateProposal(
    const ActionProposal& proposal,
    const GuardContext& context)
{
    if (proposal.urgency < 0.0f || proposal.urgency > 1.0f) {
        return std::nullopt;
    }

    if (proposal.action == "engage_target" && !context.seesTarget) {
        return std::nullopt;
    }

    if (proposal.action == "call_backup" && !context.backupAvailable) {
        return std::nullopt;
    }

    if (proposal.action == "investigate_noise" && context.noiseDistance > 18.0f) {
        return std::nullopt;
    }

    if (proposal.action != "continue_patrol" &&
        proposal.action != "investigate_noise" &&
        proposal.action != "warn_target" &&
        proposal.action != "call_backup" &&
        proposal.action != "engage_target" &&
        proposal.action != "retreat_to_guard_post") {
        return std::nullopt;
    }

    return proposal;
}

검증에 실패하면 재질문하지 않고 기본 행동으로 돌아가는 것이 중요하다. 재시도는 비용과 지연을 늘리고 실패 상황에서 NPC가 멈출 가능성을 높인다. 예를 들어 경비병은 continue_patrol 또는 기존 감지 규칙에 따른 investigate_noise를 선택할 수 있다.

블랙보드에는 결과만 기록한다

검증된 제안은 BT가 읽을 수 있는 작은 상태로 변환한다.

SuggestedAction = InvestigateNoise
SuggestedTarget = noise_42
SuggestedUrgency = 0.72
SuggestionExpiresAt = 125.0

SuggestionExpiresAt 같은 만료 시간을 두면 오래된 판단이 뒤늦게 도착했을 때 발생하는 오류를 줄일 수 있다. 적이 이미 사라졌는데 과거의 engage_target 제안을 실행하는 상황을 막는 데 유용하다.

BT의 상위 우선순위에는 LLM보다 중요한 안전 규칙을 둔다. 사망, 피격 경직, 전투 강제 상태, 플레이어가 금지 구역을 침입한 경우처럼 명확한 규칙은 항상 먼저 처리한다. LLM 제안은 그 아래에서만 선택지에 영향을 주게 한다.

flowchart TD
    Root["Selector (?)"]
    Root --> N1["1. IsDead?"] --> A1["Die"]
    Root --> N2["2. IsStunned?"] --> A2["Recover"]
    Root --> N3["3. HasConfirmedHostileTarget?"] --> A3["Combat"]
    Root --> N4["4. HasValidLLMSuggestion?"] --> A4["ExecuteSuggestedAction"]
    Root --> N5["5. Patrol (Fallback)"]

예측 불가능성을 조절하는 세 가지 장치

LLM을 붙였다고 해서 매 순간 새로운 결정을 생성할 필요는 없다. 변주가 가치 있는 지점만 골라야 한다.

1. 요청 시점을 사건 중심으로 제한한다

소리 감지, 시야 상실, 대화 시작, 퀘스트 단계 변경처럼 의미 있는 사건에서만 요청한다. 같은 사건이 짧은 시간에 반복되면 쿨다운과 중복 요청 방지 키를 적용한다.

2. 무작위성보다 후보 선택을 사용한다

LLM이 완전히 새로운 계획을 만들게 하기보다 미리 설계한 행동 후보의 점수나 우선순위를 조정하게 한다. 예를 들어 경고 후 지원 요청즉시 추격 중 하나를 고르게 하면 디자이너가 가능한 결과의 범위를 알 수 있다.

행동 점수는 LLM의 제안과 게임 규칙을 함께 반영해 계산할 수 있다.

score(action)=wrruleScore(action)+wlllmScore(action)+wccontextScore(action)score(action) = w_r \cdot ruleScore(action) + w_l \cdot llmScore(action) + w_c \cdot contextScore(action)

여기서 ruleScore는 위험도나 퀘스트 규칙처럼 결정적인 게임 규칙을 반영한다. llmScore는 대화 맥락이나 NPC 성격에 따른 선호도이고 contextScore는 거리, 체력, 아군 수처럼 실시간 상태에 따른 점수다. 일반적으로는 w_r을 가장 크게 두어 게임 규칙이 흔들리지 않게 한다.

3. 결과를 관찰 가능하게 만든다

개발 빌드에서는 요청에 사용한 상태 요약, 모델 응답, 검증 실패 사유, 최종 실행 행동을 로그로 남긴다. 이 기록이 없으면 NPC가 이상하게 행동했을 때 BT 문제인지 프롬프트 문제인지 판단하기 어렵다.

LLM 제안, 검증 결과, 블랙보드 값, 최종 Behavior Tree 실행 노드를 한 화면에서 확인하는 디버그 패널 예시

성능과 운영에서 확인할 점

LLM 호출은 지연, 비용, 장애 가능성을 모두 가진 외부 의존성으로 다뤄야 한다. 전투 중 즉각 반응이 필요한 NPC에는 로컬 규칙과 BT를 우선 사용하고 LLM은 전투 진입 전의 전술 선택이나 대사처럼 수백 밀리초 이상의 지연을 감당할 수 있는 역할에 배치하는 편이 낫다.

  • 응답 시간 제한을 두고 초과 시 기본 행동을 사용한다.
  • NPC별 요청 횟수와 토큰 예산을 제한한다.
  • 동일한 상태 요약은 캐시하거나 하나의 분대 판단으로 묶는다.
  • 모델 응답과 프롬프트 버전을 기록해 재현 가능한 테스트를 만든다.
  • 온라인 게임이라면 플레이어 입력과 모델 출력 모두를 신뢰하지 않고 서버에서 최종 검증한다.

테스트에서는 “자연스러운가”만 보지 말고 불변 조건을 정의해야 한다. 예를 들어 체력이 0 이하인 NPC는 공격 태스크를 시작하지 않는다, 보이지 않는 대상은 추격 시작 대상이 될 수 없다, 허용 목록 밖의 행동은 절대 실행되지 않는다는 조건을 자동 테스트로 검증한다.

마무리

BT와 LLM의 결합에서 중요한 것은 LLM을 더 똑똑한 상태 기계로 바꾸는 일이 아니다. LLM이 잘하는 해석과 표현은 활용하되 실행 가능한 행동의 범위와 최종 결정 권한은 게임 시스템이 보유하는 구조를 만드는 일이다.

이 경계를 지키면 NPC는 정해진 대사를 반복하는 느낌에서 벗어나면서도 디자이너가 의도한 규칙과 플레이 경험을 벗어나지 않는다. 먼저 하나의 NPC와 4~6개의 행동 후보로 시작해 로그와 기본값을 갖춘 뒤 대화와 기억 기능을 단계적으로 넓히는 접근이 가장 안전하다.

#AI#Behavior Tree#LLM#NPC#게임 AI

계속 읽어보기

이런 글은 어떠세요?

< Back to Logs