Raisonnement avancé#

Le chapitre précédent a introduit le Chain-of-Thought (CoT) comme première stratégie pour inciter un LLM à raisonner étape par étape. Si le CoT linéaire améliore notablement les performances sur de nombreuses tâches, il reste fondamentalement contraint par sa nature séquentielle : le modèle ne peut ni explorer plusieurs pistes en parallèle, ni revenir sur une erreur intermédiaire, ni intéragir avec le monde extérieur pour vérifier une hypothèse.

Ce chapitre présente les paradigmes de raisonnement avancé qui surmontent ces limites. Le Tree-of-Thought (ToT) structure l’exploration en arbre avec retour arrière ; le Graph-of-Thought (GoT) généralise cette idée en autorisant la fusion et le raffinement de pensées ; ReAct entrelace raisonnement et action pour ancrer les réponses dans des observations externes. Enfin, le tool use et le function calling concrétisent ces paradigmes dans les API modernes, transformant le LLM en véritable agent outillé.

L’objectif est double : comprendre formellement chaque paradigme et savoir choisir la bonne stratégie selon la tâche. Chaque section est accompagnée de simulations en Python pur, sans appel à une API externe, afin d’illustrer la mécanique interne de ces approches.

Au-delà du raisonnement linéaire#

Le CoT linéaire produit une unique chaine de pensées \(t_1 \to t_2 \to \cdots \to t_n\) menant à la réponse. Cette structure souffre de trois limitations majeures :

  1. Pas d’exploration : si la première pensée \(t_1\) part dans une mauvaise direction, toute la chaine est compromise.

  2. Pas de retour arrière (backtracking) : une erreur à l’étape \(t_k\) se propage mécaniquement jusqu’à la réponse finale.

  3. Pas d’ancrage externe (grounding) : le raisonnement se déroule entièrement « dans la tête » du modèle, source d’hallucinations pour les questions factuelles ou les calculs numériques.

Remarque 37 (Stratégies de recherche et raisonnement)

L’analogie avec les algorithmes de recherche en intelligence artificielle classique est éclairante. Le CoT correspond à une recherche en profondeur sans retour arrière (greedy depth-first). Le ToT introduit une veritable recherche en arbre (BFS ou DFS avec évaluation). Le GoT généralise au cas des graphes. ReAct ajoute la possibilité d’observer l’environnement entre deux étapes de recherche.

Paradigme

Exploration

Retour arrière

Interaction externe

CoT

Non

Non

Non

ToT

Oui (arbre)

Oui

Non

GoT

Oui (graphe)

Oui

Non

ReAct

Séquentielle

Non

Oui (outils)

Tree-of-Thought (ToT)#

Le Tree-of-Thought a été proposé par Yao et al. (2023) pour surmonter la linéarité du CoT en structurant le raisonnement comme un arbre de recherche.

Définition 34 (Tree-of-Thought (ToT))

Le Tree-of-Thought est un cadre de raisonnement dans lequel :

  1. L”espace de pensées \(\mathcal{T}\) est un ensemble de pensées intermédiaires cohérentes, où chaque pensée \(t\) représente un pas de raisonnement partiel.

  2. Un générateur \(G(t_{\text{parent}}) \to \{t_1, \ldots, t_b\}\) produit \(b\) pensées candidates à partir d’une pensée parente, formant un arbre de facteur de branchement \(b\).

  3. Un évaluateur \(V(t) \to \mathbb{R}\) estime la qualité d’une pensée intermédiaire (par le LLM lui-même, via un prompt d’évaluation).

  4. Un algorithme de recherche (BFS, DFS, beam search) explore l’arbre en utilisant \(V\) pour guider l’exploration et décider du retour arrière.

La résolution d’un problème \(p\) consiste à trouver le chemin \(t_0 \to t_1 \to \cdots \to t_n\) qui maximise la qualité de la solution finale, où \(t_0\) est la racine (enoncé du problème).

Le processus ToT alterne trois phases : génération de \(b\) pensées candidates, évaluation par le LLM (score numérique, vote ou jugement catégoriel), et sélection avec retour arrière sur les branches jugées sans issue.

Remarque 38 (Stratégies d’évaluation dans le ToT)

