Phase 3 de l’epic #2926 — pont entre NB-12 (ToT demontre sur le 24-game), NB-13/NB-14 (routeur + memoire agentiques) et les series Search (CSP) / Sudoku.
Ou ToT bat le single-shot le plus nettement
Cadrage editorial (fidelite au papier source). Le ToT de Yao et al. (2023) met un LLM dans la boucle de recherche : il genere et evalue lui-même les pensées candidates (etat evaluator LLM, beam search de largeur b). Ce notebook adapte l’idee de ToT – recherche structuree sur un arbre de pensées – a un CSP déterministe ou la “pensee” = une affectation lettre->chiffre et l’“evaluation” = la contrainte arithmetique exacte de la colonne. Le LLM n’est donc pas dans la boucle de recherche (voir NB-12 pour le ToT LLM-guide sur le 24-game, et Exercice 2 ci-dessous pour un prototype LLM-guide). Cette distinction est explicite tout au long du notebook.
NB-12 demontre Tree-of-Thoughts sur le 24-game (combinatoire numérique). Mais c’est sur les problemes de satisfaction de contraintes exacts (CSP) – cryptarithmetic, Sudoku, N-queens – que l’ecart entre single-shot (un LLM qui devine la solution en un appel) et recherche structuree (ToT : developper des etats, evaluer, elaguer, backtrack) est le plus grand. Un LLM est mauvais au calcul arithmetique exact et aux contraintes d’unicite : il hallucine des chiffres, viole la règle “chaque lettre = un chiffre distinct”.
Tree-of-Thoughts (ToT) (Yao et al., 2023) generalise le chain-of-thought (Wei et al., 2022) et la self-consistency (Wang et al., 2022) en explorant un arbre d’etats ou chaque noeud est une pensee candidate, evaluee puis elaguee plutot que suivie en ligne droite.
Ide centrale de ce notebook : sur un CSP, ToT = recherche structuree sur l’espace des affectations. Chaque “pensee” = affecter une (ou plusieurs) lettre(s) a un chiffre ; l’evaluation = verifier la contrainte de colonne (+ retenue) ; le backtrack = abandonner la branche inconsistante. C’est exactement le backtracking + propagation des solveurs CSP formels (series Search / Sudoku, OR-Tools, Choco).
Plan
Single-shot : le LLM resout un cryptarithme en un appel -> on mesure sa fiabilite (basse).
ToT : recherche structuree (DFS colonne par colonne + retenue) -> solution garantie.
Comparaison ToT vs single-shot (le livrable Phase 3).
Pont Search/Sudoku : c’est du CSP, les LLM sont faibles sur les CSP exacts -> solveurs formels.
Note: you may need to restart the kernel to use updated packages.
.env charge depuis : GenAI/.env
FAST_MODEL=meta-llama/llama-3.3-70b-instruct | BATCH_MODE=False
1. Single-shot — le LLM resout-il un cryptarithme en un appel ?
On demande au LLM de resoudre CROSS + ROADS = DANGER (cryptarithme moins celebre que SEND+MORE=MONEY, pour eviter la memorisation) en un seul appel, puis on verifie la proposition (unicite des chiffres + validite arithmetique). On repete pour mesurer la fiabilite – un LLM est typiquement peu fiable sur ce genre de tâche exacte.
PUZZLES = {"SEND+MORE=MONEY": (["SEND", "MORE"], "MONEY"),"CROSS+ROADS=DANGER": (["CROSS", "ROADS"], "DANGER"),}def valider_affectation(affect, addends, result):"""Verifie qu'une affectation lettre->chiffre est valide : unicite, pas de 0 en tete, et l'egalite arithmetique est vraie. Renvoie (ok, detail)."""ifnot affect:returnFalse, "affectation vide" lettres =set(ch for w in addends + [result] for ch in w)ifset(affect.keys()) != lettres:returnFalse, f"lettres manquantes/supplementaires ({set(affect.keys())} vs {lettres})" digits =list(affect.values())iflen(set(digits)) !=len(digits):returnFalse, "chiffres non distincts"for w in addends + [result]:if affect[w[0]] ==0:returnFalse, f"zero en tete ({w[0]})"try: v =lambda w: int("".join(str(affect[ch]) for ch in w)) ok =sum(v(w) for w in addends) == v(result)return ok, ("correct"if ok else"egalite fausse")exceptExceptionas exc:returnFalse, f"erreur eval : {exc}"def extraire_affectation(reponse):"""Extrait lettre->chiffre d'une reponse LLM (tolere JSON ou 'S=9' ligne par ligne).""" affect = {}for lettre, chiffre in re.findall(r'([A-Za-z])\s*[:=]\s*([0-9])', reponse or""): affect[lettre.upper()] =int(chiffre)return affectdef single_shot(addends, result, model=FAST_MODEL, temperature=0.7):"""Le LLM resout en un appel. Renvoie (affect, ok, detail).""" puzzle =" + ".join(addends) +" = "+ result prompt = (f"Resous le cryptarithme {puzzle}. Chaque lettre = un chiffre distinct (0-9). "f"Une lettre en tete de mot ne vaut jamais 0. "f"Reponds avec UNE affectation par ligne au format 'LETTRE=chiffre' (ex: S=9).") rep = chat(prompt, model=model, temperature=temperature, max_tokens=400) affect = extraire_affectation(rep) ok, detail = valider_affectation(affect, addends, result)return affect, ok, detail, repprint("single_shot + validateur definis.")
single_shot + validateur definis.
# Demo single-shot sur CROSS+ROADS=DANGER, repete N fois (BATCH_MODE -> petit N).N_RUNS =3if BATCH_MODE else8addends, result = PUZZLES["CROSS+ROADS=DANGER"]succes =0print(f"Single-shot sur CROSS+ROADS=DANGER ({N_RUNS} essais, FAST_MODEL={FAST_MODEL})")print("-"*60)for i inrange(N_RUNS): affect, ok, detail, rep = single_shot(addends, result, temperature=0.8) succes +=int(ok)print(f" essai {i+1} : {detail:24s} | reponse LLM : {(rep[:50]).strip()!r}")print("-"*60)print(f"Fiabilite single-shot : {succes}/{N_RUNS} = {100*succes/N_RUNS:.0f}%")print("=> Le LLM est peu fiable sur ce CSP exact (calcul arithmetique + unicite).")
Single-shot sur CROSS+ROADS=DANGER (8 essais, FAST_MODEL=meta-llama/llama-3.3-70b-instruct)
------------------------------------------------------------
essai 1 : egalite fausse | reponse LLM : 'R=5\nO=2\nC=1\nS=9\nD=8\nA=7\nN=4\nG=6\nE=3'
essai 2 : affectation vide | reponse LLM : 'Pour résoudre ce cryptarithme, nous devons trouver'
essai 3 : lettres manquantes/supplementaires ({'R', 'S'} vs {'N', 'O', 'S', 'E', 'G', 'C', 'R', 'D', 'A'}) | reponse LLM : 'Pour résoudre ce cryptarithme, commençons par les'
essai 4 : egalite fausse | reponse LLM : 'R=4\nO=1\nS=9\nC=3\nD=6\nA=0\nN=8\nG=7\nE=5'
essai 5 : lettres manquantes/supplementaires ({'O', 'S', 'E', 'C', 'R', 'D', 'A'} vs {'N', 'O', 'S', 'E', 'G', 'C', 'R', 'D', 'A'}) | reponse LLM : 'Je vais essayer de résoudre le cryptarithme étape'
essai 6 : lettres manquantes/supplementaires ({'N', 'O', 'B', 'S', 'E', 'G', 'C', 'R', 'D', 'A'} vs {'N', 'O', 'S', 'E', 'G', 'C', 'R', 'D', 'A'}) | reponse LLM : 'Voici la solution du cryptarithme :\n\nC = 1\nR = 8\nO'
essai 7 : lettres manquantes/supplementaires ({'R', 'S'} vs {'N', 'O', 'S', 'E', 'G', 'C', 'R', 'D', 'A'}) | reponse LLM : 'Pour résoudre ce cryptarithme, nous allons procéde'
essai 8 : egalite fausse | reponse LLM : 'R=5\nO=2\nC=1\nS=9\nD=8\nA=7\nN=4\nG=6\nE=3'
------------------------------------------------------------
Fiabilite single-shot : 0/8 = 0%
=> Le LLM est peu fiable sur ce CSP exact (calcul arithmetique + unicite).
Lecture : pourquoi le LLM echoue a 0/8 – anatomie des modes d’echec
Le resultat 0/8 n’est pas un echec uniforme : les 8 essais se repartissent en quatre modes d’echec distincts, chacun revelateur d’une limitation fondamentale des LLM autoregressifs sur les problemes combinatoires exacts.
Mode d’echec
Essais
Ce que fait le LLM
Limitation revelee
Egalite fausse
1, 4, 8
Produit une affectation complete et bien formee (9 lettres, chiffres uniques, pas de 0 en tete)… mais l’arithmetique est fausse
Le LLM n’a aucun verificateur interne : il genere une reponse plausible, sans pouvoir la tester. C’est le piege le plus dangereux, car la sortie a l’air valide.
Affectation vide
2
Se lance dans une prose explicative (« Pour resoudre ce cryptarithme, nous devons… ») sans jamais produire l’affectation demandee
Le LLM discute au lieu de resoudre : il optimise la fluidite du texte, non la production d’une structure contrainte.
Lettres manquantes/supplementaires
3, 5, 7
Donne une affectation partielle ou sur-complete (jeu de lettres != celui du puzzle)
Le LLM ne suit pas l’ensemble exact des variables du probleme : il perd des lettres ou en ajoute au fil de la generation.
Hallucination de lettre
6
Invente une lettre « B » qui n’appartient pas a CROSS+ROADS=DANGER
Hallucination structurelle : le modele fabrique des elements absents du contexte, d’autant plus sur un puzzle moins connu (anti-memorisation).
La lecon generale. Ces quatre modes pointent vers une cause unique : un LLM predit le prochain token le plus plausible, il ne resout pas de contraintes. Resoudre un cryptarithme exige trois choses qu’aucune prediction de token ne garantit – (1) du calcul arithmetique exact (retenues de colonne), (2) le suivi d’un ensemble precis de variables (les lettres du puzzle), et (3) une verification (l’egalite est-elle vraie ?). Le LLM n’a ni l’un, ni l’autre, ni le troisieme de facon fiable : il « devine » une reponse qui ressemble a une solution sans pouvoir la controler.
Transition vers Tree-of-Thoughts. C’est precisement ce diagnostic qui motive la section suivante : au lieu de demander au LLM une solution en un coup (et de subir ces quatre modes d’echec), on externalise la recherche et la verification dans un algorithme explicite – un backtracking colonne par colonne avec propagation de retenue. Cet algorithme est deterministe et exact : il trouve l’unique solution (cf. sortie suivante : 96233+62513=158746) ou prouve l’absence, sans aucune hallucination ni derive arithmetique. Le LLM etait le mauvais outil pour le calcul exact ; le CSP, lui, est fait pour cela.
On implemente le cryptarithme comme une recherche Tree-of-Thoughts : un arbre dont les noeuds sont des affectations partielles lettre->chiffre. On developpe colonne par colonne (de la droite, les unites, vers la gauche), on essaie les chiffres disponibles, on verifie la contrainte de colonne + la retenue, et on backtrack si inconsistante.
C’est du backtracking + propagation – le paradigme CSP classique (Russell & Norvig, 2020, ch. 6 ; series Search / Sudoku de ce depot). Pour le cryptarithme, la recherche est déterministe et exacte : elle trouve la solution (unique ici) ou prouve l’absence, sans hallucination. Le LLM n’intervient pas dans la recherche elle-même – c’est honnete (G.2) : sur un CSP exact, la structure de recherche apporte la valeur, pas le LLM (voir Exercice 2 pour un ToT LLM-guide).
def resoudre_cryptarithme(addends, result, find_all=False):"""Tree-of-Thoughts : DFS colonne par colonne (droite->gauche) avec propagation de retenue. Chaque 'pensee' = affecter les lettres non-affectees d'une colonne a des chiffres disponibles ; evaluation = contrainte de colonne (somme % 10 == chiffre resultat) ; backtrack si inconsistante. Renvoie la liste des solutions (dicts lettre->chiffre).""" words =list(addends) + [result] leading = {w[0] for w in words} # lettres de tete (jamais 0) ncol =max(len(w) for w in words) solutions, assign, used = [], {}, set()def dfs(col, carry):if col == ncol: # toutes colonnes traiteesif carry ==0: solutions.append(dict(assign))returnnot find_all # True = stop (premiere solution)returnFalse# lettres de cette colonne (addends puis resultat) ac = [w[-(col +1)] for w in addends if col <len(w)] rc = result[-(col +1)] if col <len(result) elseNone col_letters =list(dict.fromkeys(ac + ([rc] if rc else []))) unassigned = [ch for ch in col_letters if ch notin assign] avail = [d for d inrange(10) if d notin used]for combo in itertools.permutations(avail, len(unassigned)): nouveau, ok = {}, Truefor ch, d inzip(unassigned, combo):if ch in leading and d ==0: # pas de 0 en tete ok =False;break nouveau[ch] = difnot ok:continue assign.update(nouveau); used.update(combo) s =sum(assign[ch] for ch in ac) + carry nouveau_carry = s //10# contrainte de colonne : le chiffre des unites de la somme == resultatif (rc isNone) or (rc in assign and s %10== assign[rc]):if dfs(col +1, nouveau_carry):returnTrue used.difference_update(combo) # backtrackfor ch in unassigned: assign.pop(ch, None)returnFalse dfs(0, 0)return solutions# Demo : ToT resout les deux cryptarithmes (deterministe, garanti correct).for nom, (addends, result) in PUZZLES.items(): sols = resoudre_cryptarithme(addends, result)print(f"\n{nom} : {len(sols)} solution(s) trouvee(s) par ToT")for s in sols[:2]: ok, detail = valider_affectation(s, addends, result) v =lambda w: int("".join(str(s[ch]) for ch in w))print(f" {s} | {detail} | {v(addends[0])}+{v(addends[1])}={v(result)}")
3. Comparaison ToT vs single-shot (livrable Phase 3)
On compare sur le même puzzle : fiabilite (taux de succes) et garantie de correction. ToT est déterministe et garantit la solution ; single-shot depend du modèle/du tirage.
def benchmark(addends, result, n_runs=3):"""Compare single-shot (N tirages) vs ToT (deterministe) sur un puzzle.""" nom =" + ".join(addends) +" = "+ result# Single-shot ok_ss =0for _ inrange(n_runs): _, ok, _, _ = single_shot(addends, result, temperature=0.8) ok_ss +=int(ok)# ToT sols = resoudre_cryptarithme(addends, result) tot_correct =len(sols) >=1and valider_affectation(sols[0], addends, result)[0]print(f"\n{nom}")print(f" Single-shot : {ok_ss}/{n_runs} reussis ({100*ok_ss/n_runs:.0f}%) -- non garanti")print(f" ToT : {'GARANTI correct'if tot_correct else'AUCUNE solution'} -- deterministe")return {"puzzle": nom, "single_shot_pct": 100*ok_ss/n_runs, "tot_garanti": tot_correct}N =3if BATCH_MODE else6resultats = [benchmark(add, res, n_runs=N) for add, res in PUZZLES.values()]print("\n=> Conclusion : sur les CSP exacts, la recherche structuree (ToT) garantit la ""solution la ou le single-shot est peu fiable. C'est l'ecart max de valeur de ToT.")
SEND + MORE = MONEY
Single-shot : 3/6 reussis (50%) -- non garanti
ToT : GARANTI correct -- deterministe
CROSS + ROADS = DANGER
Single-shot : 0/6 reussis (0%) -- non garanti
ToT : GARANTI correct -- deterministe
=> Conclusion : sur les CSP exacts, la recherche structuree (ToT) garantit la solution la ou le single-shot est peu fiable. C'est l'ecart max de valeur de ToT.
4. Pont vers les series Search / Sudoku
Ce qu’on vient de faire – backtracking avec propagation de contraintes – c’est exactement le paradigme CSP (Constraint Satisfaction Problem) des series Search (CSP, Mixed) et Sudoku de ce depot. Le cryptarithme est un CSP : variables = lettres, domaines = {0..9}, contraintes = “toutes différentes” + egalite arithmetique colonne par colonne.
La serie Search formalise CSP / backtracking / forward-checking / AC-3, avec des solveurs dedies (Python, parfois OR-Tools ou Choco pour les grandes instances).
La serie Sudoku applique le même paradigme a des grilles (variables = cases, contraintes = ligne/colonne/carre).
Pourquoi un LLM echoue la ou la recherche reussit ? Les LLM sont entraines a predire du texte, pas a respecter des contraintes exactes (unicite, arithmetique). Sur un CSP, la recherche exhaustive elaguee est complete et correcte par construction ; le LLM, lui, “devine”. D’ou le pont : pour les CSP, un solveur formel battra toujours un LLM single-shot (et c’est ce que demontre cette comparaison).
Exercice 1 te fait ajouter de la propagation (forward-checking) ; Exercice 3 te fait resoudre une grille de Sudoku partielle avec la même boucle de recherche.
5. Travaux pratiques
Les exercices sont a completer (convention C.1 : pas d’erreur volontaire).
Exercice 1 : propagation de contraintes (forward-checking / AC-3 leger)
La recherche actuelle elague quand une colonne echoue. Ajoute de la propagation : des qu une lettre est affectee, retire sa valeur des domaines des lettres encore libres (forward- checking), et coupe une branche des qu’un domaine devient vide.
Indice : remplace avail = [d for d in range(10) if d not in used] par un suivi de domaines par lettre ; a chaque affectation, reduis les domaines ; backtrack = restaurer.
def resoudre_avec_propagation(addends, result, find_all=False):"""Exercice 1 : ToT + forward-checking (propagation de contraintes)."""# TODO etudiant : adapter resoudre_cryptarithme avec un suivi de domaines par lettre# et elaguer une branche des qu'un domaine devient vide (forward-checking).returnNone_r = resoudre_avec_propagation(["SEND", "MORE"], "MONEY")print(f"Exercice 1 - forward-checking : {'implemente'if _r isnotNoneelse'a completer'}")
Exercice 1 - forward-checking : a completer
Exercice 2 : ToT LLM-guide (le LLM propose / classe les branches)
La recherche actuelle est purement combinatoriale (pas de LLM). Implemente un ToT ou le LLM propose des affectations candidates pour la colonne courante et les classe (les plus prometteuses d’abord), la recherche verifiant ensuite la contrainte. Compare le nombre de noeuds expliores vs le ToT combinatoire pur.
Indice : a chaque colonne, demande au LLM “propose 3 affectations pour D,E,Y telles que D+E finisse par Y” ; essaie-les dans l’ordre ; backtrack si aucune ne valide.
def tot_llm_guide(addends, result, model=FAST_MODEL):"""Exercice 2 : ToT ou le LLM propose/classe les branches de recherche."""# TODO etudiant : a chaque colonne, appeler le LLM pour proposer des candidats,# les essayer dans l'ordre prometteur, verifier la contrainte, backtracker.returnNone_r2 = tot_llm_guide(["SEND", "MORE"], "MONEY")print(f"Exercice 2 - ToT LLM-guide : {'implemente'if _r2 isnotNoneelse'a completer'}")
Exercice 2 - ToT LLM-guide : a completer
Exercice 3 (avance) : Sudoku partiel (pont serie Sudoku)
Adapte la boucle de recherche (DFS + contraintes + backtrack) a une grille de Sudoku partielle : variables = cases vides, domaines = {1..9}, contraintes = ligne/colonne/carre. Resous une grille facile et compare avec un solveur dedie de la serie Sudoku.
Indice : represente la grille comme un dict (ligne, col) -> valeur ; contraintes = ligne, colonne, bloc 3x3 ; DFS sur les cases vides dans l’ordre. Pont vers la serie Sudoku.
def resoudre_sudoku(grille):"""Exercice 3 : DFS + contraintes (ligne/colonne/carre) sur une grille Sudoku partielle. `grille` = dict {(ligne, col): valeur} pour les cases connues (1-9)."""# TODO etudiant : DFS sur les cases vides, essai 1..9, verif ligne/colonne/carre, backtrack.returnNoneprint(f"Exercice 3 - Sudoku : {'implemente'ifFalseelse'a completer'}")
Exercice 3 - Sudoku : a completer
6. Conclusion et suite
On a etendu Tree-of-Thoughts (NB-12, 24-game) aux vrais problemes de recherche : cryptarithmetic (SEND+MORE=MONEY, CROSS+ROADS=DANGER) via DFS colonne-par-colonne avec propagation de retenue. La comparaison ToT vs single-shot montre l’ecart maximal de valeur de la recherche structuree : sur un CSP exact, ToT garantit la solution, le single-shot est peu fiable.
Limites honnetes (G.2) : - Le LLM n’apporte rien a la recherche exacte : sur le cryptarithme, la recherche combinatoire est déterministe et optimale ; un LLM “guide” (Exercice 2) ajoute du bruit/cout. La valeur de ToT ici est la structure de recherche, pas l’LLM. - Echelle : la DFS colonne-par-colonne est efficace pour quelques mots, mais sur de grands CSP (Sudoku difficile, emploi du temps), il faut de la propagation (AC-3, forward- checking, Exercice 1) voire un solveur dedie (OR-Tools, Choco). - Single-shot peut reussir par chance/memorisation : sur SEND+MORE=MONEY (celebre), un grand modèle peut avoir memorise la solution. C’est pourquoi la demo principale utilise CROSS+ROADS=DANGER (moins celebre).
Suite de l’epic #2926 : - Phase 4 — courbes de scaling Snell 2024 (quand echantillonner large vs chercher profond). - Phase 5 — hand-rolled vs modèles a raisonnement natif (gpt-5-thinking), cout-normalise. - Phase 6 — plugin Semantic Kernel (pont series SemanticKernel).
References academiques : - Yao, S. et al. (2023) - Tree of Thoughts: Deliberate Problem Solving with Large Language Models. arXiv:2305.10601. (Framework ToT original, LLM-guided.) - Wei, J. et al. (2022) - Chain-of-Thought Prompting Elicits Reasoning in Large Language Models. arXiv:2201.11903. (Precurseur CoT.) - Wang, X. et al. (2022) - Self-Consistency Improves Chain of Thought Reasoning in Language Models. arXiv:2203.11171. (Self-consistency, voisin de ToT.) - Snell, C. et al. (2024) - Scaling LLM Test-Time Compute Optimally can be More Effective than Scaling Model Parameters. (Scaling test-time, Phase 4.) - Russell, S. & Norvig, P. (2020) - Artificial Intelligence: A Modern Approach (4e ed.), ch. 6. (Backtracking + propagation CSP.)
Ressources internes : series Search (CSP) / Sudoku de ce depot ; NB-12 (moteurs ToT LLM-guide), NB-13 (routeur), NB-14 (memoire).