Skip to content

3.4 PREDIBAG: Construire des Agents IA en Prolog

Claude Roux edited this page Aug 28, 2024 · 23 revisions

PREDIBAG: Agents et Prédicats: un couple pour la vie

English Version

Ressusciter les morts est généralement une opération risquée. Ça demande soit des connaissances en latin ou en hébreux, soit un bon orage avec des éclairs monstrueux. Le résultat d'après les experts est rarement à la hauteur des espérances. Il suffit de voir l'échec du docteur Frankenstein pour s'en convaincre.

Nous en informatique, on fait ça tout le temps...

C'est normal, si on y réfléchit, les données qu'on manipule finalement sont toujours plus ou moins constituées du même cocktail: des données numériques ou textuelles. Même une image ou une vidéo, ça reste des nombres à la queue leu-leu qu'on projette sur un écran.

Qui par exemple se douterait que numpi doit tout à Fortran?

Les idées zombies en informatique, des idées à moitié morte qui retrouvent une seconde vie, ça n'a rien d'exceptionnel.

Architecture agentique

Prenons un exemple récent. Les IA de quelque milliards de paramètres telles que llama3-7b ou mistral-7b sont rarement aussi puissantes que les IA frontières qui elles frisent le trillon de paramètres. Mais, si on les place dans une architecture d'agents, grosso-modo des prompts différents, chacun avec la tâche particulière de générer des trucs ou de juger des trucs déjà générés, on fait exploser les compteurs. Les IA savent mieux juger leurs voisins que de faire des trucs elles-mêmes. Et il y a encore des gens pour douter que ces IA auront du mal à trouver leur place dans les sociétés humaines!!!

Une architecture agentique a comme objectif de planifier les différentes étapes d'une analyse, de générer des réponses possibles, de les corriger ou de les transformer de façon itérative.

Dans tous les cas de figure, le but d'une telle architecture est de parcourir un graphe pour trouver un chemin vers la ou les solutions.

Une vieille idée: Prolog

Parcourir un graphe d'analyse, voilà une tâche connue pour laquelle on a même inventé un langage: Prolog.

Prolog est né des travaux de Alain Colmerauer et Philippe Roussel à l'université de Marseille aux débuts des années 70.Il a atteint son apogée dans les années 1980 et au début des années 1990, trouvant des applications dans le traitement du langage naturel, les systèmes experts et la recherche en intelligence artificielle.

Cependant, avec l'arrivée de l'hiver de l'IA et la montée en puissance des approches axées sur les données, l'étoile de Prolog a commencé à pâlir. Aujourd'hui, il est largement tombé dans l'oubli, relégué aux cercles académiques et aux applications de niche. Mais si le langage connait une certaine obscurité, il existe encore bon nombre de communauté qui continue de le faire vivre sous de nombreuses formes.

Unification et chainage arrière

Un programme Prolog s'appuie sur l'unification des variables et le chainage arrière pour trouver le meilleur chemin dans un graphe d'analyse défini par des règles et des faits:

Illustrons cela avec un exemple simple :

Exemple : Relations familiales

Considérons une simple base de connaissances avec des faits sur les relations familiales :

parent("george", "sam").
parent("george", "andy").
parent("sam", "john").

Nous pouvons définir une règle pour déterminer si quelqu'un est un ancêtre d'une autre personne :

ancestor(?X, ?Y) :- parent(?X, ?Y).
ancestor(?X, ?Z) :- parent(?X, ?Y), ancestor(?Y, ?Z).

Voici comment le moteur Prolog fonctionne avec ces règles et faits :

  1. Requête : Nous demandons au moteur si "george" est un ancêtre de "john" :

    bool b = ancestor("george", "john").

Puisque nous devons seulement prouver que ce prédicat est vrai, nous utilisons un booléen comme variable récipiendaire. D'autre part, si nous voulions trouver tous les ancêtres possibles pour "george" par exemple :

vector v = ancestor("george", ?Descendant);
println(v);

