En mai 2023, une équipe de chercheurs a publié un papier pour présenter Voyager, un agent qui s'appuie notamment sur un LLM pour jouer en totale autonomie à Minecraft. A l'époque marginale, cette approche qui consiste à exploiter le potentiel de l'IA générative en faisant d'un modèle une brique parmi d'autres d'un système est désormais devenue centrale.
Dans ces conditions, la relecture attentive du papier et du code mis à disposition à l'époque permet de comprendre comment s'y prendre pour ne pas être dépassé par les développements d'une ingénierie très particulière, visiblement promise à un bel avenir depuis que la croissance des scaling laws semble ralentir, et qu'en conséquence l'industrie cherche à cueillir les low-hanging fruits autrement que par le prompt engineering.
C'est le deuxième article d'une série. Dans le premier article, Voyager a été présenté dans son principe, de même que les technologies sur lesquelles il s'appuie, et la lecture du code a commencé pour étudier la manière dont Voyager collecte des informations sur l'environnement dans lequel il contrôle le bot – le personnage dans Minecraft. Désormais, il est possible d'étudier la manière dont Voyager utilise un LLM pour générer la prochaine tâche à faire accomplir par le bot.
NB : Ce billet a été rédigé début décembre par un humain et non une boîte de conserve, et sera publié dans un prochain numéro de Programmez!.
L'IA, nous voulons l'IA !
Après ce détour par le serveur HTTP écrit en JavaScript, retour au client, donc au programme principal écrit en Python. Pour rappel, l'on abordait la boucle principale,
VoyagerEnv.step ()
venant d'être appelée, qui s'est contentée de soumettre la requête /step. D'ailleurs, que devient le tableau bot.cumulativeObs
retourné ? Il est stocké dans Voyager.last_events
, c'est tout.
Va-t-on enfin parler d'IA dans cet article ? Oui, car l'impatient lecteur qui parcourt le code de Voyager en même temps que cet article peut constater que le premier appel dans la boucle, c'est :
task, context = self.curriculum_agent.propose_next_task( events=self.last_events, chest_observation=self.action_agent.render_chest_observation(), max_retries=5, )
L'agent Curriculum est donc le premier agent rencontré. La manière dont il est utilisé est commune à tous les agents : constituer des prompts système et utilisateur en injectant des données dans des modèles, les fournir au chat, récupérer sa réponse – qu'il lui a été demandé de produire dans un certain format –, et l'exploiter. Mais avant de se plonger dans le code de
CurriculumAgent.propose_next_task ()
pour voir cela en détail, il faut préciser en quoi consistent les paramètres que Voyager.learn ()
lui passe. Le premier a déjà été présenté, et le troisième est trivial. Pour ce qui concerne le deuxième, c'est est le résultat retourné par ActionAgent.render_chest_observation ()
.
ActionAgent.render_chest_observation ()
se contente de traduire en texte le contenu de ActionAgent.chest_memory
. Cette propriété est un dictionnaire initialement vide. Son contenu est actualisé par ActionAgent.update_chest_memory ()
, appelée ainsi par Voyager.step ()
:
self.action_agent.update_chest_memory(events[-1][1]["nearbyChests"])
Pour rappel,
events
est le résultat retourné par VoyagerEnv.step ()
, donc le tableau bot.cumulativeObs
retourné par le serveur HTTP en réponse à une requête /step, autrement dit le tableau des observations. Ici, Voyager.step ()
passe donc les données relatives aux coffres trouvés à proximité lors la dernière observation. Plus précisément, il s'agit d'un dictionnaire dont les clés sont les positions des coffres à proximité, et les valeurs sont les contenus de ces coffres. Par ailleurs, quand il est connu, le contenu est lui-même un dictionnaire dont les clés sont les noms des objets, et les valeurs sont les nombres de ces objets dans le coffre :
Contenu connu ? | Entrée |
Oui | <position>: { <nom>: <nombre>, ... } |
Non | <position>: "Unknown" |
ActionAgent.update_chest_memory ()
actualise ActionAgent.chest_memory
en recoupant son contenu avec celui qui lui est fourni :
- un nouveau coffre est ajouté ;
- un coffre déjà inventorié dont le contenu est fourni est mis à jour avec ce contenu ;
- un coffre déjà inventorié dont le contenu est "Invalid" est supprimé.
Comment la valeur associée à la position d'un coffre peut-elle être "Invalid" ? Pour le comprendre, il faut retourner dans le code du serveur HTTP, plus particulièrement dans le constructeur de la classe
Chests
dont la méthode .observe ()
fournit l'observation relative aux coffres à proximité. En particulier, ce code contient :
bot.on("closeChest", (chestItems, position) => { this.chestsItems[position] = chestItems; }); bot.on("removeChest", (chestPosition) => { this.chestsItems[chestPosition] = "Invalid"; });
Ainsi, le tableau
Chests.chestItems
est mis à jour en fonction d'actions du bot signalées par des événements :
- quand le bot referme un coffre, le contenu du coffre est mise à jour avec l'inventaire communiqué ;
- quand le bot se rend à une position où il était censé tomber sur un bloc, mais où il ne le trouve pas, le coffre est retiré.