Free

Faire changer d'avis Claude

La recommandation « plus légère » de Claude cachait de vrais coûts. Une seule question en retour les a tous fait apparaître.


Je bossais sur smarts — une app Rails qui restitue des smart contracts Ethereum comme de la « documentation développeur ». L'autre jour, je suis retombé sur un vieux problème : les contracts se font upgrader. Si le système rend un proxy upgradé avec une ABI périmée, il ment à l'utilisateur.

J'ai demandé à Claude de concevoir avec moi la détection des upgrades de proxy. Il m'a sorti un plan technique solide, avec compatibilité EIP-1967, Beacon proxies et ZeppelinOS. Au moment de « quand déclencher la vérification », il a posé deux options :

# Option A : ajouter last_proxy_check_at en BDD, throttle de 5 min
if @contract.last_proxy_check_at.nil? || @contract.last_proxy_check_at < 5.minutes.ago
  CheckProxyUpgradeJob.perform_later(@contract)
end

# Option B : throttle via Solid Cache, plus léger
unless Rails.cache.exist?("proxy_check:#{@contract.id}")
  Rails.cache.write("proxy_check:#{@contract.id}", true, expires_in: 5.minutes)
  CheckProxyUpgradeJob.perform_later(@contract)
end

Sa phrase de clôture :

B est plus simple, pas besoin d'ajouter de colonne.

J'ai fixé les deux extraits un moment. B était effectivement plus simple, pas de migration, que des appels au cache, bien dans l'idiome Rails. Mais quelque chose ne passait pas.

J'ai tapé cinq caractères et fait entrée :

tu recommandes a ?

Claude a recalculé

Sa réponse (quelques blocs de code coupés) :

Je change d'avis — recommande A.

Raison : tout l'intérêt de la détection de proxy, c'est de ne pas faire confiance à la fraîcheur de l'ABI. Si on throttle via Solid Cache, on remet « est-ce qu'on a déjà vérifié ? » dans un stockage volatil :

  • Après un deploy / cache wipe, tous les contracts entrent en même temps dans l'état « non vérifié » → la prochaine vague de trafic déclenche une tempête de vérifications
  • Impossible de répondre à « depuis combien de temps ce contract n'a pas été revérifié ? »
  • Impossible de faire un filet de sécurité programmé : les contracts froids peuvent passer des semaines sans trafic et le déclenchement lazy n'arrivera jamais

Ce qu'ajoute une colonne last_proxy_check_at :

  1. Déclenchement lazy (même logique que B)
  2. Filet programmé via recurring.yml qui met en file par lot les contracts à plus de 24h, indépendamment du trafic utilisateur
  3. Afficher la fraîcheur dans l'UI : « Implementation verified 12 min ago »

Coût : une migration + un update_column par job (sans callbacks, quasi gratuit).

En résumé : B paraît « plus Rails-native, plus léger », mais il manque une couche de capacité. A, c'est l'option qui livre vraiment le « on ne fait pas confiance à la fraîcheur de l'ABI ».

Regarde ce qui s'est passé entre les deux recommandations :

  • Premier passage : lister les options → comparer le volume de code → choisir B (« plus simple »)
  • Deuxième passage : se caler sur pourquoi on fait ça → remarquer que B remet l'état dans du volatile, ce qui contredit le « ne pas faire confiance à la fraîcheur de l'ABI » → choisir A

Aucun fait nouveau n'est entré entre les deux. Volume de code, idioms Rails, sémantique du cache — tout est resté pareil. Ce qui a changé, c'est depuis quelle couche il évaluait.

Le premier passage comparait du volume de code. Le second comparait « est-ce que ce changement résout vraiment le problème qu'on s'était posé ? ».

Ce qu'ont fait ces cinq caractères

En tapant « tu recommandes a ? », je ne cherchais à pousser nulle part. Je restais bloqué sur le code de B et, par réflexe, je redemandais sa préférence.

Mais l'effet, c'est de l'avoir tiré de « choisis une des deux options » à « pourquoi on fait ça ? ». C'est tout l'écart qui compte.

Quand un LLM recommande, il part presque toujours d'optimums locaux : quel diff est plus petit, quel snippet est plus idiomatique, quelle implémentation se lit le mieux. Ce sont de vrais axes. Mais ils ratent souvent la connexion avec à quoi sert vraiment ce PR.

A et B sont équivalents sur l'axe « throttle de 5 min ». Mais A débloque en plus trois choses que B ne peut pas faire — filet programmé, observabilité, fraîcheur dans l'UI. Si tu regardes uniquement la couche throttle, B gagne. Si tu te soucies de pourquoi ce job existe, A gagne.

Claude n'a pas vu cette couche au premier passage non parce qu'il en est incapable. C'est que le cadre par défaut de « compare ces options » est la couche syntaxe/Rails. Une question retour l'a remonté à la couche problème, et il a calculé les coûts de B tout seul.

En faire une habitude

Quelques petits ajustements dans mon propre workflow :

Quand Claude finit par « plus simple / plus léger / plus natif », regarde deux fois. Ce sont des mots esthétiques, pas des mots de jugement. Ils décrivent ce qui se lit bien, pas ce qui tient.

Le coût de redemander est nul. « tu recommandes X ? » — cinq caractères. Claude est forcé d'évaluer depuis un angle qu'il n'avait pas pris. Même s'il atterrit sur la même recommandation, le raisonnement sera mieux fondé, suffisamment pour décider si tu lui fais confiance.

Fais-lui dérouler les coûts, pas seulement la recommandation. Ce qui a bien tourné cette fois, ce n'est pas moi qui ai forcé A — c'est Claude qui a écrit les coûts cachés de B (tempête après cache wipe, absence d'observabilité, impasse pour les tâches programmées). Je n'avais articulé aucun de ces points avant qu'il ne le fasse.

J'ai déployé A : ajout de la colonne last_proxy_check_at, déclenchement lazy + filet programmé à 24h, et l'UI affiche maintenant une ligne Implementation verified 12 min ago. Trois jours plus tard, sur un travail sans rapport, cette colonne m'a permis d'écrire Contract.where("last_proxy_check_at < ?", 24.hours.ago).find_each — une requête qui n'aurait tout simplement pas pu exister avec B.

Cinq caractères de question retour, ça vaut à peu près une refacto une semaine plus tard.