
LLM이 대화형 게임을 플레이하게 만드는 방법
요약
LLM을 활용하여 마피아(Werewolf) 게임을 플레이하는 기술적 아키텍처와 프롬프트 설계 방법을 다룹니다. 단순한 규칙 준수를 넘어 사용자 경험(UX), 재플레이성, 환각 방지 및 역할극 구현을 위한 해결책을 제시합니다.
핵심 포인트
- LLM 기반 게임 플레이를 위한 아키텍처 및 프롬프트 설계 방법론 제시
- 사용자 경험(UX)과 재플레이성을 고려한 AI 에이전트 구현
- 게임 규칙 준수와 역할극(Role-play) 사이의 균형 및 환각 문제 해결
- 오픈 소스 프로젝트 및 실제 플레이 가능한 웹 서비스 제공
이 글은 거의 2년 동안의 실험 결과물입니다. 네, 시간이 좀 걸렸네요... 이 글은 두 부분으로 나뉩니다. 이번 글은 약간 기술적인 측면에 초점을 맞추고, 다음 글은 게임 전반에서의 AI 능력에 대한 좀 더 철학적인 내용을 다룰 것입니다.
만약 '마피아(Werewolf)' 규칙이 익숙하지 않다면 다음을 확인하세요:
또는 Wiki 페이지.자, 그런데 이게 왜 큰 문제일까요? 그냥 규칙을 주면 알아서 플레이하겠죠, 그렇지 않나요? 그렇죠?
꼭 그렇지는 않습니다. 해결해야 할 몇 가지 문제들이 있습니다:
- 사용자에게 좋은 경험(User Experience)을 제공할 수 있을까요? 단순히 규칙을 따르는 것을 넘어, 실제로 저에게 게임을 흥미롭게 만들어 줄 수 있을까요? 어떻게 이를 달성할 수 있을까요? 단순히 "재밌게 만들어줘"라고 프롬프트(Prompt)를 입력한다고 해결되지는 않습니다.
- 재플레이성(Replayability)은 어느 정도일까요?
- 환각(Hallucinations) 현상은 여전히 문제 아닐까요? 규칙을 한 번이라도 어기면 게임은 망가집니다. 이를 어떻게 해결해야 할까요?
- 역할극(Role-play)이 가능할까요? 예를 들어 해리 포터 캐릭터들이 마피아 게임을 한다면 어떨까요? 이 역할극이 규칙보다 우선시되지는 않을까요?
- 사용자들이 Python 프로그래밍을 위해 봇을 사용하거나, 봇과 관계를 맺기로 결정한다면 어떻게 될까요?
- AI가 나쁜 행동을 해도 괜찮을까요? 예를 들어 다른 AI를 죽이는 것 말입니다. 물론 그것이 완전히 괜찮다는 것은 알고 있지만, 항상 그런 경우만 있을까요?
여러분의 생각은 어떨지 모르겠지만, 저에게 이 질문들은 그리 당연한 것들이 아닙니다.
이 글은 '어떻게(how)'에 관한 것입니다. 아키텍처(Architecture), 프롬프트(Prompts), 그리고 배관(Plumbing) 작업에 대해 다룹니다. 질문 1번부터 4번까지에 대한 답이 여기에 있습니다. 질문 5번과 6번—즉, 모델에게 제약을 풀었을 때 실제로 어떤 행동을 하는지에 대해서는 2부에서 다룹니다.
아래의 모든 내용은 오픈 소스(Open source)입니다: github.com/hiper2d/werewolf-ai-party-game. 제가 인용하는 모든 프롬프트는 저장소(Repo)의 정확한 라인으로 연결됩니다. 게임은 aiwerewolf.net에서 실제로 플레이할 수 있습니다. 궁금하시다면 무료로 이용해 보세요.
이와 유사한 다른 프로젝트들
이와 유사한 프로젝트들이 많이 있습니다. 유튜브에는 AI를 활용한 수많은 마피아(Mafia) 및 웨어울프(Werewolf) 프로젝트들이 있습니다. 이 주제에 특화된
, 과학 논문 (science papers), 전체 채널 (full channels)들이 존재합니다. "AI D&D"나 다양한 문명 시뮬레이션(civilization simulations)을 검색해 보시면 엄청난 양의 정보에 압도될 것입니다.하지만 한 가지 문제가 있습니다. 이들 모두는 대개 결과에만 집중합니다. "내가 AI에게 X를 시켰더니 Y가 일어났다. Claude는 이렇게 했고, Gemini는 저렇게 했다... 그리고 Grok이 무엇을 했는지 믿지 못할 것이다. 정말 지독한 녀석이다." 같은 식이죠. 아무도 사용자 경험(user experience)에 진정으로 집중하지 않습니다. 재플레이 가능성(replayability)이나 깊이(depth)에 대해서도 말이죠.
그리고 보통 이 모든 것이 실제로 어떻게 구현되는지는 아무도 다루지 않습니다. 마치 중요하지 않다는 듯이 말입니다. 어떻게든 이 모델들이 모여서 그냥 플레이하는 것처럼 보여주죠. 그런데 그거 아세요? 그것은 매우 중요합니다. 많은 AI 행동(AI behavior)은 당신이 어떻게 프롬프트(prompt)를 작성하는지, 게임 루프(game loop)를 어떻게 구현하는지, 정보를 어떻게 전달하는지, 메모리(memory)를 어떻게 제어하는지 등에 따라 달라집니다. 이 모든 것이 중요합니다.
정말 그럴까요? 한번 봅시다. 저도 처음부터 이 모든 것을 알고 있었던 것은 아닙니다.
주말 동안 진행한 프로젝트
단순할 줄 알았습니다. 그냥 여러 AI를 가져와서 그룹 채팅방에 넣고, 규칙을 전달한 뒤, '플레이'를 누르면 되는 것이었죠. 쉬워 보였습니다.
음... 그런데 LLM을 위한 그룹 채팅을 실제로 어떻게 만들까요? 모든 LLM 제공업체는 한 명의 사용자(user)와 한 명의 어시스턴트(assistant) 사이의 대화를 가정합니다. 각 메시지는 이 두 역할 중 하나로 라벨이 지정됩니다. 채팅 기록(chat history)은 다음과 같은 형태를 띱니다:
[
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "What's the capital of France?" },
...
이것이 전체 어휘(vocabulary)입니다. 하나의 system 역할이 있고, 그 다음 user와 assistant가 컨텍스트(context)가 다할 때까지 번갈아 가며 나타납니다. role: "alice" 같은 역할은 존재하지 않습니다.
이것은 제가 필요로 하는 것과 일치하지 않습니다:
- 그룹 채팅에서 저는 사용자(user)이고 다른 모든 이들은 어시스턴트(assistant)입니다. 하지만 각 봇은 서로 다른 구도를 봅니다. 한 명의 어시스턴트(자기 자신)와 나머지 모두를 사용자로 인식합니다.
- 그리고 YouTube의 대부분의 예시처럼 이들이 순서대로 하나씩 진행하는 것을 원하지 않습니다. 저는 여러 플레이어가 토론하고, 서로를 대화에 끌어들이며, 일관성을 유지하고 생동감 있게 이어가는 자연스러운 대화를 원합니다.
- 또한 실시간 타이핑 경쟁을 원하지 않는다는 사실도 빠르게 깨달았습니다. 저에게는 생각할 시간이 필요하며, 오늘 게임을 종료했다가 내일 다시 이어서 하고 싶습니다.
- 비용을 최소화하면 좋을 것입니다. 각 메시지를 모든 LLM에 브로드캐스팅(Broadcasting)하는 것은 최적의 방법이 아닙니다.
라우터(Router)로서의 게임 마스터
이를 구현하는 방법은 많으며, 작업 자체가 그렇게 어렵지는 않습니다. 저는 에이전트 설계(agentic design)에서 널리 채택되는 패턴인 라우터(router)를 가져왔습니다.
저는 게임에 AI를 하나 더 추가했습니다. 바로 라우터(Router)입니다. 이는 메시지를 읽고 다음에 누가 말해야 할지를 결정하는 지능형 구성 요소입니다.
- 제가 말을 하면 이 메시지는 라우터로 전달됩니다: "헤이, 모두에게 전달해줘"
- 라우터는 채팅 기록을 읽고 결정합니다: "Bob과 Alice가 다음에 말해야 해"
- 라우터가 Bob에게 말을 요청합니다: "헤이 Bob, 네가 말할 차례야. 그런데 Alex로부터 메시지가 왔었어"
- Bob이 라우터에게 답장합니다: "Alex에게 꺼지라고 전해줘. 그리고 Alice가 아마 늑대인간일 것 같다고 알려줘"
- 그런 다음 라우터가 Alice를 핑(ping)합니다: "Alice, 채팅에 답장해줘. Alex와 Bob으로부터 새로운 메시지가 왔어"
라우터 메시지는 해당 봇에게만 보입니다. 저에게 채팅은 다음과 같이 보입니다:
[
{ "author": "Alex", "msg": "Bob이 오늘 매우 조용하네. 수상할 정도로 조용해." },
{ "author": "Bob", "msg": "조용한 건 죄가 아니야. 시끄럽다고 알리바이가 되는 것도 아니고, Alex." },
...
실제 앱에서는 일반적인 그룹 채팅입니다. 모두가 한 방에 있고, 사이드바는 Harry는 Grok, Ron은 DeepSeek, Snape는 Haiku, Fred는 Gemini라고 조용히 알려줍니다:
[
Bob에게는 이 동일한 세 개의 메시지가 다음과 같이 보입니다. 즉, 완전히 합법적이고 지루한, 1인 1어시스턴트(one-user-one-assistant) 대화입니다:
[
{ "role": "system", "content": "당신은 Bob입니다. <규칙, 당신의 역할, 당신의 캐릭터>" },
{ "role": "user", "content": "Bob, 토론 중인 플레이어들에게 답장하세요.\n\n## 다른 플레이어들의 메시지\n\n**1. Alex:** Bob은 오늘 매우 조용합니다. 수상할 정도로 조용하네요." }
...
Alice에게 어떤 일이 일어났는지 주목하세요. 그녀는 Bob의 대화 참여자가 아닙니다. 그녀는 Router의 메시지 안에 인용된 _콘텐츠(content)_입니다. 이 평탄화(flattening)를 수행하는 코드는 convertToAIMessages입니다. 이 코드는 공유된 게임 로그를 훑으며, 현재 봇 자신의 메시지는 assistant로 유지하고, 그 외의 모든 것—Game Master의 명령과 다른 모든 플레이어의 대사—을 하나의 user 블록으로 접어 넣습니다.
봇들은 서로의 존재를 인지하고 있지만, Router를 통해 그들에게 말하도록 지시받습니다. 봇들에게 Router는 유저(user)입니다. 저에게 Router는 어시스턴트(assistant)입니다.
Router는 짧은 system prompt와 이름 외에는 아무것도 반환하지 못하도록 강제하는 스키마(schema)를 가진 또 다른 모델일 뿐입니다:
{
"selected_bots": ["Bob", "Alice", "Cho"],
"reasoning": "Alex가 Bob의 이름을 불렀습니다. Alice는 직접적으로 비난받았습니다. Cho는 오늘 한 번도 말하지 않았으며 NEEDS TURN으로 표시되었습니다."
...
그 NEEDS TURN 플래그는 제가 예상했던 것보다 더 중요한 역할을 했습니다. 이 플래그가 없으면 라우터 (Router)는 계속해서 가장 목소리가 큰 쪽을 선택하게 되고, 두세 개의 봇은 살아있음에도 불구하고 게임에서 조용히 사라져 버립니다. 이제 프롬프트 (Prompt)는 매 배치 (Batch)마다 최소한 한 명의 조용한 플레이어를 포함하도록 강제합니다.
이는 또한 위 목록의 4번 문제도 해결합니다. 메시지가 12개의 모델로 전달되는 것이 아니라, 의도적으로 선택된 2개에서 5개의 모델로만 전달됩니다.
최적의 메모리로서의 시스템 프롬프트 (System Prompt)
좋습니다, 그들은 서로 대화할 수 있고 저와도 대화할 수 있습니다. 대화가 자연스럽게 흘러가고 있죠. 그렇다면 규칙은 어떨까요? 다행히 마피아 (Werewolf) 규칙은 간결하여 시스템 프롬프트 (System Prompt)에 쉽게 들어갑니다. 역할 (Role), 동료 (Werewolves), 그리고 유용할 수 있는 기타 정보와 같은 개인 정보와 함께 말이죠. RAG (Retrieval-Augmented Generation)나 추가적인 복잡함은 필요 없습니다.
그 구조는 다음과 같습니다 (전체 내용은 리포지토리에서 확인할 수 있습니다):
## 캐릭터 정체성 (Character Identity)
**이름:** %name%
**개인 스토리:** %personal_story%
...
그래서 저는 역할을 부여하고 게임을 시작했습니다.
결과는 좋았을까?
아니요. 반복적이고 지루했습니다. Gemini는 사랑에 빠지지 않았고, Grok은 제3제국을 재건하러 떠나지도 않았으며, Claude는 누구도 속이지 못했습니다. 그들은 이성적이어야 한다는 점, 사실 없이 판단하지 말아야 한다는 점, 팀으로서 협력해야 한다는 점 등을 계속해서 말할 뿐이었습니다. 그들은 어처구니없는 이유로 누군가를 지목하더니, 다 함께 그 플레이어를 처형 (Lynched)해 버렸습니다.
보통 처형당하는 대상은 저였습니다. 제 텍스트는 달랐고, 이는 분명히 제가 마피아임을 드러냈습니다. 제가 무슨 말을 하든 그들은 이 생각을 더욱 굳혔습니다. 상황은 점점 더 악화되었습니다. 제가 스스로를 방어하려고 노력할수록, 그들은 저를 더 죽이고 싶어 했습니다. 죽고 싶어 하지 않는 것이야말로 너무나 마피아다운 모습이었죠. 그들의
그들은 게임을 진전시키지 않았습니다. 마피아 게임(Werewolf)이나 그와 유사한 게임에는 한 가지 특징이 있습니다. 아이디어를 던져야 하고, 기만해야 하며, 사람들을 비난하고, 토론에 참여시키고, 타겟을 바꾸고, 팀을 결성하거나 동맹을 깨야 합니다. 단순히 이성적으로 판단하고 증거를 수집하기에는 정보가 턱없이 부족합니다.
또 다른 문제는 규칙과 이름에 대해 환각 (hallucination)을 일으켰다는 점입니다. 여기저기서 그들의 메시지에 새로운 역할이 나타났습니다. 자신을 잘못된 이름으로 소개하기도 했습니다. 나중에 투표와 밤 행동 (night actions) 기능을 추가했을 때, 그들은 사건의 순서를 명확히 파악하지 못하고 자신들만의 규칙을 만들어냈습니다.
이를 개선할 방법이 있을까요?
네, 몇 가지 방법이 있습니다.
게임 엔진 (Game Engine)
저는 모델의 컨텍스트 (context)에 방대한 데이터를 쏟아붓고 모델이 알아서 파악하게 만드는 방식이 작동하지 않는다는 것을 빠르게 깨달았습니다. 긴 대화가 이어지면 시스템 프롬프트 (system prompt)에 있는 정보조차 녹아 없어지기 시작합니다. 이를 컨텍스트 부패 (context rot)라고 하며, 이미 알려진 현상입니다. 하지만 저는 보통 마지막 메시지가 가장 큰 영향력을 가진다는 점에 주목했습니다. 따라서 이 특정 순간에 중요한 내용을 그곳에 배치하기로 했습니다.
게임은 상태 머신 (state machine)입니다. 매 순간, 게임은 모델에게 매우 구체적인 행동을 명령합니다:
- 낮 토론 (Day discussion) - 채팅에 말하기
- 낮 투표 (Day voting) - 이름과 이유 제시하기
- 밤 행동 (Night action) - 역할에 기반하여 타겟을 정하고 행동 선택하기
모든 명령에는 제약 조건 (constraints) 목록을 가질 수 있습니다. 모델이 투표할 때, 목록에서 이름을 선택하도록 요청할 수 있습니다. 반드시 그럴 필요는 없습니다. 모델은 과거 사건으로부터 생존한 플레이어를 추론할 수 있으니까요. 하지만 정확한 목록을 단순히 전달할 수 있는데 왜 거기에 의존해야 할까요? 행동을 선택하도록 요청할 때는 사용 가능한 행동 목록을 제공하십시오.
투표 명령(vote command)은 결국 다음과 같은 형태가 됩니다:
**투표 가능한 후보자 - 이들 중 정확히 '한 명'에게만 투표할 수 있습니다:**
Akira, Yuki, Mizuki, Takeshi, Emiko, Daichi, yoshiteru
...
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기