Nous utiliserions un vecteur qui stockerait chaque solution.

  1. Résolution : Le moteur essaie de faire correspondre la requête aux règles et aux faits.

    • Il essaie d'abord la règle ancestor(?X, ?Y) :- parent(?X, ?Y).

      • Il vérifie si parent("george", "john"). est vrai.
      • Puisque ce fait n'est pas dans la base de connaissances, ce chemin échoue.
    • Le moteur revient alors en arrière et essaie la deuxième règle ancestor(?X, ?Z) :- parent(?X, ?Y), ancestor(?Y, ?Z).

      • Il vérifie si parent("george", ?Y). est vrai.
      • Il trouve parent("george", "sam").
      • Il essaie ensuite de satisfaire ancestor("sam", "john").
        • Il vérifie si parent("sam", "john"). est vrai.
        • Il trouve parent("sam", "john").
        • Puisque les deux conditions sont satisfaites, la requête réussit.
  2. Résultat : Le moteur confirme que "george" est un ancêtre de "john".

Cet exemple simple montre comment le moteur Prolog utilise la résolution et le chainage arrière pour explorer différents chemins de raisonnement afin de satisfaire un objectif. Le moteur fait correspondre l'objectif aux règles et aux faits dans la base de connaissances, utilisant le chainage arrière pour explorer des chemins alternatifs lorsque cela est nécessaire.

Tamgu

Le formalisme ci-dessus est celui d'un langage de programmation qui a été développé au sein de Naver Labs Europe ou NLE, un laboratoire appartenant au groupe Coréen Naver.

Tamgu est un langage FIL qui mélange dans un même formalisme les approches impérative, fonctionnelle et logique. De plus, Tamgu offre des bibliothèques pour utiliser cURL et surtout exécuter du code Python.

On peut ainsi mélanger un moteur de prédicats à la Prolog avec un langage impératif qui ressemble à un Python qui aurait eu une longue conversation avec TypeScript.

Il devient possible d'écrire un programme Prolog qui fait appel à des fonctions plus traditionnelles. Cependant, et c'est là la subtilité de ce mélange, il est aussi possible d'unifier des variables dans ces fonctions.

Simple exemple

Prenons l'exemple suivant:

Nous avons une fonction qui permet par exemple d'exécuter un prompt en appelant l'API REST de OLLAMA.

//Le type ?_ permet l'unification de res dans une fonction externe

function vprompt(string model, string p, ?_ res) {
    res = ollama.promptnostream(uri, model, p);
    return true;
}

Supposons le prompt suivant: Ecrit un programme Python pour décompter les nombres premiers entre 1 et 1000.

On peut alors simplement appeler notre programme de la façon suivante:

create(?Prompt,?Resultat) :-
    vprompt("mistral-large", ?Prompt, ?Generation),
    extractcode(?Generation, ?Code),
    execution(?Code, ?Resultat).

vprompt applique notre modèle sur le prompt et reçoit la réponse dans ?Generation. extractcode extrait le code depuis la réponse générée execution lance l'exécution du code Python via la bibliothèque Python de Tamgu.

On peut déjà imaginer des architectures plus complexes où différentes règles Prolog peuvent s'échanger des résultats de prompts différents.

A propos d'échange, il y a un élément très important qu'il faut absolument mentionner. La base de connaissance présenté plus haut est en fait dynamique: il est possible d'ajouter ou de retrancher des faits dans cette base.

D'ailleurs, on peut aller encore plus loin. Les classes dans Tamgu sont appelées frames. Et là, où ça devient intéressant, c'est qu'on peut comparer des frames entre elles en surchargeant les opérateurs idoines.

Voici un exemple particulièrement parlant à cet effet...

Implémentation d'un système RAG simple avec Tamgu

Ces frames permettent de créer des structures de données complexes avec des méthodes associées, que le moteur de prédicats peut utiliser de façon transparente. En effet, l'une des spécificités de ce prolog est que lorsque le moteur doit unifier des données externes, telles que des instances de classe par exemple, il s'appuie alors sur l'opérateur d'égalité pour effectuer cette opération.

