תאריך פרסום: 10 באוקטובר 2025

משחק הלוח הקלאסי נחשו מי? הוא דוגמה מצוינת לחשיבה דדוקטיבית. כל שחקן מתחיל עם לוח של פרצופים, ובאמצעות סדרה של שאלות שניתן להשיב עליהן בחיוב או בשלילה, מצמצם את האפשרויות עד שהוא יכול לזהות בוודאות את הדמות הסודית של היריב.
אחרי שצפיתי בהדגמה של AI מובנה ב-Google I/O Connect, חשבתי לעצמי: מה אם אוכל לשחק במשחק 'מי זה?' נגד AI שפועל בדפדפן? בעזרת AI בצד הלקוח, התמונות יפורשו באופן מקומי, כך שמשחק מותאם אישית של 'מי זה?' עם חברים ובני משפחה יישאר פרטי ומאובטח במכשיר שלי.
הרקע שלי הוא בעיקר בפיתוח של ממשקי משתמש וחוויית משתמש, ואני רגיל ליצור חוויות מושלמות. קיוויתי שאוכל לעשות בדיוק את זה עם הפרשנות שלי.
האפליקציה שלי, AI Guess Who?, מבוססת על React ומשתמשת ב-Prompt API ובמודל מובנה בדפדפן כדי ליצור יריב בעל יכולות מפתיעות. במהלך התהליך הזה, גיליתי שלא כל כך פשוט להשיג תוצאות מושלמות. אבל האפליקציה הזו מדגימה איך אפשר להשתמש ב-AI כדי ליצור לוגיקה מחושבת למשחק, ואת החשיבות של הנדסת הנחיות כדי לשפר את הלוגיקה הזו ולקבל את התוצאות הרצויות.
בהמשך המאמר מוסבר על השילוב המובנה של AI, על האתגרים שנתקלתי בהם ועל הפתרונות שמצאתי. אפשר לשחק במשחק ולמצוא את קוד המקור ב-GitHub.
הבסיס של המשחק: אפליקציית React
לפני שנעיין בהטמעה של ה-AI, נבדוק את המבנה של האפליקציה. בניתי אפליקציית React רגילה עם TypeScript, עם קובץ App.tsx מרכזי שמשמש כמנהל של המשחק. הקובץ הזה מכיל:
- מצב המשחק: ערך enum שמציין את השלב הנוכחי במשחק (למשל,
PLAYER_TURN_ASKING,AI_TURN,GAME_OVER). זהו המצב הכי חשוב, כי הוא קובע מה מוצג בממשק ואילו פעולות זמינות לשחקן. - רשימות דמויות: יש כמה רשימות שמציינות את הדמויות הפעילות, את הדמות הסודית של כל שחקן ואילו דמויות הוצאו מהלוח.
- צ'אט במשחק: יומן שמתעדכן כל הזמן של שאלות, תשובות והודעות מערכת.
הממשק מחולק לרכיבים לוגיים:
ככל שתכונות המשחק התרחבו, כך גם המורכבות שלו. בתחילה, כל הלוגיקה של המשחק נוהלה בתוך React hook מותאם אישית גדול אחד, useGameLogic, אבל מהר מאוד הוא הפך גדול מדי מכדי לנווט בו ולבצע בו ניפוי באגים. כדי לשפר את יכולת התחזוקה, ביצעתי רפקטורינג של ה-hook הזה לכמה hooks, שלכל אחד מהם יש אחריות אחת.
לדוגמה:
useGameStateמנהל את מצב הליבה-
usePlayerActionsהוא התור של השחקן -
useAIActionsהוא ללוגיקה של ה-AI
ה-hook הראשי useGameLogic פועל עכשיו ככלי ליצירת קומפוזיציה, וממקם את ה-hooks הקטנים האלה יחד. השינוי הזה בארכיטקטורה לא שינה את הפונקציונליות של המשחק, אבל הוא הפך את בסיס הקוד להרבה יותר נקי.
לוגיקת משחק עם Prompt API
הליבה של הפרויקט הזה היא השימוש ב-Prompt API.
הוספתי את לוגיקת המשחק של ה-AI אל builtInAIService.ts. אלה התפקידים העיקריים שלו:
- אפשר לתת תשובות מגבילות ובינאריות.
- ללמד את המודל אסטרטגיית משחק.
- ללמד את המודל לנתח.
- לגרום למודל לשכוח את מה שהוא יודע.
אפשר לתת תשובות מגבילות ובינאריות
איך הצופה מקיים אינטראקציה עם ה-AI? כששחקן שואל: "האם לדמות שלך יש כובע?", ה-AI צריך "להסתכל" על התמונה של הדמות הסודית ולתת תשובה ברורה.
הניסיונות הראשונים שלי היו בלאגן. התשובה הייתה בצורת שיחה: "לא, הדמות שאני חושב עליה, איזבלה, לא נראית עם כובע", במקום להציע תשובה בינארית של כן או לא. בהתחלה, פתרתי את הבעיה הזו באמצעות הנחיה מאוד מדויקת, שבעצם הכתיבה למודל להגיב רק ב"כן" או ב"לא".
השיטה הזו עבדה, אבל גיליתי שיטה טובה יותר באמצעות פלט מובנה. הצלחתי להבטיח שהתשובה תהיה נכונה או לא נכונה על ידי מתן סכימת JSON למודל.
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, בלי לשפר באופן משמעותי את הדיוק. אם המודל היה שגוי בפעם הראשונה, הוא היה שגוי גם בפעם השנייה. החזרתי את ההנחיה לבדיקה פעם אחת בלבד.
שיפור במקום הוספת ניתוח
הסתמכתי על עיקרון UX: הפתרון לא היה ניתוח נוסף, אלא ניתוח טוב יותר.
השקעתי מאמצים רבים בשיפור ההנחיה, והוספתי הוראות מפורטות למודל כדי שיבדוק שוב את העבודה שלו ויתמקד בתכונות ייחודיות. התברר שזו אסטרטגיה יעילה יותר לשיפור הדיוק. כך פועל התהליך הנוכחי, שהוא אמין יותר:
תור של AI (קריאה ל-API): המודל מקבל הנחיה ליצור גם את השאלה וגם את הניתוח הפנימי שלו בו-זמנית, ומחזיר אובייקט JSON יחיד.
- שאלה: "הדמות שלך מרכיבה משקפיים?"
- ניתוח (נתונים):
[ {character_id: 'brad', has_feature: true}, {character_id: 'alex', has_feature: false}, {character_id: 'gina', has_feature: true}, ... ]תור השחקן: הדמות הסודית של השחקן היא אלכס (בלי משקפיים), אז הוא עונה 'לא'.
סיום הסיבוב: קוד ה-JavaScript של האפליקציה משתלט. הוא לא צריך לשאול את ה-AI שום דבר אחר. היא מבצעת איטרציה על נתוני הניתוח משלב 1.
- השחקן אמר 'לא'.
- הקוד מחפש כל תו שבו
has_featureהוא true. - היא מפילה את בראד וג'ינה. הלוגיקה היא דטרמיניסטית ומיידית.
הניסוי הזה היה חיוני, אבל דרש הרבה ניסוי וטעייה. לא ידעתי אם המצב ישתפר. לפעמים המצב היה אפילו גרוע יותר. הדרך לקבלת התוצאות העקביות ביותר היא לא מדע מדויק (עדיין, אם בכלל...).
אבל אחרי כמה סיבובים עם יריב ה-AI החדש שלי, הופיעה בעיה חדשה ומדהימה: מצב של תיקו.
יציאה ממצב קיפאון
כשהיו נשארים רק שניים או שלושה תווים דומים מאוד, המודל היה נתקע בלולאה. הוא ישאל שאלה לגבי תכונה משותפת לכולם, למשל: "הדמות שלך חובשת כובע?"
הקוד שלי יזהה את זה בצורה נכונה כהזדמנות מבוזבזת, ו-AI ינסה תכונה רחבה נוספת שכל הדמויות גם חולקות, כמו "האם הדמות שלך מרכיבה משקפיים?".
שיפרתי את ההנחיה באמצעות כלל חדש: אם ניסיון ליצור שאלה נכשל ויש פחות משלושה תווים שנשארו, האסטרטגיה משתנה.

