Date de publication : 10 octobre 2025
Le jeu de société classique Qui est-ce ? est un excellent moyen de développer votre raisonnement déductif. Chaque joueur commence avec un plateau de visages et, grâce à une série de questions auxquelles il ne peut répondre que par oui ou non, réduit les possibilités jusqu'à ce qu'il puisse identifier avec certitude le personnage secret de son adversaire.
Après avoir vu une démo de l'IA intégrée à Google I/O Connect, je me suis demandé : et si je pouvais jouer à "Qui est-ce ?" contre une IA qui vit dans le navigateur ? Avec l'IA côté client, les photos seraient interprétées localement. Ainsi, un jeu "Devine qui ?" personnalisé avec des amis et de la famille resterait privé et sécurisé sur mon appareil.
Mon expérience est principalement axée sur le développement d'UI et d'UX, et je suis habitué à créer des expériences au pixel près. J'espérais pouvoir faire exactement cela avec mon interprétation.
Mon application, AI Guess Who?, est conçue avec React et utilise l'API Prompt et un modèle intégré au navigateur pour créer un adversaire étonnamment performant. Au cours de ce processus, j'ai découvert qu'il n'était pas si simple d'obtenir des résultats "parfaits". Toutefois, cette application montre comment l'IA peut être utilisée pour créer une logique de jeu réfléchie et l'importance du prompt engineering pour affiner cette logique et obtenir les résultats attendus.
Poursuivez votre lecture pour en savoir plus sur l'intégration de l'IA, les difficultés que j'ai rencontrées et les solutions que j'ai trouvées. Vous pouvez jouer au jeu et trouver le code source sur GitHub.
Fondations du jeu : une application React
Avant d'examiner l'implémentation de l'IA, nous allons passer en revue la structure de l'application. J'ai créé une application React standard avec TypeScript, avec un fichier App.tsx
central pour servir de chef d'orchestre du jeu. Ce fichier contient :
- État du jeu : énumération qui suit la phase actuelle du jeu (par exemple,
PLAYER_TURN_ASKING
,AI_TURN
,GAME_OVER
). Il s'agit de l'élément d'état le plus important, car il détermine ce que l'interface affiche et les actions disponibles pour le joueur. - Listes de personnages : plusieurs listes désignent les personnages actifs, le personnage secret de chaque joueur et les personnages qui ont été éliminés du plateau.
- Chat du jeu : journal continu des questions, des réponses et des messages système.
L'interface est divisée en composants logiques :