Or il est possible de surcharger cette fonction égalité dans une frame pour implanter une comparaison sur mesure entre des instances de frame différentes.

Prenons l'exemple suivant, où l'on enregistre dans une instance de frame, des plongements (embeddings) pour des énoncés particuliers. On peut alors décider qu'une égalité entre deux instances consiste en une distance cosinus supérieur à une valeur donnée.

Lorsque le moteur de prédicats doit extraire avec l'opérateur findall une liste de faits en mémoire, proche d'une requête donnée, cet opérateur s'exécutera tranquillement en arrière-plan pour offrir un petit RAG sur mesure.

frame Vectordb {
    string énoncé;
    fvector embedding;

    function _initial(string u) {
        énoncé = u;
        // get_embedding retourne les embeddings de lnoncé
        // tels que calculés par le modèle "command-r".
        embedding = get_embedding("command-r", énoncé);
    }

    function string() {
        return énoncé;
    }

    function ==(Vectordb e) {
        return (cosine(embedding, e.embedding) > 0.3);
    }
}

//Nous bouclons sur tous les faits pour les insérer dans la base de connaissances
//Pour chaque fait, une simple chaîne, nous construisons un objet Vectordb
//qui calculera les embeddings pour chaque énoncé.
append([]).
append([?K | ?R]) :-
    ?X is Vectordb(?K),
    assertz(record(?X, ?K)),
    append(?R).

//findall utilisera la fonction sous-jacente `==` exposée par `Vectordb`
//L'unification d'un objet non-prolog est obtenue par `equality`.
retrieve(?Query, ?Results) :-
    ?Q is Vectordb(?Query),
    findall(?K, record(?Q, ?K), ?Results).

generate_response(?Query, ?Context, ?Response) :-
    ?Q is ?Context + ?Query,
    vprompt("llama3", ?Q, ?Response).

rag_query(?Query, ?Response) :-
    retrieve(?Query, ?RelevantDocs),
    ?Context is "Informations pertinentes: " + ?RelevantDocs,
    generate_response(?Query, ?Context, ?Response).

// Peupler la base de connaissances
// Nous utilisons une variable récipiendaire booléenne pour forcer l'exécution du prédicat
bool x = append(["La capitale de la France est Paris.",
                 "La Tour Eiffel est située à Paris.",
                 "Londres est la capitale du Royaume-Uni."]);

// Interroger le système RAG
// ?:- est une variable de prédicat qui attend une unification complète de ?Response pour réussir
?:- result = rag_query("Parlez-moi de la capitale de la France", ?Response);
println(result);

Dans cet exemple, nous avons étendu notre configuration précédente pour créer un système RAG simple :

  1. Le frame Vectordb représente nos documents intégrés.

  2. Le prédicat retrieve utilise notre recherche de similarité sémantique pour trouver des documents pertinents en fonction de la requête.

  3. generate_response utilise un LLM (dans ce cas, "llama3") pour générer une réponse basée sur la requête et le contexte récupéré.

  4. Le prédicat rag_query rassemble tout cela :

    • Il récupère d'abord les documents pertinents en utilisant notre recherche sémantique.
    • Il construit ensuite une chaîne de contexte à partir de ces documents.
    • Enfin, il génère une réponse en utilisant le LLM, augmentée du contexte récupéré.

Ce système RAG simple montre comment l'intégration des frames avec le moteur de prédicats de Tamgu peut être utilisée pour créer des systèmes d'IA sophistiqués. La capacité de recherche sémantique, activée par notre opérateur d'égalité personnalisé, permet une récupération intelligente d'informations pertinentes. Cela est combiné de manière transparente avec la capacité de Tamgu à s'interfacer avec les LLM, résultant en un système capable de générer des réponses informées basées sur une base de connaissances gérée dynamiquement.

