發布日期:2025 年 10 月 10 日
經典桌遊「猜猜我是誰?」 是演繹推理的經典之作。每位玩家一開始都會拿到一張臉孔棋盤,然後透過一連串的是非題縮小可能性,直到能自信地找出對手的祕密角色為止。
在 Google I/O Connect 大會上觀看內建 AI 的示範後,我開始思考:如果能與瀏覽器中的 AI 玩「是誰?」遊戲,會怎麼樣呢?有了用戶端 AI,相片會在裝置上解讀,因此親友的自訂「是誰?」遊戲內容會安全地保留在裝置上,不會外洩。
我的背景主要是使用者介面和使用者體驗開發,習慣打造完美無瑕的體驗。我希望自己的詮釋能做到這一點。
我使用 React 建構的應用程式「AI Guess Who?」,會透過 Prompt API 和瀏覽器內建模型,建立出乎意料的強大對手。在過程中,我發現要獲得「完美像素」的結果並不容易。不過,這個應用程式展示了如何運用 AI 建構周全的遊戲邏輯,以及提示工程在改善這項邏輯並獲得預期結果方面的重要性。
請繼續閱讀,瞭解內建的 AI 整合功能、我面臨的挑戰,以及最終採用的解決方案。您可以玩這個遊戲,並在 GitHub 上找到原始碼。
遊戲基礎:React 應用程式
在查看 AI 實作方式之前,我們先來瞭解應用程式的結構。我使用 TypeScript 建構標準 React 應用程式,並以中央 App.tsx
檔案做為遊戲的指揮。這個檔案包含:
- 遊戲狀態:追蹤遊戲目前階段的列舉 (例如
PLAYER_TURN_ASKING
、AI_TURN
、GAME_OVER
)。這是最重要的狀態片段,因為它會決定介面顯示的內容,以及玩家可執行的動作。 - 角色清單:有多個清單會指定有效角色、每位玩家的秘密角色,以及已從棋盤上淘汰的角色。
- 遊戲對話:問題、答案和系統訊息的記錄。
介面可分為下列邏輯元件:


