프롬프트 API로 추측 게임 빌드

게시일: 2025년 10월 10일

2014년에 Guess Who 게임을 플레이하는 학령기 아동

클래식 보드 게임인 Guess Who?는 연역적 추론의 대표적인 예입니다. 각 플레이어는 얼굴 보드로 시작하여 일련의 예 또는 아니요 질문을 통해 상대방의 비밀 캐릭터를 확신할 수 있을 때까지 가능성을 좁힙니다.

Google I/O Connect에서 내장 AI 데모를 본 후 브라우저에 있는 AI와 Guess Who? 게임을 할 수 있다면 어떨까 하는 생각이 들었습니다. 클라이언트 측 AI를 사용하면 사진이 로컬로 해석되므로 친구와 가족의 맞춤 '누구일까요?'가 내 기기에서 비공개로 안전하게 유지됩니다.

저는 주로 UI 및 UX 개발을 해 왔으며 픽셀 단위로 완벽한 환경을 구축하는 데 익숙합니다. 내 해석을 통해 정확히 그럴 수 있기를 바랐습니다.

내 애플리케이션인 AI Guess Who?는 React로 빌드되었으며 Prompt API와 브라우저 내장 모델을 사용하여 놀라울 정도로 유능한 상대를 만듭니다. 이 과정에서 '완벽한' 결과를 얻는 것이 간단하지 않다는 것을 알게 되었습니다. 하지만 이 애플리케이션은 AI를 사용하여 사려 깊은 게임 로직을 빌드하는 방법과 이 로직을 개선하고 원하는 결과를 얻기 위한 프롬프트 엔지니어링의 중요성을 보여줍니다.

내장된 AI 통합, 직면한 문제, 해결책에 대해 자세히 알아보세요. 게임을 플레이하고 GitHub에서 소스 코드를 확인할 수 있습니다.

게임 기반: React 앱

AI 구현을 살펴보기 전에 애플리케이션의 구조를 검토하겠습니다. 게임의 지휘자 역할을 하는 중앙 App.tsx 파일이 있는 TypeScript로 표준 React 애플리케이션을 빌드했습니다. 이 파일에는 다음이 포함됩니다.

  • 게임 상태: 게임의 현재 단계를 추적하는 열거형입니다 (예: PLAYER_TURN_ASKING, AI_TURN, GAME_OVER). 인터페이스에 표시되는 내용과 플레이어가 사용할 수 있는 작업을 결정하므로 가장 중요한 상태입니다.
  • 캐릭터 목록: 활성 캐릭터, 각 플레이어의 비밀 캐릭터, 보드에서 제거된 캐릭터를 지정하는 목록이 여러 개 있습니다.
  • 게임 채팅: 질문, 답변, 시스템 메시지의 실행 로그입니다.

인터페이스는 다음과 같은 논리적 구성요소로 나뉩니다.

GameSetup이 초기 화면입니다.
GameBoard는 모든 사용자 입력을 처리하기 위해 캐릭터 그리드와 채팅 컨트롤을 표시합니다.

게임의 기능이 늘어남에 따라 복잡성도 증가했습니다. 처음에는 전체 게임의 로직이 단일 대형 맞춤 React 후크(useGameLogic) 내에서 관리되었지만, 너무 커서 탐색하고 디버그하기가 어려워졌습니다. 유지관리 용이성을 개선하기 위해 이 후크를 각각 단일 책임을 갖는 여러 후크로 리팩터링했습니다. 예를 들면 다음과 같습니다.

  • useGameState는 핵심 상태를 관리합니다.
  • usePlayerActions는 플레이어의 차례입니다.
  • useAIActions는 AI의 논리를 위한 것입니다.

이제 기본 useGameLogic 후크가 이러한 작은 후크를 함께 배치하는 클린 컴포저 역할을 합니다. 이 아키텍처 변경으로 게임의 기능은 변경되지 않았지만 코드베이스가 훨씬 깔끔해졌습니다.

프롬프트 API를 사용한 게임 로직

