Étape 1 — retire apps/api/src/scripts/bench-tech-step-classifier.ts
(DB-backed, taxonomie ~26 techniques non comparable terme à terme au LLM).
Étape 2 — reconstruit tout dans experiments/llm-tech-step-poc, entièrement
autonome (aucune dépendance Postgres/apps/api) :
- shared/kitchen-action.ts, shared/test-sentences.ts,
shared/benchmark-harness.ts : types, 7 phrases de test et harness de
mesure/affichage désormais partagés par les trois scripts (plus de
recopie manuelle entre fichiers).
- nlp-tech-step-poc.ts : classifieur node-nlp FRAIS (NER + clauses +
classification), entraîné directement sur la taxonomie à 7 catégories du
LLM plutôt que réutiliser TechStepClassifierService — comparaison terme à
terme, et surtout un score de confiance BRUT jamais masqué (contrairement
au repli silencieux sur l'ancre NER de la version production), condition
nécessaire au pipeline hybride. Corpus qui préfère les synonymes mono-mot
("revenir") aux phrases figées, pour ne pas se faire piéger par les
pronoms clitiques français ("faites-les-revenir").
- hybrid-tech-step-poc.ts : NLP toujours en premier (chemin rapide), LLM en
secours si la confiance NLP passe sous NLP_TRUST_THRESHOLD (0.6, tunable)
ou qu'aucune action n'est trouvée — récapitulatif avec colonnes "moteur"
et "confiance NLP" pour observer les bascules.
- llm-tech-step-poc.ts : inchangé fonctionnellement, migré vers les modules
partagés.
- shared/module-entry.ts (isMainModule) : garde chaque script pour que
l'import de ses classes (par hybrid-tech-step-poc.ts) ne déclenche pas
aussi son propre benchmark comme effet de bord.
pnpm bench / bench:nlp / bench:hybrid. README réécrit en conséquence.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|---|---|---|
| .. | ||
| src | ||
| .gitignore | ||
| package.json | ||
| pnpm-lock.yaml | ||
| README.md | ||
| tsconfig.json | ||
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 :
- Comparaison vraiment terme à terme :
TechStepClassifierServiceclasse sur la taxonomie fine à ~26 techniques detech-step-training-data.ts(DB-backed), pas sur les 7 catégories deKitchenActionTypeque 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. - Un score de confiance exploitable :
TechStepClassifierServicemasque 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 colonnemoteurdu 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 (
gpuLayersdans les optionsloadModel) si la cible dispose d'un GPU.