隨著遊戲功能越來越多,複雜度也隨之提高。一開始,整個遊戲的邏輯都是在單一大型自訂 React Hook useGameLogic
中管理,但很快就變得過於龐大,難以瀏覽和偵錯。為提升可維護性,我將這個 Hook 重構為多個 Hook,每個 Hook 各自負責單一工作。例如:
useGameState
管理核心狀態usePlayerActions
代表輪到玩家useAIActions
是 AI 的邏輯
現在,主要的 useGameLogic
鉤子會做為乾淨的組合器,將這些較小的鉤子放在一起。這項架構變更並未改變遊戲功能,但讓程式碼集更加簡潔。
使用 Prompt API 處理遊戲邏輯
這項專案的核心是使用 Prompt API。
我已將 AI 遊戲邏輯新增至 builtInAIService.ts
。主要職責包括:
- 允許限制性二元答案。
- 教導模型遊戲策略。
- 教導模型分析。
- 讓模型忘記先前的對話。
允許限制性二元答案
玩家如何與 AI 互動?如果玩家問「你的角色戴帽子嗎?」,AI 必須「查看」秘密角色的圖片,並給出明確的答案。
我一開始嘗試的結果很糟糕。模型的回覆是「我認為 Isabella 這個角色沒有戴帽子」,而不是提供「是」或「否」的二元答案。一開始,我使用非常嚴格的提示解決這個問題,基本上是要求模型只能回覆「是」或「否」。
雖然這樣做可行,但我發現使用「結構化輸出」是更好的做法。只要向模型提供 JSON 結構定義,就能確保模型回覆 true 或 false。
const schema = { type: "boolean" };
const result = session.prompt(prompt, { responseConstraint: schema });
這讓我得以簡化提示,並讓程式碼可靠地處理回應:
JSON.parse(result) ? "Yes" : "No"
教導模型遊戲策略
與讓模型發起並提出問題相比,要求模型回答問題簡單許多。好的「是誰呢?」玩家不會隨機提出問題,他們會問問題,一次刪除最多字元。理想的問題是使用二進位問題,將可能的剩餘字元減少一半。
如何教導模型這項策略?再次強調,提示工程。generateAIQuestion()
的提示其實是「是誰呢?」遊戲理論的簡短課程。
一開始,我要求模型「問個好問題」,結果難以預測。為改善結果,我新增了負面限制。提示現在包含類似下列的指令:
- 「CRITICAL: Ask about existing features ONLY」(重要:只能詢問現有功能)
- 「CRITICAL: Be original. 請勿重複提問」。
這些限制會縮小模型的焦點,避免模型提出不相關的問題,讓對手體驗更愉快的對弈過程。您可以在 GitHub 上查看完整提示檔案。
教導模型分析
這項挑戰是我們面臨過最艱難,也是最重要的挑戰。如果模型問「你的角色戴帽子嗎?」,而玩家回答「否」,模型如何得知棋盤上哪些角色已淘汰?
模型應排除所有戴帽子的人。我一開始嘗試時,經常發生邏輯錯誤,有時模型會刪除錯誤的字元,有時則不會刪除任何字元。此外,「帽子」是什麼?「毛帽」算不算「帽子」?說實話,這也是人類辯論中可能發生的情況。當然,一般錯誤也難免發生。從 AI 的角度來看,頭髮可能看起來像帽子。
我重新設計了架構,將感知與程式碼推論分開:
AI 負責視覺分析。模型擅長進行圖像分析。 我指示模型以嚴格的 JSON 結構定義,傳回問題和詳細分析。模型會分析棋盤上的每個角色,並回答「這個角色是否具備這項特徵?」這個問題。模型會傳回結構化 JSON 物件:
{ "character_id": "...", "has_feature": true }
再次強調,結構化資料是獲得成功結果的關鍵。
遊戲程式碼會根據分析結果做出最終決定。應用程式程式碼會檢查玩家的答案 (「是」或「否」),並逐一查看 AI 的分析結果。如果玩家回答「否」,程式碼就會知道要刪除
has_feature
中的每個字元true
。
我發現這種分工方式是建構可靠 AI 應用程式的關鍵。 運用 AI 的分析功能,並將二元決策留給應用程式碼。
為檢查模型的感知能力,我建構了這項分析的視覺化呈現方式。這樣一來,就能更輕鬆確認模型是否正確解讀。
提示工程
不過,即使有這種區隔,我還是發現模型的感知能力可能存在缺陷。這項功能可能會誤判角色是否戴眼鏡,導致系統做出令人沮喪的錯誤淘汰決定。為解決這個問題,我嘗試了兩步驟程序:AI 會提出問題,收到玩家的答案後,系統會以該答案做為脈絡,再次進行分析。理論上,第二次檢查可能會發現第一次檢查的錯誤。
該流程的運作方式如下:
- AI 回合 (API 呼叫 1):AI 問:「你的角色有鬍子嗎?」
- 輪到玩家:玩家查看自己的秘密角色 (沒有鬍子),然後回答「沒有」。
- AI 回合 (API 呼叫 2):AI 會再次查看所有剩餘字元,並根據玩家的答案決定要刪除哪些字元。
在步驟二中,模型可能仍會將留有淺色鬍渣的角色誤認為「沒有鬍鬚」,而無法將其移除,即使使用者希望模型這麼做也一樣。核心感知錯誤並未修正,額外步驟只是延遲了結果。與人類對手對弈時,我們可以指定這方面的協議或說明;但在目前的 AI 對手設定中,並非如此。
這個程序會增加第二次 API 呼叫的延遲時間,但準確度不會大幅提升。如果模型第一次判斷錯誤,第二次也經常會出錯。我已將提示還原為只審查一次。
改善分析結果,而非新增更多分析
我依據使用者體驗原則判斷,解決方案並非「更多」分析,而是「更優質」的分析。
我投入大量心力調整提示,為模型新增明確的指令,要求模型仔細檢查工作並著重於獨特功能,結果證明這是提升準確度的有效策略。目前更可靠的流程運作方式如下:
AI 回合 (API 呼叫):系統會提示模型同時生成問題和內部分析,並傳回單一 JSON 物件。
- 問題:「你的角色戴眼鏡嗎?」
- 分析 (資料):
[ {character_id: 'brad', has_feature: true}, {character_id: 'alex', has_feature: false}, {character_id: 'gina', has_feature: true}, ... ]
輪到玩家:玩家的祕密角色是 Alex (沒有戴眼鏡),因此他們回答「否」。
回合結束:應用程式的 JavaScript 程式碼會接管。不必再向 AI 提出其他要求。並從步驟 1 逐一分析資料。
- 玩家回答「否」。
- 程式碼會尋找
has_feature
為 true 的每個字元。 - 並將 Brad 和 Gina 翻轉下來。這項邏輯是確定性的,而且會立即執行。
這項實驗至關重要,但需要經過許多反覆試驗。我不知道情況是否會好轉。有時情況甚至會更糟。如何獲得最一致的結果並非精確的科學 (至少目前不是,以後也不一定)。
但與新的 AI 對手對弈幾輪後,出現了一個很棒的新問題: 僵局。
避免死結
如果只剩下兩到三個非常相似的字元,模型就會陷入迴圈。並詢問他們共有的特徵,例如「你的角色戴帽子嗎?」
我的程式碼會正確將此視為浪費的輪次,而 AI 會嘗試另一個同樣廣泛的特徵,也就是角色也共有的特徵,例如「你的角色戴眼鏡嗎?」
我使用新規則強化提示:如果問題生成嘗試失敗且剩餘字元數為三個或更少,策略就會變更。
新指令明確指出:「請詢問更具體、獨特或組合的視覺特徵,找出差異,而非廣泛的特徵。」舉例來說,系統會提示你詢問角色是否戴著棒球帽,而不是詢問是否戴著帽子。
這會迫使模型更仔細地查看圖片,找出最終能帶來突破性進展的微小細節,讓模型在遊戲後期的策略運作得更好 (大多數時候)。
讓模型失憶
語言模型最強大的功能就是記憶。但在這款遊戲中,這項優勢卻成了弱點。開始第二場遊戲時,系統會詢問令人困惑或不相關的問題。當然,智慧 AI 對手會保留前一場遊戲的完整對話記錄。因為它試圖同時解讀兩場 (甚至更多) 賽事。
我現在會在每場遊戲結束時明確終止 AI 工作階段,而不是重複使用同一個工作階段,這基本上是讓 AI 忘記先前的遊戲內容。
點選「Play Again」後,startNewGameSession()
函式會重設棋盤並建立全新的 AI 對話。這堂課很有趣,不僅介紹如何管理應用程式中的工作階段狀態,也說明如何管理 AI 模型本身的工作階段狀態。
進階功能:自訂遊戲和語音輸入
為了讓體驗更吸引人,我新增了兩項額外功能:
自訂角色:玩家可使用
getUserMedia()
建立自己的 5 個角色。我使用 IndexedDB 儲存字元,這個瀏覽器資料庫非常適合儲存圖片 Blob 等二進位資料。建立自訂組合後,系統會將其儲存至瀏覽器,主選單中也會顯示重播選項。語音輸入:用戶端模型是多模態模型。 可處理文字、圖片和音訊。使用 MediaRecorder API 擷取麥克風輸入內容後,我就可以將產生的音訊 Blob 連同「轉錄以下音訊...」提示提供給模型。這項功能為遊戲增添樂趣,還能以有趣的方式瞭解 Google 如何解讀我的佛蘭德斯口音。我建立這個應用程式主要是為了展示這項新網路功能的多元性,但說實話,我厭倦了一遍又一遍地輸入問題。
結論
建構「AI 猜猜我是誰?」絕對是一大挑戰。不過,只要參閱文件並運用一些 AI 技術來偵錯 AI (沒錯,我這麼做了),結果發現這是一項有趣的實驗。這項技術凸顯了在瀏覽器中執行模型所帶來的巨大潛力,可打造私密、快速且不需網路連線的體驗。這項功能仍在實驗階段,有時對手不會完美出牌。這項功能並非完美無缺,生成式 AI 的結果取決於模型。
我不會追求完美,而是著重於提升結果。
這項專案也凸顯了提示工程持續面臨的挑戰。 提示成為了非常重要的一環,但並非總是很有趣。但我學到最重要的一課,是設計應用程式架構,將感知與演繹分開,並劃分 AI 和程式碼的功能。即使有這種區隔,我發現 AI 仍可能犯下 (對人類而言) 顯而易見的錯誤,例如將紋身誤認為化妝,或是忘記討論的是誰的秘密角色。
每次的解決方法都是讓提示更加明確,加入對人類來說顯而易見,但對模型來說至關重要的指令。
有時遊戲會讓人覺得不公平。有時我覺得 AI「知道」秘密字元,即使程式碼從未明確分享該資訊。這顯示了人與機器之間的重要差異:
AI 的行為不僅要正確,還必須感覺公平。
因此我更新了提示,加入直白的指示,例如「你不知道我選了哪個角色」和「不得作弊」。我發現建構 AI 代理時,定義限制所花的時間可能比指令還多。
與模型的互動方式可能會持續改善。使用內建模型會失去大型伺服器端模型的部分功能和可靠性,但可確保隱私、提升速度,並支援離線作業。對於這類遊戲,這種取捨非常值得實驗。用戶端 AI 的未來發展日新月異,模型也越來越小,我迫不及待想看看我們接下來能打造出什麼。