batchCooking/experiments/llm-tech-step-poc
Nicolas b8e599106f feat(experiments): ajoute une entrée de volumétrie par locale basée sur TEST_RECIPE
TEST_SENTENCES gagne 2 entrées supplémentaires (fr-recette-complete,
en-recette-complete) qui concatènent les 7 étapes de TEST_RECIPE ("Tarte
aux pommes rustique") en un seul step par locale — même principe que
fr-concat-volumetrie/en-concat-volumetrie, mais sur du texte de recette
réel plutôt qu'une concaténation de phrases-pièges synthétiques.

TEST_RECIPE (déjà ajoutée localement) déplacée avant TEST_SENTENCES
(nécessaire pour être référencée dans sa construction) et reformatée à la
convention du fichier (clés non citées, virgules finales) ; contenu
inchangé. Retire le stub dummyRecipeFr/dummyRecipeEn de
benchmark-harness.ts : le harness générique n'a besoin d'aucun
cas particulier, TEST_SENTENCES suffit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 22:48:39 +02:00
..
src feat(experiments): ajoute une entrée de volumétrie par locale basée sur TEST_RECIPE 2026-08-21 22:48:39 +02:00
.gitignore feat(experiments): ajoute un PoC de détection d'actions culinaires par mini LLM local 2026-08-21 18:58:28 +02:00
package.json refactor(experiments): retire le script apps/api, ajoute NLP frais + pipeline hybride 2026-08-21 21:13:20 +02:00
pnpm-lock.yaml refactor(experiments): retire le script apps/api, ajoute NLP frais + pipeline hybride 2026-08-21 21:13:20 +02:00
README.md refactor(experiments): retire le script apps/api, ajoute NLP frais + pipeline hybride 2026-08-21 21:13:20 +02:00
tsconfig.json feat(experiments): ajoute un PoC de détection d'actions culinaires par mini LLM local 2026-08-21 18:58:28 +02:00

PoC — détection d'actions culinaires : NLP, LLM local, hybride

Expérimentation autonome, hors du monorepo pnpm (pnpm-workspace.yaml ne référence que apps/*/packages/*) : ce dossier a son propre package.json/tsconfig.json et ne pollue ni les dépendances ni le build Docker de apps/api.

Objectif : comparer, sur la même tâche (structurer une étape de recette en séquence ordonnée d'actions culinaires) et le même jeu de 7 phrases, trois moteurs qui tournent tous 100 % en local :

Script Moteur Ce qu'il apporte
pnpm bench Mini LLM instruct via node-llama-cpp, sortie JSON contrainte par schéma (GBNF grammar) Généralise sans vocabulaire fixé à l'avance — au prix d'une latence de plusieurs secondes.
pnpm bench:nlp Classifieur node-nlp frais (NER + découpage en clauses + classification d'intention), entraîné directement sur la taxonomie à 7 catégories de ce PoC Rapide (centaines de ms), mais borné à son vocabulaire d'entraînement.
pnpm bench:hybrid NLP d'abord, LLM en secours si le score NLP est trop faible Le meilleur des deux : rapide sur le cas courant, généralise sur le cas difficile.

Les trois partagent le même code (src/shared/) : la taxonomie KitchenActionType/KitchenAction/RecipeStepAnalysis (shared/kitchen-action.ts), les 7 phrases de test (shared/test-sentences.ts), et le harness de mesure/affichage (shared/benchmark-harness.ts) — un seul jeu de phrases et un seul format de sortie pour que les trois runs soient directement comparables, plutôt que recopiés à la main dans trois scripts (le défaut d'une toute première version de ce PoC, où le pendant NLP vivait dans apps/api et copiait les phrases manuellement).

Installation

cd experiments/llm-tech-step-poc
pnpm install --ignore-workspace

--ignore-workspace est nécessaire : ce dossier n'étant pas dans les globs de pnpm-workspace.yaml (apps/*/packages/*), un pnpm install normal remonte jusqu'à la racine du monorepo et n'installe rien ici (aucune erreur, juste un node_modules vide/inutilisable) — piège trouvé en écrivant ce PoC.

node-llama-cpp télécharge/compile son binding natif llama.cpp à l'installation (binaire prébuilt pour les plateformes courantes, sinon compilation locale — nécessite alors un toolchain C++, voir sa doc "Troubleshooting" en cas d'échec).

1. Benchmark LLM seul — pnpm bench

pnpm bench

Télécharge le modèle GGUF choisi via LLM_TECH_STEP_MODEL (une seule fois, mis en cache dans experiments/llm-tech-step-poc/models/, jamais commité), le charge, fait un appel de warm-up (chronométré à part — le tout premier appel d'inférence sur un contexte fraîchement créé paie un coût caché de plusieurs secondes que le chargement du modèle ne couvre pas), puis lance 3 répétitions sur chacune des 7 phrases de test.

Valeur (défaut en gras) Modèle Pourquoi
qwen2.5-1.5b Qwen2.5-1.5B-Instruct, Q4_K_M Meilleure robustesse multilingue FR/EN et meilleur suivi d'instructions de structuration JSON — et, empiriquement (voir Résultats obtenus ci-dessous), aussi la latence la plus basse des deux sur ce benchmark, malgré ses ~50 % de paramètres en plus.
llama-3.2-1b Llama-3.2-1B-Instruct, Q4_K_M ~35 % de paramètres en moins, FR officiellement supporté, mais structuration JSON moins fiable à 1B — et pas plus rapide non plus dans les runs obtenus jusqu'ici. Conservé comme point de comparaison, pas comme choix "latence d'abord".

Résultats obtenus (Windows, backend Vulkan, une machine) : sur les 7 phrases, Qwen2.5-1.5B a été systématiquement plus rapide que Llama-3.2-1B malgré sa taille plus grande — l'inverse de l'hypothèse a priori "moins de paramètres = plus rapide". Un seul run sur une seule machine/un seul backend ne généralise pas forcément (CUDA/CPU pur donneraient possiblement un classement différent).

LLM_TECH_STEP_MODEL=llama-3.2-1b pnpm bench

Hors-ligne / CI : LLM_TECH_STEP_MODEL_PATH=/chemin/vers/un.gguf pnpm bench pointe directement vers un fichier déjà téléchargé, sans passer par la résolution/téléchargement Hugging Face.

2. Benchmark NLP seul — pnpm bench:nlp

pnpm bench:nlp

Aucun téléchargement, aucune base de données — tourne en quelques secondes. NlpTechStepClassifier (src/nlp-tech-step-poc.ts) est un classifieur node-nlp frais, écrit pour ce PoC plutôt qu'une réutilisation de TechStepClassifierService (apps/api/src/lib/recipe-matching/ tech-step-matcher.ts) — deux raisons :

  1. Comparaison vraiment terme à terme : TechStepClassifierService classe sur la taxonomie fine à ~26 techniques de tech-step-training-data.ts (DB-backed), pas sur les 7 catégories de KitchenActionType que le LLM produit — les nombres de détections n'étaient pas directement comparables. Ce classifieur-ci est entraîné directement sur les 7 mêmes catégories.
  2. Un score de confiance exploitable : TechStepClassifierService masque son score en retombant silencieusement sur l'ancre NER dès qu'il est sous son seuil interne — utile en prod, mais ça cache le signal dont le pipeline hybride (section 3) a besoin pour décider quand basculer vers le LLM. Ce classifieur-ci renvoie toujours le score BRUT.

Même pipeline NER → découpage en clauses → classification par clause que tech-step-matcher.ts, implémentation propre à ce PoC (simplifiée : pas de priorité aux frontières de phrase dans le découpage). Le corpus d'entraînement (TRAINING_DATA dans nlp-tech-step-poc.ts) préfère un synonyme mono-mot ("revenir") à une phrase figée ("faites revenir") quand c'est possible — une leçon tirée d'un run antérieur de ce PoC : "faites revenir" (2 mots) ratait "faites-les-revenir" (le pronom clitique français insère un mot entre les deux et casse un matching de phrase contiguë), un synonyme mono-mot matche quel que soit ce qui le précède.

3. Pipeline hybride — pnpm bench:hybrid

pnpm bench:hybrid

Combine les deux : le NLP analyse TOUJOURS en premier (chemin rapide) ; si sa confiance globale (le minimum de confiance de ses clauses) est sous NLP_TRUST_THRESHOLD (0.6, tunable dans hybrid-tech-step-poc.ts) ou qu'il n'a rien trouvé du tout, son résultat est ENTIÈREMENT écarté et l'étape est réanalysée par le LLM. Le récapitulatif affiche, par phrase, quel moteur a répondu (moteur) et la confiance NLP qui a déclenché la décision (confiance NLP) — de quoi ajuster le seuil en observant sur quelles phrases le pipeline bascule.

Nécessite le modèle LLM (même téléchargement/options LLM_TECH_STEP_MODEL/ LLM_TECH_STEP_MODEL_PATH que la section 1) puisqu'il reste le moteur de secours.

Limite assumée : quand le chemin NLP est pris, seuls action/verb sont réellement connus — ingredients/utensils restent [] et durationMinutes/temperature restent null, jamais inventés (le classifieur NLP ne peut structurellement pas les extraire). Seul le chemin LLM remplit tous les champs. Un vrai système hybride ferait probablement remonter le champ source jusqu'à l'UI pour ne promettre que ce que chaque chemin fournit réellement.

Limites de ce PoC

  • Pas de jeu d'évaluation étiqueté ni de métrique de précision automatisée — les 7 phrases sont inspectées à l'œil, pas notées.
  • La grammaire GBNF (moteur LLM) ne garantit qu'une syntaxe JSON conforme au schéma, jamais la justesse sémantique du contenu.
  • Le corpus du classifieur NLP frais est volontairement compact (PoC, pas un remplacement du corpus production tech-step-training-data.ts) — des formes non couvertes (conjugaisons, synonymes absents) manqueront, comme pour n'importe quel corpus fini.
  • NLP_TRUST_THRESHOLD (0.6) est un point de départ raisonnable, pas une valeur empiriquement optimisée — à ajuster en observant la colonne moteur du récapitulatif hybride sur des étapes réelles.
  • Le delta de RSS process est une approximation de la RAM réellement utilisée (un binding natif alloue dans le même process, donc le RSS la capture, mais au bruit du GC/de l'allocateur près) — pas une mesure isolée.
  • Latence LLM mesurée en CPU pur (pas de configuration GPU dans ce PoC) — un déploiement réel voudrait évaluer l'offload GPU (gpuLayers dans les options loadModel) si la cible dispose d'un GPU.