이 프로젝트의 핵심은 Prompt API를 사용하는 것입니다.

builtInAIService.ts에 AI 게임 로직을 추가했습니다. 주요 책임은 다음과 같습니다.

  1. 제한적인 이진 답변을 허용합니다.
  2. 모델에 게임 전략을 가르칩니다.
  3. 모델 분석을 학습시킵니다.
  4. 모델의 기억력을 지웁니다.

제한적인 이진 답변 허용

플레이어는 AI와 어떻게 상호작용하나요? 플레이어가 '내 캐릭터는 모자를 쓰고 있어?'라고 물으면 AI는 비밀 캐릭터의 이미지를 '보고' 명확한 대답을 해야 합니다.

처음에는 엉망이었어요. 대답이 대화형이었습니다. '아니요, 제가 생각하는 캐릭터인 이사벨라는 모자를 쓰고 있지 않은 것 같습니다'와 같이 이분법적인 예 또는 아니요를 제공하는 대신에 말이죠. 처음에는 매우 엄격한 프롬프트로 이 문제를 해결했습니다. 기본적으로 모델에 '예' 또는 '아니요'로만 대답하도록 지시했습니다.

이 방법도 효과적이었지만 구조화된 출력을 사용하는 더 나은 방법을 알게 되었습니다. 모델에 JSON 스키마를 제공하면 참 또는 거짓 응답을 보장할 수 있습니다.

const schema = { type: "boolean" };
const result = session.prompt(prompt, { responseConstraint: schema });

이를 통해 프롬프트를 간소화하고 코드가 응답을 안정적으로 처리하도록 할 수 있었습니다.

JSON.parse(result) ? "Yes" : "No"

모델에 게임 전략 가르치기

모델이 질문을 시작하고 질문하도록 하는 것보다 모델에 질문에 답하라고 지시하는 것이 훨씬 간단합니다. 훌륭한 추측 게임 플레이어는 무작위 질문을 하지 않습니다. 한 번에 가장 많은 글자를 제거하는 질문을 합니다. 이상적인 질문은 이진 질문을 사용하여 가능한 나머지 문자를 절반으로 줄입니다.

모델에 이 전략을 어떻게 가르치나요? 다시 말하지만 프롬프트 엔지니어링입니다. generateAIQuestion()의 프롬프트는 실제로 추측 게임 이론에 관한 간결한 수업입니다.

처음에 모델에 '좋은 질문을 해'라고 요청했습니다. 결과는 예측할 수 없었습니다. 결과를 개선하기 위해 부정 제약 조건을 추가했습니다. 이제 프롬프트에 다음과 유사한 안내가 포함됩니다.

  • '심각: 기존 기능에 대해서만 질문하세요'
  • 'CRITICAL: 독창성을 갖추세요. 질문을 반복하지 마세요.'

이러한 제약 조건은 모델의 초점을 좁히고 관련 없는 질문을 하지 못하도록 방지하여 훨씬 더 즐거운 상대로 만들어 줍니다. GitHub에서 전체 프롬프트 파일을 검토할 수 있습니다.

모델 분석 학습

이것이 가장 어렵고 중요한 과제였습니다. 모델이 '내 캐릭터가 모자를 쓰고 있나요?'와 같은 질문을 하고 플레이어가 아니라고 대답하면 모델은 보드에서 어떤 캐릭터가 제거되었는지 어떻게 알 수 있나요?

모델은 모자를 쓴 사람을 모두 제거해야 합니다. 초기 시도에는 논리적 오류가 많았고, 모델이 잘못된 문자를 삭제하거나 문자를 삭제하지 않는 경우도 있었습니다. '모자'란 무엇인가요? '비니'는 '모자'로 간주되나요? 솔직히 말해 이는 인간의 토론에서도 일어날 수 있는 일입니다. 물론 일반적인 실수도 발생합니다. AI 관점에서 머리카락은 모자처럼 보일 수 있습니다.

