Aller au contenu principal
Retour au blog

Économiser des tokens coûte parfois plus cher : l’A/B qui tranche

RTK et caveman réduisent tes tokens, mais une compression lossy peut coûter plus en relances. Mesure le coût par tâche réussie avec claude -p et un vrai A/B.

Flavien Métivier · 15 min de lecture
Sommaire · 7 parties

RTK annonce 60 à 90 % de tokens économisés sur les sorties shell ; caveman met 65 % en accroche. En conditions réelles, j'ai mesuré 31,5 % pour RTK sur mon poste, et face à une simple consigne « sois concis », caveman ne gagne que quelques pourcents à la médiane. Ces chiffres ne sont pas faux : ils sont mesurés sur le périmètre de chaque outil. Ta facture, elle, couvre toute la session.

La compression de tokens est dite lossy quand elle retire de l'information du contexte envoyé au modèle pour réduire la facture, au risque de dégrader ses décisions. Une sortie tronquée fait relancer la commande, relire un fichier ou prendre une mauvaise décision. Cet article pose la seule métrique qui vaille, le coût par tâche réussie, et décrit comment monter en une journée un A/B assez rigoureux pour en tirer une décision, scripts compris. Je ne suis ni pour ni contre ces outils : je suis pour la mesure.

Pourquoi économiser des tokens peut-il coûter plus cher ?

Parce qu'un outil de compression affiche un pourcentage sur ce qu'il retire, alors que ta facture dépend de ce que l'agent fait après. Cinq mécanismes peuvent transformer une économie locale en surcoût global :

  • La relance : la sortie est élidée, l'agent relance la commande hors du filtre ou relit le fichier. Tu paies la sortie complète, plus un tour, et chaque tour relit le préfixe et génère des tokens de sortie, la ligne la plus chère du tarif.
  • La mauvaise décision sur sortie tronquée : le coût le plus lourd et le moins visible. L'agent conclut à trois tests en échec alors que la troncature en cachait neuf, corrige à côté, relance, annule. Les issues publiques de RTK en donnent des cas réels : un git log tronqué qui fausse une réécriture d'historique, un marqueur de push perdu qui fait relancer le push.
  • Le coût fixe des règles : les consignes injectées pour faire parler le modèle autrement s'ajoutent à l'entrée de chaque session, environ 1 160 tokens pour caveman. Si ton agent produit peu de sortie, ce ticket d'entrée ne se rentabilise jamais.
  • Le cache : tout ce qui change le préfixe (règles ajoutées, outils, sorties réécrites différemment d'un run à l'autre) invalide l'entrée de cache, et tu repaies une écriture sur tout le préfixe. Les mécanismes d'invalidation sont détaillés dans l'article sur les leviers natifs.
  • La clarification : une réponse trop télégraphique ou une sortie trop dense pousse à reformuler ou à demander des précisions, soit un tour de plus.

Ordre de grandeur, avec un cache chaud : chaque token retiré d'un résultat d'outil économise une écriture en cache au premier tour, puis une relecture à prix réduit à chaque tour suivant. Une seule relance de la sortie complète suffit à annuler ce gain. Ce que la compression économise vraiment, c'est la première écriture et le risque de saturer la fenêtre. Dès que le taux de relance dépasse un certain seuil, l'outil devient négatif : c'est ce seuil que l'A/B doit estimer, pas le pourcentage de compression du README.

Que nous apprend l’affaire des +18 % et des −7,5 % ?

L'issue #582 de RTK, « RTK Hook Increases Claude Code Costs by 18% », est un cas d'école, dans les deux sens. Le rapporteur compare une seule paire de sessions : sortie de 1 600 à 2 400 tokens, coût de 0,68 à 0,80 $. Les mainteneurs répondent avec 20 runs alternés : RTK gagne dix fois, le brut six, quatre égalités, et −7,5 % de tokens de sortie en moyenne. Ils pointent deux biais de la mesure initiale : les fichiers donnés par @ ne passent pas par Bash, donc pas par le hook, et le premier mode testé paie toujours la création du cache. Ils estiment eux-mêmes cet effet d'ordre à 10 à 20 % de différence de coût, indépendamment de RTK. L'issue est fermée depuis mars 2026.

Personne n'a « raison » en général. Et par cohérence, les −7,5 % viennent de mainteneurs qui ont un intérêt dans la réponse : ce n'est pas une raison de les écarter, c'est une raison de ne croire ni l'issue ni la réfutation, et de rejouer la mesure sur tes tâches. La leçon est méthodologique : un run unique mesure surtout la variance, et un A/B non apparié mesure l'ordre des runs autant que l'outil. Même logique côté caveman : son README affiche 65 % en accroche, mais précise que face à une consigne « sois concis », /caveman ne gagne que 3 % à la médiane et /ultracave 35 %. La bonne baseline n'est pas « sans outil », c'est la meilleure option gratuite.

