פורסם: 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 צריך "להסתכל" על התמונה של הדמות הסודית ולתת תשובה ברורה.
הניסיונות הראשונים שלי היו בלאגן. התשובה הייתה בצורה של שיחה: "לא, הדמות שאני חושב עליה, איזבלה, לא נראית עם כובע", במקום להציע תשובה בינארית של כן או לא. בהתחלה, פתרתי את הבעיה הזו באמצעות הנחיה מאוד מדויקת, שבעצם הכתיבה למודל להגיב רק ב"כן" או ב"לא".
השיטה הזו עבדה, אבל למדתי שיש דרך טובה יותר להשתמש בפלט מובנה. הצלחתי להבטיח שהמודל ייתן תשובה של true או false על ידי מתן סכימת ה-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 ינסה תכונה רחבה נוספת שכל הדמויות גם חולקות, כמו "האם הדמות שלך מרכיבה משקפיים?"
שיפרתי את ההנחיה עם כלל חדש: אם ניסיון ליצור שאלה נכשל ויש פחות מ-3 תווים שנשארו, האסטרטגיה משתנה.
ההנחיה החדשה היא מפורשת: "במקום לשאול על תכונה כללית, עליך לשאול על תכונה חזותית ספציפית, ייחודית או משולבת יותר כדי למצוא הבדל". לדוגמה, במקום לשאול אם הדמות חובשת כובע, המודל מתבקש לשאול אם היא חובשת כובע מצחייה.
כך המודל נאלץ להתמקד יותר בתמונות כדי למצוא את הפרט הקטן שיכול להוביל לפריצת דרך, ולשפר את האסטרטגיה שלו בשלבים המאוחרים של המשחק, ברוב המקרים.
גורמים למודל לשכוח
היתרון הכי גדול של מודל שפה הוא הזיכרון שלו. אבל במשחק הזה, היתרון הגדול ביותר שלה הפך לנקודת תורפה. כשפתחתי משחק שני, הוא שאל שאלות מבלבלות או לא רלוונטיות. כמובן, היריב החכם שלי מבוסס ה-AI שמר את כל היסטוריית הצ'אט מהמשחק הקודם. הוא ניסה להבין שני משחקים (או יותר) בו-זמנית.
במקום לעשות שימוש חוזר באותו סשן של AI, עכשיו אני משמיד אותו במפורש בסוף כל משחק, מה שגורם ל-AI לשכוח את מה שהיה.
כשלוחצים על הפעלה חוזרת, הפונקציה startNewGameSession()
מאפסת את הלוח ויוצרת סשן חדש לגמרי של AI. היה זה שיעור מעניין בניהול מצב הסשן לא רק באפליקציה, אלא בתוך מודל ה-AI עצמו.
תכונות מתקדמות: משחקים בהתאמה אישית וקלט קולי
כדי להפוך את החוויה למעניינת יותר, הוספתי שתי תכונות נוספות:
דמויות בהתאמה אישית: באמצעות
getUserMedia()
, שחקנים יכולים להשתמש במצלמה שלהם כדי ליצור סט של 5 תווים משלהם. השתמשתי ב-IndexedDB כדי לשמור את התווים, מסד נתונים של דפדפן שמתאים באופן מושלם לאחסון נתונים בינאריים כמו כתובות Blob של תמונות. כשיוצרים קבוצה בהתאמה אישית, היא נשמרת בדפדפן, ואפשרות ההפעלה מחדש מופיעה בתפריט הראשי.קלט קולי: המודל מצד הלקוח הוא מולטי-מודאלי. הוא יכול לעבד טקסט, תמונות ואודיו. באמצעות MediaRecorder API כדי ללכוד קלט מיקרופון, יכולתי להזין את ה-blob של האודיו שנוצר למודל עם הנחיה: "תמלל את האודיו הבא...". כך תוכלו לשחק בצורה מהנה (וגם לראות איך הוא מפרש את המבטא הפלמי שלי). יצרתי את זה בעיקר כדי להראות את הרבגוניות של היכולת החדשה הזו באינטרנט, אבל האמת היא שנמאס לי להקליד שאלות שוב ושוב.
מחשבות לסיכום
בניית המשחק 'נחשו מי?' עם AI הייתה בהחלט מאתגרת. אבל עם קצת עזרה מקריאת מסמכים ומ-AI לניפוי באגים ב-AI (כן... עשיתי את זה), והתברר שזה היה ניסוי מעניין. הוא הדגיש את הפוטנציאל העצום של הפעלת מודל בדפדפן ליצירת חוויה פרטית ומהירה שלא דורשת חיבור לאינטרנט. התכונה הזו עדיין ניסיונית, ולפעמים היריב לא משחק בצורה מושלמת. הוא לא מושלם מבחינת פיקסלים או מבחינה לוגית. התוצאות של AI גנרטיבי תלויות במודל.
במקום לשאוף לשלמות, אנסה לשפר את התוצאה.
הפרויקט הזה גם הדגיש את האתגרים המתמשכים בהנדסת הנחיות. ההנחיות הפכו לחלק חשוב מאוד בתהליך, אבל לא תמיד לחלק הכי כיף. אבל הלקח הכי חשוב שלמדתי היה איך לתכנן את האפליקציה כך שתפריד בין תפיסה לבין הסקה, ותחלק את היכולות של ה-AI והקוד. גם אחרי ההפרדה הזו, גיליתי שה-AI עדיין יכול לעשות טעויות ברורות (מנקודת מבט אנושית), כמו לבלבל בין קעקועים לבין איפור או לאבד את ההקשר של מי הדמות הסודית שעליה מתנהל הדיון.
בכל פעם, הפתרון היה להפוך את ההנחיות למפורשות יותר, ולהוסיף הוראות שנראות ברורות לאדם אבל הן חיוניות למודל.
לפעמים הרגשתי שהמשחק לא הוגן. לפעמים נראה לי שה-AI 'ידע' את הדמות הסודית מראש, למרות שהקוד אף פעם לא שיתף את המידע הזה באופן מפורש. התמונה הזו ממחישה חלק חשוב בהשוואה בין בני אדם למכונות:
ההתנהגות של AI צריכה להיות לא רק נכונה, אלא גם להרגיש הוגנת.
לכן עדכנתי את ההנחיות והוספתי הוראות ברורות כמו 'אין לך מושג איזו דמות בחרתי' ו'אסור לרמות'. למדתי שכדאי להקדיש זמן להגדרת מגבלות כשיוצרים סוכני AI, ואולי אפילו יותר זמן מזה שמוקדש להגדרת ההוראות.
אפשר להמשיך לשפר את האינטראקציה עם המודל. השימוש במודל מובנה מאפשר לכם לשמור על הפרטיות, ליהנות ממהירות גבוהה ולעבוד במצב אופליין, אבל אתם מאבדים חלק מהעוצמה והמהימנות של מודל ענק בצד השרת. במקרה של משחק כזה, היה שווה לנסות את הפשרה הזו. העתיד של AI בצד הלקוח משתפר מיום ליום, והמודלים הולכים וקטנים. אני כבר לא יכול לחכות לראות מה נוכל לבנות בהמשך.