인식과 코드 추론을 분리하도록 아키텍처를 재설계했습니다.

  1. AI는 시각적 분석을 담당합니다. 모델은 시각적 분석에 탁월합니다. 모델에 질문과 상세 분석을 엄격한 JSON 스키마로 반환하도록 지시했습니다. 모델은 보드의 각 문자를 분석하고 '이 문자에 이 기능이 있나요?'라는 질문에 답합니다. 모델은 구조화된 JSON 객체를 반환합니다.

    { "character_id": "...", "has_feature": true }
    

    다시 말하지만, 성공적인 결과를 위해서는 구조화된 데이터가 중요합니다.

  2. 게임 코드에서 분석을 사용하여 최종 결정을 내립니다. 애플리케이션 코드는 플레이어의 대답 ('예' 또는 '아니요')을 확인하고 AI의 분석을 반복합니다. 플레이어가 '아니요'라고 말하면 코드는 has_featuretrue인 모든 문자를 삭제합니다.

이러한 분업이 신뢰할 수 있는 AI 애플리케이션을 빌드하는 데 핵심이라고 생각합니다. 분석 기능에는 AI를 사용하고 이진 결정은 애플리케이션 코드에 맡기세요.

모델의 인식을 확인하기 위해 이 분석을 시각화했습니다. 이를 통해 모델의 인식이 올바른지 쉽게 확인할 수 있었습니다.

프롬프트 엔지니어링

하지만 이렇게 분리해도 모델의 인식이 여전히 잘못될 수 있다는 것을 알게 되었습니다. 캐릭터가 안경을 착용했는지 잘못 판단하여 잘못된 탈락이 발생할 수 있습니다. 이 문제를 해결하기 위해 저는 2단계 프로세스를 실험했습니다. AI가 질문을 합니다. 플레이어의 대답을 받은 후 대답을 컨텍스트로 사용하여 두 번째 새로운 분석을 실행합니다. 두 번째로 살펴보면 첫 번째에서 놓친 오류를 발견할 수 있다는 이론이었습니다.

이 흐름이 작동하는 방식은 다음과 같습니다.

  1. AI 턴 (API 호출 1): AI가 '캐릭터에 수염이 있나요?'라고 묻습니다.
  2. 플레이어의 차례: 플레이어가 수염이 없는 비밀 캐릭터를 보고 '아니요'라고 대답합니다.
  3. AI 턴 (API 호출 2): AI는 남은 모든 문자를 다시 살펴보고 플레이어의 대답에 따라 어떤 문자를 제거할지 결정하도록 효과적으로 요청합니다.

2단계에서는 사용자가 예상했음에도 불구하고 모델이 가벼운 수염이 있는 캐릭터를 '수염이 없음'으로 잘못 인식하여 제거하지 못할 수 있습니다. 핵심 인식 오류가 수정되지 않았으며 추가 단계로 인해 결과가 지연되었을 뿐입니다. 사람 상대와 대결할 때는 이에 관한 합의나 설명을 지정할 수 있지만, 현재 AI 상대와의 설정에서는 그렇지 않습니다.

이 프로세스는 정확도를 크게 높이지 않고 두 번째 API 호출에서 지연 시간을 추가했습니다. 모델이 처음 잘못된 경우 두 번째도 잘못된 경우가 많았습니다. 검토를 한 번만 하도록 프롬프트를 되돌렸습니다.

분석을 추가하는 대신 개선

저는 UX 원칙을 따랐습니다. 해결책은 더 많은 분석이 아니라 더 나은 분석이었습니다.