L’évaluateur peut prendre trois formes principales : (1) un score numérique (« sur 10, quelle est la qualité de cette étape ? »), (2) un vote parmi plusieurs candidats (le LLM choisit le meilleur), ou (3) un jugement catégoriel (« sure / maybe / impossible »). Le choix de la stratégie d’évaluation impacte fortement la qualité de l’élagage : un jugement trop permissif gaspille des appels, un jugement trop strict risque d’élaguer la bonne branche.

Propriété 8 (ToT vs CoT : garanties d’exploration)

Le ToT subsume strictement le CoT : avec \(b = 1\) et sans évaluation, le ToT se réduit à un CoT standard. Avec \(b > 1\), le ToT explore un espace exponentiellement plus grand (\(b^d\) noeuds à profondeur \(d\)), augmentant la probabilité de trouver un chemin correct au prix d’un nombre d’appels LLM en \(O(b^d)\).

Remarque 39 (Coût computationnel du ToT)

Pour un arbre de profondeur \(d\) et de facteur \(b\), le nombre de générations et d’évaluations est \(O(b^d)\). En pratique, Yao et al. utilisent \(b \leq 5\) et \(d \leq 4\), soit quelques dizaines d’appels — un coût 10 à 100 fois supérieur au CoT, réservé aux problèmes où la précision le justifie.

Exemple 27 (Le Game of 24 résolu par ToT)

Le Game of 24 consiste à combiner 4 nombres avec \(+, -, \times, \div\) pour obtenir 24. Pour \(\{4, 9, 10, 13\}\), le CoT échoue fréquemment. Le ToT explore plusieurs pistes :

  • Branche 1 : \(13 - 10 = 3\), puis \(9 - 3 = 6\), puis \(6 \times 4 = 24\) — succès

  • Branche 2 : \(13 - 9 = 4\), puis \(4 + 4 = 8\), puis \(8 \times 10 = 80\) — évaluateur : « impossible » — retour arrière

  • Branche 3 : \(10 + 13 = 23\), puis \(23 + 9 = 32\) — évaluateur : « dépasse 24 » — retour arrière

L’évaluateur élague les branches 2 et 3, concentrant l’exploration sur les pistes viables.

Hide code cell source

# Import des Librairies Python
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns
import networkx as nx
from collections import deque

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

Hide code cell source

# Simulation ToT : exploration BFS du Game of 24

def tot_game24(numbers, target=24, beam_width=3):
    """ToT simplifié avec beam search. Retourne l'arbre d'exploration."""
    tree = {"nodes": [{"id": 0, "label": str(numbers), "depth": 0, "status": "root"}],
            "edges": [], "solutions": []}
    queue, node_id = [(tuple(sorted(numbers)), [], 0)], 0

    for depth in range(len(numbers) - 1):
        candidates = []
        for nums, history, pid in queue:
            if len(nums) < 2:
                continue
            for i in range(len(nums)):
                for j in range(len(nums)):
                    if i == j:
                        continue
                    a, b = nums[i], nums[j]
                    ops = {f"{a}+{b}": a+b, f"{a}-{b}": a-b, f"{a}*{b}": a*b}
                    if b != 0:
                        ops[f"{a}/{b}"] = a / b
                    for expr, val in ops.items():
                        rest = tuple(sorted([nums[k] for k in range(len(nums)) if k not in (i,j)] + [val]))
                        score = -min(abs(v - target) for v in rest)
                        node_id += 1
                        is_sol = len(rest) == 1 and abs(rest[0] - target) < 1e-9
                        tree["nodes"].append({"id": node_id, "depth": depth+1, "status": "solution" if is_sol else "candidate",
                            "label": f"{expr}={int(val) if val==int(val) else round(val,1)}"})
                        tree["edges"].append((pid, node_id))
                        candidates.append((rest, history+[expr], node_id, score))
                        if is_sol:
                            tree["solutions"].append(history + [expr])
        candidates.sort(key=lambda x: x[3], reverse=True)
        retained = {c[2] for c in candidates[:beam_width]}
        for n in tree["nodes"]:
            if n["depth"] == depth+1 and n["id"] not in retained and n["status"] != "solution":
                n["status"] = "pruned"
        queue = [(c[0], c[1], c[2]) for c in candidates[:beam_width]]
    return tree

