Les LLMs, le coeur cognitif des agents IA
Un agent IA est un système d’IA capable de planifier et d’interagir avec son environnement, à partir d’une requête, le plus souvent formulée en langage naturel. Il utilise des outils pour mener des actions et atteindre l’objectif formulé par l’utilisateur. Il a de la mémoire et est capable de décomposer une tâche en étapes successives – donc de planifier.
Le soubassement technologique principal d’un agent IA est un LLM, dont les principes de fonctionnement sont expliqués dans un autre article. Le LLM est le cerveau de l’agent IA qui lui permet de prendre des décisions. Les agents IA sont donc en quelque sorte des orchestrations de prompts qui fonctionnent sur le modèle auto-régressif propre aux LLMs. Un agent IA connaît ainsi les mêmes limites qu’un LLM : un LLM applique des statistiques, il prédit le prochain token mais n’a pas de raisonnement logique au sens humain.
Les outils à la disposition d’un agent IA consistent par exemple en une recherche internet, la génération d’une image, l’extraction d’informations de documents fournis par l’utilisateur, une interaction avec une API etc. C’est ici qu’il interagit avec un environnement délimité : un navigateur web, une interface, des fichiers, une base de données etc.
Architecture d’un agent IA
Un agent IA consiste en :
- Un prompt initial et un rôle. Par exemple : « tu es un codeur Python senior ».
- Une tâche à accomplir, souvent exprimée en langage naturel. Par exemple : « développe une API ».
- Un LLM qui reçoit cette tâche et planifie les actions à mener pour la mener à bien : il décompose la tâche en étapes, propose une stratégie, priorise.
- Un orchestrateur (par exemple un code Python) qui gère les prompts, mémorise les étapes précédentes, appelle les outils, éventuellement chaîne plusieurs appels de LLMs.
Prenons l’exemple d’une étude marketing. On demande à l’agent IA d’évaluer l’intérêt des 18-25 ans pour un service de livraison de repas végétariens en ville.
L’orchestrateur (un code Python par exemple) ajuste la demande de l’utilisateur en la transformant en prompt adapté pour le LLM. Il attribue également un rôle clair au LLM : « tu es un expert en marketing ». Le LLM planifie ensuite les étapes nécessaires, à partir du prompt reçu de l’orchestrateur. Par exemple : définir la cible, choisir une méthodologie d’enquête, organiser la diffusion de l’enquête, recueillir les réponses, analyser les données etc. L’orchestrateur appelle le LLM pour chacun de ses sous-tâches et mémorise les réponses. Il utilise les outils nécessaires qui sont soit définis strictement par l’orchestrateur ou choisis par le LLM dans le cas d’un agent IA avancé, c’est-à-dire plus autonome.
Concepts plus avancés
Chain-of-thoughts
Une chain-of-thoughts (CoT) est une méthode de conception de prompts qui pousse un LLM à raisonner par étape, plutôt qu’à fournir immédiatement une réponse finale. Comme on l’a vu, un LLM est programmé pour prédire le token suivant et non pour raisonner. Mais lorsqu’on le contraint à répondre en plusieurs étapes, la précision des réponses augmente pour les questions qui demandent de la logique.
RAG
Un Retrieval Augmented Generation (RAG) est une architecture hybride dans laquelle un LLM ne génère pas une réponse seulement à partir de sa mémoire internet mais va chercher des informations externes (fichiers, base de données, API etc.) avant de répondre. L’architecture d’un RAG fonctionne sur ce principe : la question de l’utilisateur est encodée (embedding), une base de donnée vectorielle (représentations numériques de données textuelles) est interrogée afin d’identifier les documents les plus pertinents par similarité sémantique (retrieval), ces documents sont injectés dans le prompt du LLM (contexte injection) et enfin la réponse est générée, éclairée par les informations trouvées.
Un RAG permet d’avoir un assistant personnalisé avec des données propres, des connaissances à jour et fiables dans des domaines précis.
Orchestration multi-agents
Dans le cas d’un système multi-agent l’orchestrateur définit plusieurs agents IA et les fait interagir entre eux. Chacun a un rôle spécifique (l’expert marketing, le code, l’analyse data, etc.) et une tâche ou une spécialité. Dans le cas d’une orchestration centralisée, l’orchestrateur déclenche les agents, achemine les messages entre eux, se souvient de toutes les étapes effectuée par chaque agent et contrôle ainsi le flux de la tâche globale. C’est le cas par exemple de CrewAI ou de simples orchestrateurs Python. Il existe également une autre organisation, l’orchestration distribuée : les agents dialoguent entre eux jusqu’à consensus. Par exemple, AutoGen de Microsoft. L’orchestrateur laisse les agents s’améliorer au cours de leurs échanges et définit un critère, une règle d’évaluation, pour mettre fin aux dialogues entre les agents (la boucle peut sinon être sans fin).
Un mini agent IA en code Python très simplifié
import openai
import faiss
import numpy as np
On importe les librairies nécessaires : OpenAI pour l’API, FAISS pour l’index vectorial et NumPy pour la manipulation des vecteurs. Une API (Application Programming Interface) est une interface qui permet à des systèmes logiciels de communiquer entre eux de façon standardisée. Des points de terminaison (URL via lequel les services sont accessibles), des méthodes HTTP (pour lire, créer, modifier, ou supprimer une ressource), des méthodes d’authentification, entre autres sont définis. Ici, notre code Python va pouvoir formuler des requêtes auprès de l’interface d’OpenAI : il va pouvoir utiliser chatGPT. Ce sera le cerveau de notre mini agent IA.
import os
openai.api_key = os.environ.get("OPENAI_API_KEY")
On stocke la clé API qui permet d’accéder à l’API d’OpenAI (similaire à mot de passe).
dimension = 1536 # dimension pour embeddings OpenAI
index = faiss.IndexFlatL2(dimension)
memoire_textes = []
On crée un index FAISS pour stocker les vecteurs d’embedding (expliqué ici) : avant leur entrée dans le réseau, les indices sont transformés en vecteurs continus qui permettent de représenter numériquement les liens existants entre chaque mot (relations sémantiques, syntaxiques, contextuelles – autant de dimensions que souhaité). C’est la phase d’embedding.
Les index FAISS permettent des recherches de similarité sur des vecteurs de grande dimension. Concrètement, ils permettent de retrouver les plus proches voisins d’un vecteur donné parmi les vecteurs stockés. Par exemple, cela permet de savoir quelle ressource est la plus pertinente pour une requête utilisateur donnée.
Dans le code, une liste Python, memoire_textes, est créée. Elle sera utilisée pour stocker les textes en langage naturel, associés aux vecteurs. Comme FAISS ne stocke que les vecteurs, on a besoin de stocker les textes en langage naturel ailleurs : dans cette liste.
def embed_texte(texte):
response = openai.Embedding.create(
input=[texte],
model="text-embedding-ada-002"
)
return np.array(response["data"][0]["embedding"]).astype("float32")
def memoriser(texte):
vecteur = embed_texte(texte)
index.add(np.array([vecteur]))
memoire_textes.append(texte)
def recuperer_contenu_pertinent(question, top_k=3):
vecteur_q = embed_texte(question)
distances, indices = index.search(np.array([vecteur_q]), top_k)
return "\n".join([memoire_textes[i] for i in indices[0] if i < len(memoire_textes)])
Ici on crée une fonction embed_text pour transformer un texte (par exemple la question d’un utilisateur) en vecteur d’embedding compatible FAISS. La fonction memoriser ajoute le texte dans la mémoire vectorielle (sous forme numérique) et le texte dans la liste memoire_textes (sous forme de langage naturel). Ensuite la fonction recuperer_contenu_pertinent récupère les k textes les plus proches de la question posée par l’utilisateur afin de mobiliser les ressources les plus pertinentes.
def appel_openai(system_prompt, user_prompt):
messages = [{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}]
response = openai.ChatCompletion.create(
model="gpt-4",
messages=messages,
temperature=0.7)
return response.choices[0].message["content"].strip()
Cette fonction est la fonction de base de notre mini agent.
Elle envoie un prompt à l’API d’OpenAI et récupère la réponse de chatGPT ‘ (choisi ici : model= »gpt-4″). Le prompt est composé de celui généré par l’agent IA (dans notre code) qui définit un rôle (« tu es un expert marketing ») et de la requête de l’utilisateur (la tâche à accomplir « évalue moi le besoin de 18-25 ans pour tel service »).
choices[0].message[« content »].strip() signifie que l’on prend la première réponse de chatGPT, que l’on prend le texte généré (« content ») et que l’on enlève les espaces ou sauts de ligne au début et à la fin.
def agent_planificateur(tache):
prompt = "Tu es un économiste assistant. Découpe cette tâche en sous-tâches exploitables par un autre agent :"
return appel_openai(prompt, tache)
On définit ici un rôle à l’un de nos agents. On lui demande de découper la tâche globale en plusieurs étapes. Ce prompt est envoyé à chatGPT.
def outil_calcul(expression):
try:
result = eval(expression, {"__builtins__": {}, "math": __import__("math")})
return f"Résultat du calcul : {result}"
except Exception as e:
return f"Erreur : {e}"
On définit un premier outil : une fonction qui permet de faire des calculs à partir d’une expression mathématique exprimée sous forme de texte. Toutes les opérations mathématiques du module math son accessibles ici.
def outil_recherche_web_simulee(question):
corpus = {
"impact de l'IA sur l'emploi": "Selon l'OCDE, jusqu'à 27% des emplois pourraient être automatisés d'ici 2030.",
"secteurs les plus touchés": "Industrie manufacturière, logistique, services financiers.",
"création d'emplois IA": "L'IA crée aussi de nouveaux métiers dans la data science, cybersécurité, et l’éthique."
}
for clé, valeur in corpus.items():
if clé in question.lower():
return valeur
return "Pas de données disponibles."
Ici, on donne un second outil à notre agent : une recherche internet.
On crée une mini base de données simulée, à partir de laquelle notre mini-agent va travailler. On crée une fausse recherche web (ce qui aurait pu être le résultat d’une recherche). question correspond à la demande de l’utilisateur. corpus est un dictionnaire qui correspond ici à la base de connaissance simulée. La clé, à gauche, est un mot-clé et la valeur à droite est la réponse associée. On simule donc une recherche internet qu’aurait pu faire notre mini agent : il entre un mot clé, et extrait la valeur, le résultat de sa recherche.
Si dans la question de l’utilisateur ne figure aucun mot clé, alors le code renvoie « pas de données disponibles ».
def agent_executeur(sous_tache):
if "calcule" in sous_tache.lower():
expression = sous_tache.split(":")[-1].strip()
return outil_calcul(expression)
elif "cherche" in sous_tache.lower():
question = sous_tache.split(":")[-1].strip()
return outil_recherche_web_simulee(question)
else:
prompt = "Tu es un assistant intelligent qui exécute des sous-tâches économiques."
return appel_openai(prompt, sous_tache)
La fonction agent_executeur correspond à l’agent qui effectue les tâches concrètes, qui interagit avec son environnement. Il choisit quel outil utiliser parmi les deux outils que notre mini agent a à sa disposition et il exécute l’outil (dans notre code Python, la fonction) correspondant.
Si la tâche contient le mot clé « calcule », alors la fonction outil_calcul sera appelée. Si le mot clé est « cherche », alors c’est la fonction outil_recherche_web_simulee qui est utilisée.
sous_tache.split(« : »)[-1].strip() coupe la chaîne autour de « : », prend ce qu’il y a après les deux points et enlève les espaces autour. Si la sous-tâche était « calcul : 2 + 2 », il ne conserve que « 2+2 ».
Si la sous-tâche ne contient ni « calcul », ni « cherche », l’agent n’a pas d’outils à sa disposition. Il renvoie alors la tâche à chatGPT (le LLM, le cerveau) pour qu’il improvise.
def agent_maitre(tache):
print(">> 1. Planification de la tâche")
plan = agent_planificateur(tache)
print(plan, "\n")
sous_taches = [s.strip() for s in plan.split("\n") if s.strip()]
résultats = []
print(">> 2. Exécution des sous-tâches")
for st in sous_taches:
print(f"- Sous-tâche : {st}")
réponse = agent_executeur(st)
print(f" Réponse : {réponse}")
résumé = f"Tâche : {st}\nRéponse : {réponse}"
memoriser(résumé)
résultats.append(résumé)
print("\n>> 3. Synthèse à partir de la mémoire")
contexte = recuperer_contenu_pertinent(tache)
prompt_final = "Tu es un chercheur en économie. Rédige une synthèse cohérente à partir des éléments suivants :"
résumé = appel_openai(prompt_final, contexte)
print("\n>>> Résumé final :\n")
print(résumé)
On définit ici l’agent principal, l’orchestrateur. Il planifie, exécute, mémorise et synthétise.
Il reçoit la tâche globale. Il envoie à chatGPT un prompt pour qu’il la découpe en sous-tâches. Concrètement il appelle la fonction agent_planificateur. Pour transformer la réponse de chatGPT en une liste de sous-tâches, il découpe chaque ligne (.split(« \n »)), enlève les espaces autour (s.strip()) et garde seulement les lignes non vides (if s.strip()). Il retourne alors une liste, par exemple : [« Identifier les secteurs les plus touchés », « Estimer les pertes d’emplois potentielles », …].
Il boucle ensuite sur chaque élément de cette liste, c’est-à-dire sur chaque sous-tâche : for st in sous_taches:. Pour chacune, il appelle l’agent exécutant : il n’appelle la fonction agent_executeur que s’il détecte le mot « calcul », et appelle la fonction de calcul (outil 1) ; s’il détecte le mot « cherche », appelle la fonction de recherche web (outil 2) ; et s’il ne trouve aucun de ces deux mots, renvoie la sous-tâche à chatGPT. Chaque réponse est ensuite mémorisée : la fonction memoriser est appelée.
L’orchestrateur rédige ensuite un prompt final à envoyer à chatGPT. Le rôle est pré-défini dans le code. Le contexte est créé par l’orchestrateur : il appelle la fonction recuperer_contenu_pertinent qui récupère les résumés des sous-tâches les plus proches de la tâche initiale. Cela est effectué grâce à une recherche vectorielle (proximité sémantique grâce aux embeddings + FAISS).
Avec ce prompt final, l’orchestrateur appelle chatGPT de nouveau, cette fois pour lui demander une synthèse.