프롬프트를 미세 조정하고 모델이 작업을 다시 확인하고 고유한 기능에 집중하도록 명시적인 안내를 추가하는 데 많은 투자를 했습니다. 이는 정확성을 개선하는 데 더 효과적인 전략인 것으로 입증되었습니다. 현재의 더 안정적인 흐름은 다음과 같이 작동합니다.

  1. AI 턴 (API 호출): 모델에 질문과 내부 분석을 동시에 생성하라는 메시지가 표시되어 단일 JSON 객체를 반환합니다.

    1. 질문: '내 캐릭터가 안경을 쓰고 있나요?'
    2. 분석 (데이터):
    [
      {character_id: 'brad', has_feature: true},
      {character_id: 'alex', has_feature: false},
      {character_id: 'gina', has_feature: true},
      ...
    ]
    
  2. 플레이어의 차례: 플레이어의 비밀 캐릭터는 안경을 쓰지 않은 Alex이므로 '아니요'라고 대답합니다.

  3. 라운드 종료: 애플리케이션의 JavaScript 코드가 인계됩니다. AI에 다른 질문을 할 필요가 없습니다. 1단계의 분석 데이터를 반복합니다.

    1. 플레이어가 '아니요'라고 말했습니다.
    2. 코드는 has_feature가 true인 모든 문자를 찾습니다.
    3. 브래드와 지나가 아래로 뒤집힙니다. 로직은 확정적이고 즉각적입니다.

이 실험은 매우 중요했지만 많은 시행착오가 필요했습니다. 상황이 나아질지 알 수 없었습니다. 때로는 상황이 더 악화되기도 했습니다. 가장 일관된 결과를 얻는 방법을 결정하는 것은 정확한 과학이 아닙니다 (아직은, 앞으로도 아닐 수도 있음).

하지만 새로운 AI 상대와 몇 라운드를 진행한 후 새로운 문제가 발생했습니다. 바로 스테일메이트입니다.

교착 상태 방지

매우 유사한 문자 2~3개만 남으면 모델이 루프에 갇히게 됩니다. '캐릭터가 모자를 쓰고 있나요?'와 같이 모두가 공유하는 기능에 관한 질문을 합니다.

내 코드는 이를 낭비된 턴으로 올바르게 식별하고 AI는 캐릭터가 모두 공유하는 또 다른 광범위한 기능(예: '캐릭터가 안경을 쓰고 있나요?')을 시도합니다.

질문 생성 시도가 실패하고 남은 문자가 3개 이하인 경우 전략이 변경된다는 새로운 규칙으로 프롬프트를 개선했습니다.

새로운 지침은 명시적입니다. '차이점을 찾으려면 광범위한 기능 대신 더 구체적이고 고유하거나 결합된 시각적 기능에 대해 질문해야 합니다.' 예를 들어 캐릭터가 모자를 쓰고 있는지 묻는 대신 야구 모자를 쓰고 있는지 묻는 메시지가 표시됩니다.

이렇게 하면 모델이 이미지를 훨씬 더 자세히 살펴서 최종적으로 획기적인 결과를 가져올 수 있는 작은 세부정보를 찾게 되므로 대부분의 경우 후반 전략이 약간 더 잘 작동합니다.

모델의 기억력 지우기

언어 모델의 가장 큰 강점은 메모리입니다. 하지만 이 게임에서는 가장 큰 강점이 약점이 되었습니다. 두 번째 게임을 시작하면 혼란스럽거나 관련 없는 질문을 합니다. 물론 내 스마트 AI 상대는 이전 게임의 전체 채팅 기록을 유지하고 있었습니다. 한 번에 두 개 이상의 게임을 이해하려고 했습니다.

이제 동일한 AI 세션을 재사용하는 대신 각 게임이 끝날 때 명시적으로 파괴하여 AI에 기억상실증을 부여합니다.

다시 플레이를 클릭하면 startNewGameSession() 함수가 보드를 재설정하고 완전히 새로운 AI 세션을 만듭니다. 이는 앱뿐만 아니라 AI 모델 자체 내에서 세션 상태를 관리하는 데 있어 흥미로운 학습이었습니다.

다양한 기능: 맞춤 게임 및 음성 입력