tree = tot_game24([1, 2, 3, 4], target=24, beam_width=5)
print(f"Noeuds explorés : {len(tree['nodes'])}")
print(f"Solutions trouvées : {len(tree['solutions'])}")
for sol in tree["solutions"][:3]:
    print(f"  -> {' puis '.join(sol)}")
Noeuds explorés : 209
Solutions trouvées : 15
  -> 3*4 puis 2*12 puis 1*24
  -> 3*4 puis 2*12 puis 24*1
  -> 3*4 puis 2*12 puis 24/1

Hide code cell source

# Visualisation de l'arbre ToT
G = nx.DiGraph()
nodes = tree["nodes"][:40]
node_ids = {n["id"] for n in nodes}
for n in nodes:
    G.add_node(n["id"], label=n["label"], depth=n["depth"], status=n["status"])
for s, d in tree["edges"]:
    if s in node_ids and d in node_ids:
        G.add_edge(s, d)

# Disposition par profondeur
pos, depth_groups = {}, {}
for n in nodes:
    depth_groups.setdefault(n["depth"], []).append(n["id"])
for d, ids in depth_groups.items():
    for i, nid in enumerate(ids):
        pos[nid] = (i - len(ids)/2, -d)

cmap = {"root": "#4C72B0", "candidate": "#55A868", "pruned": "#CCCCCC", "solution": "#C44E52"}
colors = [cmap.get(G.nodes[n]["status"], "#55A868") for n in G.nodes]

fig, ax = plt.subplots(figsize=(14, 5))
nx.draw(G, pos, ax=ax, with_labels=False, node_color=colors, node_size=500,
        edge_color="#888888", arrows=True, arrowsize=10)
nx.draw_networkx_labels(G, pos, {n: G.nodes[n]["label"] for n in G.nodes}, font_size=6, ax=ax)
ax.legend(handles=[mpatches.Patch(color=c, label=l) for l, c in
    [("Racine","#4C72B0"),("Retenu","#55A868"),("Elagué","#CCCCCC"),("Solution","#C44E52")]],
    loc="upper right", fontsize=9)
ax.set_title("Arbre d'exploration Tree-of-Thought (Game of 24)", fontsize=13)
plt.show()
_images/552434d88b0804c3541b2e0a515bf84c1bea6fba80d82db5c320250f3588860c.png

Graph-of-Thought (GoT) et variantes#

L’arbre du ToT impose une structure hiérarchique stricte : chaque pensée a un unique parent. Le Graph-of-Thought relache cette contrainte en autorisant la fusion, le raffinement et la recombinaison de pensées issues de branches différentes.

Définition 35 (Graph-of-Thought (GoT))

Le Graph-of-Thought (Besta et al., 2023) modélise le raisonnement comme un graphe orienté \(G = (V, E)\) où :

  • Chaque noeud \(v \in V\) représente une pensée (un état intermédiaire du raisonnement).

  • Chaque arête \((v_i, v_j) \in E\) représente une transformation d’une pensée en une autre.

  • Trois operations sont definies sur les noeuds :

    • Génération : produire de nouvelles pensées à partir d’une pensée existante.

    • Agrégation (merging) : fusionner plusieurs pensées en une synthèse, créant des arêtes convergentes.

    • Raffinement : améliorer une pensée existante par auto-critique.

A la différence du ToT, le GoT autorise les arêtes convergentes (plusieurs parents pour un noeud), ce qui permet de combiner les informations de plusieurs pistes de raisonnement.

Remarque 40 (Variantes du GoT)

Plusieurs travaux étendent l’idée du GoT. Algorithm of Thoughts (Sel et al., 2023) encode la logique d’exploration (DFS, BFS) directement dans le prompt, guidant le LLM pour qu’il simule l’exploration en un seul appel. Skeleton-of-Thought décompose le problème en sous-questions résolues en parallèle. Cumulative Reasoning valide chaque pensée avant de l’ajouter au graphe de connaissances du problème.

Propriété 9 (Expressivité du GoT)

Le GoT est strictement plus expressif que le ToT : tout arbre est un graphe sans cycle ni arête convergente, mais le GoT autorise en plus (1) les arêtes convergentes (agregation), (2) les boucles (raffinement itératif) et (3) les arêtes de saut (connexion entre noeuds de profondeurs non adjacentes). Cette expressivité supplémentaire permet de représenter des raisonnements où la synthèse de plusieurs sous-résultats est nécessaire.

