Planners-10: LLMs pour la Planification

Navigation : Précédent | Suivant | Index


Objectifs d’apprentissage

A l’issue de ce notebook, vous saurez :

  • 🎯 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 :

  • Raisonnement symbolique : Guaranties formelles, verifiable
  • 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 :

  1. Programmation Python intermediaire (dataclasses, type hints, comprehensions).
  2. Notions de planification classique (etats, actions, plans). Voir Planners-1 et Planners-2 pour une introduction.
  3. Familiarite avec les appels API LLM (pattern OpenAI standard).
  4. 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.

1. Configuration et Imports

# Imports standards
import os
import json
from typing import Dict, List, Optional, Any
from dataclasses import dataclass
from pathlib import Path

# API clients (OpenAI et Anthropic) - Optionnels
try:
    from openai import OpenAI
    HAS_OPENAI = True
except ImportError:
    HAS_OPENAI = False
    OpenAI = None

try:
    from anthropic import Anthropic
    HAS_ANTHROPIC = True
except ImportError:
    HAS_ANTHROPIC = False
    Anthropic = None

# Charger les variables d'environnement
try:
    from dotenv import load_dotenv
    load_dotenv()
    HAS_DOTENV = True
except ImportError:
    HAS_DOTENV = False

print("Imports charges avec succes")
print(f"OpenAI disponible: {HAS_OPENAI}")
print(f"Anthropic disponible: {HAS_ANTHROPIC}")
print(f"Dotenv disponible: {HAS_DOTENV}")
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
@dataclass
class 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 = 2000

config = LLMConfig()

# Initialiser les clients (si disponibles et configures)
openai_client = None
anthropic_client = None

if 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 or None
    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.

@dataclass
class PlanningState:
    """Represente un etat de planification"""
    objects: Dict[str, List[str]]  # type -> liste d'objets
    predicates: List[str]  # faits vrais

@dataclass  
class 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].text
    elif 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.content
    else:
        return []
    try:
        json_start = content.find('[')
        json_end = content.rfind(']') + 1
        if json_start != -1 and 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")
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 voyage
initial = 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 in enumerate(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 :

  1. Memoire de travail limitee : le LLM doit suivre l’etat mentalement au fil des actions. Au-dela de ~10 objets, il perd la coherence.
  2. Pas de verification intermediaire : le LLM genere tout le plan d’un coup, sans valider chaque etape.
  3. 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 but
2. Identifie les differences entre l'etat actuel et le but
3. Pour chaque difference, determine quelle action peut la resoudre
4. Verifie les dependances entre actions
5. Construis le plan final

Montre 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, reasoning
    
    return [], "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 simplifiee
    return 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 effets

Format 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].text
    
    return ";; 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].text
    
    return ";; API non configuree"
print("Fonctions PDDL definies : text_to_pddl_domain, text_to_pddl_problem")
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 logistique
logistics_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] + "..." if len(pddl_domain) > 1000 else 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) :

  1. Syntaxique : verifier les parentheses, les mots-cles, la structure (define ...).
  2. Semantique : verifier que chaque predicat est declare, chaque type est utilise, chaque action a :parameters/:precondition/:effect.
  3. Validite du plan : verifier que le plan satisfait les contraintes du domaine, une fois le plan genere par le solveur.

Outils recommandes :

  • Syntaxique : pddlparser (Python), validate_pddl_syntax (ce notebook, section 5).
  • 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 re

def 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 base
    if is_domain:
        if '(define (domain' not in pddl_code.lower():
            errors.append("Declaration de domaine manquante")
    else:
        if '(define (problem' not in pddl_code.lower():
            errors.append("Declaration de probleme manquante")
    
    return len(errors) == 0, errors

# Test avec un exemple
test_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 predicats

def 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 ici
    return len(errors) == 0, errors

# Testez votre validateur
test_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())
            return min(max(score, 0), scale)
        except ValueError:
            return scale / 2
    
    return 5.0

print("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'heuristique
test_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

8. Comparaison des Approches

import pandas as pd

comparison_data = {
    'Approche': ['LLM Direct', 'LLM + CoT', 'LLM + ToT', 'LLM vers PDDL', 'LLM Heuristique', 'Neuro-Symbolique'],
    'Qualite Plan': ['Variable', 'Bonne', 'Tres bonne', 'Bonne', 'N/A', 'Excellente'],
    'Cout API': ['Faible', 'Moyen', 'Eleve', 'Moyen', 'Eleve', 'Variable'],
    'Garanties': ['Aucune', 'Faible', 'Faible', 'Via validateur', 'Aucune', 'Fortes'],
    'Complexite': ['Simple', 'Moyenne', 'Elevee', 'Moyenne', 'Simple', 'Elevee']
}

df = pd.DataFrame(comparison_data)
print(df.to_string(index=False))
        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 planification
PLANNING_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 Sortie
1. D'abord, analyse le probleme etape par etape
2. 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, re

DOMAIN = {
    "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-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`
      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

## Instructions
1. 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 = None

    if 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].text
    elif 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.content
    else:
        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 != -1 else "[]"

    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."""
    return f"({step['action']} {' '.join(step['params'])})"

pddl_sequence = [to_pddl_action(s) for s in llm_plan_raw]
print("\nSéquence PDDL :")
for i, a in enumerate(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 = template
    for name, val in zip(param_names, params):
        result = re.sub(r'(?<=[,(])' + re.escape(name) + r'(?=[,)])', val, result)
    return result


def apply_action(state: set, action_name: str, params: list) -> tuple:
    """
    Applique une action à un état.
    Retourne (nouvel_état, ok, message_erreur).
    """
    if action_name not in DOMAIN["actions"]:
        return state, False, f"Action inconnue : {action_name}"

    spec        = DOMAIN["actions"][action_name]
    param_names = spec["params"]

    # Vérifier les préconditions
    for pre_template in spec["pre"]:
        pre_grounded = substitute(pre_template, params, param_names)
        if pre_grounded not in 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 = True
    for i, step in enumerate(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)})")
        if not ok:
            print(f"          → ERREUR : {msg}")
            all_ok = False
        else:
            print(f"          → État : {sorted(state)}")

    print("-"*60)
    goal_reached = goal.issubset(state)
    print(f"But atteint  : {'✓ OUI' if goal_reached else '✗ NON'}")
    if not 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_reached


success = 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 plan
print("Exercice a completer")
Exercice a completer

10. Resume et Points Cles

Ce que nous avons appris

  1. LLM Direct : Simple mais sans garanties, adapte au prototypage
  2. Chain-of-Thought : Ameliore la coherence du raisonnement
  3. Tree-of-Thought : Explore plusieurs alternatives pour robustesse
  4. LLM vers PDDL : Pont entre langage naturel et planificateurs formels
  5. Plan Repair : Recuperation intelligente après echec
  6. 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
  • Tree-of-Thought : Yao et al. (2023)

Libraries


Navigation : Précédent | Suivant | Index

Resume et perspectives

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 :

  1. Dependance au modele : les chiffres varient fortement entre GPT-4, Claude, Qwen, Llama, et entre versions d’un meme modele.
  2. Dependance au domaine : un meme modele peut obtenir 95 % sur un domaine logistique simple et 30 % sur un domaine logistique complexe.
  3. 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.

Retour au sommet