Anatoly - agent IA multi-LLM d’audit de code

Analyse approfondie

Router le Claude Agent SDK vers des LLM locaux : une stack Qwen bi-tier avec TurboQuant 4-bit KV

Retour de terrain d'une exploration Anatoly : pipeline multi-étapes passé d'Anthropic à llama.cpp local. Un seul GGUF Qwen3.6-35B-A3B dans deux modes thinking (haiku no-think, sonnet thinking), quatre bugs SDK dont une astuce de désactivation du thinking qui apporte un facteur 12, KV cache 4-bit TurboQuant sur RTX 3090 Ti 24 Go, bench à 100 appels avec scoring Opus-as-judge. Le local est 5 à 9 fois plus rapide qu'Anthropic et au plafond Opus sur verify-rag.

44 min de lectureRémi Viau

Router le Claude Agent SDK vers des LLM locaux : une stack Qwen bi-tier avec TurboQuant 4-bit KV#

Note par : Rémi Viau (mainteneur Anatoly), avec Claude (Anthropic Opus 4.7) comme partenaire d'analyse.

Le Claude Agent SDK est une façade pratique. Le modèle derrière n'a pas besoin d'être un modèle Claude. Cet article est un retour de terrain issu d'une exploration Anatoly : brancher le SDK sur des serveurs llama.cpp locaux servant des modèles Qwen3, sur une seule RTX 3090 Ti de 24 Go de VRAM. Le déclencheur a été le coût d'un pipeline de fact-check documentaire multi-étapes au long cours, qui brûlait environ cinq dollars par run sur Anthropic Sonnet, principalement sur les étapes de vérification à fort volume qui martèlent le SDK avec des milliers de petits appels.

Nous avons commencé avec un setup bi-modèle (un Qwen3 dense 4 B pour le tier haiku, un Qwen3.6-35B-A3B pour le tier sonnet) puis convergé vers une architecture mono-modèle après le mini-bench du §7.8 : un seul GGUF Qwen3.6-35B-A3B tourne dans deux containers, distingués uniquement par un flag thinking. Le container haiku (thinking OFF) absorbe les milliers d'appels de vérification par run ; le container sonnet (thinking ON) traite les rares passes de correction à fort enjeu. Même fichier modèle, redémarrage de container en ~10 s pour switcher de tier (le GGUF reste dans le cache pages de l'OS). Le rôle opus reste sur Anthropic, et le câblage prod « best-of-both » route la phase correct vers Anthropic Opus là où le 35 B local rate de 2 à 3 points Opus.

Ce qui compte n'est pas le diagramme d'architecture. Ce sont les quatre bugs d'intégration SDK dont personne ne vous prévient, le flag container d'une seule ligne qui nous a donné un facteur 12 en vitesse une fois trouvé, et la découverte qui a fait atterrir l'architecture finale : un modèle MoE 35 B-A3B en no-think bat un modèle 4 B dédié sur les workloads haiku, à latence équivalente, parce que le MoE n'active que ~3 B paramètres par token. Le benchmark plus bas (100 appels, 5 providers, 4 workloads, N=5, Opus-as-judge) montre local-turbo avec --parallel 2 4 à 9 fois plus rapide qu'Anthropic sur les étapes à fort volume, à parité sur verify, battant Anthropic sur importance, et quelques points en-dessous sur extract et rewrite. Le coût tombe à zéro en pur local, à 4 $ sur le setup hybride recommandé.

Verdict rapide : la stack locale est-elle assez bonne ?#

Oui sur les workloads Haiku à fort volume. Le rewrite Sonnet a encore besoin d'Anthropic Opus. Un seul GGUF Qwen3.6-35B-A3B tourne dans deux containers, seul le flag thinking diffère. Voici le verdict Opus-as-judge, détaillé par container Anthropic remplacé :

