로컬 LLM으로 'Slay the Spire 2' 에이전트 구축하기: 교훈과 미해결 과제

개발자가 KoboldCPP/Ollama를 통해 로컬 LLM을 사용하여 Slay the Spire 2를 플레이하는 에이전트를 구축했습니다. 게임은 커뮤니티 모드를 통해 REST API로 노출되며, 에이전트는 중간에 위치합니다: 게임 상태 읽기 → 도구와 함께 LLM 호출 → 액션 실행 → 반복.
설정 및 성능
설정은 KoboldCPP를 통해 RTX 4090에서 Qwen3.5-27B(Q4_K_M)를 사용합니다. 성능 지표: 액션당 약 10초, 액션 성공률 약 88%. 최고 결과: Act 1 보스 격파. 프로젝트는 GitHub에서 확인할 수 있습니다: https://github.com/Alex5418/STS2-Agent.
작동하는 것들
- 상태 기반 도구 라우팅 — 한 번에 20개 이상의 도구를 노출하는 대신, 현재 게임 상태와 관련된 1-3개의 도구만 제공됩니다. 전투에는
play_card,end_turn,use_potion이 제공됩니다. 맵 화면에는choose_map_node가 제공됩니다. 이로 인해 환각 도구 호출이 극적으로 감소했습니다. - 단일 도구 모드 — 작은 모델은 액션 후 게임 상태가 어떻게 변하는지 예측할 수 없습니다(예: 카드를 플레이한 후 카드 인덱스가 이동함). 따라서 응답당 첫 번째 도구 호출만 실행된 후 게임 상태를 다시 가져오고 모델에게 다시 요청합니다. 느리지만 훨씬 더 신뢰할 수 있습니다.
- 텍스트 기반 도구 호출 파서(폴백) — KoboldCPP는 종종 구조화된 JSON 대신 텍스트로 도구 호출을 출력합니다. 다중 패턴 정규식 폴백은 다음과 같은 형식을 포착합니다:
json [{"name": "play_card", "arguments": {...}}],Made a function call ... to play_card with arguments = {...},play_card({"card_index": 1, "target": "NIBBIT_0"}), 그리고end_turn과 같은 인수가 없는 도구의 단순 언급. 이는 그렇지 않으면 손실될 액션의 약 15-20%를 복구합니다. - 에너지 가드 — 남은 에너지의 클라이언트 측 추적. 모델이 감당할 수 없는 카드를 플레이하려고 하면 API 호출이 차단되고 턴이 자동으로 종료됩니다. 이는 가장 일반적인 오류 루프(모델이 동일한 감당할 수 없는 카드를 3회 이상 재시도)를 방지합니다.
- 적 턴을 위한 스마트 대기 — 적의 턴 동안 게임 상태는 "Play Phase: False."라고 표시합니다. 이에 대해 LLM 호출을 낭비하는 대신, 에이전트는 플레이어의 턴이 다시 될 때까지 매 1초마다 폴링합니다.
개방형 문제
- 모델이 시스템 프롬프트 규칙을 일관되게 따르지 않음 — 시스템 프롬프트는 "적의 의도가 공격이면, 방어 카드를 먼저 플레이하세요."와 같은 내용을 말합니다. 모델은 이를 약 30%의 경우에 따릅니다. 나머지 70%는 관계없이 공격만 플레이합니다. 시도된 해결책: 더 강력한 어조("반드시 먼저 방어해야 합니다"), 프롬프트의 몇 가지 샘플 예시, 계산된 힌트 주입("경고: 15의 들어오는 피해"). 어느 것도 신뢰할 수 없습니다. 질문: 작은 모델이 조건부 규칙을 따르도록 하는 더 나은 프롬프트 전략이 있을까요? 아니면 이것이 27B에서의 근본적인 한계일까요?
- KoboldCPP와의 도구 호출 신뢰성 — 텍스트 폴백 파서가 있더라도, 약 12%의 응답은 사용 가능한 도구 호출을 생성하지 않습니다. 모델은 때때로 빈
<think></think>블록 뒤에 잘못된 JSON을 출력합니다. Ollama OpenAI 호환성 레이어도 가끔arguments를 딕셔너리 대신 문자열로 반환합니다. 질문: 14-30B 범위에서 도구 호출에 특히 신뢰할 수 있는 모델을 찾은 사람이 있을까요? 개발자는 Phi-4(14B)를 잠시 시도했지만 적절한 비교는 하지 않았습니다. Mistral-Small이나 Command-R을 고려 중입니다. - 컨텍스트 창 관리 — 각 게임 상태는 마크다운으로 약 800-1500 토큰입니다. 시스템 프롬프트(약 500 토큰)와 대화 기록을 포함하면 컨텍스트가 빠르게 채워집니다. 현재는 마지막 5개의 교환만 유지하고 상태 전환(전투 → 맵 등) 시 기록을 재설정합니다. 그러나 모델은 전투 간에 기억이 없습니다 — 실수로부터 배울 수 없습니다. 질문: 롤링 요약 접근 방식이 작동할까요? 예: 마지막 전투를 "당신은 Jaw Worm와 싸웠습니다. 턴 2에 방어하지 않아 15의 피해를 입었습니다. 4턴 만에 승리했습니다."로 압축하는 것.
- 로컬 모델의 더 나은 구조화된 출력 — 핵심 문제는 모델이 JSON 도구 호출을 출력해야 하지만, 실제로는 먼저 자연어로 생각하고 싶어 한다는 것입니다. Qwen3.5는 제거되는
<think>블록을 사용하지만, 때로는 생각과 도구 호출이 뒤섞입니다. 질문: 두 단계 접근 방식이 더 잘 작동할까요? 1단계: "게임 상태를 분석하고 무엇을 할지 결정하세요"(자유 텍스트). 2단계: "이제 정확히 하나의 도구 호출을 출력하세요"(제약됨). 이는 지연 시간을 두 배로 늘리지만 신뢰성을 향상시킬 수 있습니다. 이 패턴을 시도한 사람이 있을까요? - 모델 간 A/B 테스트 — 개발자는 비교를 위해 액션을 기록하는 JSONL 로깅 시스템을 가지고 있습니다.
📖 전체 소스 읽기: r/LocalLLaMA
👀 See Also