ההנחיה החדשה היא מפורשת: "במקום לשאול על תכונה כללית, עליך לשאול על תכונה חזותית ספציפית, ייחודית או משולבת יותר כדי למצוא הבדל". לדוגמה, במקום לשאול אם הדמות חובשת כובע, המודל מתבקש לשאול אם היא חובשת כובע בייסבול.
הפעולה הזו גורמת למודל להתמקד יותר בתמונות כדי למצוא את הפרט הקטן שיכול להוביל לפריצת דרך, וכך לשפר את האסטרטגיה שלו בשלב מאוחר במשחק, ברוב המקרים.
גורמים למודל לשכוח
היתרון הכי גדול של מודל שפה הוא הזיכרון שלו. אבל במשחק הזה, היתרון הגדול ביותר שלהם הפך לנקודת תורפה. כשפתחתי משחק שני, הוא שאל שאלות מבלבלות או לא רלוונטיות. כמובן, היריב החכם שלי מבוסס ה-AI שמר את כל היסטוריית הצ'אט מהמשחק הקודם. הוא ניסה להבין שני משחקים (או יותר) בו-זמנית.
במקום לעשות שימוש חוזר באותו סשן של AI, עכשיו אני משמיד אותו באופן מפורש בסוף כל משחק, וכך בעצם גורם ל-AI לשכוח את מה שהיה.
כשלוחצים על הפעלה חוזרת, הפונקציה startNewGameSession() מאפסת את הלוח ויוצרת סשן חדש לגמרי של AI. היה זה שיעור מעניין בניהול מצב הסשן לא רק באפליקציה, אלא בתוך מודל ה-AI עצמו.
תכונות מתקדמות: משחקים בהתאמה אישית וקלט קולי
כדי להפוך את החוויה למעניינת יותר, הוספתי שתי תכונות נוספות:
דמויות בהתאמה אישית: באמצעות
getUserMedia(), שחקנים יכולים להשתמש במצלמה שלהם כדי ליצור סט משלהם של 5 דמויות. השתמשתי ב-IndexedDB כדי לשמור את התווים, מסד נתונים של דפדפן שמתאים באופן מושלם לאחסון נתונים בינאריים כמו כתובות Blob של תמונות. כשיוצרים קבוצה בהתאמה אישית, היא נשמרת בדפדפן, ואפשרות ההפעלה מחדש מופיעה בתפריט הראשי.קלט קולי: המודל מצד הלקוח הוא רב-אופני. הוא יכול לעבד טקסט, תמונות ואודיו. באמצעות MediaRecorder API כדי לתעד קלט מהמיקרופון, יכולתי להזין את ה-blob של האודיו שנוצר למודל עם הנחיה: "תמלל את האודיו הבא...". כך תוכלו לשחק בצורה מהנה (וגם לראות איך הוא מפרש את המבטא הפלמי שלי). יצרתי את זה בעיקר כדי להראות את הרבגוניות של היכולת החדשה הזו באינטרנט, אבל האמת היא שנמאס לי להקליד שאלות שוב ושוב.
הדגמה (דמו)
אפשר לבדוק את המשחק ישירות כאן או לשחק בחלון חדש ולמצוא את קוד המקור ב-GitHub.
מחשבות לסיכום
בניית המשחק 'נחשו מי?' עם AI הייתה בהחלט מאתגרת. אבל בעזרת קריאת מסמכים ושימוש ב-AI לניפוי באגים ב-AI (כן... עשיתי את זה), והתברר שזה היה ניסוי מהנה. הוא הדגיש את הפוטנציאל העצום של הפעלת מודל בדפדפן ליצירת חוויה פרטית ומהירה שלא דורשת חיבור לאינטרנט. התכונה הזו עדיין ניסיונית, ולפעמים היריב פשוט לא משחק בצורה מושלמת. הוא לא מושלם מבחינת פיקסלים או מבחינה לוגית. התוצאות של חיפוש באמצעות AI גנרטיבי תלויות במודל.
במקום לשאוף לשלמות, אנסה לשפר את התוצאה.
הפרויקט הזה גם הדגיש את האתגרים המתמשכים של הנדסת הנחיות. ההנחיות הפכו לחלק חשוב מאוד בתהליך, ולא תמיד לחלק הכי כיף. אבל הלקח הכי חשוב שלמדתי היה איך לתכנן את האפליקציה כך שתפריד בין תפיסה לבין הסקה, ותחלק את היכולות של ה-AI והקוד. גם אחרי ההפרדה הזו, גיליתי שה-AI עדיין יכול לעשות טעויות ברורות (מנקודת מבט אנושית), כמו לבלבל בין קעקועים לאיפור או לאבד את ההקשר של מי הדמות הסודית שמדובר עליה.
בכל פעם, הפתרון היה להפוך את ההנחיות למפורשות עוד יותר, ולהוסיף הוראות שנראות מובנות מאליהן לאדם, אבל הן חיוניות למודל.
לפעמים הרגשתי שהמשחק לא הוגן. לפעמים הרגשתי שה-AI 'ידע' מראש את הדמות הסודית, למרות שהקוד אף פעם לא שיתף את המידע הזה באופן מפורש. הסרטון הזה מציג חלק חשוב בהשוואה בין בני אדם למכונות:
ההתנהגות של AI צריכה להיות לא רק נכונה, אלא גם להיראות הוגנת.
לכן עדכנתי את ההנחיות בהוראות בוטות, כמו "אין לך מושג איזו דמות בחרתי" ו "אסור לרמות". למדתי שכדאי להשקיע זמן בהגדרת מגבלות כשיוצרים סוכני AI, ואולי אפילו יותר זמן מאשר בהגדרת ההוראות.

אפשר להמשיך לשפר את האינטראקציה עם המודל. השימוש במודל מובנה מאפשר לכם לשמור על הפרטיות, ליהנות ממהירות גבוהה ולעבוד במצב אופליין, אבל אתם מאבדים חלק מהעוצמה והמהימנות של מודל ענק בצד השרת. במקרה של משחק כזה, היה שווה לנסות את הפשרה הזו. העתיד של ה-AI בצד הלקוח משתפר מיום ליום, והמודלים הולכים וקטנים. אני כבר לא יכול לחכות לראות מה נוכל לפתח בהמשך.