Un tel système pourrait être facilement étendu pour inclure un raisonnement plus complexe, une récupération multi-étapes ou une mise à jour dynamique de la base de connaissances. Le cadre logique fourni par le moteur de prédicats de Tamgu permet une expression claire de ces flux de travail complexes, tandis que le système de frame permet une gestion efficace de structures de données complexes comme les plongements.

Note : Lors de l'appel d'un prédicat dans Tamgu, le type de la variable de réception affecte la manière dont le moteur d'inférence traite la requête. Si un vector est utilisé, le moteur explore tous les chemins possibles et retourne plusieurs solutions. Si un type ?:- (variable de prédicat) est utilisé, le moteur recherche une seule solution et s'arrête après avoir trouvé le premier résultat valide. Si aucune variable ne doit être unifiée, un Booléen peut être utilisé pour vérifier s'il est vrai ou faux.

(Vous pouvez retrouver ce code ici: PREDIBAG)

Intégration de Python dans Tamgu

De plus, Tamgu permet l'exécution de code Python directement dans les prédicats. Cette fonctionnalité permet l'intégration de l'écosystème et des capacités de calcul de Python dans le processus de raisonnement logique. Voici un exemple de la manière dont cela fonctionne :

//Nous chargeons notre bibliothèque d'interpréteur Python
use('pytamgu');

//Nous déclarons une variable Python `p`
python p;

function execute_code(string c, ?_ v) {
    println("Exécution du code");
    string py_err;
    try {
        // Le deuxième paramètre de `run` nécessite que
        // le résultat final soit dans `result_variable`,
        // qui est une variable Python
        v = p.run(c, "result_variable");
        return true;
    }
    catch(py_err) {
        v = py_err;
        return false;
    }
}

execution(?model, ?code, ?result) :-
    println("Exécution"),
    ?success is execute_code(?code, ?r),
    handle_execution(?success, ?model, ?code, ?r, ?result).

handle_execution(true, ?model, ?code, ?r, ?r) :- !,
    println("Exécution réussie:", ?r).

handle_execution(false, ?model, ?code, ?error, ?result) :-
    println("Exécution échouée. Tentative de correction."),
    correct_and_retry(?model, ?code, ?error, ?result).

Dans cette configuration, nous définissons une fonction execute_code qui exécute du code Python et capture soit le résultat, soit tout message d'erreur. Le prédicat execution utilise ensuite cette fonction dans le flux logique de notre agent.

Ce qui est important ici, c'est la manière dont Tamgu nous permet de spécifier un nom de variable particulier ("result_variable" dans ce cas) à partir duquel extraire le résultat de l'exécution Python. Cela signifie que nous pouvons demander à un LLM de générer du code Python qui stocke sa sortie dans une variable spécifique, puis extraire et utiliser cette valeur de manière transparente dans notre raisonnement basé sur les prédicats.

Si l'exécution échoue, notre prédicat handle_execution peut déclencher un processus de correction, demandant potentiellement au LLM de corriger le code en fonction du message d'erreur. Cela crée un système robuste et auto-correcteur qui combine les forces de la programmation logique et de l'IA moderne.

Prolog comme langage pour l'architecture des agents

Prolog, avec ses racines dans la programmation logique, offre une base robuste et expressive pour concevoir des architectures d'agents. Ses caractéristiques uniques en font un choix particulièrement bien adapté pour créer des agents intelligents capables de raisonner, planifier et interagir avec leur environnement. Voici plusieurs raisons pour lesquelles Prolog est un excellent choix pour l'architecture des agents :

Nature déclarative

  • Raisonnement basé sur la logique : La nature déclarative de Prolog permet aux développeurs de se concentrer sur la spécification de ce que l'agent doit accomplir plutôt que sur la manière de l'accomplir. Cette abstraction de haut niveau simplifie le développement de systèmes de raisonnement complexes.
  • Systèmes basés sur des règles : Les agents peuvent être conçus en utilisant un ensemble de règles qui définissent leur comportement. Ces règles peuvent être facilement modifiées ou étendues, rendant le système flexible et adaptable aux exigences changeantes.