À mesure que les fonctionnalités du jeu se développaient, sa complexité augmentait. Au départ, toute la logique du jeu était gérée dans un seul et même grand hook React personnalisé, useGameLogic
, mais il est rapidement devenu trop volumineux pour être parcouru et débogué. Pour améliorer la maintenabilité, j'ai refactorisé ce hook en plusieurs hooks, chacun ayant une seule responsabilité.
Exemple :
useGameState
gère l'état principal.usePlayerActions
indique le tour du joueur.useAIActions
est destiné à la logique de l'IA.
Le hook useGameLogic
principal sert désormais de compositeur propre, en plaçant ces hooks plus petits ensemble. Ce changement d'architecture n'a pas modifié la fonctionnalité du jeu, mais a rendu la base de code beaucoup plus propre.
Logique de jeu avec l'API Prompt
L'utilisation de l'API Prompt est au cœur de ce projet.
J'ai ajouté la logique de jeu de l'IA à builtInAIService.ts
. Voici ses principales responsabilités :
- Autoriser les réponses binaires restrictives.
- Enseigner la stratégie de jeu au modèle
- Enseigner l'analyse du modèle
- Donnez l'amnésie au modèle.
Autoriser les réponses binaires restrictives
Comment le joueur interagit-il avec l'IA ? Lorsqu'un joueur demande "Est-ce que ton personnage porte un chapeau ?", l'IA doit "regarder" l'image de son personnage secret et donner une réponse claire.
Mes premières tentatives ont été un désastre. La réponse était conversationnelle : "Non, le personnage auquel je pense, Isabella, ne semble pas porter de chapeau", au lieu de répondre par un simple "oui" ou "non". Au départ, j'ai résolu ce problème avec une requête très stricte, en demandant essentiellement au modèle de ne répondre que par "Oui" ou "Non".
Bien que cela ait fonctionné, j'ai découvert une méthode encore plus efficace utilisant des sorties structurées. En fournissant le schéma JSON au modèle, je pouvais garantir une réponse de type "vrai" ou "faux".
const schema = { type: "boolean" };
const result = session.prompt(prompt, { responseConstraint: schema });
Cela m'a permis de simplifier la requête et de laisser mon code gérer la réponse de manière fiable :
JSON.parse(result) ? "Yes" : "No"
Enseigner la stratégie de jeu au modèle
Il est beaucoup plus simple de demander au modèle de répondre à une question que de lui demander d'en poser. Un bon joueur de Qui est-ce ? ne pose pas de questions au hasard. Ils posent des questions qui éliminent le plus de caractères à la fois. Une question idéale réduit de moitié le nombre de caractères restants possibles à l'aide de questions binaires.
Comment enseigner cette stratégie à un modèle ? Encore une fois, l'ingénierie des requêtes. La requête pour generateAIQuestion()
est en fait une leçon concise sur la théorie des jeux dans le jeu "Devine Tête".
Au départ, j'ai demandé au modèle de "poser une bonne question". Les résultats étaient imprévisibles. Pour améliorer les résultats, j'ai ajouté des contraintes négatives. La requête inclut désormais des instructions semblables à celles-ci :
- "CRITIQUE : Pose des questions UNIQUEMENT sur les fonctionnalités existantes"
- "CRITIQUE : Soyez original. NE répète PAS la question".
Ces contraintes permettent de limiter le champ d'action du modèle et de l'empêcher de poser des questions non pertinentes, ce qui en fait un adversaire beaucoup plus agréable. Vous pouvez consulter l'intégralité du fichier d'invite sur GitHub.
Enseigner l'analyse du modèle
C'était, de loin, le défi le plus difficile et le plus important. Lorsque le modèle pose une question, par exemple "Votre personnage porte-t-il un chapeau ?", et que le joueur répond non, comment le modèle sait-il quels personnages de son plateau sont éliminés ?
Le modèle doit éliminer toutes les personnes portant un chapeau. Mes premières tentatives ont été entachées d'erreurs logiques. Parfois, le modèle supprimait les mauvais caractères ou aucun caractère. Qu'est-ce qu'un "chapeau" ? Un bonnet est-il considéré comme un chapeau ? Soyons honnêtes, cela peut aussi arriver lors d'un débat entre humains. Bien sûr, des erreurs générales peuvent se produire. D'un point de vue IA, les cheveux peuvent ressembler à un chapeau.
J'ai repensé l'architecture pour séparer la perception de la déduction de code :
L'IA est responsable de l'analyse visuelle. Les modèles excellent dans l'analyse visuelle. J'ai demandé au modèle de renvoyer sa question et une analyse détaillée dans un schéma JSON strict. Le modèle analyse chaque personnage sur son plateau et répond à la question "Ce personnage a-t-il cette caractéristique ?". Le modèle renvoie un objet JSON structuré :
{ "character_id": "...", "has_feature": true }
Une fois encore, les données structurées sont essentielles pour obtenir un résultat positif.
Le code du jeu utilise l'analyse pour prendre la décision finale. Le code de l'application vérifie la réponse du joueur ("Oui" ou "Non") et parcourt l'analyse de l'IA. Si le joueur a répondu "Non", le code sait qu'il doit éliminer tous les caractères où
has_feature
esttrue
.
J'ai constaté que cette division du travail est essentielle pour créer des applications d'IA fiables. Utilisez l'IA pour ses capacités analytiques et laissez les décisions binaires au code de votre application.
Pour vérifier la perception du modèle, j'ai créé une visualisation de cette analyse. Cela a permis de confirmer plus facilement si la perception du modèle était correcte.
Prompt engineering
Cependant, même avec cette séparation, j'ai remarqué que la perception du modèle pouvait encore être erronée. Il peut mal juger si un personnage porte des lunettes, ce qui peut entraîner une élimination incorrecte et frustrante. Pour y remédier, j'ai testé un processus en deux étapes : l'IA pose sa question. Après avoir reçu la réponse du joueur, il effectuerait une deuxième analyse avec la réponse comme contexte. L'idée était qu'un deuxième examen pourrait permettre de détecter les erreurs du premier.
Voici comment ce flux aurait fonctionné :
- Tour de l'IA (appel d'API 1) : l'IA demande "Votre personnage a-t-il une barbe ?"
- Tour du joueur : le joueur regarde son personnage secret, qui est rasé de près, et répond "Non".
- Tour de l'IA (appel d'API 2) : l'IA se demande à nouveau quels sont les caractères restants et détermine ceux à éliminer en fonction de la réponse du joueur.
À l'étape 2, le modèle peut encore mal percevoir un personnage avec une barbe de trois jours comme "n'ayant pas de barbe" et ne pas l'éliminer, même si l'utilisateur s'y attendait. L'erreur de perception principale n'a pas été corrigée, et l'étape supplémentaire n'a fait que retarder les résultats. Lorsque nous jouons contre un adversaire humain, nous pouvons spécifier un accord ou une clarification à ce sujet. Ce n'est pas le cas dans la configuration actuelle avec notre adversaire IA.
Ce processus a ajouté de la latence à partir d'un deuxième appel d'API, sans améliorer considérablement la précision. Si le modèle s'était trompé la première fois, il se trompait souvent la deuxième fois aussi. J'ai rétabli l'invite pour que vous n'ayez à l'examiner qu'une seule fois.
Améliorer au lieu d'ajouter des analyses
Je me suis appuyé sur un principe d'UX : la solution n'était pas d'analyser davantage, mais d'analyser mieux.
J'ai beaucoup investi dans l'affinage de la requête, en ajoutant des instructions explicites pour que le modèle vérifie son travail et se concentre sur des caractéristiques distinctes. Cette stratégie s'est avérée plus efficace pour améliorer la précision. Voici comment fonctionne le flux actuel, plus fiable :
Tour d'IA (appel d'API) : le modèle est invité à générer à la fois sa question et son analyse interne, en renvoyant un seul objet JSON.
- Question : "Votre personnage porte-t-il des lunettes ?"
- Analyse (données) :
[ {character_id: 'brad', has_feature: true}, {character_id: 'alex', has_feature: false}, {character_id: 'gina', has_feature: true}, ... ]
Au tour du joueur : le personnage secret du joueur est Alex (sans lunettes). Il répond donc "Non".
Fin du tour : le code JavaScript de l'application prend le relais. Il n'a pas besoin de poser d'autres questions à l'IA. Il parcourt les données d'analyse de l'étape 1.
- Le joueur a répondu "Non".
- Le code recherche chaque caractère pour lequel
has_feature
est défini sur "true". - Il fait tomber Brad et Gina. La logique est déterministe et instantanée.
Cette expérimentation était cruciale, mais nécessitait beaucoup d'essais et d'erreurs. Je ne savais pas si ça allait s'améliorer. Parfois, la situation empirait. Déterminer comment obtenir les résultats les plus cohérents n'est pas une science exacte (pour l'instant, du moins).
Mais après quelques parties contre mon nouvel adversaire IA, un nouveau problème fantastique est apparu : une partie nulle.
Échapper à un interblocage
Lorsque seuls deux ou trois caractères très similaires restaient, le modèle se retrouvait bloqué dans une boucle. Il posait une question sur une caractéristique qu'ils partageaient tous, par exemple : "Votre personnage porte-t-il un chapeau ?"
Mon code identifierait correctement ce tour comme étant inutile, et l'IA essaierait une autre caractéristique tout aussi large que les personnages partagent également, par exemple : "Votre personnage porte-t-il des lunettes ?"
J'ai amélioré l'invite avec une nouvelle règle : si une tentative de génération de question échoue et qu'il reste trois caractères ou moins, la stratégie change.
La nouvelle instruction est explicite : "Au lieu d'une fonctionnalité générale, vous devez poser une question sur une fonctionnalité visuelle plus spécifique, unique ou combinée pour trouver une différence." Par exemple, au lieu de demander si le personnage porte un chapeau, il est invité à demander s'il porte une casquette de baseball.
Cela oblige le modèle à examiner les images de beaucoup plus près pour trouver le petit détail qui peut enfin mener à une percée, ce qui améliore un peu sa stratégie de fin de partie, la plupart du temps.
Donner l'amnésie au modèle
La plus grande force d'un modèle de langage est sa mémoire. Mais dans ce jeu, sa plus grande force est devenue une faiblesse. Lorsque je commençais une deuxième partie, il me posait des questions déroutantes ou non pertinentes. Bien sûr, mon adversaire IA intelligent conservait l'intégralité de l'historique des discussions de la partie précédente. Il essayait de comprendre deux jeux (voire plus) à la fois.
Au lieu de réutiliser la même session d'IA, je la détruis explicitement à la fin de chaque partie, ce qui donne en quelque sorte à l'IA une amnésie.
Lorsque vous cliquez sur Rejouer, la fonction startNewGameSession()
réinitialise le plateau et crée une toute nouvelle session d'IA. C'était une leçon intéressante sur la gestion de l'état de la session, non seulement dans l'application, mais aussi au sein du modèle d'IA lui-même.
Les petits plus : jeux personnalisés et saisie vocale
Pour rendre l'expérience plus attrayante, j'ai ajouté deux fonctionnalités supplémentaires :
Personnages personnalisés : avec
getUserMedia()
, les joueurs peuvent utiliser leur caméra pour créer leur propre ensemble de cinq personnages. J'ai utilisé IndexedDB pour enregistrer les caractères, une base de données de navigateur idéale pour stocker des données binaires telles que des blobs d'image. Lorsque vous créez un ensemble personnalisé, il est enregistré dans votre navigateur et une option de relecture s'affiche dans le menu principal.Saisie vocale : le modèle côté client est multimodal. Il peut traiter du texte, des images et de l'audio. En utilisant l'API MediaRecorder pour capturer l'entrée du micro, j'ai pu transmettre le blob audio résultant au modèle avec une invite : "Transcrivez l'audio suivant…". C'est une façon amusante de jouer (et de voir comment il interprète mon accent flamand). J'ai créé cette fonctionnalité principalement pour montrer la polyvalence de cette nouvelle fonctionnalité Web, mais en vérité, j'en avais marre de taper les questions encore et encore.
Conclusions
Créer "Devine qui ? IA" a été un véritable défi. Mais avec un peu d'aide de la documentation et de l'IA pour déboguer l'IA (oui, J'ai fait ça), c'est devenu une expérience amusante. Il a mis en évidence l'immense potentiel d'exécution d'un modèle dans le navigateur pour créer une expérience privée, rapide et sans connexion Internet. Il s'agit toujours d'une fonctionnalité expérimentale. Il arrive que l'adversaire ne joue pas parfaitement. Elle n'est pas parfaite au pixel près ni au niveau de la logique. Avec l'IA générative, les résultats dépendent du modèle.
Au lieu de viser la perfection, je vais essayer d'améliorer le résultat.
Ce projet a également mis en évidence les défis constants de l'ingénierie des requêtes. Les requêtes sont devenues un élément essentiel, mais pas toujours le plus amusant. Mais la leçon la plus importante que j'ai apprise a été de concevoir l'application pour séparer la perception de la déduction, en divisant les capacités de l'IA et du code. Même avec cette séparation, j'ai constaté que l'IA pouvait encore commettre des erreurs évidentes (pour un humain), comme confondre des tatouages avec du maquillage ou perdre le fil de la discussion sur le personnage secret.
À chaque fois, la solution consistait à rendre les requêtes encore plus explicites, en ajoutant des instructions qui semblent évidentes pour un humain, mais qui sont essentielles pour le modèle.
Parfois, le jeu semblait injuste. Parfois, j'avais l'impression que l'IA "connaissait" le caractère secret à l'avance, même si le code n'avait jamais explicitement partagé cette information. Voici un élément essentiel de la comparaison entre l'humain et la machine :
Le comportement d'une IA ne doit pas seulement être correct, il doit aussi paraître équitable.
C'est pourquoi j'ai mis à jour les requêtes avec des instructions claires, comme "Vous ne savez PAS quel personnage j'ai choisi" et "Pas de triche". J'ai appris que, lorsque vous créez des agents d'IA, vous devez passer du temps à définir des limites, probablement encore plus que des instructions.
L'interaction avec le modèle pourrait encore être améliorée. En utilisant un modèle intégré, vous perdez une partie de la puissance et de la fiabilité d'un modèle côté serveur massif, mais vous gagnez en confidentialité, en rapidité et en capacité hors connexion. Pour un jeu comme celui-ci, ce compromis valait vraiment la peine d'être testé. L'avenir de l'IA côté client s'améliore de jour en jour, les modèles deviennent également plus petits, et j'ai hâte de voir ce que nous pourrons créer ensuite.