# PoC — détection d'actions culinaires par mini LLM local 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), le pipeline `node-nlp` déjà en place (`apps/api/src/lib/recipe-matching/tech-step-matcher.ts` — `TechStepClassifierService`) à un mini LLM instruct tournant 100 % en local via [`node-llama-cpp`](https://node-llama-cpp.withcat.ai/), avec sortie JSON strictement contrainte par un schéma (GBNF grammar), sur trois axes : précision, robustesse multilingue FR/EN, latence. ## Installation ```bash cd experiments/llm-tech-step-poc pnpm install ``` `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"](https://node-llama-cpp.withcat.ai/guide/troubleshooting) en cas d'échec). ## Modèle Le script télécharge automatiquement (une seule fois, mis en cache dans `experiments/llm-tech-step-poc/models/`, jamais commité) le GGUF choisi via `LLM_TECH_STEP_MODEL` : | 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 à taille comparable — recommandation par défaut vu que "robustesse FR/EN" est un critère explicite de ce PoC. | | `llama-3.2-1b` | Llama-3.2-1B-Instruct, `Q4_K_M` | ~35 % de paramètres en moins (plus rapide/plus léger), FR officiellement supporté, mais structuration JSON moins fiable à 1B — utile en comparaison "latence d'abord". | ```bash 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. ## Lancer le benchmark ```bash pnpm bench ``` Charge le modèle, puis lance 3 répétitions sur chacune des 7 phrases de test (4 FR + 3 EN, voir `TEST_SENTENCES` dans [`src/llm-tech-step-poc.ts`](./src/llm-tech-step-poc.ts)) — les 3 premières couvrent le cas courant (actions enchaînées, dont le cas piège documenté dans `tech-step-matcher.ts` lui-même : "jusqu'à ce que le beurre ait disparu dans la poêle", aucun verbe de cuisson littéral, seul le sens implique `COOK`), les 4 suivantes poussent délibérément plus loin pour chercher le point de rupture : actions simultanées plutôt que séquentielles, action conditionnelle ("if the batter looks too thick..."), négation explicite d'action ("sans jamais laisser bouillir"), fin de cuisson par état/test de résultat plutôt que par durée fixe, et un champ température qui désigne un seuil de cuisson à cœur plutôt qu'un réglage de feu. Imprime, par phrase : le JSON détaillé de chaque action détectée, puis un tableau récapitulatif (latence moyenne/min/max, delta RSS moyen, nombre d'actions détectées). ## Méthodologie de comparaison avec le pipeline `node-nlp` Ce script reste volontairement autonome (aucune dépendance vers `apps/api`, donc pas de connexion Postgres requise pour le faire tourner). Pour comparer manuellement sur les mêmes phrases : ```ts // Dans apps/api, un script ponctuel (ou un REPL tsx) : import { techStepClassifier } from "./src/lib/recipe-matching/tech-step-matcher.js"; console.log(await techStepClassifier.matchTechStepSpans( "Émincez finement les oignons puis faites-les revenir 10 minutes à feu moyen dans une poêle avec un filet d'huile d'olive, puis réservez.", "fr", )); ``` (nécessite une base Postgres accessible et `TechStep` seedée — voir `apps/api/prisma/seed.ts` — puisque `matchTechStepSpans` résout ses `uid` vers de vrais `TechStep.id`). Les deux sorties ne sont pas directement isomorphes (`TechStepMatch` renvoie un `techStepId` + des spans de caractères contre un `KitchenAction` structuré avec ingrédients/durée/température/ustensiles) — la comparaison porte sur : le nombre d'actions/techniques détectées par phrase, si la catégorie/technique choisie est correcte, et le comportement sur la phrase piège FR sans verbe littéral. ## 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 ne garantit qu'une syntaxe JSON conforme au schéma, jamais la justesse sémantique du contenu (catégorie choisie, durée correctement extraite...) — voir le commentaire sur `KITCHEN_ACTION_JSON_SCHEMA` dans le script. - Le delta de RSS process est une approximation de la RAM réellement utilisée par l'inférence (le 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 du seul processus llama.cpp. - Latence 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.