Quel indicateur mesurer : tokens, coût ou coût par tâche réussie ?

Le coût par tâche réussie est égal au coût total des runs, échecs compris, divisé par le nombre de runs qui passent le critère de succès. Cette définition règle deux problèmes d'un coup : un bras moins cher par run mais qui échoue plus souvent sort perdant, et un run qui « réussit » mal ne compte pas, parce que le succès est vérifié par un test et non par l'agent. Pour une équipe PHP, c'est une suite PHPUnit qui passe et un PHPStan au niveau attendu, regroupés dans une commande comme le bin/verify décrit dans l'article sur la vérification d'abord.

Relève aussi, pour chaque run, les tokens par type (entrée, sortie, lecture et écriture de cache), le nombre de tours, le nombre d'appels d'outils et la durée. Un coût stable avec 30 % d'appels d'outils en plus est le signal typique de relances. Côté RTK, rtk gain --recalls compte les récupérations de sorties élidées ; attention, rtk gain --failures liste les échecs de parsing retombés en exécution brute, pas les troncatures problématiques.

Comment concevoir un A/B honnête ?

  • Cinq à dix tâches figées, prises dans ton historique réel : un bug avec test rouge, une fonctionnalité de trois ou quatre fichiers, un refactoring guidé par PHPStan, une investigation de logs longs, là où la compression a une chance de gagner. Chaque tâche a un prompt figé, un commit de départ et une commande de vérification déterministe.
  • Un worktree jetable par run, recréé depuis le même commit et au même chemin : le répertoire de travail fait partie du prompt système, donc du préfixe de cache. Aucun run ne voit les fichiers modifiés d'un autre.
  • Des runs appariés en ordre ABBA : pour chaque tâche et chaque répétition, A et B à la suite, en alternant l'ordre (A puis B, puis B puis A). C'est ce qui neutralise l'effet « le premier paie la création du cache ».
  • Un régime de cache tenu : chaud (un run d'échauffement jeté, puis tout enchaîné sous le TTL) ou froid (attente au-delà du TTL). Fixe le TTL explicitement, il vaut cinq minutes par défaut en clé API.
  • Des variables épinglées : même modèle, même effort, même version de Claude Code.

Pour la reproductibilité, la doc du mode non interactif recommande --bare : il saute hooks, CLAUDE.md, serveurs MCP, mémoire et skills. La condition B s'injecte alors explicitement, via --settings pour un hook RTK ou --append-system-prompt-file pour des règles caveman. Deux réserves. --bare n'utilise pas ton login d'abonnement : il faut ANTHROPIC_API_KEY, sinon le run échoue sur « Not logged in ». Et je n'ai pas pu confirmer que les hooks passés par --settings s'exécutent sous --bare : lance un run canari en B qui exécute git status et vérifie qu'il apparaît dans rtk gain --history. Sinon, remplace --bare par --setting-sources project dans les deux bras.

Votre équipe utilise Claude Code ?

Formation intra sur vos projets réels : de la demi-journée à 2 jours, 6 devs max.

Voir les formats →

Comment récupérer le coût de chaque run avec claude -p ?

Avec --output-format stream-json --verbose, la dernière ligne est un message result qui contient notamment total_cost_usd, usage.input_tokens, usage.output_tokens, usage.cache_read_input_tokens, usage.cache_creation_input_tokens, num_turns, duration_ms et is_error, constatés avec Claude Code 2.1.299. Les appels d'outils se comptent dans les blocs tool_use des messages de l'assistant. Pièges documentés dans le suivi des coûts de l'Agent SDK : total_cost_usd est une estimation client, pas la facture, mais elle convient pour comparer deux bras ; usage exclut les sous-agents, que total_cost_usd inclut ; et une session reprise avec --resume cumule la dépense précédente.

bash
# extract.jq : flux stream-json (lu avec jq -s) vers une ligne CSV
(map(select(.type=="result")) | last) as $r
| ([.[] | select(.type=="assistant") | .message.content[]? | select(.type=="tool_use")] | length) as $tools
| [ ($r.total_cost_usd // 0), ($r.usage.input_tokens // 0), ($r.usage.output_tokens // 0),
    ($r.usage.cache_read_input_tokens // 0), ($r.usage.cache_creation_input_tokens // 0),
    ($r.num_turns // 0), $tools, ($r.duration_ms // 0),
    ($r.is_error | if . == null then true else . end) ]
| @csv

Détail qui piège : écrire $r.is_error // true renverrait true quand la valeur vaut false, car l'opérateur // de jq traite false comme absent. Voici le harnais qui enchaîne les runs en ordre ABBA et écrit un CSV. Un run en échec reste une donnée : il est gardé avec ok=0, sinon le coût par tâche réussie est faussé.

bash
#!/usr/bin/env bash
# ab.sh <repo> <commit> <N> [tasks_dir] : N répétitions ABBA par tâche, écrit results.csv
# tasks_dir/<nom>.md = prompt ; bin/verify dans le worktree : exit 0 = succès
set -euo pipefail

REPO=$(realpath "$1"); COMMIT=$2; N=$3; TASKS=$(realpath "${4:-tasks}")
OUT=results.csv; RAW=raw; WT=/var/tmp/ab-wt   # chemin fixe : même préfixe de cache
MODEL=${MODEL:-claude-sonnet-5-5}; EFFORT=${EFFORT:-medium}
COND_B=(--settings "$PWD/rtk-hook.json")      # ou --append-system-prompt-file caveman.md

mkdir -p "$RAW"
[ -f "$OUT" ] || echo "task,rep,cond,cost_usd,input,output,cache_read,cache_create,turns,tools,duration_ms,is_error,ok" > "$OUT"

run_one() { # $1=tâche $2=répétition $3=condition
  local task=$1 rep=$2 cond=$3 raw="$RAW/$1-$2-$3.jsonl" extra=()
  [ "$cond" = B ] && extra=("${COND_B[@]}")

  git -C "$REPO" worktree remove --force "$WT" 2>/dev/null || true
  git -C "$REPO" worktree add --detach "$WT" "$COMMIT" > /dev/null

  ( cd "$WT" && claude --bare -p "$(cat "$TASKS/$task.md")" \
      --model "$MODEL" --effort "$EFFORT" \
      --permission-mode acceptEdits --allowedTools "Bash,Read,Edit,Write" \
      --max-turns 40 --max-budget-usd 3 --no-session-persistence \
      --output-format stream-json --verbose "${extra[@]}" \
      < /dev/null > "$OLDPWD/$raw" ) || true    # un échec reste une donnée

  local ok=0; ( cd "$WT" && bin/verify > /dev/null 2>&1 ) && ok=1
  echo "$task,$rep,$cond,$(jq -s -r -f extract.jq "$raw"),$ok" >> "$OUT"
}

for rep in $(seq 1 "$N"); do
  for f in "$TASKS"/*.md; do
    task=$(basename "$f" .md)
    if (( rep % 2 )); then order=(A B); else order=(B A); fi   # ABBA
    for cond in "${order[@]}"; do run_one "$task" "$rep" "$cond"; done
  done
done
git -C "$REPO" worktree remove --force "$WT" 2>/dev/null || true

Le fichier rtk-hook.json de la condition B reprend le bloc PreToolUse présenté dans l'article d'installation de RTK. Lance le tout dans un conteneur avec git, jq et Claude Code, monté sur ton dépôt : le script ne dépend que de bash, git et jq. --max-turns et --max-budget-usd plafonnent le coût de l'expérience. Exemple de budget : 8 tâches, 6 répétitions et 2 conditions font 96 runs, soit quelques dizaines de dollars selon la taille des tâches. C'est le prix d'une décision d'équipe, à comparer au coût de rester sur une intuition.

Comment lire les résultats sans se raconter d’histoires ?

Les coûts d'un agent ont une forte variance et une queue lourde : un run qui boucle coûte plusieurs fois la médiane. Les mainteneurs de RTK observent eux-mêmes des écarts individuels de −33 % à +41 % entre runs. La moyenne seule est donc trompeuse : rapporte la médiane et l'écart interquartile par condition, puis analyse les différences appariées (B moins A, par tâche et répétition) sur les paires où les deux runs ont réussi. Ce script, en Python sans dépendance, donne un intervalle de confiance bootstrap et une p-value par permutation de signes :

bash
#!/usr/bin/env python3
"""analyze.py results.csv [metrique] : médiane, IQR, delta apparié, IC bootstrap."""
import csv, random, statistics as st, sys
from collections import defaultdict

rows = list(csv.DictReader(open(sys.argv[1])))
metric = sys.argv[2] if len(sys.argv) > 2 else "cost_usd"
random.seed(42)

for cond in ("A", "B"):
    runs = [r for r in rows if r["cond"] == cond]
    xs = [float(r[metric]) for r in runs]
    q1, _, q3 = st.quantiles(xs, n=4, method="inclusive")
    ok = sum(r["ok"] == "1" for r in runs)
    cost_ok = sum(float(r["cost_usd"]) for r in runs) / max(ok, 1)
    print(f"{cond}: n={len(xs)} médiane={st.median(xs):.4f} IQR=[{q1:.4f};{q3:.4f}] "
          f"succès={ok}/{len(runs)} coût/tâche réussie={cost_ok:.4f}")

pairs = defaultdict(dict)
for r in rows:
    pairs[(r["task"], r["rep"])][r["cond"]] = r
diffs = [float(p["B"][metric]) - float(p["A"][metric]) for p in pairs.values()
         if p.get("A", {}).get("ok") == "1" and p.get("B", {}).get("ok") == "1"]
if len(diffs) < 5:
    sys.exit(f"Trop peu de paires valides ({len(diffs)}) pour conclure.")

obs = st.median(diffs)
boot = sorted(st.median(random.choices(diffs, k=len(diffs))) for _ in range(10_000))
perm = sum(abs(st.median([d * random.choice((-1, 1)) for d in diffs])) >= abs(obs)
           for _ in range(20_000))
print(f"paires={len(diffs)} delta médian (B-A)={obs:+.4f} "
      f"IC95=[{boot[250]:+.4f};{boot[9749]:+.4f}] p={(perm + 1) / 20_001:.4f}")

Lis l'intervalle de confiance avant la p-value. Sur un jeu de données synthétique de 30 paires où B coûtait 10 % de plus, l'intervalle allait d'environ −0,05 à +0,08 $ : un surcoût réel de 10 % y était indétectable. Si l'intervalle contient zéro et reste large, la conclusion est « on ne sait pas », pas « aucun effet » : relance avec plus de répétitions ou accepte que l'effet soit inférieur à ton bruit. Ordre de lecture recommandé : le taux de succès d'abord, puis le coût par tâche réussie, tâche par tâche (un outil peut gagner sur les logs et perdre sur le refactoring), puis les tours et appels d'outils. Les benchmarks publics souffrent des mêmes biais, comme le montre notre article sur le reward hacking des benchmarks d'agents.

Quels pièges rendent un A/B d’agent trompeur ?

  • Comparer des sessions différentes : deux sessions interactives de ta semaine ne sont pas un A/B. Même prompt, même commit, même moment, sinon tu mesures la tâche.
  • L'apprentissage du dépôt : si un run voit les fichiers modifiés du précédent, il triche. Worktree jetable et --no-session-persistence, et caches externes (vendor, var/cache) identiques pour tous.
  • Un nombre de runs décidé après coup : démarre à deux tâches et quatre répétitions pour estimer la variance, fixe ensuite N, et écris-le avant de lancer.
  • Goodhart : si l'équipe optimise « tokens par session », elle gagne en tronquant. Le succès de la vérification reste une contrainte dure, et une relecture humaine en aveugle d'un échantillon de diffs couvre la qualité que les tests ne voient pas.
  • Un résultat figé : un nouveau modèle ou une nouvelle version de l'outil peut déplacer le résultat. Le README de caveman admet lui-même que ses mesures n'ont pas été relancées depuis une ancienne version de ses règles.

La décision tient alors en une règle : garde l'outil si le coût par tâche réussie baisse avec un intervalle qui exclut une hausse inacceptable pour toi, à taux de succès inchangé. Sinon, ne le jette pas forcément : limite-le aux commandes où il gagne (logs et builds longs) et exclus celles où une troncature coûte cher (sorties de tests, diffs, git). Le suivi dans la durée passe ensuite par la mesure continue de la consommation.

Cette semaine : choisis trois à cinq tâches réelles de ton dépôt avec un critère de succès déterministe, lance le harnais avec la consigne « sois concis » seule contre l'outil, et calcule le coût par tâche réussie à la médiane. Garde les scripts et les résultats dans le dépôt, avec la version du modèle et de l'outil : c'est cette trace que tu opposeras à la prochaine promesse à 90 %. Ces réglages ne font gagner du budget que s'ils sont décidés et appliqués pour toute l'équipe. Mesurer le coût ne dit rien, en revanche, de ce que l'agent a le droit de faire : c'est le sujet du prochain article, consacré aux garde-fous déterministes.

Cet article vous a été utile ? Partagez-le.

Portrait de Flavien Métivier, fondateur de The Bearded Bear : barbe longue, lunettes, sur fond sombre

Écrit par

Flavien Métivier

Expert PHP/Symfony indépendant depuis 2014, ancien CTO de Kartable et d'Extrabat. Plus de 80 projets menés, de l'audit à la migration.

Mon parcours

À lire ensuite

Votre équipe utilise Claude Code ?

Formation intra sur vos projets réels : de la demi-journée à 2 jours, 6 devs max.

Newsletter Bear Bytes

Veille Claude Code, Symfony et PHP, retours d'expérience terrain et conseils techniques, directement dans votre boîte mail.