batchCooking/experiments/llm-tech-step-poc/README.md
Nicolas f5f30923d0 feat(experiments): ajoute un 4e moteur — même LLM via Ollama
Nouveau src/ollama-tech-step-poc.ts : même tâche/SYSTEM_PROMPT (exporté
depuis llm-tech-step-poc.ts et réutilisé tel quel) que le moteur
node-llama-cpp, mais via Ollama — une implémentation architecturalement
différente plutôt qu'une redite :

- Ollama tourne comme serveur HTTP local séparé (ollama serve), pas comme
  binding natif dans ce process — le paquet npm ollama n'a aucune
  dépendance native (rien à compiler à l'install, contrairement à
  node-llama-cpp).
- Modèle géré par Ollama lui-même (ollama.pull(), cache dans
  ~/.ollama/models), pas par ce projet — progression de pull journalisée
  palier par palier plutôt que silencieuse.
- Schéma JSON imposé via `format` (JSON Schema standard, `type:
  ["string","null"]` pour un champ nullable) — plus simple que le détour
  `oneOf` qu'exige la grammaire GBNF de node-llama-cpp.
- initialize() échoue avec un message explicite si le serveur Ollama n'est
  pas joignable, plutôt que l'erreur fetch brute.
- Caveat documenté en tête de fichier et rappelé avant le récapitulatif :
  la colonne RSS du harness ne mesure rien d'utile ici, l'inférence tourne
  dans le process ollama serve, pas dans ce script.

OllamaStepAnalyzer.dispose() décharge le modèle du serveur (keep_alive: 0,
best effort). Env vars OLLAMA_TECH_STEP_MODEL/OLLAMA_TECH_STEP_HOST,
scripts pnpm bench:ollama.

Vérifié en conditions réelles (Ollama tournait déjà dans l'environnement) :
pull + inférence structurée + parsing JSON fonctionnels, latence nettement
inférieure à node-llama-cpp sur les mêmes phrases (748-1260 ms vs 3-13 s),
delta RSS confirmé proche de zéro/bruit comme attendu.

README mis à jour (4 moteurs, section Ollama avec tableau comparatif
architectural, limites).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 23:14:03 +02:00

12 KiB

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 11 phrases, quatre moteurs qui tournent tous 100 % en local :

Script Moteur Ce qu'il apporte
pnpm bench Mini LLM instruct via node-llama-cpp (binding natif dans ce process), 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:ollama Le même LLM (même SYSTEM_PROMPT, même tâche), mais via Ollama — un serveur HTTP local séparé plutôt qu'un binding embarqué Compare l'impact de l'architecture (client-serveur vs in-process) à sémantique identique, pas juste un autre modèle.
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 (node-llama-cpp) 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 quatre partagent le même code (src/shared/) : la taxonomie KitchenActionType/KitchenAction/RecipeStepAnalysis (shared/kitchen-action.ts), les 11 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 runs soient directement comparables, plutôt que recopiés à la main dans chaque script (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). Les deux moteurs LLM (node-llama-cpp/Ollama) partagent en plus le SYSTEM_PROMPT lui-même (exporté par llm-tech-step-poc.ts, importé par ollama-tech-step-poc.ts) — même sémantique testée sur les deux, seul le mécanisme de contrainte JSON change.

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). ollama (le paquet npm) n'a lui aucune dépendance native — c'est un simple client HTTP, rien à compiler.

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 LLM via Ollama — pnpm bench:ollama

Prérequis : Ollama installé, et son serveur lancé (ollama serve dans un terminal séparé, ou l'app de bureau Ollama qui le lance automatiquement) — ce script ne démarre pas le serveur lui-même, contrairement à pnpm bench qui charge son modèle directement.

ollama serve      # si pas déjà lancé (ou l'app de bureau Ollama)
pnpm bench:ollama

Même tâche, même SYSTEM_PROMPT, mêmes modèles (OLLAMA_TECH_STEP_MODEL, mêmes valeurs qwen2.5-1.5b/llama-3.2-1b que LLM_TECH_STEP_MODEL) que la section 1 — mais une implémentation architecturalement différente, testée pour ça plutôt que comme une simple redite :

node-llama-cpp (section 1) Ollama (cette section)
Où tourne l'inférence Dans CE process Node (binding natif) Dans le process ollama serve, séparé
Installation Binaire natif compilé/téléchargé au pnpm install Client HTTP pur, rien à compiler
Gestion du modèle resolveModelFile télécharge le GGUF dans models/ de ce projet ollama pull — Ollama gère son propre cache (~/.ollama/models)
Schéma JSON nullable oneOf: [{type:"null"}, {type:"..."}] (contrainte de la grammaire GBNF) type: ["string", "null"] (JSON Schema standard, plus simple)
Mesure RSS du benchmark Fiable — le binding alloue dans ce process Sans intérêt — l'inférence tourne ailleurs, voir ci-dessous

La colonne RSS moy. du récapitulatif ne veut RIEN dire pour ce scriptprocess.memoryUsage() mesure ce process Node, pas le process ollama serve où l'inférence a réellement lieu. Le script l'affiche quand même (même harness que les trois autres) mais rappelle ce point juste avant le tableau.

OLLAMA_TECH_STEP_MODEL=llama-3.2-1b pnpm bench:ollama

Hôte Ollama personnalisé (serveur distant, port non standard) : OLLAMA_TECH_STEP_HOST=http://mon-serveur:11434 pnpm bench:ollama (défaut : http://127.0.0.1:11434).

3. 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.

4. 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 11 phrases sont inspectées à l'œil, pas notées.
  • La contrainte JSON (grammaire GBNF ou JSON Schema Ollama) 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 pour node-llama-cpp/node-nlp (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, et carrément sans valeur pour bench:ollama (l'inférence tourne dans ollama serve, un process séparé, voir la section 2).
  • Latence node-llama-cpp 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. Ollama, lui, détecte et utilise l'accélération matérielle disponible automatiquement — une différence qui peut à elle seule expliquer un écart de latence entre les deux moteurs LLM, indépendamment du modèle choisi.