Chainage arrière et recherche

  • Exploration des solutions : Le mécanisme de chainage arrière de Prolog permet aux agents d'explorer différents chemins de raisonnement. Cela est crucial pour la résolution de problèmes et la prise de décision, car les agents peuvent évaluer différentes options et sélectionner la plus appropriée.
  • Recherche efficace : Les capacités de recherche intégrées de Prolog permettent aux agents de naviguer efficacement dans de grandes bases de connaissances, trouvant des informations pertinentes et tirant des conclusions basées sur les données disponibles.

Représentation des connaissances

  • Bases de connaissances dynamiques : La capacité de Prolog à mettre à jour et à interroger dynamiquement les bases de connaissances en fait un choix idéal pour les agents qui doivent maintenir et manipuler des informations complexes et évolutives. Cela est particulièrement utile dans les domaines où l'environnement change constamment.
  • Raisonnement symbolique : Prolog excelle dans le raisonnement symbolique, permettant aux agents de représenter et de manipuler des concepts et des relations abstraits. Cela est essentiel pour les tâches qui nécessitent une compréhension et un raisonnement sur le monde.

Intégration avec les techniques d'IA modernes

  • Systèmes hybrides : Prolog peut être intégré avec des techniques d'IA modernes, telles que l'apprentissage automatique et les grands modèles de langage (LLMs). Cette combinaison permet aux agents de tirer parti à la fois du raisonnement symbolique et des approches axées sur les données, créant des systèmes plus puissants et polyvalents.
  • Interfaçage avec des systèmes externes : Prolog peut s'interfacer avec des systèmes externes et des bibliothèques, permettant aux agents d'interagir avec une large gamme d'outils et de technologies. Cela inclut l'exécution de code Python, l'interrogation de bases de données et la communication avec d'autres agents.

Transparence et explicabilité

  • IA explicable : La structure logique de Prolog rend plus facile la compréhension et l'explication du processus de raisonnement des agents. Cela est crucial pour construire la confiance dans les systèmes d'IA, en particulier dans les domaines où la transparence et la responsabilité sont importantes.
  • Débogage et vérification : La nature déclarative et les fondements logiques de Prolog rendent plus facile le débogage et la vérification de la correction des comportements des agents. Cela est essentiel pour garantir la fiabilité et la robustesse des systèmes d'IA.

Collaboration multi-agents

  • Bases de connaissances partagées : La capacité de Prolog à maintenir et à interroger des bases de connaissances partagées fournit un cadre pour la collaboration multi-agents. Les agents peuvent échanger des informations, mettre à jour des connaissances partagées et coordonner leurs actions pour atteindre des objectifs communs.
  • Protocoles de communication : Prolog peut être utilisé pour définir des protocoles de communication et des modèles d'interaction entre agents. Cela permet le développement de systèmes d'agents complexes et coopératifs capables de travailler ensemble pour résoudre des problèmes.

Conclusion

Les architectures d'agents basées sur Prolog offrent un mélange unique de raisonnement logique, de programmation déclarative et de représentation dynamique des connaissances. En comparaison avec les implémentations actuelles, les agents basés sur Tamgu offrent un cadre robuste et expressif pour construire des systèmes d'IA intelligents et adaptables. Ils sont particulièrement précieux dans les domaines où l'explicabilité, la représentation dynamique des connaissances et la collaboration multi-agents sont importantes. À mesure que l'IA continue d'évoluer, la revitalisation de Prolog, via des approches originales comme Tamgu, offre des possibilités passionnantes pour l'avenir des systèmes basés sur des agents.

L'héritage de Prolog, loin d'être confiné à l'histoire, pourrait trouver une nouvelle vie à l'ère des grands modèles de langage et des systèmes d'IA hybrides.

Clone this wiki locally