클로드 코드를 사용하여 10일 만에 스팀 게임 제작하기: 기술적 도전과 작업 흐름
한 개발자가 Claude Code를 사용하여 직접 코드를 작성하지 않고 10일 만에 Steam에 게임을 제작 및 출시했으나, 논리 설계와 AI 생성 코드 디버깅에서 상당한 어려움을 겪었습니다.

금융 모델러, Claude Code로 로컬 음성-도구 데스크톱 앱 구축
재무 모델링 배경을 가진 개발자가 Claude Code를 사용하여 GPU에서 Whisper를 실행하는 로컬 Windows 음성-텍스트 애플리케이션인 Sotto를 만들었습니다. 이 앱은 시스템 전체 핫키, 자동 정지 감지 및 Qt UI를 특징으로 하며, 17개 파일에 걸쳐 약 2,200줄의 Python 코드로 구성되어 있습니다.

스타트업 창업자, 고객 지원 및 경쟁사 조사에 AI 에이전트 활용
한 스타트업 창업자가 AI 에이전트를 문서에 연결하여 고객 지원을 자동화하여 하루 2시간에서 20분으로 시간을 단축하고, 매주 경쟁사 연구 요약을 Slack으로 전달하도록 설정했습니다.

비기술 프리랜서가 MaxClaw와 MiniMax 에이전트를 활용해 서비스 범위를 확장합니다
코딩 능력이 없는 소셜 미디어 전략가가 MiniMax Agent를 사용하여 랜딩 페이지를 구축하고 MaxClaw를 활용해 클라이언트 브리핑과 콘텐츠 리서치를 처리하여 프로젝트 단가를 $1,500에서 $3,200으로 높였습니다.