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> |
||
|---|---|---|
| .. | ||
| 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 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 script —
process.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 :
- 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.
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 colonnemoteurdu 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 pourbench:ollama(l'inférence tourne dansollama serve, un process séparé, voir la section 2). - Latence
node-llama-cppmesuré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. 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.