-
events
events, dont l'on rappelle que c'est la version Python du tableau – une liste en Python, pour adopter une terminologie rigoureuse – des observations bot.cumulativeObs
bot.cumulativeObs retourné par le serveur HTTP en réponse à une requête /step ;
-
chest_observation
chest_observation, dont l'on rappelle que c'est un texte qui décrit le contenu des coffres alentour, identifiés par leurs positions, constitué à partir d'informations relatives à la dernière observation en date dans events
events, ou "Chests: None" s'il ne s'en trouve point.
Pour commencer, CurriculumAgent.render_human_message ()
CurriculumAgent.render_human_message () appelle CurriculumAgent.render_observation ()
CurriculumAgent.render_observation (). Après quelques contrôles – vérifier que la dernière observation est bien le produit d'un événement "observe" ; identifier le biome comme "underground" si jamais la liste des blocs à proximité events[-1][1]["voxels"]
events[-1][1]["voxels"] ne contient pas certain blocs –, cette méthode produit quelques bouts de texte :
-
La concaténation de la liste des noms des blocs que le bot a croisés
events[-1][1]["blockRecords"]
events[-1][1]["blockRecords"], à condition qu'ils ne figurent pas dans celle des blocs à proximité events[-1][1]["voxels"]
events[-1][1]["voxels"] ni dans celle des blocs qui figurent dans son inventaire events[-1][1]["inventory"]
events[-1][1]["inventory"]. Noter que dans ce bout de texte, un nom de bloc ne figure qu'une fois.
-
La concaténation des noms des mobs à proximité
events[-1][1]["status"]["entities"]
events[-1][1]["status"]["entities"] triés par ordre croissant de distance au bot – c'est un dictionnaire généré par Status.getEntities ()
Status.getEntities () sur le serveur HTTP, qui recense les mobs qui se trouvent à moins de 32 blocs de distance du bot, en ne faisant figurer que le mob le plus proche pour un nom de mob donné. Noter que dans ce bout de texte, un nom de mob ne figure qu'une fois.
-
La concaténation des descriptions des tâches accomplies. Pour rappel, une tâche est décrite par un énoncé, comme "Mine 1 wood log", et un contexte, comme "You can mine one of oak, birch, spruce, jungle, acacia, dark oak, or mangrove logs.", mais seul l'énoncé est donc repris ici.
-
La concaténation des tâches ratées. Même remarque.
Un bout de texte prend la valeur "None" si jamais il n'y a rien à concaténer.
Ensuite, CurriculumAgent.render_observation ()
CurriculumAgent.render_observation () procède à un filtrage des objets d'une copie de l'inventaire events[-1][1]["inventory"]
events[-1][1]["inventory"] :
# filter out optional inventory items if required
if self.progress < self.warm_up["optional_inventory_items"]:
inventory = {
k: v
for k, v in inventory.items()
if self._core_inv_items_regex.search(k) is not None
}
- # filter out optional inventory items if required
- if self.progress < self.warm_up["optional_inventory_items"]:
- inventory = {
- k: v
- for k, v in inventory.items()
- if self._core_inv_items_regex.search(k) is not None
- }
# filter out optional inventory items if required
if self.progress < self.warm_up["optional_inventory_items"]:
inventory = {
k: v
for k, v in inventory.items()
if self._core_inv_items_regex.search(k) is not None
}
Pourquoi ? Le commentaire – l'un des rares dans le code –, n'est pas très éclairant, mais cela n'a pas l'air anecdotique. En creusant, il apparaît que, dans le constructeur de CurriculumAgent
CurriculumAgent :
-
CurriculumAgent.warm_up["optional_inventory_items"]
CurriculumAgent.warm_up["optional_inventory_items"] est initialisé à 7 ;
-
CurriculumAgent._core_inv_items_regex
CurriculumAgent._core_inv_items_regex est le fruit de la compilation de l'expression régulière r".*_log|.*_planks|stick|crafting_table|furnace|cobblestone|dirt|coal|.*_pickaxe|.*_sword|.*_axe".
Autrement dit, tant que le bot n'a pas réussi au moins 7 tâches, la copie de son inventaire est purgée de tout objet dont le nom ne correspond pas à l'expression régulière. Sachant que cette copie est reprise pour décrire l'inventaire du bot dans le résultat retourné par CurriculumAgent.render_observation ()
CurriculumAgent.render_observation (), l'hypothèse qu'il faut faire, c'est qu'au début de l'exploration, cela permet de focaliser l'attention du LLM sur des objets primordiaux en lui offrant une vision de l'inventaire qui s'y réduit.
Si tel était bien l'objectif des créateurs de Voyager, cela viendrait en rajouter à la remarque formulée précédemment, à savoir que toute l'intelligence de Voyager ne réside pas que dans les LLMs qu'il mobilise, et qu'en plus de faire appel à des programmes extérieurs pour faire accomplir des tâches au bot, et d'intervenir sur le bot pour débloquer des situations, une autre technique consiste à jouer avec ce qui est donné à voir de la situation du bot au LLM. Intéressant...
Au final, CurriculumAgent.render_observation ()
CurriculumAgent.render_observation () reprend les bouts de texte et d'autres valeurs tirées de la dernière observation en date events[-1][1]
events[-1][1], et retourne un dictionnaire :
observation = {
"context": "",
"biome": f"Biome: {biome}\n\n",
"time": f"Time: {time_of_day}\n\n",
"nearby_blocks": f"Nearby blocks: {', '.join(voxels) if voxels else 'None'}\n\n",
"other_blocks": f"Other blocks that are recently seen: {other_blocks}\n\n",
"nearby_entities": f"Nearby entities: {nearby_entities}\n\n",
"health": f"Health: {health:.1f}/20\n\n",
"hunger": f"Hunger: {hunger:.1f}/20\n\n",
"position": f"Position: x={position['x']:.1f}, y={position['y']:.1f}, z={position['z']:.1f}\n\n",
"equipment": f"Equipment: {equipment}\n\n",
"inventory": f"Inventory ({inventory_used}/36): {inventory if inventory else 'Empty'}\n\n",
"chests": chest_observation,
"completed_tasks": f"Completed tasks so far: {completed_tasks}\n\n",
"failed_tasks": f"Failed tasks that are too hard: {failed_tasks}\n\n",
}
- observation = {
- "context": "",
- "biome": f"Biome: {biome}\n\n",
- "time": f"Time: {time_of_day}\n\n",
- "nearby_blocks": f"Nearby blocks: {', '.join(voxels) if voxels else 'None'}\n\n",
- "other_blocks": f"Other blocks that are recently seen: {other_blocks}\n\n",
- "nearby_entities": f"Nearby entities: {nearby_entities}\n\n",
- "health": f"Health: {health:.1f}/20\n\n",
- "hunger": f"Hunger: {hunger:.1f}/20\n\n",
- "position": f"Position: x={position['x']:.1f}, y={position['y']:.1f}, z={position['z']:.1f}\n\n",
- "equipment": f"Equipment: {equipment}\n\n",
- "inventory": f"Inventory ({inventory_used}/36): {inventory if inventory else 'Empty'}\n\n",
- "chests": chest_observation,
- "completed_tasks": f"Completed tasks so far: {completed_tasks}\n\n",
- "failed_tasks": f"Failed tasks that are too hard: {failed_tasks}\n\n",
- }
observation = {
"context": "",
"biome": f"Biome: {biome}\n\n",
"time": f"Time: {time_of_day}\n\n",
"nearby_blocks": f"Nearby blocks: {', '.join(voxels) if voxels else 'None'}\n\n",
"other_blocks": f"Other blocks that are recently seen: {other_blocks}\n\n",
"nearby_entities": f"Nearby entities: {nearby_entities}\n\n",
"health": f"Health: {health:.1f}/20\n\n",
"hunger": f"Hunger: {hunger:.1f}/20\n\n",
"position": f"Position: x={position['x']:.1f}, y={position['y']:.1f}, z={position['z']:.1f}\n\n",
"equipment": f"Equipment: {equipment}\n\n",
"inventory": f"Inventory ({inventory_used}/36): {inventory if inventory else 'Empty'}\n\n",
"chests": chest_observation,
"completed_tasks": f"Completed tasks so far: {completed_tasks}\n\n",
"failed_tasks": f"Failed tasks that are too hard: {failed_tasks}\n\n",
}
Retour donc à CurriculumAgent.render_human_message ()
CurriculumAgent.render_human_message (), dont la lecture est loin d'être terminée. Si jamais le nombre de tâches réussies par le bot est supérieur ou égal à CurriculumAgent.warm_up["context"]
CurriculumAgent.warm_up["context"], initialisé à 15 dans le constructeur de la classe, elle appelle CurriculumAgent.run_qa ()
CurriculumAgent.run_qa () pour poser des questions au LLM. En effet, comme le lecteur attentif n'aura pas manqué de le constater, le prompt utilisateur est censé contenir un mini-FAQ...
Des questions ! Toujours des questions !
Tout comme CurriculumAgent.render_human_message ()
CurriculumAgent.render_human_message () qui l'appelle, CurriculumAgent.run_qa ()
CurriculumAgent.run_qa () prend en paramètres events
events, la liste des observations, et chest_observation
chest_observation, le texte qui décrit le contenu des coffres alentour. La méthode commence par appeler CurriculumAgent.run_qa_step1_ask_questions ()
CurriculumAgent.run_qa_step1_ask_questions (), en lui passant ces paramètres qui décidemment sont beaucoup relayés.
Cette nouvelle méthode commence par appeler CurriculumAgent.render_system_message_qa_step1_ask_questions ()
CurriculumAgent.render_system_message_qa_step1_ask_questions () et CurriculumAgent.render_human_message_qa_step1_ask_questions ()
CurriculumAgent.render_human_message_qa_step1_ask_questions () pour constituer des prompts système et utilisateur.
CurriculumAgent.render_system_message_qa_step1_ask_questions ()
CurriculumAgent.render_system_message_qa_step1_ask_questions () se contente de retourner le contenu du fichier texte /prompts/curriculum_qa_step1_ask_questions.txt. Ici encore, c'est un prompt assez long, et l'on détaillera sa structure en s'appuyant sur de larges extraits – le lecteur est renvoyé au fichier pour consulter le prompt dans son intégralité. Sa structure reproduit en tout point celle du premier prompt qui a été décrit : (1) donner un rôle au LLM ; (2) lui décrire les informations que l'on va lui donner ; (3) lui donner les règles à respecter ; (4) lui donner le format que sa réponse doit adopter ; (5) lui donner des exemples. A ceci près que les deux dernières étapes sont ici interverties.
-
il donne un rôle au LLM :
You are a helpful assistant that asks questions to help me decide the next immediate task to do in Minecraft. My ultimate goal is to discover as many things as possible, accomplish as many tasks as possible and become the best Minecraft player in the world.
-
il lui décrit les informations qu'il va lui fournir, c'est-à-dire une série des états du bot et de son environnement, ainsi que la liste des tâches réussies et ratées par le bot :
Biome: ...
Time: ...
Nearby blocks: ...
Other blocks that are recently seen: ...
Nearby entities (nearest to farthest): ...
Health: ...
Hunger: ...
Position: ...
Equipment: ...
Inventory (xx/36): ...
Chests: ...
Completed tasks so far: ...
Failed tasks that are too hard: ...
-
il lui donne des règles à respecter :
1) You should ask at least 5 questions (but no more than 10 questions) to help me decide the next immediate task to do. Each question should be followed by the concept that the question is about.
2) Your question should be specific to a concept in Minecraft.
Bad example (the question is too general):
Question: What is the best way to play Minecraft?
Concept: unknown
Bad example (axe is still general, you should specify the type of axe such as wooden axe):
What are the benefits of using an axe to gather resources?
Concept: axe
Good example:
Question: How to make a wooden pickaxe?
Concept: wooden pickaxe
3) Your questions should be self-contained and not require any context.
Bad example (the question requires the context of my current biome):
Question: What are the blocks that I can find in my current biome?
Concept: unknown
Bad example (the question requires the context of my current inventory):
Question: What are the resources you need the most currently?
Concept: unknown
Bad example (the question requires the context of my current inventory):
Question: Do you have any gold or emerald resources?
Concept: gold
Bad example (the question requires the context of my nearby entities):
Question: Can you see any animals nearby that you can kill for food?
Concept: food
Bad example (the question requires the context of my nearby blocks):
Question: Is there any water source nearby?
Concept: water
Good example:
Question: What are the blocks that I can find in the sparse jungle?
Concept: sparse jungle
4) Do not ask questions about building tasks (such as building a shelter) since they are too hard for me to do.
-
il lui fournit des exemples (extrait) :
Here are some more question and concept examples:
Question: What are the ores that I can find in the sparse jungle?
Concept: sparse jungle
(the above concept should not be "ore" because I need to look up the page of "sparse jungle" to find out what ores I can find in the sparse jungle)
Question: How can you obtain food in the sparse jungle?
Concept: sparse jungle
(the above concept should not be "food" because I need to look up the page of "sparse jungle" to find out what food I can obtain in the sparse jungle)
Question: How can you use the furnace to upgrade your equipment and make useful items?
Concept: furnace
<...>
-
il lui décrit le format de la réponse attendue (extrait) :
Reasoning:
Question 1: ...
Concept 1: ...
Question 2: ...
Concept 2: ...
<...>
Autant pour le prompt système. Pour ce qui concerne le prompt utilisateur, CurriculumAgent.render_human_message_qa_step1_ask_questions ()
CurriculumAgent.render_human_message_qa_step1_ask_questions () appelle CurriculumAgent.render_observation ()
CurriculumAgent.render_observation () déjà longuement détaillée, et retourne la concaténation de toutes les valeurs du dictionnaire qu'elle lui retourne.
CurriculumAgent.run_qa_step1_ask_questions ()
CurriculumAgent.run_qa_step1_ask_questions () utilise les deux prompts ainsi générés pour interroger le LLM. Enfin, une première interaction avec un LLM ! Comment cela se passe ? Roulement de tambour, et... :
qa_response = self.qa_llm(messages).content
- qa_response = self.qa_llm(messages).content
qa_response = self.qa_llm(messages).content
Ah ! ouais, une ligne code tout de même.
En soi, ce n'est pas très surprenant qu'il soit aussi simple d'interfacer une application avec un LLM depuis le code de cette dernière, mais c'est tout de même intéressant à constater, car cela semble devoir rendre les choses à la fois plus faciles et plus risquées. Dans le code de combien d'applications ne verra-t-on pas fleurir ce type d'appel par paresse, alors qu'il n'est en rien anodin ? C'est qu'il faut prendre la mesure de toutes les conséquences en termes de régularité et de niveau de performance, de coûts financiers, de conséquences sur l'environnement, de sécurité du système d'information – sur ce point le lecteur intéressé se reportera à cet article –, d'accumulation de dette technique, etc.
Comment la réponse d'un LLM, qui est nécessairement du texte, peut-elle être exploitée par un programme ? Le détail de la manière dont CurriculumAgent.run_qa_step1_ask_questions ()
CurriculumAgent.run_qa_step1_ask_questions () retraite le résultat est la suivante :
# Regex pattern to extract question and concept pairs
pattern = r"Question \d+: (.+)\nConcept \d+: (.+)"
# Extracting all question and concept pairs from the text
pairs = re.findall(pattern, qa_response)
# Storing each question and concept in separate lists
questions_new = [pair[0] for pair in pairs]
concepts_new = [pair[1] for pair in pairs]
assert len(questions_new) == len(concepts_new)
questions.extend(questions_new)
concepts.extend(concepts_new)
- # Regex pattern to extract question and concept pairs
- pattern = r"Question \d+: (.+)\nConcept \d+: (.+)"
- # Extracting all question and concept pairs from the text
- pairs = re.findall(pattern, qa_response)
- # Storing each question and concept in separate lists
- questions_new = [pair[0] for pair in pairs]
- concepts_new = [pair[1] for pair in pairs]
- assert len(questions_new) == len(concepts_new)
- questions.extend(questions_new)
- concepts.extend(concepts_new)
# Regex pattern to extract question and concept pairs
pattern = r"Question \d+: (.+)\nConcept \d+: (.+)"
# Extracting all question and concept pairs from the text
pairs = re.findall(pattern, qa_response)
# Storing each question and concept in separate lists
questions_new = [pair[0] for pair in pairs]
concepts_new = [pair[1] for pair in pairs]
assert len(questions_new) == len(concepts_new)
questions.extend(questions_new)
concepts.extend(concepts_new)
Comme il est possible de le constater, le texte est retraité à l'aide d'expressions régulières pour en extraire les questions et les concepts qu'elles abordent, en misant sur le fait que le LLM aura respecté le format qui lui était demandé. Au passage, la capacité d'un LLM à générer une réponse dans un certain format est un sujet en soi – quelque part, il en sera de nouveau question quand il s'agira de lui demander de générer du code JavaScript –, mais beaucoup de progrès ont été accomplis depuis Voyager pour obtenir notamment du JSON.
Finalement, CurriculumAgent.run_qa_step1_ask_questions ()
CurriculumAgent.run_qa_step1_ask_questions () retourne une liste des textes des questions et une liste des textes des concepts qu'elles abordent à CurriculumAgent.run_qa ()
CurriculumAgent.run_qa (), dont il devient possible de reprendre la lecture du code après la première instruction. Pour qui veut comprendre comment il est possible de s'appuyer sur un LLM pour créer un agent, les choses deviennent alors encore plus intéressantes, car une base de données vectorielle est alors exploitée.
Les questions à la moulinette vectorielle
En effet, chacune des questions générées par le LLM est prétexte à interroger une base de données vectorielles. S'agissant d'une étape importante dans l'utilisation de l'IA par Voyager, il faut en donner à voir le code intégralement :
if self.qa_cache_questions_vectordb._collection.count() > 0 :
docs_and_scores = (
self.qa_cache_questions_vectordb.similarity_search_with_score(
question, k=1
)
)
if docs_and_scores and docs_and_scores[0][1] < 0.05 :
question_cached = docs_and_scores[0][0].page_content
assert question_cached in self.qa_cache
answer_cached = self.qa_cache[question_cached]
questions.append(question_cached)
answers.append(answer_cached)
continue
answer = self.run_qa_step2_answer_questions(question=question)
assert question not in self.qa_cache
self.qa_cache = answer
self.qa_cache_questions_vectordb.add_texts(
texts=,
)
U.dump_json(self.qa_cache, f"{self.ckpt_dir}/curriculum/qa_cache.json")
self.qa_cache_questions_vectordb.persist()
questions.append(question)
answers.append(answer)
- if self.qa_cache_questions_vectordb._collection.count() > 0 :
- docs_and_scores = (
- self.qa_cache_questions_vectordb.similarity_search_with_score(
- question, k=1
- )
- )
- if docs_and_scores and docs_and_scores[0][1] < 0.05 :
- question_cached = docs_and_scores[0][0].page_content
- assert question_cached in self.qa_cache
- answer_cached = self.qa_cache[question_cached]
- questions.append(question_cached)
- answers.append(answer_cached)
- continue
- answer = self.run_qa_step2_answer_questions(question=question)
- assert question not in self.qa_cache
- self.qa_cache = answer
- self.qa_cache_questions_vectordb.add_texts(
- texts=,
- )
- U.dump_json(self.qa_cache, f"{self.ckpt_dir}/curriculum/qa_cache.json")
- self.qa_cache_questions_vectordb.persist()
- questions.append(question)
- answers.append(answer)
if self.qa_cache_questions_vectordb._collection.count() > 0 :
docs_and_scores = (
self.qa_cache_questions_vectordb.similarity_search_with_score(
question, k=1
)
)
if docs_and_scores and docs_and_scores[0][1] < 0.05 :
question_cached = docs_and_scores[0][0].page_content
assert question_cached in self.qa_cache
answer_cached = self.qa_cache[question_cached]
questions.append(question_cached)
answers.append(answer_cached)
continue
answer = self.run_qa_step2_answer_questions(question=question)
assert question not in self.qa_cache
self.qa_cache = answer
self.qa_cache_questions_vectordb.add_texts(
texts=,
)
U.dump_json(self.qa_cache, f"{self.ckpt_dir}/curriculum/qa_cache.json")
self.qa_cache_questions_vectordb.persist()
questions.append(question)
answers.append(answer)
La base en question est CurriculumAgent.qa_cache_questions_vectordb
CurriculumAgent.qa_cache_questions_vectordb, créée par le constructeur de CurriculumAgent
CurriculumAgent, et sauvegardée dans /ckpt/curriculum/vectordb/ :
self.qa_cache_questions_vectordb = Chroma(
collection_name="qa_cache_questions_vectordb",
embedding_function=OpenAIEmbeddings(),
persist_directory=f"{ckpt_dir}/curriculum/vectordb",
)
- self.qa_cache_questions_vectordb = Chroma(
- collection_name="qa_cache_questions_vectordb",
- embedding_function=OpenAIEmbeddings(),
- persist_directory=f"{ckpt_dir}/curriculum/vectordb",
- )
self.qa_cache_questions_vectordb = Chroma(
collection_name="qa_cache_questions_vectordb",
embedding_function=OpenAIEmbeddings(),
persist_directory=f"{ckpt_dir}/curriculum/vectordb",
)
Le lecteur ayant toutes les chances de tomber sur une base de données vectorielles pour la première fois, il convient-de dire un mot sur le concept.
L'une des grandes innovations qui a permis le surgissement des LLMs a été la vectorisation de textes, notamment avec l'apparition de word2vec – la chaîne StatQuest a produit
un excellent épisode sur le sujet. Grâce à cela, les mots peuvent être représentés par des points dans un espace de grande dimension, où la proximité géométrique qui sépare deux points tient la proximité sémantique des mots correspondants dans le corpus d'entraînement. Les dimensions de l'espace sont désignées comme les features, et les coordonnées comme les embeddings. Cela permet de se livrer à des opérations vectorielles telles qu'additionner les vecteurs de
"river" et
"Russia" pour obtenir quelque chose comme celui de
"Volga river". Pourtant récipiendaire du prix John Von Neuman de l'IEEE en 2024 pour ses travaux sur le sujet, Christopher Manning explique dans
un entretien accordé à TWIM, l'excellent podcast animé par le toujours très pertinent Sam Charrington :
"it still seems to me kind of incredible how well something that in retrospect is relatively simple manages to capture word meaning". Comme l'éminent savant, l'humble béotien qui écrit ces lignes n'en revient lui aussi toujours pas...
Comme rapporté dans
une explication d'IBM, la vectorisation ne s'est pas limitée au texte, et des systèmes de base de données ont été élaborés sur ce principe pour permettre le stockage et l'indexation de l'association de vecteurs et de métadonnées relatives aux données. Lorsque l'utilisateur saisit une requête sous forme de texte, cette dernière est vectorisée, et le système recherche les vecteurs les plus proches dans la base via l'index – il peut s'agir de calculer la distance géométrique, ou le cosinus de l'angle et donc le produit scalaire. Comme on l'imagine, il peut y avoir plusieurs candidats, et un score, qui découle nécessairement du calcul, peut permettre à l'utilisateur de les départager.
Pour revenir à CurriculumAgent.run_qa ()
CurriculumAgent.run_qa (), elle vérifie si la base contient des entrées. Si oui, elle en appelle la méthode Chroma.similarity_search_with_score ()
Chroma.similarity_search_with_score () en lui passant la question et le nombre de résultats à retourner, un seul en l'occurrence. La méthode retourne une liste de tuples, ici réduite à un seul, composés d'un document et de son score. Si le score du document strictement est inférieur à 0,05, la réponse à la question est tirée de CurriculumAgent.qa_cache
CurriculumAgent.qa_cache. Il s'agit d'un dictionnaire, initialisé à vide par le constructeur de CurriculumAgent, dont le contenu est calqué sur celui de la base : pour chaque question qui se trouve sous forme de document dans la base, l'on trouve une entrée dans ce dictionnaire, dont la clé est la question, et la valeur la réponse à cette dernière.
Si la base ne contient pas d'entrées ou si la requête n'a produit aucun document satisfaisant, CurriculumAgent.run_qa ()
CurriculumAgent.run_qa () rajoute la question dans la base, ainsi que cette question et sa réponse à CurriculumAgent.qa_cache. Pour obtenir cette réponse, CurriculumAgent.run_qa ()
CurriculumAgent.run_qa () appelle CurriculumAgent.run_qa_step2_answer_questions ()
CurriculumAgent.run_qa_step2_answer_questions ().
Comme le lecteur s'en doute, cette nouvelle méthode fonctionne sur le modèle de CurriculumAgent.run_qa_step1_answer_questions ()
CurriculumAgent.run_qa_step1_answer_questions (). Elle appelle CurriculumAgent.render_system_message_qa_step2_answer_questions ()
CurriculumAgent.render_system_message_qa_step2_answer_questions () et CurriculumAgent.render_human_message_qa_step2_answer_questions ()
CurriculumAgent.render_human_message_qa_step2_answer_questions () pour constituer un prompt système et un prompt utilisateur, respectivement, donne cela au LLM et retourne le texte qu'il génère. La première méthode se contente de reprendre le contenu du fichier texte /prompts/curriculum_qa_step2_answer_questions.txt. Encore un prompt, mais celui-là est particulièrement bref :
You are a helpful assistant that answer my question about Minecraft.
I will give you the following information:
Question: ...
You will answer the question based on the context (only if available and helpful) and your own knowledge of Minecraft.
1) Start your answer with "Answer: ".
2) Answer "Answer: Unknown" if you don't know the answer.
Quant au prompt utilisateur que la seconde méthode génère, c'est encore plus simple, puisqu'il ne s'agit que de la question ou presque.
Au final, qu'elle ait trouvée une question proche dans la base de données vectorielles, et sa réponse dans le dictionnaire des questions et réponses, ou qu'elle ait dû rajouter la question à la base, et cette question et sa réponse, générée pour l'occasion par le LLM, au dictionnaire, CurriculumAgent.run_qa ()
CurriculumAgent.run_qa () retourne une liste de questions et une liste de réponses qui ont donc été, les unes comme les autres, générées par le LLM.
Petit décrassage avant la route
Ce qui permet de rependre le fil de CurriculumAgent.render_human_message ()
CurriculumAgent.render_human_message (), dont l'on rappelle qu'elle a été appelée par CurriculumAgent.propose_next_task ()
CurriculumAgent.propose_next_task () pour générer le texte décrivant la prochaine tâche à accomplir par le bot, et le contexte de cette dernière.
Ce qui suit dans CurriculumAgent.render_human_message ()
CurriculumAgent.render_human_message () coule de source. Les textes des questions et de leurs réponses, ainsi que les textes tirés de la dernière observation, sont utilisés pour compléter le prompt utilisateur tiré de /prompts/curriculum.txt, déjà présenté. Hormis que ce sont au plus cinq questions et leurs réponses qui sont reprises dans ce modèle, la seule subtilité concerne la reprise des textes de l'observation par ce bout de code :
for key in self.curriculum_observations :
if self.progress >= self.warm_up[key] :
if self.warm_up[key] != 0 :
should_include = random.random() < 0.8
else :
should_include = True
if should_include :
content += observation[key]
- for key in self.curriculum_observations :
- if self.progress >= self.warm_up[key] :
- if self.warm_up[key] != 0 :
- should_include = random.random() < 0.8
- else :
- should_include = True
- if should_include :
- content += observation[key]
for key in self.curriculum_observations :
if self.progress >= self.warm_up[key] :
if self.warm_up[key] != 0 :
should_include = random.random() < 0.8
else :
should_include = True
if should_include :
content += observation[key]
Le dictionnaire CurriculumAgent.warm_up
CurriculumAgent.warm_up a déjà été croisé deux fois, pour conditionner l'apparition de certaines informations dans le prompt utilisateur utilisé par CurriculumAgent.propose_next_task ()
CurriculumAgent.propose_next_task () pour demander au LLM de générer l'énoncé et le contexte de la prochaine tâche à accomplir par le bot :
-
son entrée "optional_inventory_items", initialisée à 7, correspond au nombre de tâches réussies par le bot en-deçà duquel la reprise prévue – plus loin, l'on verra que cette reprise est conditionnée – de l'inventaire du bot dans le prompt est limitée aux objets dont les noms correspondent à l'expression régulière r".*_log|.*_planks|stick|crafting_table|furnace|cobblestone|dirt|coal|.*_pickaxe|.*_sword|.*_axe" ;
-
son entrée "context", initialisée à 15, correspond au nombre de tâches réussies par le bot à partir duquel une série de questions et de réponses générées par
CurriculumAgent.run_qa ()
CurriculumAgent.run_qa () est reprise dans le ce même prompt.
En fait, ce dictionnaire contient bien d'autres entrées :
context
|
15
|
biome
|
10
|
time
|
15
|
nearby_blocks
|
0
|
other_blocks
|
10
|
nearby_entities
|
5
|
health
|
15
|
hunger
|
15
|
position
|
0
|
equipment
|
0
|
inventory
|
0
|
optional_inventory_items
|
7
|
chests
|
0
|
completed_tasks
|
0
|
failed_tasks
|
0
|
Dans le bout de code, une entrée sert à déterminer s'il faut reprendre dans le prompt l'information correspondante dans le dictionnaire retourné par CurriculumAgent.render_observation ()
CurriculumAgent.render_observation () – pour rappel, cette méthode a traduit en textes les informations figurant dans la dernière observation en date. Une telle information n'est reprise que sous plusieurs conditions :
-
évidemment, il faut qu'une entrée correspondante figure dans le dictionnaire
CurriculumAgent.warm_up
CurriculumAgent.warm_up ;
-
le nombre de tâches réussies par le bot est supérieur ou égal à la valeur de cette entrée ;
-
si cette valeur n'est pas nulle, une valeur aléatoire comprise dans [0, 1[ doit être strictement inférieure à 0,8 – bref, l'information n'a approximativement que 8 chances sur 10 d'être reprise.
Comme il a déjà été possible de le constater, la lecture attentive du code de Voyager se justifie par le fait que le diable se loge dans les détails. Ici, c'est tout un tour de chauffe qui est mis en évidence. Dans le prompt fourni au LLM pour lui demander de générer la prochaine tâche à accomplir, les états du bot et de son environnement sont moins documentés tant que le bot n'a pas réussi un certain nombre de tâches, et ils ne le sont ensuite que dans une certaine mesure – tirage au sort. Par exemple, tant que le bot n'a pas réussi 15 tâches, son nombre de points de vie n'est pas documenté. Mention spéciale pour l'inventaire, sur lequel le LLM n'a qu'une vision réduite aux objets primordiaux (bûches, planches, bâtons, établi, four, pierre, terre, charbon, pioche, épée et hache) dans un premier temps.
Vu d'ensemble, il apparaît que durant un tour de chauffe, le LLM n'est pas renseigné sur le biome, l'heure, les autres blocs – ceux que le bot a croisé mais qui ne figurent pas à proximité immédiate ni dans son inventaire –, les créatures alentour, l'état de santé et de satiété du bot, et qu'il ne voit de son inventaire que les objets primordiaux. Il devient assez vite renseigné sur les créatures alentour, puis sur le biome et les autres blocs, et plus tard sur le reste.
Sachant que dans Minecraft, les débuts sont chauds car l'on débarque en slip, et qu'il faut rapido trouver à grailler pour compenser l'énergie claquée pour crafter ses premiers outils, avant que la nuit tombe car c'est alors que les streums déboulent, le LLM apparaît mis en risque durant le warm-up.
C'est celui qui dit, qui fait
C'est ainsi, au terme de bien des tours et détours dans le code, que CurriculumAgent.render_human_message ()
CurriculumAgent.render_human_message () retourne le prompt utilisateur. Pour récompenser le lecteur qui a tenu jusqu'ici, voici à quoi il peut ressembler :
Question 1: How to obtain a diamond ore?
<réponse>
Question 2: What are the benefits of using a stone pickaxe over a wooden pickaxe?
<réponse>
Biome: sparse jungle
Time: sunrise
Nearby blocks: diorite, diorite, granite
Other blocks that are recently seen: dirt, water
Nearby entities: cow, enderman
Health: 5.0/20
Hunger: 2.0/20
Position: x=109.3, y=45.0, z=123.6
Equipment: ['iron_helmet', 'diamond_chestplate', 'iron_leggings', 'iron_boots', 'iron_sword', 'torch']
Inventory (2/36): {'golden_apple': 2, 'diamond': 12}
Chests:
(1.1, -12.0, 7.2): {'iron_axe': 2, 'apple': 1, 'tnt': 4}
(-6.0, 12.3, 4.2): Unknown items inside
(3.5, 5.1, -2.0): Empty
Completed tasks so far: {'Mine 1 wood log', 'Craft 1 iron sword'}
Failed tasks that are too hard: {'Kill 1 zombie', 'Make 1 golden apple'}
Dès lors, CurriculumAgent.propose_next_task ()
CurriculumAgent.propose_next_task () peut appeler CurriculumAgent.propose_next_ai_task ()
CurriculumAgent.propose_next_ai_task () en lui passant ces prompts. Cette méthode les utilise pour interroger le LLM, mais ne s'en tient pas là, car elle doit retourner non seulement l'énoncé d'une tâche, mais aussi son contexte.
Pour l'énoncé, c'est assez simple : CurriculumAgent.propose_next_ai_task ()
CurriculumAgent.propose_next_ai_task () appelle CurriculumAgent.parse_ai_message ()
CurriculumAgent.parse_ai_message (), laquelle se contente de rechercher dans la réponse du LLM une ligne qui débute par "Task: " et de retourner tout ce qui suit ce préfixe.
Pour ce qui concerne le contexte, CurriculumAgent.propose_next_ai_task ()
CurriculumAgent.propose_next_ai_task () appelle CurriculumAgent.get_task_context ()
CurriculumAgent.get_task_context () en lui passant l'énoncé de la tâche qu'elle vient de récupérer. Cette méthode commence par retraiter l'énoncé pour former le texte d'une question – pour rappel, en Python, la syntaxe utilisée produit le résultat de la concaténation des chaînes entre parenthèses :
# if include ore in question, gpt will try to use tool with skill touch enhancement to mine
question = (
f"How to {task.replace('_', ' ').replace(' ore', '').replace(' ores', '').replace('.', '').strip().lower()}"
f" in Minecraft?"
)
- # if include ore in question, gpt will try to use tool with skill touch enhancement to mine
- question = (
- f"How to {task.replace('_', ' ').replace(' ore', '').replace(' ores', '').replace('.', '').strip().lower()}"
- f" in Minecraft?"
- )
# if include ore in question, gpt will try to use tool with skill touch enhancement to mine
question = (
f"How to {task.replace('_', ' ').replace(' ore', '').replace(' ores', '').replace('.', '').strip().lower()}"
f" in Minecraft?"
)
Ces quelques lignes méritent d'être rapportées, car le rare commentaire fournit une explication qui n'est en rien anodine. En effet, c'est donc pour pallier l'incompétence du LLM sur un point très précis qu'il faut retraiter la question qui doit lui être posée, ce qui impose de le connaître à l'avance.
Pour le lecteur qui l'ignore, "Silk touch" – donc mal orthographié dans le commentaire – est un enchantement qui peut être appliqué sur un outil qui sert à miner, pour récupérer le bloc lui-même, et non le résultat de son minage. Par exemple, miner un bloc de charbon avec une pioche enchantée de la sorte produit le bloc de charbon, et non plus ou moins de minerai de charbon. Cela peut être très utile pour récupérer un bloc afin de le réutiliser pour décorer une construction, ou alors pour chercher à le miner plus tard, lorsqu'il aura été possible d'enchanter un outil avec "Fortune" qui permet d'augmenter la chance de tirer beaucoup de minerai du bloc – l'on pense à un bloc rare, comme un bloc de diamant. Le problème qui doit être résolu ici, c'est que si le bot mine un bloc de minerai avec un outil enchanté avec "Silk touch", il va récupérer le bloc et non le minerai, et donc ne pas pouvoir fabriquer d'objets à base de ce minerai. Dès lors, il sera bloqué à l'âge de pierre dans le schéma classique d'évolution en vigueur alors dans Minecraft : bois, pierre, fer, et enfin diamant.
La question formée, CurriculumAgent.get_task_context ()
CurriculumAgent.get_task_context () procède comme CurriculumAgent.run_qa ()
CurriculumAgent.run_qa (), c'est-à-dire qu'elle la recherche dans le cache des questions de l'agent pour récupérer la réponse ; si elle ne la trouve pas, elle pose la question au LLM et stocke la question dans la base de données vectorielle des questions, et la question et la réponse dans le cache.
C'est CurriculumAgent.run_qa_step2_answer_questions ()
CurriculumAgent.run_qa_step2_answer_questions () qui est appelée au besoin pour poser la question au LLM. C'est une version simplifiée de CurriculumAgent.render_system_message_qa_step1_ask_questions ()
CurriculumAgent.render_system_message_qa_step1_ask_questions () croisée plus tôt, qui se contente de reprendre le contenu du fichier texte "curriculum_qa_step2_answer_questions.txt" pour le prompt système... :
You are a helpful assistant that answer my question about Minecraft.
I will give you the following information:
Question: ...
You will answer the question based on the context (only if available and helpful) and your own knowledge of Minecraft.
1) Start your answer with "Answer: ".
2) Answer "Answer: Unknown" if you don't know the answer.
...et une bête chaîne formée du préfixe "Question: " suivi de la question pour le prompt utilisateur.
C'est terminé. CurriculumAgent.propose_next_ai_task ()
CurriculumAgent.propose_next_ai_task () récupère ainsi l'énoncé d'une nouvelle tâche à accomplir pour permettre au bot d'évoluer, générée par le LLM sur la base du curriculum, et le contexte de cette tâche, lui aussi généré par le LLM auquel il a simplement été demandé... comment accomplir cette tâche !
S'il ne parvient pas à obtenir du LLM l'énoncé d'une tâche et son contexte, la méthode repart pour un tour en s'appelant elle-même, dans la limite de 5 fois, après quoi elle soulève une exception "Max retries reached, failed to propose ai task." Ici, il faut pointer que CurriculumAgent.propose_next_ai_task ()
CurriculumAgent.propose_next_ai_task () ne change strictement rien d'une itération à l'autre, ce qui paraît intriguant. En effet, si la température du LLM est à 0, ce dernier n'est-il pas censé générer toujours la même réponse à une même demande ? Or, d'une itération à l'autre, même les prompts restent les mêmes. A moins qu'il ne s'agisse de tenir compte d'un possible plantage de l'appel au serveur d'OpenAI...
CurriculumAgent.propose_next_task ()
CurriculumAgent.propose_next_task () retourne ce beau résultat à Voyager.learn ()
Voyager.learn (), ce qui permet de passer le cap de la première étape de la boucle principale. Il reste maintenant à voir comment la tâche et son contexte sont utilisés pour générer le code JavaScript qui permet de faire accomplir la tâche par le bot. Bravo au lecteur qui a tenu jusqu'ici.