Container local Modèle & mode Remplace Workloads (appels par run) Verdict Opus
llm-haiku Qwen3.6-35B-A3B, thinking OFF Anthropic Haiku verify-rag (~1 300), extract (~88), importance (~300) verify : parité à 9/10. Importance : 9/10, un point au-dessus d'Anthropic Haiku lui-même. Extract : 4 à 5/10 contre un plafond de 6/10 (le 4 B notait 4 à 5/10 ; non rebenché sur le 35 B, attendu au même niveau ou au-dessus puisque le 35 B bat le 4 B sur chaque workload testé au §7.8). Anthropic Haiku lui-même ne dépasse pas 6/10 sur cette rubrique.
llm-sonnet Qwen3.6-35B-A3B, thinking ON Anthropic Sonnet correct-section-rewrite (~8) 6/10 contre un plafond de 9/10. Applique tous les fixes demandés mais omet les tags de citation [#filename] qu'Opus produit par défaut. L'écart ne se ferme ni avec le thinking, ni avec un tweak de prompt, ni avec un modèle plus gros : Opus reste obligatoire pour ce workload en prod.

La conclusion. Un seul modèle MoE 35 B en mode no-think égale Anthropic Haiku sur chaque workload à fort volume (et le bat sur importance, où le 4 B qu'on livrait initialement notait 2/10). Le même modèle avec thinking ON sur la phase rewrite atterrit 3 points Opus sous Anthropic Sonnet/Opus et nous n'avons pas réussi à fermer l'écart. Le setup prod livré est donc hybride : local pour le milieu tier Haiku (extract, verify, importance, omissions), Anthropic Opus pour les huit rewrites de la phase correct. Bout-en-bout ~59 minutes en local-avec-Opus-correct vs ~4 heures full Anthropic ; ~4 $ par run vs ~5 $ full Anthropic ou 0 $ si vous acceptez l'écart sur le rewrite.

En un coup d'œil : le local, à quel point proche d'Anthropic ?#

Qualité. Note Opus-as-judge par workload, sur une échelle 0 à 10 où 10 signifie « indistinguable de la référence Anthropic ». Barres dorées = plafond Anthropic (Anthropic exécuté deux fois sur le même prompt, second run noté contre le premier). Barres menthe = le 35 B-A3B local dans sa config thinking de prod (no-think pour le tier haiku, thinking ON pour le rewrite). Les valeurs importance et correct viennent du mini-bench §7.8 sur le 35 B ; extract et verify sont reprises du bench 4 B du §7.4 car nous ne les avons pas rejouées sur le 35 B (le graphe prend la borne haute de la plage mesurée pour extract).

Le local atterrit à parité sur verify-rag (1 300 appels par run, le workload qui économise le coût) et un point au-dessus du plafond Anthropic-vs-Anthropic sur importance grâce à la config 35B-A3B no-think. Extract est un à deux points sous le plafond. L'écart persistant est sur correct-rewrite (6/10 vs 9/10) : les locaux omettent les tags de citation [#filename] qu'Opus produit, et ni le thinking ni le prompting ne le ferment. La recette prod (§7.9) route ces huit appels vers Anthropic Opus.

Vitesse. Secondes wall-clock par appel. Barres dorées = Anthropic. Barres menthe = 35 B-A3B local sur local-turbo-parallel. La barre correct affichée est le fallback pur-local (35 B thinking ON, 17,57 s) ; la prod route correct vers Anthropic Opus à ~13 s par appel (voir §7.9), donc ni l'une ni l'autre barre ne reflète le chemin livré pour ce workload.

Les barres locales sont 4 à 9 fois plus courtes sur les trois workloads Haiku et un peu plus de 2 fois plus courtes sur le rewrite Sonnet. Le chiffre importance est passé de 1,52 s (le modèle 4 B livré initialement) à 2,42 s (le 35 B no-think livré maintenant) : toujours 4,3 fois plus rapide qu'Anthropic, avec le point de qualité en plus qui va avec. Bout-en-bout, le pipeline local tourne en environ une heure contre quatre heures sur Anthropic.

Conclusions principales (TL;DR)#

Si vous ne lisez qu'une section au-delà du verdict ci-dessus, lisez celle-ci.

  • Le routing drop-in fonctionne. Le Claude Agent SDK relit ANTHROPIC_BASE_URL et ANTHROPIC_DEFAULT_*_MODEL à chaque appel, donc pointer vers un endpoint local llama-server compatible Anthropic ne demande aucun fork du SDK.
  • Un GGUF, deux modes thinking. La prod tourne un seul fichier Qwen3.6-35B-A3B dans deux containers : le container haiku (thinking OFF) gère les workloads JSON à fort volume ; le container sonnet (thinking ON) gère le rewrite. Le MoE 35B-A3B n'active que ~3 B paramètres par token, donc c'est la même latence qu'un 4 B avec un point de qualité en plus sur les workloads haiku. C'est l'architecture qu'on livre après qu'un mini-bench de suivi a rejeté le setup dual-modèle initial.
  • Quatre bugs d'intégration SDK à connaître. --alias par tier pour /v1/models ; parallel=1, ctx_per_slot=32768 pour les prompts longs ; bannir les 27 built-in tools, pas les 12 dont vous vous souvenez ; et désactiver le thinking au moteur, pas dans le prompt.
  • La désactivation du thinking au moteur est le plus gros gain unique. La directive /no_think au niveau prompt est ignorée 92 % du temps sur Qwen3.5 et Qwen3.6. Passer --jinja --reasoning off --chat-template-kwargs '{"enable_thinking": false}' à llama-server donne un facteur 12 en vitesse sur les workloads haiku à fort volume. Le même flag est aussi un levier qualité : sur les workloads JSON-stricts, thinking ON fait chuter la note Opus parce que le modèle préfixe le JSON par de la prose.
  • Config recommandée : TurboQuant avec --parallel 2, pas llama.cpp mainline. 4 à 9 fois plus rapide qu'Anthropic sur les étapes à fort volume, au plafond Opus 9/10 sur verify-rag (parité complète) et un point au-dessus du plafond sur importance.
  • La phase correct reste sur Anthropic Opus. Un 35 B local (thinking ON ou OFF) atterrit 3 points Opus sous Opus sur le rewrite, principalement parce qu'il omet les tags [#filename] de citation. Le setup prod est hybride : local pour le milieu à fort volume, Anthropic Opus pour les 8 appels de rewrite. ~4 $ par run vs ~5 $ full Anthropic ou 0 $ si vous acceptez l'écart sur le rewrite.
  • Bout-en-bout : ~59 minutes en local vs ~4 heures sur Anthropic (~×4 plus rapide, coût quasi nul à l'électricité près et aux 8 appels Opus).

Méthodologie du benchmark : 100 appels, 5 providers, 4 workloads, N=5 trials, Opus LLM-as-judge avec plafond Anthropic-vs-Anthropic pour la calibration. Host : RTX 3090 Ti, build llama.cpp épinglé. Un mini-bench de suivi (§7.8) a comparé 4 B vs 35 B dans les deux modes thinking sur un workload, et a motivé l'architecture mono-modèle.

1. Pourquoi le Claude Agent SDK permet ça#

Le Claude Agent SDK lit sa cible dans un petit ensemble de variables d'environnement qu'il consulte à chaque appel : ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_MODEL, et les trois ANTHROPIC_DEFAULT_*_MODEL pour haiku/sonnet/opus. Si vous pointez la base URL vers un serveur qui parle la forme /v1/messages, le SDK ne sait pas et se moque que le répondeur ne soit pas Anthropic. Le serveur de llama.cpp implémente un endpoint compatible Anthropic depuis quelques mois (voir le README serveur), et ça marche.

Le pipeline garde deux containers locaux derrière les deux tiers budgétaires du SDK, avec le container d'embedding comme troisième occupant du même GPU. Les containers haiku et sonnet font tourner le même GGUF Qwen3.6-35B-A3B ; seul le flag thinking diffère au démarrage :

Trois containers, un seul up à la fois. Passer du container haiku au container sonnet est un redémarrage de container avec des flags différents, pas un autre modèle : le GGUF reste dans le cache pages de l'OS, donc le swap prend environ 10 secondes en pratique. Un petit context manager échange les huit variables d'environnement pertinentes à l'entrée, les restaure à la sortie. Appeler opus efface les overrides et laisse le SDK frapper Anthropic nativement ; appeler haiku ou sonnet redirige vers le serveur local correspondant. Le pipeline peut enchaîner opus → haiku → sonnet → opus dans un même run sans problème.

Ça, c'est la moitié facile. La moitié dure, c'est tout ce que le SDK fait avant et autour de l'appel.

2. Bug 1 : exposer un alias modèle stable par tier via /v1/models#

Le SDK valide le nom du modèle à l'init de session, avant tout appel /v1/messages. Il le fait en frappant /v1/models et en vérifiant que l'ID demandé est dans la liste renvoyée. Avec les défauts de llama.cpp, /v1/models retourne le nom de fichier GGUF, qwen3.6-35b-a3b-Q4_K_M.gguf, qui n'est ni stable ni ce que le SDK a été instruit de demander. Le SDK refuse alors l'appel avec une erreur trompeuse « model not found ».

Le fix est de passer --alias <nom-stable> à llama-server au démarrage, avec un nom différent par tier :

# container haiku
llama-server ... --alias local-haiku
# container sonnet
llama-server ... --alias local-sonnet

Après, curl -s http://127.0.0.1:11451/v1/models | jq .data[0].id retourne local-haiku, le port sonnet retourne local-sonnet, et l'ANTHROPIC_DEFAULT_HAIKU_MODEL=local-haiku + ANTHROPIC_DEFAULT_SONNET_MODEL=local-sonnet correspondant côté client valident proprement.

Un piège subtil qui nous a mordus en ajoutant le second tier : quand vous swappez de haiku vers sonnet, il faut aussi effacer la variable haiku du modèle dans l'environnement. Sinon le SDK peut router un appel résiduel taggué haiku vers un container haiku qui n'est plus en marche. Le fix est un snapshot/restore propre du set complet de huit variables à chaque swap de tier, et un test de régression dédié aux swaps consécutifs.

3. Bug 2 : contexte par slot vs contexte total#

L'erreur suivante que vous croisez est plus brutale :

API Error: 400 request (22 584 tokens) exceeds the available context size (4 096 tokens)

llama.cpp divise --ctx-size par --parallel pour calculer le contexte par slot. Les défauts sont parallel=4, ctx_size=16384, soit 4 096 tokens par slot. Un prompt de correction avec 22 k d'entrée plus 10 k de sortie attendue explose ça au premier token.

Il y a deux façons de corriger : monter --ctx-size, ou baisser --parallel. Sur une machine mono-GPU où le pipeline sérialise de toute façon les appels (l'orchestrateur lance les sections via asyncio.gather mais le GPU est le goulot), parallel=1, ctx_per_slot=32768 est le choix le plus simple. Le --cont-batching interne de llama.cpp amortit déjà les expert loads sur un modèle MoE comme Qwen3.6-35B-A3B (3 B de paramètres actifs sur 35 B), donc on ne perd pas grand-chose en débit en posant parallel=1.

Si vous voulez de la concurrence, la bonne manœuvre est de monter --ctx-size à parallel × per_slot_voulu. Souvenez-vous juste que le KV cache croît avec le contexte total, pas avec le contexte par slot.

4. Bug 3 : bannir les 27 built-in tools, pas les 12 dont vous vous souvenez#

Le bug le plus coûteux contre lequel nous avons livré, et le plus surprenant. Sur la phase d'extraction (qui attend du JSON, pas des tool calls), on a brutalement vu 84 erreurs tool.builtin ERR AskUserQuestion en cinq minutes, cascadant en « Reached maximum number of turns (3) » et en extractions vides.

Le mécanisme est le suivant. Le Claude Agent SDK expose un set par défaut de built-in tools à chaque modèle, sauf si vous les désactivez explicitement. La liste de tools qu'on désactivait était longue de 12 (file ops, web, task). Le set par défaut réel fait environ 27 entrées, et il inclut AskUserQuestion, EnterPlanMode, Skill, CronCreate, TaskOutput, PushNotification, ScheduleWakeup, et une douzaine d'autres. Poser allowed_tools=[] ne suffit pas : le SDK expose quand même les built-ins ; il faut les lister explicitement dans disallowed_tools.

Pourquoi ça a cassé avec le modèle Qwen local et pas avec Sonnet ? Deux raisons. D'abord, Sonnet a été entraîné à utiliser les built-ins avec parcimonie et à préférer une sortie JSON directe quand le prompt le demande. Le modèle Qwen non, et il appelait joyeusement AskUserQuestion pour « réfléchir à voix haute » en cas de doute (observé sur la variante 4 B qu'on livrait initialement ; le 35 B avec thinking OFF est meilleur mais la discipline de la disable-list reste le bon défaut). Ensuite, notre max_turns=3 faisait que trois appels à AskUserQuestion suffisaient à épuiser le budget sans jamais émettre le JSON attendu.

Le fix est une constante unique listant les 27 built-ins, appliquée à chaque étape JSON-only :

BUILTIN_TOOLS_ALL = [
    "AskUserQuestion", "EnterPlanMode", "ExitPlanMode",
    "Skill", "CronCreate", "CronDelete", "CronList",
    "TaskOutput", "TaskStop", "PushNotification",
    "ScheduleWakeup", "RemoteTrigger", "Monitor",
    "WebFetch", "WebSearch", "TodoWrite",
    "Read", "Write", "Edit", "NotebookEdit",
    "Bash", "Glob", "Grep", "ToolSearch",
    "EnterWorktree", "ExitWorktree", "BashOutput",
]

Pour les étapes qui ont besoin d'un tool précis (le correcteur a besoin de Write), la disallow list devient [t for t in BUILTIN_TOOLS_ALL if t != "Write"].

L'avant/après sur la phase d'extraction raconte tout :

Avant Après
tool.builtin ERR sur 5 minutes 84 0
Échecs de chunk 11+ 0
Facts extraits par chunk 1 (toujours en échec) 14 à 31

La leçon se généralise à quiconque fait tourner un modèle non-Anthropic derrière le SDK : ne supposez pas que la liste par défaut de tools désactivés est exhaustive. Affichez ce que le SDK expose réellement au modèle et bannissez la liste complète.

5. Bug 4 : désactiver le thinking au container, pas dans le prompt#

C'est le bug le plus coûteux que nous n'avions pas corrigé au départ, et celui qui nous a discrètement coûté des heures par run avant qu'on trouve le fix. Qwen3.5 et Qwen3.6 sont des modèles « thinking » unifiés : ils émettent une trace de raisonnement interne avant la réponse finale, dans la même forme que DeepSeek-R1 ou les modèles o1-style. Pour une réécriture longue côté sonnet, c'est utile. Pour un appel de vérification qui doit décider oui / non sur un fait en 35 tokens de sortie, c'est catastrophique.

Première tentative : suffixer /no_think à chaque prompt utilisateur, une convention livrée dans le chat template Qwen3 original. Taux de prise en compte : 8 % sur 24 calls de test. Les 92 % restants continuaient à penser. Conséquence : chaque appel de vérification passait 15 à 50 s à raisonner avant d'émettre le JSON, faisant exploser la phase verify à plusieurs heures par run.

Le diagnostic a pris plus de temps qu'il n'aurait dû. Qwen3.5 et Qwen3.6 ont retiré la directive /no_think de leur chat template (elle était présente dans Qwen3 original, retirée à partir de 3.5). La voie supportée passe par chat_template_kwargs: {enable_thinking: false}, qui est documenté pour l'endpoint OpenAI mais ignoré sur l'endpoint Anthropic de llama.cpp.

Le fix est de passer le flag au démarrage du container, pas par requête, via les arguments llama-server :

llama-server \
  --jinja \
  --reasoning off \
  --chat-template-kwargs '{"enable_thinking": false}' \
  ...

--jinja force l'utilisation du chat template embarqué dans le GGUF (seul template qui sait interpréter enable_thinking). --reasoning off est le drapeau récent de llama.cpp qui désactive le bloc de raisonnement au niveau moteur. --chat-template-kwargs est la forme portable. Les trois flags combinés sont belt-and-suspenders ; la documentation Qwen recommande de les combiner pour la robustesse.

La bonne politique est par container, les deux pointant sur le même GGUF :

  • Container llm-haiku (disable_thinking=True par défaut) : workloads à fort volume, faible latence, sortie JSON stricte. Verify, extract, importance scoring. Thinking ON coûte à la fois en latence et en qualité ici, parce que le modèle préfixe le JSON par du raisonnement en prose (cf. mini-bench §7.8).
  • Container llm-sonnet (disable_thinking=False par défaut) : workloads à faible volume, sorties plus longues. Rewrite de la phase correct, vérifications de cohésion. La profondeur de raisonnement aide le texte final, même si avec thinking ON le 35 B local n'égale pas Anthropic Opus sur ce workload (la prod route correct vers Opus de toute façon, voir §7.9).

Le résultat empirique sur le workload importance-scoring, mesuré sous TurboQuant, raconte tout :

Avant (thinking leak) Après (flag honoré) Speedup
Wall mean 21,7 s 1,83 s ×12
Output tokens (mean) 358 9 ×40 moins de gaspillage
usage.output_tokens côté llama.cpp 0 (cassé) 9 (correct) corrigé

Un facteur 12 sur un workload qui tourne 300 fois par run, et un speedup similaire sur verify (1 300 appels par run) qui souffrait exactement de la même fuite thinking. Sur verify, le calcul donne environ 40 minutes après le fix (1 300 × 1,87 s) contre environ 8 heures avant (1 300 × 21,7 s).

La leçon généralisée : avec les modèles thinking, le bouton de désactivation doit être au niveau moteur, pas dans le prompt. Les directives au niveau prompt sont une politesse que le modèle peut refuser. Les flags au niveau moteur, non.

6. TurboQuant : KV cache 4-bit pour tenir la charge sur 24 Go#

Avec Qwen3.6-35B-A3B UD-IQ4_XS occupant environ 18 Go de VRAM, une fenêtre de contexte de 32 k tokens avec KV cache FP16 en consomme 3 Go de plus. Il reste environ 3 Go de marge sur une carte 24 Go, aucune place pour le batching, et zéro place pour le modèle d'embedding que le même pipeline utilise pour la retrieval. Le même arbitrage revient dans tout système qui mélange embeddings et chat sur une seule carte : il faut soit échanger les modèles à chaud (ce qu'on fait aussi, voir §8), soit réduire la mémoire runtime de l'un des deux.

TurboQuant est un schéma de quantification du KV cache. Les poids du modèle restent dans leur quant d'origine (UD-IQ4_XS dans notre cas) ; seul le KV cache est recodé au runtime en 4,25 bits par valeur (turbo4, environ 3,8× de compression vs FP16) ou 3,25 bits par valeur (turbo3_tcq, environ 5×). La technique est décrite dans l'article TurboQuant et a depuis atterri dans plusieurs forks de llama.cpp. Elle s'invoque par l'interface standard de llama.cpp, -ctk turbo4 -ctv turbo4, donc l'intégrer ne tient qu'à un flag, une fois le binaire compilé avec les bons kernels.

6.1 Choisir un fork#

Quatre forks de llama.cpp embarquent les kernels TurboQuant à l'heure où nous écrivons :

Fork Statut Plateforme Notes
TheTom/turboquant_plus Metal-first Apple Silicon Parité avec q8_0 sur M5
Aaryan-Kapoor/turboquant-tq3_0 CPU only x86_64 Pas utile avec une carte CUDA
Madreag/turbo3-cuda RTX 5090 (Ada Lovelace) sm_89 Mauvaise archi cible pour nous
spiritbuun/llama-cpp-turboquant-cuda RTX 3090 (Ampere) sm_86 Benchmarks publiés sur une 3090

Nous avons choisi spiritbuun parce que le README rapporte un débit de décodage constant à environ 30 tok/s de 4 K à 128 K de contexte sur une 3090, et une perplexité qui bat même q8_0 grâce à une « norm correction » documentée. Il y a un risque bus factor sur un fork à environ 600 étoiles ; la mitigation est d'épingler le commit upstream et de pouvoir rebuilder sans dépendre de la CI du fork.

6.2 Dockerfile multi-stage#

Le build a besoin de l'image CUDA devel (nvcc, cuBLAS) ; le runtime n'a besoin que de l'image runtime. Le multi-stage coupe l'image finale à environ 3 Go.

# stage builder
FROM nvidia/cuda:12.6.3-devel-ubuntu22.04 AS builder
 
RUN apt-get update && apt-get install -y \
    git cmake build-essential ninja-build libcurl4-openssl-dev
 
# Stub libcuda.so.1 pour l'étape de link ; voir §6.3.1
RUN ln -sf libcuda.so /usr/local/cuda/lib64/stubs/libcuda.so.1
ENV LIBRARY_PATH=/usr/local/cuda/lib64/stubs:${LIBRARY_PATH}
 
RUN git clone https://github.com/spiritbuun/llama-cpp-turboquant-cuda.git /src
WORKDIR /src
 
RUN cmake -B build -G Ninja \
        -DGGML_CUDA=ON \
        -DGGML_CUDA_FA_ALL_QUANTS=ON \
        -DCMAKE_CUDA_ARCHITECTURES=86 \
        -DCMAKE_EXE_LINKER_FLAGS="-Wl,-rpath-link=/usr/local/cuda/lib64/stubs" \
        -DCMAKE_SHARED_LINKER_FLAGS="-Wl,-rpath-link=/usr/local/cuda/lib64/stubs" \
    && cmake --build build --target llama-server
 
# stage runtime
FROM nvidia/cuda:12.6.3-runtime-ubuntu22.04
 
RUN apt-get update && apt-get install -y \
    libcurl4 libgomp1 ca-certificates wget
 
COPY --from=builder /src/build/bin/llama-server /usr/local/bin/
COPY --from=builder /src/build/bin/*.so /usr/local/lib/
COPY --from=builder /src/codebooks /opt/codebooks
 
HEALTHCHECK CMD wget -qO- http://localhost:8080/health || exit 1
ENTRYPOINT ["llama-server"]

6.3 Pièges de build à retenir#

Le linker cherche libcuda.so.1 (le soname versionné), mais l'image CUDA devel ne livre que libcuda.so (sans version) dans /usr/local/cuda/lib64/stubs/. Le vrai libcuda.so.1 vient du driver host à l'exécution via --gpus all. Le link doit quand même résoudre le symbole, donc il faut trois choses combinées :

  1. Un symlink pour simuler le soname versionné : ln -sf libcuda.so /usr/local/cuda/lib64/stubs/libcuda.so.1.
  2. ENV LIBRARY_PATH=/usr/local/cuda/lib64/stubs:... pour que gcc trouve -lcuda au compile-link.
  3. -Wl,-rpath-link=/usr/local/cuda/lib64/stubs pour que ld puisse résoudre les entrées NEEDED qui pointent vers libcuda de manière transitive (libggml dépend de libcuda, et le link final de llama-server vérifie la chaîne).

Si un seul des trois manque, le link échoue avec undefined reference to 'cuMemCreate' et compagnie.

6.3.2 libgomp.so.1 manquant au runtime#

L'image CUDA runtime ne livre pas OpenMP. Le binaire llama-server linke contre libgomp.so.1 pour la parallélisation CPU des helpers. Démarrez le container sans, et vous obtenez :

/usr/local/bin/llama-server: error while loading shared libraries:
libgomp.so.1: cannot open shared object file

Le fix est une ligne dans le stage runtime : apt-get install -y libgomp1.

7. Benchmark : cinq providers, quatre workloads, Opus-as-judge#

Les fixes ci-dessus ne valent la peine d'être livrés que si la stack locale est réellement compétitive sur les workloads qui comptent. Le benchmark ci-dessous est ce qui tranche la question. Il est dans le repo, reproductible par une seule commande, et lit dans les mêmes fichiers JSONL que le reste du pipeline produit, donc les workloads sont les vrais workloads.

7.1 Méthodologie#

Le benchmark cycle séquentiellement sur 5 providers × 4 workloads × N=5 trials, pour 100 appels au total. Les cinq providers :

  • anthropic : API Anthropic native. Référence.
  • local-mainline : Qwen sur llama.cpp mainline, KV cache FP16.
  • local-turbo : Qwen sur le fork spiritbuun, KV cache turbo4, parallel=1.
  • local-turbo-parallel : identique à local-turbo mais parallel=2. Les 1,5 Go libérés par turbo4 nous laissent batcher deux slots concurrents, que l'asyncio.gather de l'orchestrateur peut remplir.
  • local-turbo-haiku-think : ablation qui fait tourner les workloads du tier haiku à fort volume sur le modèle sonnet (Qwen3.6-35B-A3B) avec thinking ON. Teste la question « un raisonnement plus profond améliore-t-il assez la qualité de verify/extract pour valoir la latence ? ».

Le trial 1 d'Anthropic est la référence qualité. Les outputs locaux sont scorés de trois manières :

  • Similarité cosinus entre l'embedding de l'output local (Qwen3-Embedding-8B) et l'embedding de la référence Anthropic.
  • F1 de ROUGE-L sur des tokens-mots minuscules (signal lexical complémentaire).
  • Opus-as-judge sur une échelle d'équivalence absolue de 0 à 10. Un jugement par cellule (provider, workload). Surtout, le même juge note Anthropic trial 2 contre Anthropic trial 1 comme plafond empirique : le score « indistinguable » pour deux runs du même backend sur la même entrée. Tout provider local noté au plafond est à parité avec Anthropic.

Les workloads :

Label Tier Profil Description
extract-chunk haiku ~3 k in, ~700 out, JSON Extraire des faits atomiques (88 appels par run)
verify-rag-fact haiku ~3 k in, ~50 out, JSON Appel interne Verify-RAG (~1 300 par run)
importance-score haiku ~1 k in, ~15 out, JSON Score 0 à 1 sur un fait
correct-section-rewrite sonnet ~4 k in, ~1,5 à 3 k out, markdown Réécrire une section pour corriger des problèmes

Host : RTX 3090 Ti, WSL2 Ubuntu, Python 3.12, ctx_per_slot=32 768, build llama.cpp épinglé dans le manifest du bench. 100 appels au total, 0 erreur.

7.2 Résultats de performance#

Workload Provider Wall mean (s) Output tps In tokens Out tokens
extract-chunk anthropic 53,80 182,8 3 284 9 801
extract-chunk local-mainline 8,08 100,2 1 232 809
extract-chunk local-turbo 7,40 100,3 1 230 745
extract-chunk local-turbo-parallel 5,79 101,4 1 230 590
extract-chunk local-turbo-haiku-think 82,36 68,1 1 228 2 737
importance-score anthropic 10,39 67,5 2 966 713
importance-score local-mainline 1,95 8,1 968 16
importance-score local-turbo 1,85 8,4 965 15
importance-score local-turbo-parallel 1,52 9,9 965 15
importance-score local-turbo-haiku-think 41,34 10,5 963 412
verify-rag-fact anthropic 10,90 97,5 3 297 1 064
verify-rag-fact local-mainline 2,50 20,1 1 279 50
verify-rag-fact local-turbo 2,13 23,0 1 277 49
verify-rag-fact local-turbo-parallel 1,87 27,3 1 277 50
verify-rag-fact local-turbo-haiku-think 14,13 53,1 1 275 633
correct-section-rewrite anthropic 39,78 82,1 3 956 3 230
correct-section-rewrite local-mainline 34,59 62,1 2 021 2 418
correct-section-rewrite local-turbo 24,12 66,4 2 018 1 611
correct-section-rewrite local-turbo-parallel 17,57 93,0 2 018 1 642
correct-section-rewrite local-turbo-haiku-think 29,16 59,4 2 018 1 475

Trois choses à remarquer. D'abord, local-turbo-parallel est le plus rapide sur chaque workload : le second slot se remplit sous asyncio.gather et les expert loads MoE s'amortissent entre les deux requêtes. Ensuite, les comptes d'output tokens d'Anthropic sont désormais corrects (9 801 sur extract, 3 230 sur correct) : une version antérieure de ce bench avait un bug de propagation qui les bloquait entre 0 et 8, ce qui a depuis été corrigé. Enfin, l'ablation thinking-haiku est dramatiquement plus lente (×5 à ×20) parce que le modèle brûle 400 à 2 700 tokens de raisonnement avant d'émettre la réponse JSON de 15 à 50 tokens.

7.3 Résultats de qualité : métriques proxy#

Workload Provider Cosinus vs ref F1 ROUGE-L JSON valide Clé JSON OK
extract-chunk local-mainline 0,639 0,223 60 % 60 %
extract-chunk local-turbo 0,659 0,203 80 % 80 %
extract-chunk local-turbo-parallel 0,634 0,174 60 % 60 %
extract-chunk local-turbo-haiku-think 0,857 0,298 40 % 40 %
importance-score local-mainline 0,427 0,019 100 % 100 %
importance-score local-turbo 0,434 0,018 100 % 100 %
importance-score local-turbo-parallel 0,450 0,014 100 % 100 %
importance-score local-turbo-haiku-think 0,926 0,368 80 % 80 %
verify-rag-fact local-mainline 0,873 0,107 100 % 100 %
verify-rag-fact local-turbo 0,867 0,101 100 % 100 %
verify-rag-fact local-turbo-parallel 0,870 0,107 100 % 100 %
verify-rag-fact local-turbo-haiku-think 0,932 0,403 80 % 80 %
correct-section-rewrite local-mainline 0,889 0,460 n/a n/a
correct-section-rewrite local-turbo 0,925 0,505 n/a n/a
correct-section-rewrite local-turbo-parallel 0,901 0,480 n/a n/a
correct-section-rewrite local-turbo-haiku-think 0,921 0,513 n/a n/a

Deux résultats ressortent. D'abord, local-turbo bat local-mainline sur la cosinus de correct-section-rewrite (0,925 vs 0,889), inversant la conclusion d'un bench précédent à un seul trial. Le tradeoff KV q8 vs turbo4 ne coûte pas de qualité mesurable sur les sorties longues une fois qu'on moyenne sur N=5. Ensuite, local-turbo-haiku-think gagne sur chaque métrique cosinus parce que le plus gros modèle avec thinking produit une sortie sémantiquement plus proche de celle d'Anthropic. Que cette proximité se traduise en correction réelle est la question à laquelle répond le juge Opus.

Une note sur les valeurs cosinus d'importance-score (0,43 à 0,45) : la sortie du workload fait environ 15 tokens, et la cosinus sur des sorties aussi courtes est du bruit statistique. La validité JSON à 100 % raconte la vraie histoire : les locaux produisent la bonne forme, juste avec une formulation différente d'Anthropic. Le même piège revient dans tout système de retrieval qui score sur des fragments courts, et c'est la raison pour laquelle notre pipeline de détection de conflit sémantique fait du NLI sur des sections entières plutôt que de la cosinus sur des chunks.

7.4 Résultats de qualité : Opus-as-judge#

Opus note chaque sortie contre la référence Anthropic sur une échelle d'équivalence absolue de 0 à 10. La ligne anthropic-baseline est Anthropic trial 2 jugé contre trial 1 (même backend, échantillon différent) : elle pose le plafond empirique pour « indistinguable » sur ce workload.

Workload Provider Note Opus Note
extract-chunk anthropic-baseline (plafond) 6/10 Citations verbatim, comptes de faits comparables, variations mineures
extract-chunk local-mainline 5/10 Quelques faits composés violant l'atomicité
extract-chunk local-turbo 5/10 Moins de faits atomiques que la référence (8 vs 17)
extract-chunk local-turbo-parallel 4/10 Une citation utilise '...' pour rassembler du texte non contigu
extract-chunk local-turbo-haiku-think 4/10 Viole l'atomicité (faits composés)
importance-score anthropic-baseline (plafond) 8/10 Sortie JSON identique
importance-score local-mainline 2/10 Score 0,7 / bucket 'important' diverge de la référence 'marquant'
importance-score local-turbo 2/10 Bucket faux par rapport à la référence
importance-score local-turbo-parallel 4/10 Score proche (0,8 vs 0,9) mais bucket faux
importance-score local-turbo-haiku-think 8/10 JSON identique, au plafond
verify-rag-fact anthropic-baseline (plafond) 9/10 Même verdict, citation, src_idx, notes
verify-rag-fact local-mainline 9/10 Verdict, source, citation corrects
verify-rag-fact local-turbo 9/10 À parité avec Anthropic
verify-rag-fact local-turbo-parallel 9/10 À parité avec Anthropic
verify-rag-fact local-turbo-haiku-think 7/10 Correct mais émet du raisonnement en prose avant le JSON
correct-section-rewrite anthropic-baseline (plafond) 9/10 Les quatre hallucinations corrigées, les trois omissions intégrées
correct-section-rewrite local-mainline 6/10 Tous les fixes appliqués, mais préambule de raisonnement non sollicité
correct-section-rewrite local-turbo 6/10 Tous les fixes appliqués, différences de formatage légères
correct-section-rewrite local-turbo-parallel 6/10 Version française, tous les fixes appliqués
correct-section-rewrite local-turbo-haiku-think 7/10 Tous les fixes appliqués, sortie légèrement plus propre

Le juge Opus change la lecture de toutes les autres tables. La bonne manière de le lire est en pourcentage du plafond, pas en déficit absolu, parce que le plafond lui-même varie selon le workload : Anthropic-vs-Anthropic n'atteint que 6/10 sur extract (Opus pénalise ses propres violations de la règle « pas de prose »), tandis qu'il atteint 9/10 sur verify et rewrite.

  • verify-rag-fact : parité avec Anthropic. Local-mainline, local-turbo et local-turbo-parallel notent tous 9/10, comme le score Anthropic-vs-Anthropic. C'est le workload avec le plus d'appels par run (~1 300) et celui avec la plus grosse économie de coût, donc c'est le résultat phare. Re-rouler Anthropic sur le même prompt ne fait pas mieux.
  • extract-chunk : 66 à 83 % du plafond. Les locaux notent 4 à 5/10 contre un plafond de 6/10 (Opus est strict sur ce workload même face à Anthropic). Sur une échelle relative au plafond, les locaux sont à 66-83 % du faisable, pas à 40-50 % comme le chiffre absolu le suggère. Les violations d'atomicité (le 4 B émet 8 faits atomiques là où Anthropic Haiku en émet 17) sont le mode d'échec récurrent ; la vraie marge est d'environ un point.
  • correct-section-rewrite : un vrai écart de 2 à 3 points. Plafond 9, locaux 6 à 7. Les locaux appliquent bien tous les fixes demandés (Opus : « les quatre hallucinations adoucies et les trois omissions intégrées avec citations correctes »), mais ils perdent des points sur la dérive de formatage et le préambule de raisonnement occasionnel. C'est le seul workload où l'écart est mesurable et pas un artefact du plafond, et c'est là qu'un upgrade du sonnet-tier paierait le plus.
  • importance-score : la seule vraie défaillance qualité, et celle avec un fix propre. Les locaux notent 2/10 contre un plafond de 8/10 sans thinking, parce que le 4 B choisit systématiquement le mauvais bucket sur les cas limites (Opus : « le score et le bucket du candidat divergent du 'marquant' de la référence »). Activer thinking ON pour ce seul workload remonte la note à 8/10 = plafond, JSON identique inclus. Le thinking est l'unique levier qualité ici.

7.5 Anthropic vs local-turbo-parallel, le résumé#

Métrique Anthropic local-turbo-parallel Delta
Wall extract 53,80 s 5,79 s ×9,3 plus rapide
Wall importance 10,39 s 1,52 s ×6,8 plus rapide
Wall verify 10,90 s 1,87 s ×5,8 plus rapide
Wall correct 39,78 s 17,57 s ×2,3 plus rapide
Note Opus verify 9/10 (plafond) 9/10 au plafond
Note Opus correct 9/10 (plafond) 6/10 un point sous
Coût par run (pur local) ~5 $ 0 $ gratuit ; l'hybride prod (Opus sur correct) coûte ~4 $, voir §7.9

7.5 bis Caveat : N=5 ne converge pas sur la phase correct#

Avant de tirer des conclusions du §7.5, un caveat honnête. Le workload correct-section-rewrite a une variance trial-to-trial large : sur un run A/B isolé de local-mainline à N=5, les trials individuels allaient de 22 à 56 secondes (stdev 11,9 s sans optimisations, 6,6 s avec). À N=5 la moyenne n'a pas convergé, et l'écart entre local-mainline et local-turbo sur ce seul workload est dans le bruit.

Les trois autres workloads (extract, verify, importance) sont assez stables à N=5 pour être lus avec confiance. Toute affirmation comparative qui s'appuie sur les wall times du correct phase devrait être contrôlée par un rerun à plus haut N ou par des intervalles de confiance bootstrapés avant d'être traitée comme définitive. Les notes Opus sur correct-phase sont moins bruitées que les wall times (la rubrique est assez déterministe pour que les quatre variantes locales atterrissent à 6 ou 7), donc ces notes-là restent lisibles.

7.6 Estimation bout-en-bout sur un run complet#

Un run complet sur un document de taille moyenne fait 88 chunks de document × extract + 1 300 facts × verify + 300 × importance + 8 sections × correct.

Phase Mode Local-turbo-parallel Anthropic
Extract haiku no-think ~9 min ~30 min
Verify-RAG haiku no-think ~40 min ~3 h
Importance haiku no-think ~8 min ~25 min
Correct sonnet thinking ON ~2 min ~5 min
Total ~59 min ~4 h

Le pipeline local est environ quatre fois plus rapide qu'Anthropic bout-en-bout tout en restant gratuit, à l'électricité près. L'estimation Anthropic utilise la concurrence observée plutôt qu'une extrapolation strictement séquentielle des wall times par appel. L'essentiel du speedup local vient du flag de désactivation du thinking du §5 combiné à parallel=2 sur TurboQuant.

7.7 Quel provider local choisir#

Le brouillon précédent de cet article disait « mainline par défaut, turbo quand la VRAM l'impose ». Avec N=5 trials et l'Opus-as-judge ajouté, cette recommandation s'inverse. La liste ci-dessous est la reco post-bench pour le setup bi-modèle bi-tier ; le §7.8 réduit ensuite l'architecture à un seul modèle 35 B, et le §7.9 fixe le câblage prod final. Lisez cette section comme le cadre et le §7.9 comme la recette livrée.

  • Défaut : local-turbo avec --parallel 2 (c.-à-d. le provider local-turbo-parallel). Le plus rapide sur chaque workload, bat local-mainline sur la qualité de correct-rewrite (cosinus 0,901 vs 0,889, modulo le caveat du §7.5 bis), et atterrit au plafond Opus de 9/10 sur verify. Les 1,5 Go libérés par le KV turbo4 sont ce qui rend parallel=2 viable sur une carte 24 Go.
  • Sauter mainline : il est plus lent que turbo sur chaque workload dans ce run, et l'avantage qualité du brouillon précédent a disparu une fois qu'on a moyenné sur plus de trials.
  • Si vous devez faire tourner correct en local, activez thinking ON sur sonnet : ça fait passer le 35 B de 5/10 à 6/10 contre le plafond 9/10 (voir §7.8). La prod route correct vers Anthropic Opus à la place (§7.9) et ne charge pas le container sonnet, donc ce levier ne compte que si vous acceptez l'écart de qualité local.
  • Éviter thinking-ON sur verify et extract : le boost cosinus est réel mais le juge Opus note ces variantes plus bas que thinking-OFF sur les mêmes modèles. Les fuites de raisonnement polluent la sortie JSON, coûtant plus en échecs de parsing que ce que ça gagne en profondeur sémantique.

7.8 Mini-bench : du bi-modèle au mono-modèle#

Après le bench v3 ci-dessus, deux mini-benchs de suivi (28 mai) ont retourné l'architecture bi-modèle qu'on livrait initialement. Le résultat phare : un modèle MoE 35 B-A3B en mode no-think bat un modèle 4 B dédié sur les workloads haiku à latence équivalente, et thinking ON est contre-productif sur les workloads JSON strict.

Mini-bench A, workload importance, quatre configs locales :

Config Wall mean Stdev Note Opus Sortie trial 1
4 B no-think 2,13 s 0,02 s 8/10 {"score": 0.8, "bucket": "marquant"}
4 B thinking ON 17,14 s 16,43 s 6/10 prose puis JSON (pénalisé)
35 B no-think 2,42 s 0,15 s 9/10 {"score": 1.0, "bucket": "marquant"}
35 B thinking ON 13,76 s 1,94 s 6/10 prose puis JSON (pénalisé)

Trois enseignements :

  1. Le 35 B no-think bat le 4 B no-think sur tout : un point Opus de plus (9 vs 8), variance de latence serrée (0,15 s vs le 4 B qui varie encore trial-à-trial sur les buckets borderline), et essentiellement le même wall-clock (2,42 s vs 2,13 s). Le MoE n'active que ~3 B paramètres par token, donc son taux de décodage est comparable à un modèle 4 B dense.
  2. Le 4 B no-think est inconsistant entre trials. Sur le workload importance, il renvoie parfois important au lieu du marquant de référence sur les cas limites. Le 35 B choisit le bon bucket à chaque mesure.
  3. Thinking ON est nuisible sur les workloads JSON strict. Le 4 B et le 35 B tombent tous les deux à 6/10 avec thinking ON, parce qu'ils préfixent le JSON par de la prose (« Let me analyse... ») qui viole le No prose. No fences. du prompt. Opus déduit des points. Le thinking est un levier qualité, pas un upgrade gratuit : sur les workloads dont la rubrique interdit la prose, il vous coûte.

Décision tirée du mini-bench A : retirer le 4 B de la prod et faire tourner le 35 B-A3B no-think sur le tier haiku. L'inventaire GGUF sur disque devient un seul modèle chat plus l'embedder.

Mini-bench B, workload correct, le 35 B peut-il remplacer Anthropic Opus sur le rewrite ?

Config Wall mean Note Opus Verdict
35 B no-think 9,61 s 5/10 rapide mais qualité insuffisante
35 B thinking ON 23,50 s 6/10 le thinking aide marginalement
Anthropic Opus (direct) 12,88 s 9/10 qualité plafond, 4 $/run
Anthropic Sonnet 26,01 s 9/10 qualité Opus, latence doublée

Verdict : Opus reste obligatoire pour la phase correct. Les locaux 35 B plafonnent à 5 à 6/10 quel que soit le thinking. Le juge Opus pointe un seul mode d'échec reproductible : le 35 B local n'inclut pas les tags de citation [#filename] dans les phrases d'omission qu'il insère, et Opus les met. C'est une instruction du prompt que le modèle local ignore peu importe comment on le formule, et l'écart est visible dans le markdown final. Manque mesuré : 3 à 4 points sur l'échelle 0 à 10.

7.9 Architecture prod et recette#

Les deux mini-benchs ci-dessus définissent l'architecture qu'on livre aujourd'hui :

Phase Backend Thinking Pourquoi
extract, verify, importance, omissions local Qwen3.6-35B-A3B (container haiku) OFF 9/10 sur importance et verify, 5/10 sur extract (un point sous le plafond), ~2 à 3 s par appel, gratuit
correct (8 appels) Anthropic Opus n/a plafond 9/10, ~13 s par appel, ~4 $ par run

Le container sonnet existe dans le code pour la complétude, mais le mode prod « best-of-both » ne le charge jamais : quand la phase correct se déclenche, le SDK route ces huit appels vers Anthropic Opus et le container sonnet est sauté, économisant 30 s de chargement GPU et 18 Go de VRAM.

L'économie :

Configuration Coût / run Note Opus correct
Full Anthropic ~5 $ 9/10 (plafond)
Pure local (container sonnet avec thinking ON) 0 $ 6/10
Hybride (milieu local + Opus correct) ~4 $ 9/10 (plafond)

Vous économisez environ un dollar par rapport au full Anthropic, atteignez le plafond plein sur la phase qui touche le contenu, et gardez le speedup de ×4 à ×9 sur le milieu à fort volume. Le switch provider est déjà en place (juste provider_for("opus") sur la phase correct). C'est le défaut livré en mode --correct-via-opus.

8. Notes opérationnelles qui nous ont mordus#

Quelques points qui ne sont pas dans la liste de bugs ci-dessus mais qui nous ont coûté du temps quand même.

Mutex VRAM à trois voies sur le même GGUF. La même carte 24 Go fait tourner le container NLP (Qwen3-Embedding-8B, ~5 Go), le container LLM haiku (Qwen3.6-35B-A3B no-think, ~18 Go) et le container LLM sonnet (même GGUF avec thinking ON). Les paires ne coexistent pas ; le container actif doit s'arrêter avant que le suivant démarre. Le swap haiku → sonnet est rapide (~10 s) parce que le GGUF reste dans le cache pages de l'OS et que seul le process llama-server redémarre avec de nouveaux flags. Le swap nlp → llm, où le modèle chat doit être chargé en VRAM à froid, prend environ 30 s. Chaque phase qui a besoin d'un tier appelle ensure_llm_running(tier=...) elle-même, plutôt que de s'en remettre à un orchestrateur top-level. Le même pattern « chaque tâche gère son container » revient dans notre pipeline de détection de conflit sémantique.

Le coût des swaps est petit. Un run complet fait deux à trois swaps de tier au total (nlp → haiku → sonnet, ou nlp → sonnet direct si les facts extraits sont cachés d'un run précédent). À ~30 s pour un swap nlp ↔ llm et ~10 s pour un swap haiku ↔ sonnet (même GGUF), l'overhead total est d'environ 1 à 1,5 minute sur un run de ~59 minutes.

Routing opt-in. Le routing local doit être explicite. Une seule variable d'environnement, USE_LOCAL_LLM=1, l'active ; sans elle, le SDK frappe Anthropic nativement. La raison est opérationnelle : si les containers locaux sont down ou si le GPU est pris, le pipeline doit pouvoir tourner sur Sonnet plutôt que de bloquer.

Les jobs critiques restent sur Anthropic. provider_for("opus") efface l'override et force Anthropic. Tout ce sur quoi on ne peut pas accepter de régression (la passe de vérification finale, le résumé visible utilisateur) reste sur un modèle frontier. La stack locale gère le milieu de pipeline à fort volume et à plus faibles enjeux.

Fraîcheur du fork. Le fork spiritbuun bouge vite. Builder avec --no-cache --ref <new-sha> rebuilde sur un commit upstream précis. L'épinglage n'est pas optionnel avec un fork qui pousse quotidiennement sur le chemin critique.

Faites N ≥ 5 trials avant de tirer des conclusions. Une version antérieure du bench, à un seul trial, avait local-mainline battant local-turbo sur la qualité de la réécriture. Avec N=5, ça s'est inversé, et la recommandation du §7.7 s'est retournée. Les benchs à un trial sur des décodeurs stochastiques sont des anecdotes ; nous les traitons comme telles désormais.

9. Ce qu'on dirait à quelqu'un qui démarre aujourd'hui#

Si vous prévoyez de router le Claude Agent SDK vers un ou plusieurs LLM locaux, l'ordre que nous recommanderions :

  1. Choisissez un seul modèle MoE de taille moyenne et utilisez-le sur les deux tiers. Un MoE de classe 30 B qui n'active que ~3 B paramètres par token (Qwen3.6-35B-A3B dans notre cas) a la latence d'un dense 4 B et la qualité d'un dense 30 B. Le faire tourner sur les deux tiers avec des flags thinking différents est plus simple que de maintenir un 4 B séparé pour le tier à fort volume et un 30 B pour le tier à faible volume, et notre mini-bench (§7.8) montre que c'est aussi de meilleure qualité.
  2. Obtenez d'abord un alias modèle correct par tier. Des noms stables dans /v1/models font la différence entre « model not found » et « le modèle marche ». Ajoutez --alias <nom-tier> à chaque commande llama-server avant tout le reste, et assurez-vous que votre swap de tier efface les variables modèle résiduelles.
  3. Désactivez le thinking au niveau moteur, pas dans le prompt. Les directives au niveau prompt sont une politesse que le modèle refusera 90 % du temps. La combinaison --jinja --reasoning off --chat-template-kwargs '{"enable_thinking": false}' est ce qui a marché chez nous sur Qwen3.6. Le speedup est de ×12 et la qualité s'améliore aussi sur les workloads JSON strict, parce que la prose de raisonnement viole les rubriques No prose. No fences..
  4. Auditez la liste des built-in tools du SDK. Affichez ce que le SDK expose réellement à votre modèle et bannissez le set complet. Ne faites pas confiance à votre souvenir des « 12 tools » ; le vrai nombre dérive à mesure que le SDK livre des features.
  5. Benchmarkez avec N ≥ 5 trials et un plafond Opus-as-judge. Les benchs à un trial mentent sur les décodeurs stochastiques. La note de baseline Anthropic-vs-Anthropic est le plafond qui vous laisse dire « à parité » honnêtement plutôt que de courir après une cosinus de 0,96 qui ne veut rien dire.
  6. Par défaut, TurboQuant avec --parallel 2. llama.cpp mainline est plus lent et pas mesurablement supérieur en qualité une fois qu'on moyenne sur assez de trials. TurboQuant + flash attention + les 1,5 Go de marge que le KV 4-bit vous donne pour le second slot parallèle, c'est la configuration de départ.
  7. Traitez le thinking comme un outil, pas un défaut. Il aide le rewrite long-form et nuit aux sorties JSON strict. La politique par défaut est OFF sur le container haiku-tier et ON sur le container sonnet-tier ; résistez à l'envie de basculer thinking ON globalement « pour la qualité », ça dégradera silencieusement les workloads à fort volume.
  8. Acceptez que certains workloads restent sur Anthropic. Notre rewrite de phase correct plafonne à 6/10 contre un plafond de 9/10 sur toutes les configs locales testées, parce que les locaux omettent systématiquement les tags de citation [#filename]. Routez ces appels vers Anthropic Opus et arrêtez de courir après l'écart.

Le pattern plus large, c'est qu'une exécution agnostique du LLM est au centre de toute stack de production sérieuse. La même intuition revient dans notre note PageIndex vs RAG Anatoly (le même pipeline de retrieval tourne sur le LLM le moins cher du moment) et dans Audit IA vs Review IA de code en 2026 (le harnais d'audit doit survivre à n'importe quelle génération de modèle). Si votre architecture ne marche qu'avec un seul fournisseur, le cycle de release de modèles vous mangera vivant.

Pour les questions d'implémentation, ouvrez une discussion sur le dépôt GitHub ou contactez l'auteur directement. L'index recherche complet a tout le contexte autour de la retrieval, la détection de conflit, et le harnais d'audit que cette couche de routing alimente.

Dernière mise à jour :

local-llmllama-cppclaude-agent-sdkquantizationturboquantbenchmark

Articles liés