Remarque 41 (Ancrage et coéerence dans les graphes de pensées)

L’avantage principal du GoT est la capacité de synthèse : fusionner les meilleurs éléments de plusieurs pistes. Cependant, cette fléxibilité introduit un risque de contradiction entre pensées fusionnées. La vérification de cohérence est donc un composant critique, souvent implémentée comme un prompt de validation supplémentaire.

Hide code cell source

# Graphe GoT : generation, fusion et raffinement
G = nx.DiGraph()
thoughts = {
    "P": ("Problème:\nx^2 - 5x + 6 = 0", "problem"),
    "T1": ("Factoriser\nle trinôme", "generation"),
    "T2": ("Calculer le\ndiscriminant", "generation"),
    "T3": ("(x-2)(x-3)=0", "generation"),
    "T4": ("D=25-24=1\nx=(5+-1)/2", "generation"),
    "T5": ("Fusion: x=2, x=3\n(2 méthodes)", "aggregation"),
    "T6": ("Vérif: 4-10+6=0\n9-15+6=0", "refinement"),
}
for tid, (label, ntype) in thoughts.items():
    G.add_node(tid, label=label, ntype=ntype)
G.add_edges_from([("P","T1"),("P","T2"),("T1","T3"),("T2","T4"),("T3","T5"),("T4","T5"),("T5","T6")])

tcol = {"problem":"#4C72B0","generation":"#55A868","aggregation":"#C44E52","refinement":"#DD8452"}
pos = {"P":(0,3),"T1":(-2,2),"T2":(2,2),"T3":(-2,1),"T4":(2,1),"T5":(0,0),"T6":(0,-1)}

fig, ax = plt.subplots(figsize=(10, 7))
nx.draw(G, pos, ax=ax, with_labels=False, node_shape="s", node_size=2500,
        node_color=[tcol[G.nodes[n]["ntype"]] for n in G.nodes],
        edge_color="#555", arrows=True, arrowsize=14)
nx.draw_networkx_labels(G, pos, {n: G.nodes[n]["label"] for n in G.nodes}, font_size=7, ax=ax)
ax.legend(handles=[mpatches.Patch(color=c, label=l) for l, c in
    [("Problème","#4C72B0"),("Génération","#55A868"),("Agregation","#C44E52"),("Raffinement","#DD8452")]],
    loc="upper right", fontsize=9)
ax.set_title("Graph-of-Thought : génération, fusion et raffinement", fontsize=13)
plt.show()
_images/8fa0727249663caa6181e7eaab020ac6973d92cff5f316049e0fd0edda959868.png

ReAct : raisonner et agir#

Les paradigmes précédents fonctionnent « en vase clos ». ReAct (Yao et al., 2022) rompt cette autarcie en entrelacant raisonnement et action, permettant au modèle d’interagir avec des outils externes.

Définition 36 (ReAct (Reasoning + Acting))

ReAct est un paradigme où le LLM alterne trois types d’étapes :

  1. Thought : le modèle raisonne sur la situation courante et planifie la prochaine action.

  2. Action : le modèle émet une commande vers un outil externe (recherche, calculatrice, API, interpréteur de code).

  3. Observation : le résultat de l’action est retourné au modèle sous forme textuelle.

Le cycle se repète jusqu’à la réponse finale. Formellement :

\[\text{ReAct} : (q, T_1, A_1, O_1, T_2, A_2, O_2, \ldots, T_n, A_n, O_n) \to r\]

\(q\) est la question, \(T_i\) la \(i\)-ème pensée, \(A_i\) la \(i\)-ème action, \(O_i\) la \(i\)-ème observation et \(r\) la réponse.

ReAct est le prototype de la boucle agent (agent loop) qui sera approfondie au chapitre 12. Le modèle n’est plus un système question-réponse statique mais un agent qui perçoit, raisonne et agit dans un environnement.

Propriété 10 (ReAct et ancrage factuel)

