
동일한 프롬프트, 4개의 LLM: 로그라이크(Roguelike) 대결
요약
동일한 프롬프트를 사용하여 4개의 서로 다른 LLM이 Python 기반 로그라이크 게임을 생성하는 과정을 비교 분석했습니다. 각 모델이 코드 구조, 아키텍처 설계, 프롬프트 요구사항 준수 여부에서 보여준 차이점을 다룹니다.
핵심 포인트
- 동일 프롬프트에 대해 모델별로 코드 길이와 아키텍처 접근 방식이 상이함
- DeepSeek V4 Flash는 다형성을 활용한 확장성 높은 구조를 보여줌
- 모델에 따라 프롬프트의 세부 요구사항(예: 누적 골드 계산) 누락 가능성 존재
- LLM의 성능은 단순 리더보드보다 실제 구현 방식의 차이로 확인 가능
저는 네 가지 서로 다른 LLM(Large Language Model)에 정확히 동일한 프롬프트를 제공했습니다: 표준 라이브러리만을 사용하여 단일 파일로 구성된 완전한 터미널 기반 Python 3 로그라이크(Roguelike) 게임을 제작할 것. 세 개의 모델은 실행 가능한 코드를 생성했습니다. 한 모델은 아무것도 생성하기 전에 타임아웃(Timeout)이 발생했습니다.
목표는 승자를 뽑는 것이 아니었습니다. 목표는 프롬프트가 일정하게 유지될 때 무엇이 변하는지 확인하는 것이었으며, 그 결과는 그 어떤 리더보드(Leaderboard)보다 LLM의 동작 방식에 대해 더 많은 것을 보여주었습니다.
프롬프트 (The Prompt)
사양은 상세하고 명확했습니다:
플레이어 능력치(체력 100, 골드 0, 공격력 15), 정확히 20턴, 4개 이상의 이벤트 유형(전투, 상자, 주점, 상인) 풀에서 턴당 3개의 무작위 선택지, ANSI 색상 코드 출력, 화면 지우기, 입력 유효성 검사, 그리고 게임 종료 요약이 포함된 터미널 기반 로그라이크를 제작할 것.
전체 프롬프트는 저장소 README에 있습니다.
아키텍처 다이어그램 (Architecture Diagram)
모델별 분석 (Model-by-Model Analysis)
DeepSeek V4 Flash (524행)
DeepSeek는 아키텍처 측면에서 가장 절제된 접근 방식을 취했습니다. 이 모델은 추상 Event 기본 클래스와 네 개의 구체적인 하위 클래스를 정의했습니다:
class Event:
def describe(self, player): ...
def can_afford(self, player): return True
...
선택지는 리스트에서 이벤트 클래스를 샘플링한 다음, 이를 인스턴스화하여 생성됩니다:
EVENT_CLASSES = [FightMonsterEvent, ChestEvent, TavernEvent, MerchantEvent]
chosen_types = random.sample(EVENT_CLASSES, k=3)
choices = [cls() for cls in chosen_types]
이러한 다형성(Polymorphic) 접근 방식은 깔끔하고 확장 가능합니다. 새로운 이벤트 유형을 추가하려면 클래스 하나를 추가하고 리스트에 등록하기만 하면 됩니다.
버그: 게임 종료 요약(end-game summary)에서 플레이어가 누적한 총 골드(lifetime gold)가 아닌 현재 보유 중인 골드를 표시합니다. 주점(tavern)이나 상인(merchant)에게 골드를 사용하면 최종 수치가 실제보다 적게 보고됩니다. 프롬프트는 "누적된 총 골드(total Gold accumulated)"를 요구했지만, 생애 누적 카운터(lifetime counter)가 구현되지 않았습니다.
MiniMax M3 (802 lines)
MiniMax는 사용자 경험(UX)에 올인했습니다. 색상 폴백 감지(color fallback detection), 색상이 변하는 체력 바(초록 > 노랑 > 빨강), 한 글자씩 느리게 출력되는 내레이션(character-by-character slow-print narration), ASCII 아트 배너, 그리고 다단계 판정 시스템(multi-tier verdict system)을 추가했습니다.
아키텍처 측면에서는 Callable 클로저(closure)를 전달하는 Choice 클래스를 사용했습니다:
class Choice:
def __init__(self, key, title, description, color, action: Callable):
self.key = key
...
게임 엔진은 단순히 choice.action(self.player)를 호출하며, 이는 깔끔한 디스패치 패턴(dispatch pattern)입니다.
선택지 생성(Choice generation)에는 흥미로운 전략이 사용되었습니다. 항상 모든 옵션을 보여주는 대신, 구매할 수 없는 옵션은 필터링하고 중복 항목으로 채워 넣었습니다(padding):
def generate_choices(player):
pool = [make_fight_choice(), make_chest_choice()]
tavern = make_tavern_choice(player) # 구매할 수 없으면 None
...
버그: 이 패딩(padding) 전략은 게임 초반 턴에 중복된 선택지(예: fight, chest, chest)를 생성할 수 있습니다. 또한, DeepSeek와 마찬가지로 생애 획득 골드가 아닌 현재 골드를 보고합니다.
Mimo 2.5 (800 lines)
Mimo는 네 개의 클래스에 책임을 분산하여 가장 깔끔한 아키텍처를 만들어냈습니다:
GameState — 플레이어 능력치 및 요약 카운터 관리
EventHandler — 이벤트 실행, GameState 변경
ChoiceGenerator — 메뉴 생성, 구매 가능 여부 확인
...
이 모델은 total_gold_earned를 올바르게 추적한 유일한 모델이었습니다:
class GameState:
def __init__(self):
...
...
숨겨진 버그: Mimo는 살아있는 Monster 인스턴스들을 전역 MONSTER_POOL에 저장하고 전투 중에 이를 변형(mutate)했습니다. 만약 동일한 몬스터 객체가 이후의 전투에서 다시 선택된다면, 손상된 능력치로 시작하거나 심지어 죽은 상태로 시작할 수 있었습니다. 이는 바로 표면적인 검토만으로는 살아남는 상태 관리(state-management) 결함의 전형적인 사례입니다.
또한 턴 추적(turn tracking) 과정에서 오프 바이 원(off-by-one) 오류가 있었습니다. 20턴을 완전히 생존한 경로는 21/20 턴으로 보고되었습니다.
Nemotron 3 Ultra (0줄)
Nemotron은 게임 소스 코드를 생성하지 못했습니다. 캡처된 에러는 다음과 같습니다:
write Failed
"Upstream idle timeout exceeded"
이는 코드 품질 데이터가 아닌 신뢰성(reliability) 데이터 포인트입니다. 실험의 목적이 "모델들에게 동일한 작업을 수행하도록 요청하는 것"이라면, 완료 신뢰성 또한 결과의 일부이므로 비교 대상에 포함되어야 합니다.
LLM 생성 코드를 사용하는 엔지니어를 위한 교훈
1. 표면적 준수 ≠ 정확성 (Surface Compliance ≠ Correctness)
성공한 세 모델 모두 ANSI 색상, 턴 루프, 전투, 요약 등 가시적인 요구사항을 구현했습니다. 하지만 버그는 인간 검토자가 직접 추적해야 하는 엣지 케이스(edge cases)와 장부 관리(bookkeeping) 부분에서 발생했습니다.
2. 카디널리티 제약 조건은 설계상의 트레이드오프를 강제함 (Cardinality Constraints Force Design Tradeoffs)
"정확히 3개의 선택지"라는 요구사항은 각 모델을 서로 다른 전략으로 몰아넣었습니다:
- DeepSeek: 모든 옵션을 보여주되, 실행 시점에 구매 불가능한 옵션을 차단함 (플레이어의 턴을 낭비하게 만듦)
- MiniMax: 구매 불가능한 옵션을 필터링하고, 중복된 항목으로 채움 ("고유함(distinct)"이라는 의도를 위반함)
- Mimo: 라벨과 함께 구매 불가능한 옵션을 보여주고, 어쨌든 핸들러(handler)로 라우팅함 (가장 실용적임)
이 중 명백히 정답인 것은 없습니다. 프롬프트가 모호했기 때문에 각 모델은 그 모호함을 서로 다르게 해결했습니다.
3. 아키텍처 품질과 정확성은 독립적인 축임 (Architecture Quality and Correctness Are Independent Axes)
Mimo는 상태/핸들러/제너레이터(state/handler/generator) 분리가 가장 깔끔했으며, 가장 흥미로운 숨겨진 상태 버그(변형된 전역 몬스터)를 가지고 있었습니다. MiniMax는 가장 세련된 UX를 보여주었지만, 가장 명백한 선택지 생성 문제를 보였습니다. 깔끔한 아키텍처는 좋은 것이지만, 그것이 올바른 동작을 보장하지는 않습니다.
4. 장부 기록(Bookkeeping)이 취약점이다
성공한 세 모델 중 두 모델이 "총 획득 골드(total gold accumulated)" 요구 사항을 틀렸는데, 이는 "현재 골드(current gold)"와 "누적 획득 골드(lifetime earned gold)"를 혼동했기 때문입니다. 이는 하나의 패턴입니다. LLM은 눈에 보이는 특징(visible features)을 생성하는 데는 능숙하지만, 정확한 의미론적 장부 기록(semantic bookkeeping)은 자주 놓칩니다.
5. 실패한 실행도 여전히 유용하다
Nemotron의 타임아웃(timeout)은 인프라 신뢰성에 대해 무언가를 알려줍니다. 실제 업무를 위해 LLM을 평가할 때, 완료율(completion rate)은 코드 품질과는 별개인 정당한 지표입니다.
실무적인 리뷰 체크리스트
이 실험을 바탕으로, 생성된 코드에 대한 리뷰 관점은 다음과 같습니다:
- 가시적인 프롬프트 준수 여부 확인 — 전반적인 구조가 일치하는가?
- 엣지 케이스(edge-case) 의미론 확인 — 숫자가 단순히 존재하는 것을 넘어 실제로 정확한가?
- 숨겨진 가변 상태(hidden mutable state) 확인 — 복사되어야 할 곳에서 객체가 공유되고 있지는 않은가?
- 최종 요약 장부 기록 확인 — "총 X"가 말 그대로의 의미를 갖는가?
- 모델 품질과 인프라 신뢰성 분리 — 타임아웃은 나쁜 코드가 아닙니다.
게임 실행하기
누구나 로컬에서 실행할 수 있습니다 — 별도의 의존성(dependencies)이 필요하지 않습니다:
python3 deepseek-v4-flash/roguelike.py
python3 minimax-m3/rogue.py
python3 mimo-2.5/roguelike.py
이 실험에서 가장 유용한 결과는 순위가 아닙니다. 동일한 프롬프트 비교를 통해 모델마다 서로 다른 본능이 드러난다는 관찰입니다:
- 한 모델은 **간결하고 프롬프트에 충실한 커버리지(concise, prompt-faithful coverage)**에 최적화되었습니다.
- 한 모델은 **프레젠테이션과 게임 플레이 느낌(presentation and game feel)**에 최적화되었습니다.
- 한 모델은 **깔끔하고 분리된 아키텍처(clean, separated architecture)**에 최적화되었습니다.
- 한 모델은 코드가 평가되기도 전에 **실행 계층(execution layer)**에서 실패했습니다.
실제 엔지니어링 업무를 위해서는 다음과 같은 실무적인 접근 방식이 제안됩니다: LLM을 초안 작성용으로 사용하고, 정확성을 위해 구조화된 리뷰를 적용하며, 준수하는 것처럼 보이는 출력이 반드시 정확한 출력이라고 가정해서는 안 됩니다.
출력물
Code and more: https://www.dailybuild.xyz/project/157-same-prompt-multiple-local-models
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기
