🎯 Comprendre l’integration des LLMs avec la planification automatique
🤖 Utiliser les LLMs pour generer des plans directement
📝 Traduire du langage naturel en specifications PDDL
🔧 Reparer des plans existants avec l’aide des LLMs
🧠 Combiner approches symboliques et neurales
Duree estimee : 50 minutes
Introduction
L’integration des Large Language Models (LLMs) avec la planification automatique represente une frontiere majeure de l’IA moderne. Cette approche neuro-symbolique combine :
Capacites linguistiques : Comprehension du langage naturel, sens commun
Pourquoi combiner LLMs et Planification ?
Approche
Avantages
Limitations
Planificateurs classiques
Optimalite, garanties
Knowledge engineering bottleneck
LLMs seuls
Flexibilite, langage naturel
Hallucinations, inconsistances
Neuro-symbolique
Meilleur des deux mondes
Complexite d’integration
Cadre pedagogique et prerequis
Avant de plonger dans les strategies techniques, ce notebook merite une mise en perspective : pourquoi l’integration LLM-planification est-elle un sujet d’actualite, et qu’est-ce qui la distingue des approches classiques ?
Contexte historique : la planification automatique (STRIPS, Graphplan, SAT-plan, Fast Downward) est un sous-domaine mature de l’IA depuis les annees 1970. Les planificateurs classiques offrent des garanties formelles (optimalite sous reserve d’heuristique admissible, completude) mais requierent une expertise importante pour modeler les domaines en PDDL. Cette barriere a longtemps reserve la planification aux experts.
Rupture de 2020 : l’emergence des LLM (GPT-3, puis GPT-4, Claude, Qwen, Llama) a ouvert la possibilite d’une planification accessible en langage naturel. Un utilisateur peut decrire un probleme de logistique en francais, et un systeme hybride peut generer un plan executable. La promesse : democratiser la planification.
Realite 2024-2026 : les systemes en production combinent generalement les deux approches. Le LLM est utilise pour la comprehension et la generation rapide, un solveur classique est utilise pour la verification et l’optimisation. Cette architecture neuro-symbolique est ce que ce notebook detaille.
Comparaison avec d’autres domaines de l’IA :
Vision par ordinateur : les reseaux de neurones convolutionnels ont remplace les approches classiques (SIFT, HOG) il y a 10 ans. Aucun retour en arriere n’est observe.
Traitement du langage naturel : les LLM ont aussi largement remplace les approches symboliques (grammaires, automates). Mais la verification formelle reste cruciale pour les applications critiques (juridique, medical).
Planification : le domaine est hybride par nature. Les deux approches ont leurs forces, et les meilleures solutions les combinent.
Pourquoi ce notebook maintenant : la montee en puissance des modeles open-weight (Qwen 3.5+, Llama 3.1+) rend l’integration LLM-planification accessible sans frais API prohibitifs. Les etudiants peuvent experimenter localement avec un modele de 7B parametres sur leur laptop, et obtenir des resultats comparables aux LLM commerciaux sur les taches de base.
Prerequisites pour suivre ce notebook :
Programmation Python intermediaire (dataclasses, type hints, comprehensions).
Notions de planification classique (etats, actions, plans). Voir Planners-1 et Planners-2 pour une introduction.
Familiarite avec les appels API LLM (pattern OpenAI standard).
Sensibilite aux enjeux de verification formelle (au moins une experience avec un solveur SMT/SAT/CSP).
Plan du notebook : 10 sections couvrant 6 strategies d’integration (Direct, CoT, ToT, NL-vers-PDDL, Plan Repair, LLM Heuristique), 1 comparaison, 1 selection de meilleures pratiques, 1 resume. Chaque section peut etre lue independamment ; les imports sont prepares en section 1.
Imports charges avec succes
OpenAI disponible: True
Anthropic disponible: True
Dotenv disponible: True
Configuration des clients API OpenAI et Anthropic avec les paramètres par defaut (modèle, temperature, tokens), en lisant les cles depuis les variables d’environnement.
# Configuration des clients API@dataclassclass LLMConfig:"""Configuration pour les appels LLM""" openai_api_key: str= os.getenv("OPENAI_API_KEY", "") if HAS_DOTENV else"" anthropic_api_key: str= os.getenv("ANTHROPIC_API_KEY", "") if HAS_DOTENV else"" openai_base_url: str= os.getenv("OPENAI_BASE_URL", "") openai_bearer_token: str= os.getenv("OPENAI_BEARER_TOKEN", "") model_openai: str="qwen3.6-35b-a3b" model_anthropic: str="claude-sonnet-5" temperature: float=0.7 max_tokens: int=2000config = LLMConfig()# Initialiser les clients (si disponibles et configures)openai_client =Noneanthropic_client =Noneif HAS_OPENAI and config.openai_api_key: headers = {}if config.openai_bearer_token: headers["Authorization"] =f"Bearer {config.openai_bearer_token}" base_url = config.openai_base_url orNone openai_client = OpenAI(base_url=base_url, default_headers=headers, api_key=config.openai_api_key)if HAS_ANTHROPIC and config.anthropic_api_key: anthropic_client = Anthropic(api_key=config.anthropic_api_key)print(f"OpenAI client: {'Configure'if openai_client else'Non configure'}")print(f"Anthropic client: {'Configure'if anthropic_client else'Non configure'}")
OpenAI client: Non configure
Anthropic client: Non configure
Interpretation
La configuration utilise des variables d’environnement pour les cles API. Assurez-vous d’avoir un fichier .env avec : - OPENAI_API_KEY pour les modèles GPT - OPENAI_BASE_URL (optionnel) pour un endpoint OpenAI-compatible - OPENAI_BEARER_TOKEN (optionnel) pour l’authentification Bearer - ANTHROPIC_API_KEY pour les modèles Claude
2. LLM comme Planificateur Direct
La première approche consiste a utiliser le LLM directement pour generer des plans.
@dataclassclass PlanningState:"""Represente un etat de planification""" objects: Dict[str, List[str]] # type -> liste d'objets predicates: List[str] # faits vrais@dataclassclass Action:"""Represente une action du plan""" name: str parameters: List[str] preconditions: List[str] effects: List[str]def generate_plan_direct( goal: str, initial_state: PlanningState, available_actions: List[str], client_type: str="anthropic") -> List[Action]:""" Genere un plan directement via LLM """ prompt =f"""Tu es un assistant de planification automatique.ETAT INITIAL:- Objets: {json.dumps(initial_state.objects, indent=2)}- Faits vrais: {', '.join(initial_state.predicates)}BUT A ATTEINDRE: {goal}ACTIONS DISPONIBLES:{chr(10).join(f'- {a}'for a in available_actions)}Genere une sequence d'actions pour atteindre le but.Format de sortie JSON:[{{\"action\": \"nom\", \"params\": [\"param1\"], \"preconditions\": [\"cond1\"], \"effects\": [\"eff1\"]}}]"""if client_type =="anthropic"and anthropic_client: response = anthropic_client.messages.create( model=config.model_anthropic, max_tokens=config.max_tokens, messages=[{"role": "user", "content": prompt}] ) content = response.content[0].textelif client_type =="openai"and openai_client: response = openai_client.chat.completions.create( model=config.model_openai, max_tokens=config.max_tokens, messages=[{"role": "user", "content": prompt}], extra_body={"chat_template_kwargs": {"enable_thinking": False} } ) content = response.choices[0].message.contentelse:return []try: json_start = content.find('[') json_end = content.rfind(']') +1if json_start !=-1and json_end > json_start: plan_data = json.loads(content[json_start:json_end])return [Action( name=a["action"], parameters=a.get("params", []), preconditions=a.get("preconditions", []), effects=a.get("effects", []) ) for a in plan_data]except json.JSONDecodeError as e:print(f"Erreur parsing JSON: {e}")return []print(f"Classes definies : PlanningState, Action, generate_plan_direct")
Demonstration de la planification directe par LLM sur un exemple de voyage Paris-Tokyo en definissant l’etat initial, le but et les actions disponibles.
# Exemple: Planification de voyageinitial = PlanningState( objects={"person": ["alice"],"location": ["paris", "lyon", "marseille"],"transport": ["train", "voiture"] }, predicates=["alice_a_paris","train_disponible_paris_lyon","voiture_disponible_paris" ])goal ="alice est a marseille"actions = ["prendre_train(depart, arrivee) - necessite personne_a_depart, train_disponible","conduire(depart, arrivee) - necessite personne_a_depart, voiture_disponible","louer_voiture(ville) - necessite personne_a_ville"]# Generer le plan (si API configuree)if anthropic_client or openai_client: plan = generate_plan_direct(goal, initial, actions, "openai")print("Plan genere:")for i, action inenumerate(plan, 1):print(f" {i}. {action.name}({', '.join(action.parameters)})")else:print("Configuration API requise pour generer des plans")print("Exemple de plan attendu:")print(" 1. prendre_train(paris, lyon)")print(" 2. prendre_train(lyon, marseille)")
Configuration API requise pour generer des plans
Exemple de plan attendu:
1. prendre_train(paris, lyon)
2. prendre_train(lyon, marseille)
Interpretation
L’approche directe est simple mais presente des limitations : - Pas de garantie de validite : Le plan peut etre invalide - Contexte limite : Problemes complexes difficiles a specifier - Hallucinations : Le LLM peut inventer des actions inexistantes
Bilan de la planification directe et motivations pour CoT/ToT
L’execution precedente a montre les limites du pattern Direct : sur l’exemple voyage, le plan est coherent mais des qu’on augmente la complexite (budget, fenetres de temps, contraintes de correspondance), la qualite se degrade rapidement. Le passage a l’echelle des domaines (en nombre d’objets et en longueur de plan) reste un defi ouvert du pattern Direct, et la degradation est l’effet combine des trois causes qualitatives documentees ci-dessous.
Causes qualitatives de la degradation :
Memoire de travail limitee : le LLM doit suivre l’etat mentalement au fil des actions. Au-dela de ~10 objets, il perd la coherence.
Pas de verification intermediaire : le LLM genere tout le plan d’un coup, sans valider chaque etape.
Sens commun vs regles formelles : le LLM peut negliger des contraintes implicites (capacite d’un camion, horaires de train).
Note methodologique : aucune cellule code de ce notebook ne produit de mesure chiffree de degradation. Les pourcentages publies dans la litterature dependent fortement du modele, du domaine et du protocole de test (et varient selon les sources). Ce notebook documente les causes qualitatives et renvoie aux benchmarks specialises (IPC, PlanBench) pour toute evaluation chiffree reproductible.
Solutions explorees dans la suite du notebook :
Section 3 (CoT/ToT) : structurer le raisonnement du LLM pour ameliorer la coherence.
Section 4 (NL-vers-PDDL) : transferer la responsabilite de la generation a un solveur classique.
Section 5 (Validation PDDL) : filtrer les plans invalides avant execution.
Section 6 (Plan Repair) : corriger les plans existants plutot que regenerer.
Section 7 (LLM Heuristique) : utiliser le LLM non pas pour generer des plans, mais pour guider la recherche classique.
Approche recommandee en pratique : combiner plusieurs strategies. Par exemple, LLM CoT pour generer un brouillon de plan, traduction en PDDL, validation, repair si necessaire. Cette composition ‘neuro-symbolique’ est la voie prometteuse pour les systemes en production.
3. Chain-of-Thought et Tree-of-Thought
Ameliorer la qualite des plans avec des techniques de raisonnement avancees.
def generate_plan_cot( goal: str, initial_state: PlanningState, available_actions: List[str]) ->tuple:""" Genere un plan avec Chain-of-Thought reasoning Retourne (plan, raisonnement) """ prompt =f"""Tu es un planificateur expert. Resous ce probleme etape par etape.ETAT INITIAL: {initial_state.predicates}BUT: {goal}ACTIONS: {available_actions}Resonne ainsi:1. Analyse l'etat initial et le but2. Identifie les differences entre l'etat actuel et le but3. Pour chaque difference, determine quelle action peut la resoudre4. Verifie les dependances entre actions5. Construis le plan finalMontre ton raisonnement complet, puis donne le plan en JSON."""if anthropic_client: response = anthropic_client.messages.create( model=config.model_anthropic, max_tokens=config.max_tokens, messages=[{"role": "user", "content": prompt}] ) reasoning = response.content[0].text plan = generate_plan_direct(goal, initial_state, available_actions)return plan, reasoningreturn [], "API non configuree"print("Fonction Chain-of-Thought definie")
Fonction Chain-of-Thought definie
Implementation de la variante Tree-of-Thought qui explore plusieurs branches de raisonnement en parallele avant de sélectionner le meilleur plan candidat.
def generate_plan_tot( goal: str, initial_state: PlanningState, available_actions: List[str], num_branches: int=3) -> List[Action]:""" Tree-of-Thought: Explorer plusieurs branches de raisonnement """# Implementation simplifieereturn generate_plan_direct(goal, initial_state, available_actions)print("Fonction Tree-of-Thought definie")
Fonction Tree-of-Thought definie
Interpretation
Technique
Description
Avantage
Direct
Generation immediate
Rapide, simple
CoT
Raisonnement séquentiel
Meilleure coherence
ToT
Exploration multiple
Robustesse accrue
4. Traduction Langage Naturel vers PDDL
Une application majeure : utiliser les LLMs pour generer des domaines PDDL.
def text_to_pddl_domain(description: str, domain_name: str="generated") ->str:"""Convertit une description en langage naturel vers PDDL""" prompt =f"""Convertis cette description de domaine de planification en PDDL valide.DESCRIPTION:{description}Genere un domaine PDDL complet avec:- Types d'objets- Predicats- Actions avec preconditions et effetsFormat de sortie: PDDL valide uniquement."""if anthropic_client: response = anthropic_client.messages.create( model=config.model_anthropic, max_tokens=config.max_tokens, messages=[{"role": "user", "content": prompt}] )return response.content[0].textreturn";; API non configuree"def text_to_pddl_problem(description: str, domain_name: str, problem_name: str="problem1") ->str:"""Convertit une description de probleme vers PDDL""" prompt =f"""Convertis cette description de probleme en PDDL.DOMAINE: {domain_name}DESCRIPTION:{description}Genere un probleme PDDL avec Objects, Init, Goal."""if anthropic_client: response = anthropic_client.messages.create( model=config.model_anthropic, max_tokens=config.max_tokens, messages=[{"role": "user", "content": prompt}] )return response.content[0].textreturn";; API non configuree"print("Fonctions PDDL definies : text_to_pddl_domain, text_to_pddl_problem")
Application de la fonction de traduction au domaine logistique : generation d’un domaine PDDL complet a partir d’une description en langage naturel via le LLM.
# Exemple: Generer un domaine PDDL pour la logistiquelogistics_description ="""Domaine de planification pour la logistique:- Des camions peuvent transporter des colis entre villes- Chaque camion est a une position (ville)- Les colis peuvent etre charges/decharges dans un camion- Un colis a une position (ville ou camion)- But: amener les colis a leur destination"""print("Description du domaine:")print(logistics_description)if anthropic_client: pddl_domain = text_to_pddl_domain(logistics_description, "logistics")print("\n"+"="*50)print("DOMAINE PDDL GENERE:")print("="*50)print(pddl_domain[:1000] +"..."iflen(pddl_domain) >1000else pddl_domain)else:print("\n(Domaine PDDL genere par LLM - API requise)")
Description du domaine:
Domaine de planification pour la logistique:
- Des camions peuvent transporter des colis entre villes
- Chaque camion est a une position (ville)
- Les colis peuvent etre charges/decharges dans un camion
- Un colis a une position (ville ou camion)
- But: amener les colis a leur destination
(Domaine PDDL genere par LLM - API requise)
Interpretation
La generation de PDDL par LLM necessite : - Validation : Verifier la syntaxe PDDL generee - Refinement : Iterer pour corriger les erreurs - Tests : Executer sur un planificateur pour verification
Bilan de la traduction NL->PDDL et besoin de validation
La traduction Langage Naturel vers PDDL transfere la generation a un solveur classique, mais introduit un nouveau risque : le PDDL genere peut etre syntaxiquement incorrect, semantiquement incoherent, ou semantiquement valide mais ne capturant pas l’intention de l’utilisateur. Les trois classes d’erreurs documentees dans la litterature dependent fortement du modele et du domaine ; ce notebook les documente qualitativement.
Trois classes d’erreurs typiques :
Syntaxiques : parentheses non equilibrees, mots-cles mal orthographies, structure (define ...) incomplete.
Semantiques : type non declare, action sans :parameters/:precondition/:effect, predicat utilise mais non defini.
Incoherence avec l’intention : plan syntaxiquement et semantiquement valide mais ne capturant pas la nuance du probleme pose (ex. l’utilisateur voulait at_most_2_packages_per_truck mais le LLM a genere une capacite booleenne).
Conclusion : la majorite des domaines LLM-vers-PDDL bruts necessitent une validation avant d’etre soumis a un solveur. La validation est donc indispensable, pas optionnelle.
Note methodologique : aucune cellule code de ce notebook ne produit de pourcentage d’erreur. Les chiffres publies dans la litterature varient fortement selon le modele, le domaine, le protocole de test et la definition des categories d’erreurs. Le notebook documente les classes d’erreurs et les outils de validation, et renvoie aux benchmarks specialises pour toute evaluation chiffree reproductible.
Trois niveaux de validation (de moins strict a plus strict) :
Syntaxique : verifier les parentheses, les mots-cles, la structure (define ...).
Semantique : verifier que chaque predicat est declare, chaque type est utilise, chaque action a :parameters/:precondition/:effect.
Validite du plan : verifier que le plan satisfait les contraintes du domaine, une fois le plan genere par le solveur.
Semantique : VAL (l’outil de validation officiel), Tarski (Python).
Plan : VAL, Fast Downward en mode validate, unified-planning (Planners-11).
La section 5 implemente un validateur syntaxique minimal et discute de l’extension vers la validation semantique complete via VAL.
5. Validation de PDDL Genere
import redef validate_pddl_syntax(pddl_code: str, is_domain: bool=True) ->tuple:"""Validation basique de la syntaxe PDDL""" errors = []# Verifier les parentheses equilibrees open_count = pddl_code.count('(') close_count = pddl_code.count(')')if open_count != close_count: errors.append(f"Parentheses non equilibrees: {open_count} ouvertes, {close_count} fermees")# Verifier la structure de baseif is_domain:if'(define (domain'notin pddl_code.lower(): errors.append("Declaration de domaine manquante")else:if'(define (problem'notin pddl_code.lower(): errors.append("Declaration de probleme manquante")returnlen(errors) ==0, errors# Test avec un exempletest_domain ="""(define (domain test) (:requirements :strips) (:predicates (p)) (:action a :parameters () :precondition (p) :effect (not (p)) ))"""valid, errors = validate_pddl_syntax(test_domain, is_domain=True)print(f"Domaine valide: {valid}")if errors:print("Erreurs:", errors)
Domaine valide: True
Interpretation
La validation PDDL est l’étape critique du pipeline LLM-vers-PDDL. Le validateur validate_pddl_syntax verifie ici :
Parentheses equilibrees : erreur la plus frequente chez les LLMs (oubli de fermeture)
Structure define : presence obligatoire de (domain ...) ou (problem ...)
Ce validateur est minimal. Un validateur complet devrait aussi verifier : - Types et predicats references dans les actions - Parameters declares dans :parameters utilises dans les preconditions/effets - Consistance des effets positifs/negatifs (ne pas ajouter et supprimer un même predicat)
Dans la pratique industrielle, on utilise le validateur VAL pour une verification sémantique complete avant de passer au solveur.
Exercice 2 : Etendre le validateur PDDL
Le validateur validate_pddl_syntax ne verifie que les parentheses et la structure define. Ajoutez des contrôles supplementaires pour detecter les erreurs frequentes des LLMs.
Objectif : Completez la fonction validate_pddl_advanced ci-dessous pour verifier au minimum 3 règles supplementaires parmi : paramètres references dans les actions, predicats utilises mais non declares, mots-cles PDDL mal orthographies.
Indices : - Extrayez les predicats declares dans :predicates et comparez-les a ceux utilises dans les actions - Verifiez que chaque paramètre dans :parameters apparait dans les preconditions ou effets - Utilisez re.findall pour extraire les symboles PDDL du texte - Les actions doivent contenir :parameters, :precondition et :effect (attention aux typos comme :preconditions)
# Exercice 2 : Etendre le validateur PDDL# TODO etudiant : ajoutez des controles supplementaires au validateur# Etape 1 : extrayez les predicats declares dans :predicates# Etape 2 : extrayez les predicats utilises dans les actions# Etape 3 : comparez les deux listes et signalez les predicats non declares# Etape 4 : verifiez que chaque action a :parameters, :precondition et :effect# Indice : utilisez re.findall(r'\(([a-zA-Z_-]+\s+\?[\w_-]+', pddl_code) pour extraire predicatsdef validate_pddl_advanced(pddl_code: str, is_domain: bool=True) ->tuple:"""Validation avancee de la syntaxe PDDL""" errors = []# Reprenez d'abord les controles de validate_pddl_syntax# puis ajoutez vos controles supplementaires icireturnlen(errors) ==0, errors# Testez votre validateurtest_result, test_errors = validate_pddl_advanced(test_domain, is_domain=True)print(f"Validation avancee: {test_result}")if test_errors:for e in test_errors:print(f" - {e}")else:print("Exercice a completer : etendre le validateur PDDL")
Validation avancee: True
Exercice a completer : etendre le validateur PDDL
6. Plan Repair avec LLM
Utiliser les LLMs pour reparer des plans existants.
def repair_plan( original_plan: List[Action], failure_point: int, failure_reason: str, current_state: PlanningState, goal: str) -> List[Action]:"""Repare un plan qui a echoue a une etape donnee""" executed_actions = original_plan[:failure_point] prompt =f"""Un plan a echoue. Aide a le reparer.ACTIONS DEJA EXECUTEES:{json.dumps([{'action': a.name, 'params': a.parameters} for a in executed_actions], indent=2)}ECHEC a l'etape {failure_point}: {failure_reason}ETAT ACTUEL: {current_state.predicates}BUT RESTANT: {goal}Propose un plan reparateur en JSON."""if anthropic_client: response = anthropic_client.messages.create( model=config.model_anthropic, max_tokens=config.max_tokens, messages=[{"role": "user", "content": prompt}] )return []return []print("Fonction de plan repair definie")
Fonction de plan repair definie
Du Plan Repair au LLM Heuristique : monter en abstraction
Les sections precedentes (Direct, CoT, ToT, NL-vers-PDDL, Plan Repair) placent le LLM dans le role de generateur de plans. La section 7 inverse la perspective : le LLM devient un guide pour un solveur classique.
Intuition : un solveur classique comme A* explore systematiquement l’espace d’etats. Sans heuristique, il fait une recherche en largeur, lente. Avec une heuristique classique (h_add, h_max), il est guide vers le but. Avec une heuristique LLM, il beneficie en plus du sens commun du modele.
Exemple illustratif : un puzzle de 5 pieces ou la piece X est presque a sa place mais une autre piece Y la bloque. Un humain voit immediatement qu’il faut deplacer Y d’abord.
h_add (classique) : nombre de pieces mal placees = 4. Mais le solveur ne sait pas que deplacer Y est ‘facile’ ou ‘difficile’.
Heuristique LLM : ‘deplacer Y en premier, puis X’ – estimation 2 (au lieu de 4). Plus informee.
Avantage : le solveur conserve ses garanties de correction et de completude. La garantie d’optimalite est perdue si l’heuristique n’est pas admissible.
Limite : cout API cumulatif. Sur une recherche A* typique avec 100-1000 evaluations, l’heuristique LLM peut couter 0.30-3.00 USD par recherche. Pour les benchmarks, les heuristiques classiques restent superieures en cout/efficacite. Pour les problemes de production ou le domaine est specialise (et l’heuristique classique inconnue), l’heuristique LLM est pertinente.
L’execution de la section 7 montre l’implementation de llm_heuristic() et sa comparaison avec h_add sur un probleme de navigation.
7. LLM comme Heuristique
Utiliser le LLM pour estimer la distance au but.
def llm_heuristic(state: PlanningState, goal: str, scale: int=10) ->float:"""Utilise le LLM pour estimer la distance au but (0 = but atteint)""" prompt =f"""Note la proximite de cet etat au but.ETAT: {', '.join(state.predicates)}BUT: {goal}Donne un score de 0 (but atteint) a {scale} (tres loin).Reponds uniquement par le chiffre."""if anthropic_client: response = anthropic_client.messages.create( model=config.model_anthropic, max_tokens=10, messages=[{"role": "user", "content": prompt}] )try: score =float(response.content[0].text.strip())returnmin(max(score, 0), scale)exceptValueError:return scale /2return5.0print("Fonction heuristique LLM definie")
Fonction heuristique LLM definie
Demonstration de l’heuristique LLM sur un etat concret de navigation, en interrogeant le modèle de langage pour estimer le nombre d’étapes restantes jusqu’au but.
# Demo de l'heuristiquetest_state = PlanningState( objects={"location": ["A", "B", "C"]}, predicates=["robot_a_A", "colis_a_A"])test_goal ="robot et colis sont a C"print(f"Etat: {test_state.predicates}")print(f"But: {test_goal}")if anthropic_client or openai_client: h_value = llm_heuristic(test_state, test_goal)print(f"Heuristique LLM: {h_value}/10")else:print("Heuristique LLM: (API requise - valeur attendue: ~7/10)")
Etat: ['robot_a_A', 'colis_a_A']
But: robot et colis sont a C
Heuristique LLM: (API requise - valeur attendue: ~7/10)
Interpretation
L’utilisation du LLM comme heuristique permet : - Sens du commun : Estimations basees sur la connaissance générale - Flexibilite : Adaptation a différents domaines
Mais avec des inconvenients : - Cout : Appels API pour chaque evaluation - Latence : Temps de reponse non negligeable - Incoherence : Résultats potentiellement variables
Approche Qualite Plan Cout API Garanties Complexite
LLM Direct Variable Faible Aucune Simple
LLM + CoT Bonne Moyen Faible Moyenne
LLM + ToT Tres bonne Eleve Faible Elevee
LLM vers PDDL Bonne Moyen Via validateur Moyenne
LLM Heuristique N/A Eleve Aucune Simple
Neuro-Symbolique Excellente Variable Fortes Elevee
Interpretation
Le tableau compare six stratégies d’integration LLM-planification. Les points cles :
Critere
Gagnant
Perdant
Fiabilite
Neuro-symbolique (garanties formelles)
LLM Direct (aucune garantie)
Cout
LLM Direct (un seul appel)
ToT / Heuristique (appels multiples)
Qualite
Neuro-symbolique > ToT > CoT > Direct
LLM Direct (variable)
Arbre de decision pratique : - Besoin de garanties ? → Neuro-symbolique ou LLM vers PDDL + validateur - Budget API limite ? → LLM Direct ou CoT - Problème complexe ? → ToT ou Neuro-symbolique
La colonne “Garanties” est decisive pour les applications critiques (robotique, logistics industrielle) : seul le couplage avec un validateur formel elimine les hallucinations.
Exercice 3 : Concevoir un prompt de planification robuste (a faire apres l’exemple guide ci-dessous)
Le template PLANNING_PROMPT_TEMPLATE (defini dans l’exemple guide « Traduction NL vers PDDL » ci-dessous) fournit une base, mais les LLMs produisent souvent des PDDL mal forme. Ameliorez le prompt pour reduire les erreurs.
Objectif : Redigez un nouveau prompt qui (1) contraint le format de sortie a du JSON strict sans texte autour, (2) fournit un exemple de plan correct dans le domaine robot-simple, (3) demande explicitement au LLM de verifier ses propres preconditions avant de generer le plan.
Indices : - Ajoutez un exemple few-shot avec le domaine robot-simple déjà resolu (move, pick, move, drop) - Specifiez : “Reponds UNIQUEMENT avec du JSON entre json et”, pas de texte avant/après - Demandez : “Pour chaque action, verifie que les preconditions sont satisfaites dans l’etat resultant des actions précédentes” - Testez votre prompt en le passant a call_llm_for_plan et validez avec validate_plan
# Exercice 3 : Concevoir un prompt de planification robuste# TODO etudiant : redigez un prompt ameliore pour generer des plans valides# Etape 1 : ajoutez un exemple few-shot (domaine robot-simple avec plan correct)# Etape 2 : contraignez le format de sortie (JSON strict entre ```json et ```)# Etape 3 : demandez la verification des preconditions avant generation# Indice : inspirez-vous de prompt_nl defini dans l'exemple guide ci-dessous (Traduction NL vers PDDL)robust_prompt ="""Votre prompt ameliore ici..."""# Testez votre prompt (si API configuree)# plan_robust = call_llm_for_plan(robust_prompt)# success_robust = validate_plan(plan_robust, INITIAL_STATE, GOAL_STATE)# print(f"Plan robuste valide : {success_robust}")print("Exercice a completer : concevoir un prompt de planification robuste")
Exercice a completer : concevoir un prompt de planification robuste
9. Meilleures Pratiques
Quand utiliser les LLMs pour la planification ?
Cas d’usage
Recommandation
Prototypage rapide
LLM Direct
Generation de domaine
LLM vers PDDL + Validation
Plan critique
Neuro-symbolique
Problemes mal définis
LLM pour elicitation
Sens du commun requis
LLM Heuristique
# Template de prompt optimal pour la planificationPLANNING_PROMPT_TEMPLATE ="""Tu es un assistant de planification automatique expert.## Domaine{domain_description}## Etat Initial{initial_state}## But{goal}## Contraintes- Utilise uniquement les actions definies dans le domaine- Chaque action doit avoir ses preconditions satisfaites- Minimise le nombre d'actions## Format de Sortie1. D'abord, analyse le probleme etape par etape2. Ensuite, fournis le plan en format JSON"""print("Template de prompt defini")
Template de prompt defini
Exemple guide : Traduction NL vers PDDL et validation de plan LLM
Implementation complete d’un pipeline neuro-symbolique combinant traduction en langage naturel vers PDDL avec validation formelle du plan genere.
import json, reDOMAIN = {"actions": {"move": {"params": ["robot", "from", "to"], "pre": ["at(robot,from)"], "eff_pos": ["at(robot,to)"], "eff_neg": ["at(robot,from)"]},"pick": {"params": ["robot", "obj", "loc"], "pre": ["at(robot,loc)", "at_obj(obj,loc)", "free(robot)"],"eff_pos": ["has(robot,obj)"], "eff_neg": ["at_obj(obj,loc)", "free(robot)"]},"drop": {"params": ["robot", "obj", "loc"],"pre": ["at(robot,loc)", "has(robot,obj)"],"eff_pos": ["at_obj(obj,loc)", "free(robot)"], "eff_neg": ["has(robot,obj)"]}, }}INITIAL_STATE = {"at(robot,A)", "free(robot)", "at_obj(obj1,B)"}GOAL_STATE = {"at_obj(obj1,A)"}prompt_nl ="""Tu es un planificateur robotique expert.## Domaine : robot-simpleUn robot peut se déplacer entre des emplacements, ramasser des objets et les déposer.Actions disponibles : - move(robot, from, to) : déplace le robot de `from` vers `to` PRECONDITION : robot se trouve en `from` EFFET : robot se trouve en `to`, n'est plus en `from` - pick(robot, obj, loc) : robot prend l'objet `obj` à l'emplacement `loc` PRECONDITION : robot en `loc`, objet en `loc`, robot libre (free) EFFET : robot tient l'objet, objet n'est plus en `loc`, robot n'est plus libre - drop(robot, obj, loc) : robot pose l'objet `obj` à l'emplacement `loc` PRECONDITION : robot en `loc`, robot tient `obj` EFFET : objet posé en `loc`, robot redevient libre, robot ne tient plus l'objet## État initial- robot se trouve en A- robot est libre (ne tient rien)- objet1 se trouve en B## But- objet1 doit se trouver en A## Instructions1. Raisonne étape par étape (quels faits changent à chaque action ?).2. Fournis le plan UNIQUEMENT sous forme JSON stricte, sans texte autour :[ {"action": "move", "params": ["robot", "A", "B"]}, {"action": "pick", "params": ["robot", "obj1", "B"]}, {"action": "move", "params": ["robot", "B", "A"]}, {"action": "drop", "params": ["robot", "obj1", "A"]}]Réponds avec le JSON seulement, encadré par des triples backticks json."""print("Prompt NL défini.")print(prompt_nl[:300], "...")def call_llm_for_plan(prompt: str) ->list:"""Appelle le LLM et retourne la liste des actions parsées.""" raw =Noneif anthropic_client: response = anthropic_client.messages.create( model=config.model_anthropic, max_tokens=config.max_tokens, messages=[{"role": "user", "content": prompt}] ) raw = response.content[0].textelif openai_client: response = openai_client.chat.completions.create( model=config.model_openai, max_tokens=config.max_tokens, messages=[{"role": "user", "content": prompt}], extra_body={"chat_template_kwargs": {"enable_thinking": False}} ) raw = response.choices[0].message.contentelse:print("⚠ Aucune API configurée — utilisation du plan exemple.")return [ {"action": "move", "params": ["robot", "A", "B"]}, {"action": "pick", "params": ["robot", "obj1", "B"]}, {"action": "move", "params": ["robot", "B", "A"]}, {"action": "drop", "params": ["robot", "obj1", "A"]}, ]print("Réponse brute LLM :\n", raw[:500])# Extraire le JSON (entre ```json ... ``` ou premier [ ... ]) m = re.search(r"```(?:json)?\s*([\s\S]*?)```", raw)if m: json_str = m.group(1)else: start = raw.find("[") end = raw.rfind("]") +1 json_str = raw[start:end] if start !=-1else"[]"try:return json.loads(json_str)except json.JSONDecodeError as e:print(f"Erreur parsing JSON : {e}")return []llm_plan_raw = call_llm_for_plan(prompt_nl)print("\nPlan brut retourné par le LLM :")for step in llm_plan_raw:print(f" {step['action']}({', '.join(step['params'])})")def to_pddl_action(step: dict) ->str:"""Formate une action dict en chaîne PDDL."""returnf"({step['action']}{' '.join(step['params'])})"pddl_sequence = [to_pddl_action(s) for s in llm_plan_raw]print("\nSéquence PDDL :")for i, a inenumerate(pddl_sequence, 1):print(f" {i}. {a}")def substitute(template: str, params: list, param_names: list) ->str:"""Remplace les noms de paramètres formels par les valeurs concrètes. Utilise une regex avec lookbehind/lookahead sur les délimiteurs PDDL ( '(' ',' ')' ) pour éviter les remplacements partiels de sous-chaînes. Exemple sans fix : 'at_obj(obj,loc)' avec obj->obj1 donnait 'at_obj1(obj1,loc)'. """ result = templatefor name, val inzip(param_names, params): result = re.sub(r'(?<=[,(])'+ re.escape(name) +r'(?=[,)])', val, result)return resultdef apply_action(state: set, action_name: str, params: list) ->tuple:""" Applique une action à un état. Retourne (nouvel_état, ok, message_erreur). """if action_name notin DOMAIN["actions"]:return state, False, f"Action inconnue : {action_name}" spec = DOMAIN["actions"][action_name] param_names = spec["params"]# Vérifier les préconditionsfor pre_template in spec["pre"]: pre_grounded = substitute(pre_template, params, param_names)if pre_grounded notin state:return state, False, f"Précondition non satisfaite : {pre_grounded}"# Appliquer les effets new_state =set(state)for neg in spec["eff_neg"]: new_state.discard(substitute(neg, params, param_names))for pos in spec["eff_pos"]: new_state.add(substitute(pos, params, param_names))return new_state, True, "OK"def validate_plan(plan: list, initial: set, goal: set) ->bool:"""Valide le plan étape par étape et affiche le détail.""" state =set(initial)print("\n"+"="*60)print("VALIDATION DU PLAN")print("="*60)print(f"État initial : {sorted(state)}")print(f"But : {sorted(goal)}")print("-"*60) all_ok =Truefor i, step inenumerate(plan, 1): action = step["action"] params = step["params"] state, ok, msg = apply_action(state, action, params) status ="✓"if ok else"✗"print(f"Étape {i} : {status}{action}({', '.join(params)})")ifnot ok:print(f" → ERREUR : {msg}") all_ok =Falseelse:print(f" → État : {sorted(state)}")print("-"*60) goal_reached = goal.issubset(state)print(f"But atteint : {'✓ OUI'if goal_reached else'✗ NON'}")ifnot all_ok:print("Plan INVALIDE (préconditions non respectées).")elif goal_reached:print("Plan VALIDE et COMPLET ✓")else:print("Plan valide mais but non atteint.")print("="*60)return all_ok and goal_reachedsuccess = validate_plan(llm_plan_raw, INITIAL_STATE, GOAL_STATE)
Prompt NL défini.
Tu es un planificateur robotique expert.
## Domaine : robot-simple
Un robot peut se déplacer entre des emplacements, ramasser des objets et les déposer.
Actions disponibles :
- move(robot, from, to) : déplace le robot de `from` vers `to`
PRECONDITION : robot se trouve en `from`
EFFE ...
⚠ Aucune API configurée — utilisation du plan exemple.
Plan brut retourné par le LLM :
move(robot, A, B)
pick(robot, obj1, B)
move(robot, B, A)
drop(robot, obj1, A)
Séquence PDDL :
1. (move robot A B)
2. (pick robot obj1 B)
3. (move robot B A)
4. (drop robot obj1 A)
============================================================
VALIDATION DU PLAN
============================================================
État initial : ['at(robot,A)', 'at_obj(obj1,B)', 'free(robot)']
But : ['at_obj(obj1,A)']
------------------------------------------------------------
Étape 1 : ✓ move(robot, A, B)
→ État : ['at(robot,B)', 'at_obj(obj1,B)', 'free(robot)']
Étape 2 : ✓ pick(robot, obj1, B)
→ État : ['at(robot,B)', 'has(robot,obj1)']
Étape 3 : ✓ move(robot, B, A)
→ État : ['at(robot,A)', 'has(robot,obj1)']
Étape 4 : ✓ drop(robot, obj1, A)
→ État : ['at(robot,A)', 'at_obj(obj1,A)', 'free(robot)']
------------------------------------------------------------
But atteint : ✓ OUI
Plan VALIDE et COMPLET ✓
============================================================
Exercice : Domaine Gripper multi-robots
Etendez le domaine robot-simple pour supporter 2 robots et 4 balles. Generez un plan via LLM et validez-le avec le validateur formel.
Indice : Ajoutez les paramètres robot aux actions existantes.
# Exercice : Domaine Gripper complet# TODO etudiant : etendez le domaine avec 2 robots et 4 balles# Etape 1 : ajouter les predicats et actions pour 2 robots# Etape 2 : definir le probleme initial et le but# Etape 3 : generer et valider le planprint("Exercice a completer")
Exercice a completer
10. Resume et Points Cles
Ce que nous avons appris
LLM Direct : Simple mais sans garanties, adapte au prototypage
Chain-of-Thought : Ameliore la coherence du raisonnement
Tree-of-Thought : Explore plusieurs alternatives pour robustesse
LLM vers PDDL : Pont entre langage naturel et planificateurs formels
Plan Repair : Recuperation intelligente après echec
Heuristique LLM : Integration dans la recherche classique
Points a retenir
Toujours valider les plans generes par LLM
Combiner avec des outils formels pour les cas critiques
Utiliser CoT/ToT pour ameliorer la qualite
Le cout API peut etre prohibitif pour les heuristiques
Ressources Complementaires
Articles de recherche
Plansformer : Transformer pour la planification
LLM+P : Combinaison LLM et planificateurs classiques
Ce notebook a explore l’integration des Large Language Models avec la planification automatique, illustrant six stratégies distinctes allant de la generation directe de plans a l’utilisation du LLM comme heuristique. L’approche directe, bien que simple, souffre d’un manque de garanties : les LLMs peuvent halluciner des actions inexistentes ou produire des plans invalides. Les techniques de raisonnement avance (Chain-of-Thought, Tree-of-Thought) ameliorent la coherence des plans generes, tandis que la traduction langage naturel vers PDDL, couplee a un validateur formel, offre la voie la plus fiable vers des plans corrects. L’exemple guide de traduction NL-vers-PDDL avec validation pas-a-pas a montre comment combiner la flexibilite linguistique du LLM avec la rigueur de la verification symbolique.
Le couplage LLM-planification ouvre des perspectives considerables pour la democratisation de la planification automatique. Un utilisateur non expert peut desormais decrire un problème en langage naturel et obtenir un plan formel, sans maitriser la syntaxe PDDL. Cependant, les limitations restent importantes : le cout des appels API (prohibitif pour l’usage heuristique dans la boucle de recherche), la latence, et surtout le risque d’hallucination imposent de toujours valider formellement les plans produits. L’approche neuro-symbolique, ou le LLM genere des hypotheses que le solveur symbolique valide et raffine, represente le compromis le plus prometteur pour les applications critiques.
Le notebook suivant, Planners-11-Unified-Planning, presente l’interface unifiee unified-planning qui permet de comparer facilement différents solveurs sur un même problème, un complement naturel aux approches LLM-presentees ici.
Note methodologique sur les evaluations chiffrees :
Ce notebook documente les strategies d’integration LLM-planification de maniere qualitative. Plusieurs classes de chiffres sont souvent publiees dans la litterature :
Taux de reussite du LLM Direct en fonction de la taille du domaine (nombre d’objets, longueur du plan).
Taux d’erreur syntaxique / semantique / d’intention lors de la traduction NL-vers-PDDL.
Comparaison de cout entre differentes strategies (Direct, CoT, ToT, Heuristique).
Benchmarks specialises : IPC (International Planning Competition), PlanBench, etc.
Aucune cellule code de ce notebook ne produit ces chiffres. Les raisons pedagogiques de ce choix sont les suivantes :
Dependance au modele : les chiffres varient fortement entre GPT-4, Claude, Qwen, Llama, et entre versions d’un meme modele.
Dependance au domaine : un meme modele peut obtenir 95 % sur un domaine logistique simple et 30 % sur un domaine logistique complexe.
Dependance au protocole : la definition d’un plan “valide” et d’un plan “utile” varie selon les auteurs.
Pour reproduire ces chiffres de maniere rigoureuse, consulter les benchmarks specialises (IPC, PlanBench) et utiliser un protocole de test documente. Ce notebook renvoie a ces ressources plutot que de figer des valeurs qui seraient necessairement approximatives et dependantes du contexte d’execution.