Nouveau script apps/api/src/scripts/bench-tech-step-classifier.ts, calqué sur experiments/llm-tech-step-poc/src/llm-tech-step-poc.ts : mêmes 7 phrases de TEST_SENTENCES (recopiées à l'identique), même structure de sortie (logs itératifs par répétition, tableau récapitulatif latence/RSS/nombre de détections), pour que les deux pipelines soient directement comparables phrase par phrase. Réutilise techStepClassifier.warmUp() (déjà prévu pour absorber le coût de l'entraînement + l'init paresseuse de node-nlp) et résout les techStepId en key lisible pour l'affichage détaillé. Nécessite une base Postgres avec TechStep seedée (matchTechStepSpans résout ses uid vers de vrais ids). README du PoC LLM mis à jour : la section "Méthodologie de comparaison" pointe vers ce script réel plutôt que le snippet REPL manuel qu'elle suggérait avant. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
6.8 KiB
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, avec sortie JSON
strictement contrainte par un schéma (GBNF grammar), sur trois axes :
précision, robustesse multilingue FR/EN, latence.
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).
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 — 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" (l'hypothèse initiale du README). |
Résultats obtenus (Windows, backend Vulkan, une machine) : sur les 7 phrases de
TEST_SENTENCES, 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". Résultat gardé ici tel quel plutôt que la doc pré-run corrigée après coup silencieusement — mais 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.
Lancer le benchmark
pnpm bench
Charge le modèle, fait un appel de warm-up (chronométré et affiché à 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 ;
sans ce warm-up c'est la première phrase du benchmark qui l'absorbe,
faussant sa latence par rapport aux six autres), 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) — 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). Le pendant
côté node-nlp vit dans apps/api/src/scripts/bench-tech-step-classifier.ts
— mêmes 7 phrases de TEST_SENTENCES (recopiées à l'identique, texte/id/
locale, à synchroniser à la main si l'une des deux listes change), même
format de sortie (logs itératifs par répétition, tableau récapitulatif) :
pnpm --filter api exec tsx src/scripts/bench-tech-step-classifier.ts
(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 (résolu en key par ce script pour être lisible) + des spans
de caractères, dans la taxonomie fine à ~26 techniques de
tech-step-training-data.ts, contre un KitchenAction structuré
ingrédients/durée/température/ustensiles sur la taxonomie à 7 catégories de
ce PoC) — 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
(fr-action-implicite).
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_SCHEMAdans 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 (
gpuLayersdans les optionsloadModel) si la cible dispose d'un GPU.