Soit \(p_{\text{halluc}}\) la probabilité qu’un LLM produise une réponse factuellement incorrecte. En mode CoT, \(p_{\text{halluc}}\) reste constant à chaque étape. En mode ReAct, chaque observation \(O_i\) fournit une contrainte factuelle qui réduit l’espace des réponses possibles. Expérimentalement, Yao et al. (2022) montrent que ReAct réduit le taux d’hallucination de 20 a 40 % sur les benchmarks de question-réponse multi-sauts (HotpotQA).

Remarque 42 (La boucle d’observation)

La force de ReAct réside dans le fait que les observations corrigent le raisonnement en temps réel. Si le modèle émet une hypothèse erronée, un appel d’outil retourne l’information correcte, et la pensée suivante intègre cette correction. Ce mécanisme d”ancrage factuel (grounding) réduit considérablement les hallucinations.

Le modèle décompose le problème en sous-questions et utilise un outil de recherche pour chaque étape factuelle.

Hide code cell source

# Simulation ReAct avec outils fictifs

class MockSearch:
    """Recherche avec résultats pré-enregistrés."""
    DB = {"tour eiffel hauteur": "La tour Eiffel mesure 330 m (avec antenne).",
          "tour eiffel poids": "La structure métallique pèse environ 7 300 tonnes.",
          "densite acier": "La densité de l'acier est d'environ 7 850 kg/m3."}
    def __call__(self, q):
        for k, v in self.DB.items():
            if all(w in q.lower() for w in k.split()):
                return v
        return "Aucun résultat."

class Calculator:
    """Calculatrice simple."""
    def __call__(self, expr):
        try:
            return f"Resultat : {eval(expr, {'__builtins__': {}})}"
        except Exception as e:
            return f"Erreur : {e}"

search, calc = MockSearch(), Calculator()
question = "Combien pèse la tour Eiffel par mètre de hauteur ?"

trace = [
    ("question",     question),
    ("thought",      "Je dois trouver le poids et la hauteur de la tour Eiffel."),
    ("action",       'search("tour Eiffel hauteur")'),
    ("observation",  search("tour Eiffel hauteur")),
    ("thought",      "330 m. Maintenant le poids."),
    ("action",       'search("tour Eiffel poids")'),
    ("observation",  search("tour Eiffel poids")),
    ("thought",      "7 300 t / 330 m. Je calcule."),
    ("action",       "calculate(7300 / 330)"),
    ("observation",  calc("7300 / 330")),
    ("thought",      "Environ 22,1 t/m. J'ai la réponse."),
    ("action",       'finish("~22,1 tonnes par mètre (7 300 t / 330 m).")'),
]

prefixes = {"question": "Question", "thought": "Thought", "action": "Action", "observation": "Observation"}
for typ, content in trace:
    print(f"[{prefixes[typ]:>11}] {content}\n")
[   Question] Combien pèse la tour Eiffel par mètre de hauteur ?

[    Thought] Je dois trouver le poids et la hauteur de la tour Eiffel.

[     Action] search("tour Eiffel hauteur")

[Observation] La tour Eiffel mesure 330 m (avec antenne).

[    Thought] 330 m. Maintenant le poids.

[     Action] search("tour Eiffel poids")

[Observation] La structure métallique pèse environ 7 300 tonnes.

[    Thought] 7 300 t / 330 m. Je calcule.

[     Action] calculate(7300 / 330)

[Observation] Resultat : 22.12121212121212

[    Thought] Environ 22,1 t/m. J'ai la réponse.

[     Action] finish("~22,1 tonnes par mètre (7 300 t / 330 m).")

Hide code cell source

# Timeline de la trace ReAct
# Reconstruire la trace pour la visualisation
search_r, calc_r = MockSearch(), Calculator()
trace_viz = [
    ("thought",      "Je dois trouver le poids et la hauteur de la tour Eiffel."),
    ("action",       'search("tour Eiffel hauteur")'),
    ("observation",  search_r("tour Eiffel hauteur")),
    ("thought",      "330 m. Maintenant le poids."),
    ("action",       'search("tour Eiffel poids")'),
    ("observation",  search_r("tour Eiffel poids")),
    ("thought",      "7 300 t / 330 m. Je calcule."),
    ("action",       "calculate(7300 / 330)"),
    ("observation",  calc_r("7300 / 330")),
    ("thought",      "Environ 22,1 t/m. J'ai la réponse."),
    ("action",       'finish("~22,1 tonnes par mètre (7 300 t / 330 m).")'),
]

