Phase 6 (finale) de l’epic #2926 — suite de NB-12 (moteurs) a NB-17 (raisonnement natif). Ce notebook clot #2926 en exposant les moteurs de test-time scaling (BoN, Reflexion, routeur) comme des plugins Semantic Kernel (SK), faisant le pont avec la serie SemanticKernel du depot.
Pourquoi des plugins ?
Jusqu’ici (NB-12 a NB-17), les moteurs etaient des fonctions Python appelees a la main. Les exposer en plugins SK les rend : - decouvrables (le kernel connait leur schema : nom, description, paramètres types), - composables (un plugin peut en invoquer un autre via le kernel), - invocables par le LLM (auto-function-calling : le modèle choisit l’outil), - interoperables avec le reste de l’ecosysteme SK (agents, process framework, MCP — serie SemanticKernel NB-03/06/08).
Ide centrale : le Kernel est un hub de composition. Les moteurs de test-time scaling deviennent des outils que le kernel peut orchestrer — exactement la couche d’integration que SK apporte au-dessus des appels API bruts.
Plan
Setup : Kernel + service SK (OpenAI/OpenRouter).
Plugin BoN (best_of_n).
Plugin Reflexion (reflexion avec verificateur).
Plugin Routeur (compose : invoque BoN ou Reflexion selon la stratégie).
Composition via le kernel + pont auto-function-calling (exercice) vers la serie SemanticKernel.
Ce notebook est le maillon outillage de la veine test-time scaling de la serie Texte : les techniques ont ete introduites sans framework (echantillonnage multiple dans le NB precedent), ici on les reformule en plugins Semantic Kernel pour qu’elles deviennent des briques composables. La difference est structurante : une fonction Python ordinaire n’est visible que du code qui l’appelle ; un plugin enregistre dans le Kernel expose sa signature et sa docstring au LLM, ce qui permet ensuite au modele lui-meme de choisir et d’invoquer la bonne strategie (auto-function-calling, exercice 1) ou a un plugin d’en invoquer un autre (routeur, section 4). C’est exactement la frontiere entre ecrire un script qui scale le test-time et construire une architecture ou le scaling du test-time est un service.
0. Setup — Kernel Semantic Kernel + service OpenRouter
Semantic Kernel (Python, v1.x) se connecte a OpenAI. On le branche sur OpenRouter (comme NB-12 a NB-17) via un client AsyncOpenAI passe au connecteur OpenAIChatCompletion.
Deux conventions de la serie : - Service unique : le Kernel est construit une fois avec le service OpenRouter ; le modele fast est lu dans l’environnement (FAST_MODEL), jamais en dur dans le code. - Determinisme affiche : le ping de la cellule suivante n’est pas une politesse de setup – il atteste que l’agent Python parle bien au service. Tout ce qui suit echoue de facon confuse si cette reponse n’est pas OK (cle absente, quota epuise, mauvais endpoint).
# Dependances pre-provisionnees (semantic-kernel, python-dotenv) :# voir GenAI/requirements.txt. Regle F : pas d'install runtime sur le PUBLIC repo.import os, re, asynciofrom pathlib import Pathfrom dotenv import load_dotenvfrom openai import AsyncOpenAIimport semantic_kernel as skfrom semantic_kernel import Kernelfrom semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, OpenAIPromptExecutionSettingsfrom semantic_kernel.contents import ChatHistoryfrom semantic_kernel.functions import kernel_function, KernelArguments_env_path =None_current = Path.cwd()for _i inrange(10):if (_current /".env").exists(): _env_path = _current /".env";breakif _current.name in ("GenAI", "MyIA.AI.Notebooks"):break _current = _current.parentif _env_path isNone:for _cand in (Path.cwd() /"MyIA.AI.Notebooks"/"GenAI"/".env", Path.cwd() /"GenAI"/".env"):if _cand.exists(): _env_path = _cand;breakif _env_path isnotNone: load_dotenv(_env_path);print(f".env charge depuis : {_env_path.name}")print(f"semantic-kernel {sk.__version__}")
semantic-kernel 1.42.0
FAST_MODEL = os.getenv("OPENAI_MODEL_FAST", "meta-llama/llama-3.3-70b-instruct")def make_kernel():"""Construit un Kernel SK branche sur OpenRouter (AsyncOpenAI custom).""" aoai = AsyncOpenAI(api_key=os.getenv("OPENROUTER_API_KEY"), base_url=os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1")) kernel = Kernel() kernel.add_service(OpenAIChatCompletion(ai_model_id=FAST_MODEL, async_client=aoai, service_id="or"))return kernelasyncdef llm(kernel, prompt, temperature=0.7, max_tokens=80):"""Helper : un appel au service SK (cache la ceremony ChatHistory).""" svc = kernel.get_service("or") ch = ChatHistory(); ch.add_user_message(prompt) settings = OpenAIPromptExecutionSettings(max_tokens=max_tokens, temperature=temperature) r =await svc.get_chat_message_contents(chat_history=ch, settings=settings)returnstr(r[0])kernel = make_kernel()_ping =await llm(kernel, "Reponds uniquement par : OK", temperature=0.0, max_tokens=10)print("Ping SK->OpenRouter :", repr(_ping[:30]))
Ping SK->OpenRouter : 'OK'
1. Le modèle plugin de Semantic Kernel
Un plugin SK = une classe Python dont les méthodes sont decorees par @kernel_function. SK extrait le schema (nom, description, paramètres + types) depuis la signature et la docstring → le kernel sait les decouvrir et les invoquer. Les arguments passent par KernelArguments(**params).
Pont vers la serie SemanticKernel : voir MyIA.AI.Notebooks/GenAI/SemanticKernel/01-SemanticKernel-Intro.ipynb (plugins de base) et 03-SemanticKernel-Agents.ipynb (agents qui orchestrent des plugins).
On définit maintenant un verificateur reutilisable (extraction d’entier, herite de NB-16/17), puis nos trois plugins.
Le modele mental a retenir : un plugin SK est une fonction ordinaire plus de la metadonnee. Le decorateur @kernel_function publie le nom, la signature et la description de la fonction dans le kernel ; le nom des parametres et leurs annotations deviennent le contrat que le LLM utilisera pour l’appel automatique. Une fois enregistre via kernel.add_plugin, le plugin vit dans kernel.plugins sous un namespace – on verra dans la cellule 8 que la cle du dictionnaire suffit a invoquer la fonction.
La fonction d’extraction qui suit est volontairement banale (un compte de passagers) : l’interet du pattern ne reside pas dans la tache mais dans ce qu’elle rend observable – une reponse unique d’un modele unique, le point de depart auquel toutes les strategies de scaling seront comparees.
def extraire_nombre(texte): m = re.findall(r'-?\d+', texte or"")returnint(m[0]) if m elseNone# Probleme de demo (reponse entiere verifiable) : sert aux demos BoN + Reflexion + routeur.# On choisit un probleme "moyen" que le modele resout fiablement, pour demontrer le MECANISME# plugin/composition SK (le but de ce notebook) — pas la robustesse du modele (cf NB-16/17).DEMO = ("Un train part avec 45 passagers. 12 montent au suivant, 7 descendent. ""Combien reste-t-il de passagers ? Reponds uniquement par le nombre.")DEMO_REPONSE =50print("Demo :", DEMO, "| attendu :", DEMO_REPONSE)
Demo : Un train part avec 45 passagers. 12 montent au suivant, 7 descendent. Combien reste-t-il de passagers ? Reponds uniquement par le nombre. | attendu : 50
2. Plugin BoN — best_of_n
Le moteur Best-of-N (NB-12/NB-16) en plugin : genere n echantillons, renvoie la liste des reponses (l’appeleur peut ensuite les verifier / voter). Le plugin depend du kernel (pour le service) — passe au constructeur.
Best-of-N est la strategie de scaling la plus simple : echantillonner n reponses independantes et garder la meilleure selon un verificateur. Ici le verificateur est trivial (egalite a la valeur attendue) ; dans la litterature (self-consistency, Wang et al. 2022) il devient un vote majoritaire, un modele juge, ou un executeur de code. Deux parametres structurent le cout : - n multiplie lineairement les appels – c’est le budget de calcul depense au test-time ; - la qualite du verificateur plafonne le gain : BoN ne remonte jamais au-dela de ce qu’il sait reconnaitre.
La classe BoNPlugin qui suit emballe cette strategie : la boucle d’echantillonnage vit dans la fonction, la metadonnee (nom, arguments prompt et n=3) dans le decorateur.
class BoNPlugin:"""Plugin SK : Best-of-N sampling (test-time scaling parallel)."""def__init__(self, kernel): self._k = kernel@kernel_function(name="best_of_n", description="Genere n reponses independantes a un prompt.")asyncdef best_of_n(self, prompt: str, n: int=3) ->str: reps = []for _ inrange(n): reps.append((await llm(self._k, prompt, temperature=0.8)).strip())return" || ".join(reps)kernel.add_plugin(BoNPlugin(kernel), plugin_name="bon")print("Plugin 'bon' enregistre : fonction best_of_n(prompt: str, n: int = 3).")
Plugin 'bon' enregistre : fonction best_of_n(prompt: str, n: int = 3).
# Demo : invoque best_of_n via le kernel (pattern plug['fn'].invoke(kernel, args))._plug_bon = kernel.plugins["bon"]_res_bon =await _plug_bon["best_of_n"].invoke(kernel, KernelArguments(prompt=DEMO, n=4))print("BoN (n=4) reponses :", _res_bon)print(f"Correctes (== {DEMO_REPONSE}) :", [r for r instr(_res_bon).split(' || ') if extraire_nombre(r) == DEMO_REPONSE])
La sortie montre les 4 echantillons (50 || 50 || 50 || 50) puis le verdict du verificateur : les quatre sont corrects. Ce n’est pas une demonstration de puissance – c’est la mesure de base du notebook. Sur une question ou un seul echantillon reussit deja systematiquement, BoN n’apporte rien : les appels supplementaires sont du budget brule. La ligne Correctes (== 50) est la carte d’identite de la tache (taux de reussite de base proche de 100 %), la reference par rapport a laquelle une tache plus dure – et une strategie plus riche – se jugent dans les sections suivantes.
Le moteur Reflexion (NB-12/NB-14) en plugin : K tours, verifie la reponse, renvoie le diagnostic a l’LLM si rate. On injecte un verificateur (fonction reponse -> bool) au constructeur — c’est le pattern d’injection de dépendance de SK (le plugin ne sait pas verifier, on lui fournit comment).
Reflexion (Reflexion, Shinn et al. 2023 ; Self-Refine, Madaan et al. 2023) attaque le meme probleme que BoN – lever le plafond d’un seul echantillon – mais depense le budget differemment : au lieu de n essais paralleles et oublies, elle fait jusqu’a k essais sequentiels et memoires. Chaque echec est suivi d’une critique verbalisee qui conditionne la tentative suivante ; le verificateur interrompt des qu’une reponse passe.
Le trade-off est inverse de BoN : moins d’appels en moyenne quand la critique est informative (arret precoce des le premier succes), mais une latence qui croit en serie, la ou BoN peut paralleliser. La classe ReflexionPlugin materialise la boucle – essai, verification, critique, nouvel essai – et le compteur K borne la depense.
class ReflexionPlugin:"""Plugin SK : Reflexion sequentielle avec verificateur injecte."""def__init__(self, kernel, verifier):self._k = kernel;self._verif = verifier@kernel_function(name="reflexion", description="K tours de Reflexion avec feedback du verificateur.")asyncdef reflexion(self, prompt: str, k: int=3) ->str: feedback =""for tour inrange(1, k +1):ifnot feedback: invite = promptelse: invite = (f"{prompt}\n\nEssai precedent INCORRECT. Feedback : {feedback}\n"f"Reessaie correctement. Reponds uniquement par le nombre.") rep = (await llm(self._k, invite, temperature=0.8)).strip()ifself._verif(rep):returnf"SUCCES tour {tour} : {rep}" feedback =f"ta reponse etait {extraire_nombre(rep)} (incorrect)"returnf"ECHEC apres {k} tours : {rep}"# Verificateur pour la demo (reponse attendue == DEMO_REPONSE, ici 50)._verif_demo =lambda t: extraire_nombre(t) == DEMO_REPONSEkernel.add_plugin(ReflexionPlugin(kernel, _verif_demo), plugin_name="reflexion")print("Plugin 'reflexion' enregistre : fonction reflexion(prompt, k=3).")
Plugin 'reflexion' enregistre : fonction reflexion(prompt, k=3).
Lecture du resultat : la boucle s’arrete des qu’elle peut
Reflexion (K=3) : SUCCES tour 1 : 50 dit deux choses : la reponse est correcte (le 50 attendu), et surtout le verificateur a interrompu la boucle au premier tour – les deux tours restants du budget K=3 n’ont jamais ete depenses. C’est l’avantage economique structurel de Reflexion sur BoN : BoN depense toujours ses n appels, Reflexion depense en moyenne 1/p appels ou p est le taux de reussite par tour. Sur cette tache facile les deux convergent (un seul appel utile de part et d’autre) ; l’ecart n’apparaitrait que sur une tache ou le taux de base est faible – exactement ce que l’exercice 1 invite a mesurer.
4. Plugin Routeur — composition (un plugin qui en invoque d’autres)
Le routeur (NB-13) en plugin : selon une stratégie (‘bon’ ou ‘reflexion’), il invoque le bon moteur via le kernel. C’est la composition SK : un plugin orchestre d’autres plugins, le kernel reste le hub central.
La composition est le vrai gain d’architecture : un plugin peut en invoquer un autre via le kernel plutot que dupliquer la logique. Le RouteurPlugin ne sait ni echantillonner ni se corriger ; il sait choisir entre bon et reflexion selon une strategie declaree, et deleguer. C’est le pattern micro-service applique au scaling : chaque strategie reste une brique testable isolement, le routeur n’est que de l’ordonnancement.
Notez dans la cellule d’enregistrement que routeur apparait dans la liste des plugins disponibles aux cotes de ceux qu’il orchestre : la composition n’est pas un cas special du framework, c’est le meme mecanisme d’enregistrement, recursif.
class RouteurPlugin:"""Plugin SK : routeur qui compose BoN/Reflexion via le kernel (pattern composition)."""def__init__(self, kernel): self._k = kernel@kernel_function(name="resoudre", description="Resout un probleme : strategie 'bon' ou 'reflexion'.")asyncdef resoudre(self, prompt: str, strategie: str="reflexion", n: int=4, k: int=3) ->str:if strategie =="bon": fn =self._k.plugins["bon"]["best_of_n"] r =await fn.invoke(self._k, KernelArguments(prompt=prompt, n=n))returnf"[routeur->BoN n={n}] {r}"elif strategie =="reflexion": fn =self._k.plugins["reflexion"]["reflexion"] r =await fn.invoke(self._k, KernelArguments(prompt=prompt, k=k))returnf"[routeur->Reflexion k={k}] {r}"returnf"[routeur] strategie inconnue : {strategie}"kernel.add_plugin(RouteurPlugin(kernel), plugin_name="routeur")print("Plugin 'routeur' enregistre. Plugins disponibles :", list(kernel.plugins))
Lecture du resultat : une strategie, deux profils de depense
Les deux lignes executent la meme question par deux chemins : - strategie=reflexion -> [routeur->Reflexion k=3] SUCCES tour 1 : 50 : un seul appel utile, arret precoce du verificateur ; - strategie=bon -> [routeur->BoN n=4] 50 || 50 || 50 || 50 : quatre appels systematiques, verificateur applique a la fin.
Le resultat final est identique (50) – la difference est entierement dans le profil de cout : latence serie minimale cote Reflexion, parallelisable mais budget fixe cote BoN. C’est la decision que fait un vrai routeur en production (et que le modele doit apprendre a prendre seul en exercice 1) : choisir la strategie dont le profil de depense correspond a la contrainte dominante, pas seulement celle qui reussit.
5. Composition via le Kernel — au-dela de l’invocation manuelle
On a invoque les plugins manuellement (plug['fn'].invoke(kernel, args)). La puissance de SK est l’auto-function-calling : on enregistre les plugins, et le LLM choisit lui-même quel plugin appeler (avec quels arguments) en voyant leur schema — c’est le routeur agentique natif de SK (cf serie SemanticKernel 03-Agents et NB-13 routeur).
Pont : l’auto-function-calling SK generalise le routeur ad-hoc de NB-13. La serie SemanticKernel (NB-01 intro, NB-03 agents, NB-06 process framework, NB-08 MCP) couvre en profondeur l’orchestration de plugins ; ce notebook est le point d’entree depuis la serie test-time scaling.
L’auto-function-calling (boucle tool-use geree par SK) est laisse en exercice 1 — il demande d’activer tool_call_behavior sur les settings et de gerer la boucle, ce qui est le sujet des notebooks dedies de la serie SemanticKernel.
6. Limites honnetes (G.2) et synthesis de l’epic #2926
Plugins = enveloppe : la logique des moteurs (BoN/Reflexion/routeur) est heritee de NB-12 a NB-17 ; ce notebook ajoute la couche d’integration SK (decouverte, composition, interop), pas de nouvel algorithme. C’est honnête : la valeur ici est l’architecture, pas une nouvelle technique de scaling.
Routeur manuel : la composition est explicite (stratégie='bon'). L’auto-routing par le LLM (exercice 1) est plus puissant mais token-ponderereux et instable — laissé en exercice.
Une seule famille de plugins (scaling) : SK brille avec des plugins heterogenes (recherche, code, BDD) ; la serie SemanticKernel montre cela en profondeur.
Synthesis de l’epic #2926 (test-time scaling / ICR) — 6 phases : - NB-13 routeur agentique (Ph1) — NB-14 memoire persistante (Ph2) — NB-15 ToT sur CSP (Ph3) — NB-16 scaling pass@k / Snell (Ph4) — NB-17 raisonnement natif vs scaling (Ph5) — NB-18 plugins SK (Ph6, ce notebook).
Cette section est volontairement sans code : l’invocation manuelle vue jusqu’ici est le degre zero. Le degre un est l’auto-function-calling – on donne un prompt au kernel et le LLM choisit lui-meme le plugin et les arguments (exercice 1) ; le degre deux est le Process Framework, qui orchestre des etapes asynchrones avec etat partage (exercice 3, pont vers la serie SemanticKernel). Les patterns ici restent volontairement au niveau ou ils sont observables dans une sortie de notebook.
7. Travaux pratiques
Les exercices sont a completer (convention C.1 : pas d’erreur volontaire).
Les trois exercices suivants montent en autonomie : (1) laisser le modele choisir le plugin, (2) ajouter de l’etat entre les appels, (3) passer a l’orchestration multi-etapes. Les cellules de code sont des squelettes – la ligne de livrable attendue est dans chaque enonce.
Exercice 1 : auto-function-calling (le LLM choisit le plugin)
Active l’auto-function-calling de SK : enregistre bon et reflexion, donne un prompt au LLM, et laisse SK appeler le bon plugin automatiquement. Compare le routing natif SK au routeur ad-hoc de NB-13.
Indice : OpenAIPromptExecutionSettings(tool_call_behavior=…) + kernel.invoke avec auto-call. Cf serie SemanticKernel 03-Agents et la doc SK “function calling”.
Indice : le kernel sait deja tout. Les plugins etant enregistres avec leurs signatures, il suffit d’appeler le service avec l’execution de fonctions activee et de laisser le modele boucler invocation/observation. Comparez ensuite le choix du modele a celui du routeur de la section 4 : quand deleguent-ils a la meme strategie ?
asyncdef auto_route(kernel, prompt):"""Exercice 1 : auto-function-calling — le LLM choisit entre bon et reflexion."""# TODO etudiant : activer tool_call_behavior, invoquer avec auto-call, retourner la trace.returnNoneprint(f"Exercice 1 - auto-function-calling : {'implemente'ifFalseelse'a completer'}")
Exercice 1 - auto-function-calling : a completer
Exercice 2 : plugin Memory (pont NB-14)
Reprend la memoire vectorielle de NB-14 et expose-la comme plugin SK : un plugin memoire.retroceder(query, k) + memoire.ajouter(lesson). Le routeur peut alors injecter les lecons passees avant d’appeler Reflexion.
Indice : wrap la MemoireVectorielle de NB-14 dans une classe avec @kernel_function sur retroceder/ajouter ; le routeur appelle memoire.retroceder puis reflexion.
Indice : un plugin n’a pas a etre sans etat. Une liste Python dans l’instance suffit pour une memoire de session ; le pont vers le NB 14 (memoire semantique) remplace cette liste par une recherche vectorielle. La question interessante est ce que le stockage change au comportement : reponses coherentes entre questions apparentees.
class MemoirePlugin:"""Exercice 2 : plugin SK wrappant la MemoireVectorielle de NB-14."""# TODO etudiant : __init__(kernel, memoire) + @kernel_function retroceder(query, k) + ajouter(lesson).passprint(f"Exercice 2 - plugin Memoire : {'implemente'ifFalseelse'a completer'}")
Exercice 2 - plugin Memoire : a completer
Exercice 3 (avance) : orchestrer via le Process Framework (serie SemanticKernel NB-06)
Encode le flux “routeur -> moteur -> verificateur -> memoire” comme un Process Framework SK (etats + transitions) plutot que des appels de plugins. Pont direct vers SemanticKernel/06-SemanticKernel-ProcessFramework.ipynb.
Indice : kernel.add_process(…) avec des steps (BoNStep, ReflexionStep, MemoryStep) et des edges ; le process orchestre les plugins sans orchestration manuelle.
Indice : le Process Framework remplace le routeur par un graphe declare d’etapes. Le NB 06 de la serie SemanticKernel en fait la demonstration complete ; ici, construire le graphe et le laisser tourner une fois suffit pour voir la difference de style avec la composition manuelle de la section 4.
def build_scaling_process(kernel):"""Exercice 3 : encode le flux test-time scaling comme un Process Framework SK."""# TODO etudiant : definir des ProcessStep + edges, retourner le process pret a demarrer.returnNoneprint(f"Exercice 3 - Process Framework : {'implemente'ifFalseelse'a completer'}")
Exercice 3 - Process Framework : a completer
8. Conclusion
On a expos les moteurs de test-time scaling (BoN, Reflexion, routeur — herites de NB-12 a NB-17) comme des plugins Semantic Kernel, demontrant le modèle de composition de SK : plugins decouvrables, invocables via le kernel, et composables (le routeur invoque BoN/Reflexion via le kernel). Ce notebook est le point d’entree depuis la serie test-time scaling vers la serie SemanticKernel (plugins, agents, process framework, MCP).
#2926 cloture : 6 phases livrees — routeur agentique (NB-13), memoire persistante (NB-14), ToT sur CSP (NB-15), scaling pass@k / Snell (NB-16), raisonnement natif vs scaling (NB-17), plugins SK (NB-18, ce notebook).
References : Semantic Kernel (Microsoft) — serie SemanticKernel/ du depot ; Snell et al. 2024 (test-time scaling) ; Yao et al. 2023 (Tree of Thoughts) ; NB-12 a NB-17 de cette serie.
Synthese : les quatre sections ont fait passer le scaling du test-time du statut de technique isolee a celui d’architecture composable. Trois choses a retenir : - Chaque strategie est un plugin : BoN (parallele, budget fixe), Reflexion (sequentielle, arret precoce), Routeur (composition) exposent le meme contrat – signature publiee au kernel, invocation uniforme. - Le cout est le vrai differentiateur : sur la tache temoin, toutes les strategies convergent vers 50 ; ce qui les distingue est le nombre et le profil temporel des appels (comparez les deux lignes du routeur). - La composition est recursive et neutre : le routeur est un plugin comme les autres, ce qui ouvre sur l’auto-function-calling (le LLM en routeur) et le Process Framework (le graphe en routeur).
Prolongements naturels : mesurer ces profils de cout sur une tache a faible taux de base (ou l’ecart BoN/Reflexion devient observable), croiser avec le NB 17 sur le raisonnement natif, et relire le chapitre test-time compute du dossier de reference de la serie.