더욱 흥미로운 경험을 제공하기 위해 두 가지 추가 기능을 추가했습니다.

  1. 맞춤 캐릭터: getUserMedia()를 사용하면 플레이어가 카메라를 사용하여 5개의 캐릭터 세트를 직접 만들 수 있습니다. IndexedDB를 사용하여 문자를 저장했습니다. IndexedDB는 이미지 blob과 같은 바이너리 데이터를 저장하는 데 적합한 브라우저 데이터베이스입니다. 맞춤 세트를 만들면 브라우저에 저장되고 기본 메뉴에 다시보기 옵션이 표시됩니다.

  2. 음성 입력: 클라이언트 측 모델은 멀티모달입니다. 텍스트, 이미지, 오디오를 처리할 수 있습니다. MediaRecorder API를 사용하여 마이크 입력을 캡처하면 '다음 오디오를 텍스트로 변환해 줘'라는 프롬프트와 함께 결과 오디오 blob을 모델에 제공할 수 있습니다. 이 기능을 사용하면 재미있는 방식으로 게임을 즐길 수 있으며, 내 플랑드르어 억양을 어떻게 해석하는지 확인할 수도 있습니다. 이 데모는 주로 이 새로운 웹 기능의 다재다능함을 보여주기 위해 만들었지만, 솔직히 말해 질문을 계속해서 입력하는 데 지쳤습니다.

최종 의견

'AI Guess Who?'를 빌드하는 것은 확실히 어려운 일이었습니다. 하지만 문서를 읽고 AI를 디버그하는 AI (네... 이것은 재미있는 실험이었습니다. 이 데모에서는 비공개로 빠르고 인터넷이 필요 없는 환경을 만들기 위해 브라우저에서 모델을 실행하는 엄청난 잠재력을 강조했습니다. 아직 실험 단계에 있으며 상대가 완벽하게 플레이하지 않는 경우도 있습니다. 픽셀 단위로 완벽하거나 논리적으로 완벽하지는 않습니다. 생성형 AI를 사용하면 결과가 모델에 따라 달라집니다.

완벽을 추구하는 대신 결과를 개선하는 데 집중할게.

이 프로젝트는 프롬프트 엔지니어링의 지속적인 어려움도 강조했습니다. 이 프롬프트가 정말 큰 부분을 차지하게 되었고 항상 가장 재미있는 부분은 아니었습니다. 하지만 가장 중요한 교훈은 인식과 추론을 분리하고 AI와 코드의 기능을 나누도록 애플리케이션을 설계하는 것이었습니다. 이러한 분리에도 불구하고 AI는 문신을 메이크업으로 혼동하거나 누가 비밀 캐릭터에 대해 이야기하고 있는지 추적하지 못하는 등 인간에게는 명백한 실수를 저지를 수 있었습니다.

매번 해결책은 프롬프트를 더욱 명시적으로 만들어 사람에게는 명백하지만 모델에는 필수적인 안내를 추가하는 것이었습니다.

때로는 게임이 불공정하게 느껴졌습니다. 코드가 해당 정보를 명시적으로 공유하지 않았는데도 AI가 비밀 문자를 미리 '알고' 있는 것처럼 느껴지는 경우가 있었습니다. 이는 사람과 머신의 중요한 부분을 보여줍니다.

AI의 행동은 올바를 뿐만 아니라 공정하다고 느껴져야 합니다.

그래서 '내가 선택한 캐릭터를 알 수 없습니다' 및 '부정행위 금지'와 같은 직설적인 안내로 프롬프트를 업데이트했습니다. AI 에이전트를 빌드할 때는 제한사항을 정의하는 데 시간을 할애해야 합니다. 아마도 지침보다 더 많은 시간을 할애해야 할 것입니다.

모델과의 상호작용은 계속 개선될 수 있습니다. 내장 모델을 사용하면 대규모 서버 측 모델의 성능과 안정성이 일부 손실되지만 개인 정보 보호, 속도, 오프라인 기능을 얻을 수 있습니다. 이러한 게임의 경우 이러한 절충안은 실험해 볼 가치가 있었습니다. 클라이언트 측 AI의 미래는 나날이 발전하고 있으며 모델도 점점 작아지고 있습니다. 앞으로 어떤 것을 만들 수 있을지 기대됩니다.