steps = [(t, c) for t, c in trace_viz if t != "question"]
tcol_r = {"thought": "#55A868", "action": "#C44E52", "observation": "#DD8452"}

fig, ax = plt.subplots(figsize=(12, 5))
for i, (typ, content) in enumerate(reversed(steps)):
    ax.barh(i, 1, color=tcol_r[typ], edgecolor="white", height=0.7)
    txt = content[:70] + ("..." if len(content) > 70 else "")
    ax.text(1.05, i, f"{typ.capitalize()}: {txt}", va="center", fontsize=7.5)

ax.set_yticks([]); ax.set_xticks([]); ax.set_xlim(0, 8)
ax.set_title("Trace ReAct : alternance Thought / Action / Observation", fontsize=13)
ax.legend(handles=[mpatches.Patch(color=c, label=l) for l, c in
    [("Thought","#55A868"),("Action","#C44E52"),("Observation","#DD8452")]],
    loc="lower right", fontsize=9)
plt.show()
_images/5020668da2d160acd849d4e5499c7fc3d7f9b0eb2cbbea8a40d81a3946cccf6e.png

Tool use et function calling#

ReAct démontre l’intérêt d’équiper le LLM d’outils. Les API modernes (Claude, GPT-4, Gemini) formalisent cette idée via le tool use et le function calling.

Définition 37 (Tool use)

Le tool use designe la capacité d’un LLM à : (1) reconnaitre qu’une question nécessite un outil externe, (2) sélectionner l’outil approprié, (3) formuler l’appel dans un format structure (JSON), et (4) intégrer le résultat dans sa réponse. Le LLM n’exécute pas l’outil : il produit une requête d’appel que le système hôte intercepte et exécute.

Définition 38 (Function calling)

Le function calling est l’implémentation concrète du tool use dans les API. Le développeur définit des fonctions via un schéma JSON décrivant le nom, la description en langage naturel et les paramètres (types, contraintes). Lors de l’inférence, le modèle peut choisir d’appeler une ou plusieurs fonctions ; le système hôte exécute et retourne le résultat.

Exemple 29 (Définition d’un outil par JSON Schema)

Voici une définition d’outil typique pour une API de LLM :

{
  "name": "get_weather",
  "description": "Obtenir la météo actuelle pour une ville donnée.",
  "input_schema": {
    "type": "object",
    "properties": {
      "city": {"type": "string", "description": "Nom de la ville"},
      "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
    },
    "required": ["city"]
  }
}

Le modèle produit alors un appel structure {"type": "tool_use", "name": "get_weather", "input": {"city": "Paris", "unit": "celsius"}}. Le système hôte exécute la fonction et retourne le résultat au modèle.

Remarque 43 (Parallèle tool use et plug-ins)

Le tool use transforme le LLM d’un système fermé en un système augmenté (tool-augmented LLM) qui peut calculer avec précision, accéder à des données actuelles, exécuter du code et intéragir avec des systèmes externes. On peut tracer un parallèle avec les plug-ins dans les logiciels classiques : le LLM est le noyau, et les outils sont des extensions qui lui confèrent des capacités spécifiques sans modifier ses poids.

Exemple 30 (Exécution de code comme outil)

Un LLM equipé d’un interpréteur Python peut résoudre des problèmes numériques :

