Free

Hacer que Claude cambie de opinión

La recomendación "más ligera" de Claude ocultaba costes reales. Una sola repregunta los puso todos sobre la mesa.


Estaba trabajando en smarts — una app Rails que renderiza contratos inteligentes de Ethereum como "documentación para desarrolladores". Hace unos días me topé con un viejo problema: los contratos se actualizan. Si el sistema renderiza un proxy actualizado usando una ABI obsoleta, le está mintiendo al usuario.

Le pedí a Claude que diseñara conmigo un mecanismo de detección de upgrades de proxy. Me devolvió un plan técnico bastante completo, con compatibilidad para EIP-1967, Beacon proxies y ZeppelinOS. Al llegar al "cuándo disparar la verificación", me ofreció dos opciones:

# Opción A: añadir last_proxy_check_at a la BD, throttle de 5 min
if @contract.last_proxy_check_at.nil? || @contract.last_proxy_check_at < 5.minutes.ago
  CheckProxyUpgradeJob.perform_later(@contract)
end

# Opción B: throttle vía Solid Cache, más ligero
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

Su frase de cierre:

B es más simple, no hace falta añadir columna.

Me quedé mirando los dos fragmentos un rato. B era más simple, sin migración, todo cache, muy idiomático en Rails. Pero algo no me terminaba de cerrar.

Tecleé cinco caracteres y pulsé enter:

recomiendas a?

Claude recalculó

Su respuesta (algunos bloques de código recortados):

Cambio de opinión — recomiendo A.

El motivo: el objetivo entero de detectar upgrades de proxy es no confiar en la frescura de la ABI. Si throttleamos vía Solid Cache, estamos volviendo a meter "¿ya verificamos?" en almacenamiento volátil:

  • Tras un deploy / cache wipe, todos los contratos entran a la vez en estado "no verificado" → la siguiente ola de tráfico dispara una tormenta de verificaciones
  • No hay forma de responder "¿hace cuánto que no se reverifica este contrato?"
  • No se puede hacer un respaldo programado: contratos fríos pueden pasar semanas sin tráfico y el lazy trigger nunca se activa

Lo que desbloquea añadir la columna last_proxy_check_at:

  1. Lazy trigger (la misma lógica que B)
  2. Respaldo programado vía recurring.yml que encola por lotes los contratos con más de 24h, independiente del tráfico de usuarios
  3. Mostrar la frescura en la UI: "Implementation verified 12 min ago"

Coste: una migración + un update_column por job (sin callbacks, prácticamente gratis).

En resumen: B parece "más Rails-native, más ligero", pero le falta una capa de capacidad. A es la opción que de verdad cumple con el "no confiamos en la frescura de la ABI".

Mira lo que pasó entre las dos recomendaciones:

  • Primera vez: listar opciones → comparar líneas de código → elegir B ("más simple")
  • Segunda vez: anclarse en por qué hacemos esto → notar que B mete el estado en almacenamiento volátil, contradiciendo la premisa de "no confiar en la frescura de la ABI" → elegir A

Entre las dos pasadas no entró ningún hecho nuevo. Líneas de código, idioms de Rails, semántica de cache — todo igual. Lo que cambió fue desde qué capa estaba evaluando.

La primera pasada comparaba volumen de código. La segunda comparaba "¿este cambio resuelve realmente el problema que nos planteamos?".

Lo que hicieron esos cinco caracteres

Cuando tecleé "recomiendas a?" no estaba intentando empujarlo a nada. Simplemente me había quedado atascado mirando el código de B y, por instinto, le pregunté otra vez su preferencia.

Pero el efecto fue sacarlo del modo "elegir una de dos opciones" para devolverlo al modo "¿por qué estamos haciendo esto?". Esa diferencia es todo el punto.

Cuando los LLM hacen recomendaciones, casi siempre arrancan desde óptimos locales: qué diff es más pequeño, qué snippet es más idiomático, qué implementación se lee más limpia. Son ejes reales. Pero suelen no conectar con para qué es realmente este PR.

A y B son equivalentes en el eje "throttle de 5 minutos". Pero A además desbloquea tres cosas que B no puede hacer — respaldo programado, observabilidad, frescura en la UI. Si solo miras la capa del throttle, gana B. Si te importa por qué existe este job, gana A.

Claude no vio esa capa la primera vez no porque no pueda. Es que el marco por defecto para "compara estas opciones" es la capa de sintaxis/Rails. Una repregunta lo subió a la capa del problema, y desde ahí calculó solo los costes de B.

Convertir esto en hábito

Algunos ajustes pequeños que hice en mi propio flujo:

Cuando Claude cierra con "más simple / más ligero / más nativo", míralo dos veces. Son palabras estéticas, no de juicio. Describen lo que se lee bien, no lo que aguanta.

El coste de volver a preguntar es cero. "¿recomiendas X?" — cinco caracteres. Claude se ve obligado a evaluar desde un ángulo que no había tomado. Aunque acabe en la misma recomendación, el razonamiento queda más sólido, suficiente para que tú decidas si confiar en él.

Hazle desplegar los costes, no solo la recomendación. Lo que hizo que esto saliera bien no fue que yo forzara A — fue que Claude escribiera los costes ocultos de B (tormenta tras cache wipe, falta de observabilidad, callejón sin salida para tareas programadas). Yo no había articulado ninguno antes de que él lo hiciera.

Acabé desplegando A: añadí la columna last_proxy_check_at, lazy trigger más respaldo programado a 24h, y la UI ahora muestra una línea: Implementation verified 12 min ago. Tres días después, en un trabajo no relacionado, esa columna me permitió escribir Contract.where("last_proxy_check_at < ?", 24.hours.ago).find_each — una consulta que sencillamente no habría existido si hubiera tomado B.

Cinco caracteres de repregunta valen, más o menos, una refactorización de la semana siguiente.