-
Notifications
You must be signed in to change notification settings - Fork 12
6.1.4 PREDIBAG: Une Approche Basée sur les Prédicats pour la Conception d'Agents
Les grands modèles de langue (LLM) sont indéniablement puissants, mais ils font souvent preuve d'une inconstance qui pose un défi majeur pour les applications traditionnelles de workflow. La plupart des solutions d'agents reposent sur l'utilisation de tests imbriqués pour gérer ces incohérences, ce qui conduit souvent à des systèmes complexes et fragiles. Avec PREDIBAG (Agent Basé sur les Prédicats), nous proposons de revisiter la programmation logique pour dompter les LLM, en plaçant leurs appels sous un contrôle précis et gérable.
Le langage de programmation logique le plus connu est évidemment Prolog, mais celui-ci impose une courbe d'apprentissage particulièrement abrupte qui risque de rebuter même le programmeur le plus chevronné. Nous avons développé notre propre version de ce langage par le passé, ce qui nous a donné une expérience très riche, non seulement sur la meilleure façon de créer un tel interpréteur (voir tamgu), mais aussi sur les stratégies optimales pour l'utiliser efficacement.
Fort de cette expérience, nous avons décidé de créer une forme simplifiée de règles logiques pour offrir le cadre minimal nécessaire à la gestion de nos agents.
Ces fonctions introduites par le mot clef defpred
font partie intégrante du langage LispE : defpred combine ainsi la programmation par motifs et le chaînage arrière pour orchestrer les interactions avec les LLM aussi naturellement que n'importe quel autre composant, offrant une alternative robuste à la conception traditionnelle d'agents. Ces fonctions de prédicat reprennent l'idée sous-jacente de Prolog, mais restreignent l'unification à leurs seuls arguments. De plus, l'exécution est déterministe : contrairement au mécanisme sous-jacent de Prolog où l'on explore systématiquement le graphe complet défini par les règles, on s'arrête ici dès qu'une règle a satisfait ses contraintes.
PREDIBAG est une méthodologie générale pour construire des agents qui explorent systématiquement plusieurs stratégies. À titre d'exemple, considérons un agent s'attaquant à des problèmes mathématiques :
- La fonction qui suit sert à exécuter du code python :
(defun execute(code)
; Exécute le code Python dans un environnement isolé avec un délai d'expiration de 2 secondes
(python_reset py) ; Réinitialise l'interpréteur Python à un état propre
(python_run py (+ pythonpaths code) "result" 2) ; Exécute le code, stockant la sortie dans 'result'
)
Et celle-ci à extraire le code python généré par le LLM:
(defun extraction(s)
; Extrait le code Python d'une chaîne, supposé être entre les marqueurs ```python et ```
(@@ s "```python" "```") ; Retourne la sous-chaîne entre ces délimiteurs
)
Voici comment les fonctions logiques sont écrites :
-
generate
: cette fonction propose deux façons différentes pour générer et exécuter un code Python- Si le code ne s'exécute pas correctement, on dispose d'une fonction finale de repli.
; Première tentative pour générer du code Python avec une invite directe
(defpred generate(chat hint pb)
; Affiche une étiquette pour suivre cette stratégie (par ex., "Python 1: problème123")
(println "Python 1:" (@ pb "unique_id"))
; Demande au LLM de générer du code Python, en s'assurant que 'result' contient la réponse
(setq chat
(tchat chat (+ "Générez le code Python correspondant à ce problème. Le résultat doit être stocké dans la variable : 'result'." hint))
)
; Extrait et nettoie le code de la réponse du LLM (contenu du dernier message)
; Si aucun code Python n'est produit, l'extraction échoue et passe à la fonction "generate" suivante
(setqv code (extraction . clean . @ chat -1 "content"))
; Exécute le code ; en cas d'échec (par ex., délai dépassé), le retour en arrière est déclenché
(setqv r (execute code))
; Construit un dictionnaire de résultats avec l'ID du problème, l'historique du chat, la réponse et la valeur attendue
(dictionary "unique_id" (@ pb "unique_id") "chat" chat "answer" r "expected" (@ pb "answer"))
)
; Deuxième tentative avec une invite légèrement différente pour plus de variété
(defpred generate(chat hint pb)
; Étiquette cette stratégie (par ex., "Python 2: problème123")
(println "Python 2:" (@ pb "unique_id"))
; Demande du code Python avec une invite reformulée, toujours ciblant 'result'
(setq chat
(tchat chat (+ "Écrivez le code Python adéquat pour résoudre ce problème. Le résultat doit être stocké dans la variable : 'result'." hint))
)
; Extrait et nettoie la réponse en code du LLM
; Si aucun code Python n'est produit, l'extraction échouera
(setqv code (extraction . clean . @ chat -1 "content"))
; Exécute le code ; un échec (par ex., erreur de syntaxe) déclenche le retour en arrière
(setqv r (execute code))
; Retourne un dictionnaire avec les résultats, même structure que ci-dessus
(dictionary "unique_id" (@ pb "unique_id") "chat" chat "answer" r "expected" (@ pb "answer"))
)
; Règle de secours si les tentatives précédentes échouent
; Cette implémentation ne peut pas échouer, garantissant un résultat
(defpred generate(chat hint pb)
; Marque ceci comme dernier recours (par ex., "Échec Python : problème123")
(println "Échec Python :" (@ pb "unique_id"))
; Ignore la génération/exécution de code, en enregistrant "FAIL" comme réponse
(dictionary "unique_id" (@ pb "unique_id") "chat" chat "answer" "FAIL" "expected" (@ pb "answer"))
)
-
agent
: cette fonction exécute un premier prompt et appellegenerate
- Nous pourrions avoir des définitions supplémentaires avec d'autres prompts au besoin
; Prédicat d'aide pour essayer une invite et générer du code
(defpred try_prompt(chat système hint pb prompt)
; Envoie l'invite au LLM, mettant à jour l'historique du chat
(setq chat (tchat chat prompt système))
; Appelle 'generate' pour traiter la réponse du LLM et stocker le résultat
(setqv res (generate chat hint pb))
; Ajoute le résultat à la liste globale 'tous' pour une sauvegarde ultérieure
(push tous res)
)
; Règle d'agent unique pour lancer le processus avec une invite descriptive
(defpred agent(chat système hint pb)
; Indique quel problème est traité (par ex., "Premier : problème123")
(println "Premier :" (@ pb "unique_id"))
; Utilise 'try_prompt' avec une invite de solution détaillée, passant le contexte du problème
(try_prompt chat système hint pb (+ "Décrivez en détail une solution au problème suivant :" (@ pb "problem") "\n"))
)
; Nous pourrions avoir d'autres agents qui s'activeraient si celle-ci échoue...
Ici, agent
explore des stratégies pour résoudre des problèmes mathématiques à partir de MATH.json
, produisant des sorties (par ex., MATHS_result.json
) qui comprennent les réponses mais aussi l'historique complet du chat. PREDIBAG ne se limite pas uniquement aux mathématiques — c'est un modèle applicable à toute tâche nécessitant une exploration structurée, de l'analyse de texte à la prise de décision.
La force de PREDIBAG réside dans defpred
, le mécanisme de prédicat de LispE. Il permet de réunir plusieurs fonctions sous un même nom. Lorsqu'une fonction est exécutée, chaque instruction est évaluée comme une expression booléenne. Si une instruction renvoie false
, la fonction échoue et le moteur essaie alors la fonction suivante. De cette façon, on évite les imbrications inextricables de if/else
si communes dans les autres approches. Les règles sont à la fois plus claires, plus lisibles, et l'ajout d'une nouvelle règle se fait sans modifier la logique interne d'un nœud, contrairement à LangGraph par exemple. De plus, les variables dans les fonctions sont unifiées à l'exécution, ce qui signifie que lorsque l'on va essayer la règle suivante, celle-ci repartira avec les mêmes données que la règle précédente. On peut donc explorer divers chemins en parallèle.
Une des caractéristiques marquantes de PREDIBAG est sa capacité à gérer l'incohérence inhérente des grands modèles de langage (LLM) en les reléguant à un rôle de soutien grâce à la génération de code Python. Plutôt que de compter sur le LLM pour fournir directement des réponses, connaissant leur propension à halluciner, nous préférons lui demander de générer un code python que l'on pourra valider à l'exécution. De cette façon, le juge final est l'interpréteur Python lui-même. Si le code présente le moindre problème — syntaxe défaillante, exécution trop longue (on peut ajouter un délai d'expiration à l'exécution du code) ou résultat incompatible avec la valeur attendue — il suffit de passer à la fonction suivante pour tester avec une nouvelle invite et générer une nouvelle version du code. Ce choix de conception ramène le LLM à un simple rôle de composant au même titre que l'interpréteur Python. De cette façon, on maintient le LLM dans un rôle, certes fondamental de générateur de code, mais sous contrôle, car seule l'exécution finale d'un code peut nous garantir un fonctionnement presque cohérent dans un workflow.
Comparons PREDIBAG avec LangGraph ou Smolagents :
-
Lisibilité :
-
PREDIBAG : L'exploration de graphe grâce aux fonctions defpred clarifie l'intention, évitant l'encombrement des
if
imbriqués tout en s'abstrayant de la complexité du LLM. -
LangGraph : Les nœuds et arêtes du graphe visualisent les flux de travail, mais le routage implique souvent des
if/else
imbriqués (par ex.,if state["mood"] == "positive"
), exposant davantage la logique du LLM. -
Smolagents : L'exécution plate (par ex.,
agent.run("solve x + 2")
) est simple, mais le code généré par le LLM peut inclure de nombreux tests pour maintenir la logique de l'exécution.
-
PREDIBAG : L'exploration de graphe grâce aux fonctions defpred clarifie l'intention, évitant l'encombrement des
-
Maintenabilité :
-
PREDIBAG : Les nouvelles règles
defpred
ajoutent des nœuds sans effort, avec une logique partagée (par ex.,try_prompt
) centralisée. Les ajustements du LLM sont basés sur les règles, non dépendants des invites. - LangGraph : Les nœuds modulaires sont ajustables, mais les changements impliquent des mises à jour des arêtes ou du routage imbriqué, avec une gestion du LLM plus explicite.
-
Smolagents : Les ajustements légers sont faciles, mais les changements de comportement du LLM reposent sur l'ingénierie des invites, moins structurée que le graphe de
defpred
.
-
PREDIBAG : Les nouvelles règles
-
Génération de données (par ex., exemple mathématique) :
-
PREDIBAG : Le chaînage arrière explore tous les nœuds, produisant des sorties diverses (par ex.,
chat
,answer
), gérant l'incohérence du LLM. - LangGraph : Les flux de travail structurés produisent des données détaillées, mais la diversité nécessite des nœuds explicites, et la variabilité du LLM est moins abstraite.
- Smolagents : Les sorties de code concises sont axées sur les solutions, mais les exécutions uniques limitent la variété, avec une incohérence du LLM plus exposée.
-
PREDIBAG : Le chaînage arrière explore tous les nœuds, produisant des sorties diverses (par ex.,
-
Évolutivité :
- PREDIBAG : Séquentiel par défaut, mais LispE est aussi multitâche, ce qui permet une exploration en parallèle au besoin.
- LangGraph : Le parallélisme natif du graphe excelle pour les tâches complexes.
- Smolagents : L'exécution légère évolue bien pour les tâches uniques.
L'exploration de graphe pilotée par defpred
de PREDIBAG et son abstraction du LLM offrent une lisibilité et une polyvalence supérieures, minimisant l'incohérence du LLM par rapport aux flux explicites de LangGraph et à la simplicité centrée sur le LLM de Smolagents.
LispE offre par défaut une programmation multitâche, ce qui permet de lancer des agents en parallèle au besoin (dethread
, wait
) :
(dethread solve_task(task nb)
(setq hint (@ category_instructions (@ task "category")))
(agent {} `Vous avez des compétences remarquables dans ce domaine.` hint task)
(threadstore "results" (json tous)))
(loop task tasks (solve_task task (incr nb)))
(wait)
On peut donc paralléliser facilement le travail des agents via des tâches particulières, avec un mécanisme très simple pour collecter les données à travers l'ensemble des threads de façon protégée dans threadstore
. Ainsi, on peut facilement implémenter des agents largement équivalents en termes de puissance et d'expressivité à LangGraph ou à Smolagents.
PREDIBAG réinvente la conception d'agents en fusionnant la logique des prédicats avec l'exploration de graphe, utilisant defpred
pour dompter l'incohérence des LLM et la repousser en arrière-plan. Ses règles plates et son retour en arrière offrent une alternative plus propre et maintenable aux conditionnels imbriqués, tandis que la flexibilité de tchat
assure une indépendance des outils. Le threading ajoute de l'évolutivité, et sa capacité à traiter les LLM comme un simple composant le rend adaptable à des tâches comme la génération de données ou au-delà. Par rapport aux graphes explicites de LangGraph et à la simplicité légère de Smolagents, PREDIBAG propose une approche unique, robuste et élégante — un paradigme basé sur les prédicats à explorer pour les praticiens de l'IA et les amateurs de Lisp.