Thought : Je vais calculer le 50e nombre premier avec un script Python.
Action : execute_python("
    primes, n = [], 2
    while len(primes) < 50:
        if all(n % p for p in primes if p*p <= n): primes.append(n)
        n += 1
    print(primes[-1])
")
Observation : 229
Réponse : Le 50e nombre premier est 229.

Sans outil de calcul, le LLM aurait probablement halluciné une valeur incorrecte.

Hide code cell source

# Simulation du pipeline function calling

import json

tools_def = [
    {"name": "get_population", "description": "Population d'un pays.",
     "input_schema": {"type": "object", "properties": {"country": {"type": "string"}}, "required": ["country"]}},
    {"name": "calculate", "description": "Calcul mathématique.",
     "input_schema": {"type": "object", "properties": {"expression": {"type": "string"}}, "required": ["expression"]}},
]

mock_db = {"France": 68_042_591, "Allemagne": 84_482_267, "Japon": 123_294_513}

# Pipeline simulé
user_msg = "Densité de population de la France (superficie : 643 801 km2) ?"
call_1 = {"type": "tool_use", "name": "get_population", "input": {"country": "France"}}
result_1 = {"population": mock_db["France"]}
call_2 = {"type": "tool_use", "name": "calculate", "input": {"expression": "68042591 / 643801"}}
result_2 = round(68042591 / 643801, 1)

print(f"[Utilisateur]   {user_msg}")
print(f"[Modèle->Outil]  {json.dumps(call_1, ensure_ascii=False)}")
print(f"[Outil->Modèle]  {json.dumps(result_1)}")
print(f"[Modèle->Outil]  {json.dumps(call_2, ensure_ascii=False)}")
print(f"[Outil->Modèle]  {result_2}")
print(f"[Modèle]         La France : {mock_db['France']:,} hab. / 643 801 km2 = {result_2} hab./km2.")
[Utilisateur]   Densité de population de la France (superficie : 643 801 km2) ?
[Modèle->Outil]  {"type": "tool_use", "name": "get_population", "input": {"country": "France"}}
[Outil->Modèle]  {"population": 68042591}
[Modèle->Outil]  {"type": "tool_use", "name": "calculate", "input": {"expression": "68042591 / 643801"}}
[Outil->Modèle]  105.7
[Modèle]         La France : 68,042,591 hab. / 643 801 km2 = 105.7 hab./km2.

Hide code cell source

# Diagramme de sequence du function calling

fig, ax = plt.subplots(figsize=(11, 6))
actors = ["Utilisateur", "LLM", "Système hôte", "Outil"]
ax_pos = {a: i*2.5 for i, a in enumerate(actors)}

for a, x in ax_pos.items():
    ax.plot([x, x], [0, -7.5], color="#AAA", lw=1, ls="--")
    ax.text(x, 0.3, a, ha="center", fontsize=9, fontweight="bold",
            bbox=dict(boxstyle="round,pad=0.3", fc="#4C72B0", ec="none", alpha=0.8), color="white")

msgs = [(0,2.5,-1,"Question",".3"), (2.5,5,-2,"tool_use: get_population","C44E52"),
        (5,7.5,-3,"Appel API","DD8452"), (7.5,5,-4,"Résultat","DD8452"),
        (5,2.5,-4.5,"population: 68M","C44E52"), (2.5,5,-5.5,"tool_use: calculate","C44E52"),
        (5,2.5,-6.5,"105.7","C44E52"), (2.5,0,-7.2,"Réponse finale","55A868")]
for xf, xt, y, lbl, c in msgs:
    ax.annotate("", xy=(xt,y), xytext=(xf,y), arrowprops=dict(arrowstyle="->", color=f"#{c}" if len(c)==6 else c, lw=1.5))
    ax.text((xf+xt)/2, y+0.22, lbl, ha="center", fontsize=7.5, color=f"#{c}" if len(c)==6 else c)

ax.set_xlim(-1, 8.5); ax.set_ylim(-8, 1); ax.set_xticks([]); ax.set_yticks([])
ax.set_title("Function calling : diagramme de séquence", fontsize=12)
plt.show()
_images/9aa6e9c1a0e81d1d2d88da5538a60aa49cf1ee5ae7562ca01fbf0ce1ff5258cf.png

Comparaison des paradigmes de raisonnement#

Les paradigmes présentés forment un spectre de stratégies. Le choix depend du compromis entre précision, coût et accès aux informations externes.

Hide code cell source

import pandas as pd

comparison = pd.DataFrame({
    "Paradigme": ["CoT", "CoT-SC", "ToT", "GoT", "ReAct"],
    "Structure": ["Chaine", "Chaines multiples", "Arbre", "Graphe", "Boucle agent"],
    "Exploration": ["Non", "Echantillonnage", "BFS/DFS", "BFS/DFS + fusion", "Séquentielle"],
    "Retour arrière": ["Non", "Non", "Oui", "Oui", "Non"],
    "Outils": ["Non", "Non", "Non", "Non", "Oui"],
    "Appels LLM": ["1", "~k", "~b^d", "~b^d + fusions", "~2n"],
    "Cas d'usage": ["Simple", "Maths", "Puzzles", "Composites", "QA factuel"],
})
print(comparison.to_string(index=False))
Paradigme         Structure      Exploration Retour arrière Outils     Appels LLM Cas d'usage
      CoT            Chaine              Non            Non    Non              1      Simple
   CoT-SC Chaines multiples  Echantillonnage            Non    Non             ~k       Maths
      ToT             Arbre          BFS/DFS            Oui    Non           ~b^d     Puzzles
      GoT            Graphe BFS/DFS + fusion            Oui    Non ~b^d + fusions  Composites
    ReAct      Boucle agent     Séquentielle            Non    Oui            ~2n  QA factuel

Hide code cell source

# Radar chart comparatif

categories = ["Précision", "Coût faible", "Latence faible", "Exploration", "Ancrage factuel"]
angles = np.linspace(0, 2*np.pi, len(categories), endpoint=False).tolist() + [0]

paradigms = {"CoT": [0.6,0.95,0.95,0.1,0.1], "ToT": [0.85,0.3,0.2,0.9,0.1],
             "GoT": [0.88,0.25,0.18,0.95,0.1], "ReAct": [0.82,0.6,0.5,0.3,0.95]}
col_r = {"CoT":"#4C72B0", "ToT":"#55A868", "GoT":"#C44E52", "ReAct":"#DD8452"}

fig, ax = plt.subplots(figsize=(7, 7), subplot_kw=dict(polar=True))
for name, vals in paradigms.items():
    v = vals + vals[:1]
    ax.plot(angles, v, "o-", lw=2, label=name, color=col_r[name])
    ax.fill(angles, v, alpha=0.1, color=col_r[name])

ax.set_xticks(angles[:-1]); ax.set_xticklabels(categories, fontsize=9)
ax.set_ylim(0, 1); ax.set_yticks([0.2, 0.4, 0.6, 0.8, 1.0])
ax.set_title("Comparaison des paradigmes de raisonnement", fontsize=13, pad=20)
ax.legend(loc="upper right", bbox_to_anchor=(1.3, 1.1), fontsize=10)
plt.show()
_images/118ea631069a55a236bdfab783ec56a0e4af88b1c094b35c7a68759478313242.png

Remarque 44 (Guide de choix)

En pratique, la sélection du paradigme dépend de trois facteurs : (1) la nature du problème — un problème combinatoire (puzzle, planification) bénéficie du ToT ou du GoT, un problème factuel du ReAct ; (2) le budget computationnel — le CoT coûte un appel, le ToT des dizaines, ReAct ajoute la latence des outils ; (3) le besoin d’ancrage — si la réponse dépend de données actuelles, le tool use est indispensable. Ces paradigmes sont combinables : un agent ReAct peut utiliser du ToT en interne.

Résumé#

Ce chapitre a présenté les paradigmes de raisonnement avancé qui etendent le Chain-of-Thought linéaire.

  1. Le CoT linéaire souffre de trois limitations : absence d”exploration, de retour arrière et d”ancrage externe.

  2. Le Tree-of-Thought (ToT) structure le raisonnement en arbre de recherche avec générateur, évaluateur et algorithme d’exploration. Il subsume le CoT au prix d’un coût en \(O(b^d)\) appels.

  3. Le Graph-of-Thought (GoT) généralise le ToT en autorisant la fusion et le raffinement de pensées issues de branches différentes.

  4. ReAct entrelace Thought, Action et Observation dans une boucle agent, permettant l’interaction avec des outils externes et l’ancrage factuel.

  5. Le tool use et le function calling formalisent l’integration d’outils dans les API via des schemas JSON.

  6. Le choix du paradigme dépend de la nature du problème, du budget computationnel et du besoin d’ancrage dans des données externes.

  7. Ces paradigmes sont combinables et préfigurent les systèmes agents autonomes — thématique développée dans la partie IV.

Remarque 45

Les paradigmes de raisonnement avancé illustrent le passage du LLM passif vers un système actif qui explore, évalue, agit et s’auto-corrige. Cette évolution brouille la frontière entre modèle de langage et agent intelligent. Comprendre ces paradigmes est essentiel pour concevoir des systèmes LLM robustes, car le choix de la stratégie de raisonnement détermine directement la qualité, le coût et la fiabilité des réponses.