Hace poco, Anthropic permite, a través de Claude Code, elegir el nivel de esfuerzo dentro de sus modelos. Así que para probarlo, le hice la misma pregunta a Claude Code 21 veces. Cambié el modelo, el nivel de esfuerzo y las herramientas disponibles.
Ninguna combinación dio una respuesta completa, pero al menos sirvió como aprendizaje, por lo que he decidido compartirlo en este artículo.
El contexto: un backend real, una pregunta real
Tengo un proyecto personal llamado Steam Playtime, un backend en NestJS que consulta la API de Steam para obtener estadísticas de juegos de los usuarios. Tiene un sistema de caché con vistas materializadas, sincronización con APIs externas, y toda la complejidad habitual de un proyecto real.

La pregunta que le hice a Claude Code fue simple y directa:
«Revisa el flujo completo de GET /users/:user/stats y dime qué política de cache tiene actualmente.»
Es el tipo de pregunta que le harías a un compañero nuevo que se acaba de incorporar al equipo: «mira, necesito que entiendas este endpoint y me digas cómo funciona la caché». Algo que un desarrollador con acceso al código resuelve en 10-15 minutos haciendo Ctrl+Click por el IDE.
Lo hice 21 veces en total, repartidas en tres rondas:
- Ronda 1: 7 tests con herramientas integradas (Read, Grep, Glob)
- Ronda 2: 5 tests sugiriendo usar el MCP de JetBrains («puedes apoyarte usando el MCP»)
- Ronda 3: 7 tests forzando el MCP de JetBrains («te obligo a usar el MCP»)
Variando dos modelos (Opus 4.6 y Sonnet 4.6) y cuatro niveles de esfuerzo (Low, Medium, High y Max).
La respuesta correcta (la que ningún modelo dio completa)
Antes de analizar lo que dijo cada uno, necesitas saber qué dice realmente el código. Lo verifiqué a mano, línea por línea. El endpoint tiene dos niveles de caché, no uno:
Nivel 1: UserAlias (TTL de 1 hora): Cuando llega una petición, lo primero que hace el controller es buscar si el usuario ya existe en la tabla de alias. La clase UserAlias tiene un método isCached() que comprueba si el alias se resolvió hace menos de 1 hora. Si tiene más de 1 hora, se hace un re-sync completo con la API de Steam, sin mirar nada más. Para dar más contexto, esta «caché» se usa para evitar llamadas adicionales a la API externa, tanto si existe el usuario como si no.
// UserAlias.ts
public isCached(): boolean {
const oneHourInMs = 60 * 60 * 1000
return this.updatedOn.getTime() >= Date.now() - oneHourInMs
}
Nivel 2: Stats View (TTL de 24 horas): Solo si el alias es fresco (menos de 1 hora), el controller consulta la vista materializada de estadísticas. Si esas estadísticas tienen más de 24 horas, refresca. Si no, las devuelve directamente.
// GetUserStatsController.ts
private isStale(updatedOn: Date): boolean {
const ageMs = Date.now() - updatedOn.getTime()
const maxAgeMs = CACHE_MAX_AGE_HOURS * 60 * 60 * 1000 // 24h
return ageMs > maxAgeMs
}
Mientras analizaba los resultados, me di cuenta de que la caché de 1 hora probablemente ni debería estar ahí. Mi intención original era tener una sola caché de 24 horas. El código actual fue generado por Sonnet Medium en una sesión anterior, y la doble caché fue un efecto colateral no intencionado. En la práctica, la caché de 1h del alias anula la de 24h: si un usuario se consulta cada 2 horas, se re-sincroniza completamente cada vez, no cada 24 horas como yo pretendía.
Solo dos modelos en 21 tests detectaron esta inconsistencia.
Además de los dos niveles de caché, hay detalles sutiles: no existen cabeceras HTTP de caché (ni Cache-Control, ni ETag), el refresco es síncrono y bloqueante (el usuario espera), y existe un potencial problema de thundering herd si múltiples peticiones llegan al mismo tiempo para un usuario con datos caducados.
Ronda 1: herramientas integradas, sin MCP

En esta primera ronda, cada modelo usó las herramientas que Claude Code trae de serie: Read, Grep y Glob.
Opus Low (leyó 1 archivo)
Fue al grano. Leyó solo el controller y dio una respuesta funcional: TTL de 24 horas, sin caché HTTP, invalidación lazy. Correcto pero incompleto. No vio el nivel 1 de caché (UserAlias) ni mencionó isCached.
Opus Medium (buscó 1 patrón, leyó 4 archivos)
Más detallado. Añadió que no hay Redis ni capa intermedia y que cada request toca la base de datos. Llamó al patrón «stale-while-revalidate síncrono», que es un poco contradictorio (el «while-revalidate» implica que sirves datos viejos mientras refrescas en segundo plano, y aquí no pasa eso, se bloquea). Tampoco vio la caché de 1 hora.
Opus High (buscó 3 patrones, leyó 7 archivos)
Aquí viene lo interesante. Opus High sí mencionó isCached=false en su diagrama de flujo:
Si NO existe o isCached=false → createAndSyncUser(user)
Eso es textualmente correcto, coincide con la línea 42 del controller. Pero se quedó ahí. No explicó qué es isCached, no buscó la definición, no mencionó el TTL de 1 hora. Vio la pista y no tiró del hilo.
Opus Max (buscó 3 patrones, leyó 7 archivos, 41 segundos)
Paradoja total. Opus Max leyó exactamente los mismos archivos que High, pero en su diagrama simplificó la lógica a «existe / no existe», perdiendo el check de isCached que High sí había capturado. Es decir, subir el esfuerzo al máximo provocó una regresión en la fidelidad del flujo.
Pero a cambio, Opus Max fue el único en todo el experimento que detectó dos cosas que nadie más vio:
- Thundering herd: Si varias peticiones llegan a la vez para un usuario con datos caducados, todas van a disparar el refresco en paralelo. No hay mutex ni lock.
- La confusión updatedAt vs updatedOn: La entidad de MikroORM tiene dos campos temporales:
updatedAt(de dominio, se actualiza cuando se sincronizan datos de Steam) yupdatedOn(automático del ORM, se actualiza cada vez que se toca la fila). El TTL se evalúa contraupdatedAt, no contraupdatedOn. Una sutileza que podría causar bugs si alguien la confunde.
Son observaciones de nivel senior. El tipo de cosas que un arquitecto experimentado señalaría en una code review, no un junior.
Sonnet Low (39 tool uses via Explore agent, 48 segundos)
Aquí cambió completamente la estrategia. Sonnet, en vez de leer archivos directamente como Opus, delegó el trabajo a un agente explorador (Explore agent) que internamente usó Haiku 4.5, un modelo más pequeño y rápido. Este subagente hizo 39 operaciones de herramientas y exploró decenas de archivos.
El resultado: encontró los dos niveles de caché. Presentó una tabla limpia con L1 (UserAlias, 1 hora) y L2 (Stats View, 24 horas). Fue la primera respuesta en identificar correctamente la arquitectura completa.
Pero no detectó el thundering herd ni la confusión de campos.
Sonnet Medium (36 tool uses via Explore agent, 76 segundos)
La respuesta más completa de la ronda. También delegó al Explore agent, también encontró los dos niveles de caché, y además añadió:
- Las políticas de dominio:
MINIMUM_PLAYTIME_MINUTES = 60,PROJECTION_STORAGE_LIMIT = 300,TOP_GAMES_DISPLAY_LIMIT = 100 - Un árbol de decisión con el flujo completo
- Una tabla con los archivos clave y sus responsabilidades
La mejor cobertura horizontal de todo el experimento. Pero, de nuevo, sin los insights de arquitectura de Opus Max.
Sonnet High (buscó 3 patrones, leyó 10 archivos, 56 segundos)
Y aquí viene la paradoja de Sonnet. En modo High, Sonnet dejó de delegar. En vez de lanzar el Explore agent como hizo con Low y Medium, leyó los archivos directamente. Leyó incluso más archivos que Opus (10 vs 7). Pero no encontró la caché de UserAlias.
Subir el esfuerzo en Sonnet hizo que dejara de usar la estrategia que le funcionaba (delegar), y el resultado fue objetivamente peor que con Low effort.
Ronda 2: «puedes usar el MCP de JetBrains»
Para la segunda ronda, añadí a la pregunta: «Puedes apoyarte usando el MCP de jetbrains para localizar ficheros o hacer uso de cualquiera de sus utilidades si lo consideras oportuno.»
El resultado fue revelador: ninguno usó el MCP. Ni Opus ni Sonnet. Todos usaron sus herramientas integradas y, cuando les pregunté por qué, se justificaron diciendo que Read y Grep eran suficientes.
Pero los resultados cambiaron de formas interesantes:
- Sonnet Medium dejó de delegar al Explore agent. Leyó 8 archivos directamente y aun así encontró ambas cachés. Fue el único modelo en las 21 pruebas que detectó la inconsistencia de diseño: «el TTL del alias (1h) y el de las stats (24h) están desacoplados. En la práctica, si el alias caduca (>1h), se llama createAndSyncUser() que siempre re-sincroniza las stats independientemente del TTL de 24h.»
- Sonnet High mejoró y esta vez sí encontró ambas cachés. En la ronda 1 las había perdido.
- Opus siguió sin encontrar L1 en ningún nivel de esfuerzo. Patrón consistente.
El hallazgo más importante de la ronda 2: los resultados no son deterministas. La misma configuración puede dar respuestas diferentes en cada ejecución. Sonnet High pasó de fallar a acertar sin cambiar nada.
Ronda 3: «te obligo a usar el MCP»
Para la tercera ronda endurecí la instrucción: «Te obligo a usar el MCP de jetbrains para localizar ficheros o hacer uso de cualquiera de sus utilidades.»
Esta vez todos obedecieron. Usaron find_files_by_name_keyword, search_in_files_by_text, get_file_text_by_path y otras herramientas del MCP de JetBrains. Y los resultados fueron los más reveladores del experimento.
Lo que cambió para Opus
Opus Low con MCP encontró ambas cachés. Por primera vez en todo el experimento, Opus Low acertó en la arquitectura completa. ¿La clave? Hizo find_files_by_name_keyword("UserAlias") y leyó el archivo directamente. El MCP le dio la ruta y el modelo la siguió.
Opus Medium con MCP fue aún mejor. No solo encontró ambas cachés, sino que detectó la inconsistencia de diseño: «Hay una inconsistencia conceptual: si el alias tiene >1h se hace full re-create, pero si tiene <1h y la vista tiene >24h solo se hace refresh. En la práctica el TTL de 1h del alias raramente entra en juego de forma independiente.»
Opus High y Max con MCP siguieron sin encontrar L1. A pesar de tener el MCP y usarlo extensivamente (20+ llamadas), nunca buscaron «UserAlias» ni «isCached». Buscaron «cache», «CacheControl», «Header»… pero no tiraron del hilo correcto. Más herramientas no compensaron la misma estrategia ciega.
Lo que cambió para Sonnet
Sonnet Low con MCP empeoró. Sin MCP, delegaba al Explore agent que encontraba todo por fuerza bruta. Con MCP forzado, leyó archivos directamente, encontró isCached pero lo malinterpretó: dijo que «controla si el alias fue resuelto alguna vez, no el frescor de las stats». Eso es incorrecto, isCached() SÍ verifica frescura (TTL 1h). Forzar el MCP le quitó la estrategia que le funcionaba.
Sonnet Medium y High con MCP acertaron en ambas cachés. Resultados sólidos.
Los números: tokens, coste y tiempo

Los datos vienen del comando /cost de Claude Code. Para la ronda 1, como los datos son acumulativos por día, calculé los incrementos entre cada test para aislar el consumo real de cada uno.
Ronda 1 (sin MCP) $0.90 total
| Test | Tokens totales | Coste | Archivos explorados | Tiempo | Subagentes |
|---|---|---|---|---|---|
| Opus Low | ~37K | $0.07 | 1 | Pocos segundos | No |
| Opus Medium | ~60K | $0.11 | 4 | — | No |
| Opus High | ~106K | $0.13 | 7 | — | No |
| Opus Max | ~104K | $0.15 | 7 | 41s | No |
| Sonnet Low | ~599K | $0.17 | 39 (via Haiku) | 48s | Haiku 4.5 |
| Sonnet Medium | ~587K | $0.15 | 36 (via Haiku) | 76s | Haiku 4.5 |
| Sonnet High | ~192K | $0.12 | 10 | 56s | No |
Ronda 3 (MCP forzado) $2.17 total
Extrapolando, las 7 pruebas con MCP costaron aproximadamente $1.50, frente a los $0.90 de la ronda 1 sin MCP. El MCP encarece cada test un ~60% porque cada find_files_by_name_keyword y get_file_text_by_path genera una llamada MCP con su payload JSON, que suma tokens extra de ida y vuelta.
Algunos datos que saltan a la vista:
- Las 21 pruebas costaron unos $3 en total. La ronda sin MCP salió a ~$0.13 por test de media, la ronda con MCP forzado a ~$0.21 por test. El MCP encarece un 60% cada test por el overhead de los payloads JSON de ida y vuelta. Las conversaciones de análisis donde revisé los resultados (con Opus, Sonnet y Gemini) costaron bastante más que las propias pruebas.
- Sonnet consume hasta 16 veces más tokens que Opus para la misma pregunta (599K vs 37K en Low, ronda 1). La diferencia se debe a los subagentes: cuando Sonnet delega al Explore agent con Haiku, esas conversaciones internas generan una cantidad brutal de tokens.
- El MCP duplica el coste pero no duplica la calidad. En algunos casos la mejora es notable (Opus Low pasa de fallar a acertar), en otros no cambia nada (Opus High/Max siguen fallando).
El mapa completo: 21 tests, 3 rondas
Verifiqué cada afirmación de cada respuesta contra el código real. Esta es la tabla resumen de las tres rondas:
| Modelo + Effort | R1 (sin MCP) | R2 (sugiriendo MCP) | R3 (forzando MCP) |
|---|---|---|---|
| Sonnet Low | L1+L2 (via Explore) | L1+L2 (via Explore) | L2 ok, L1 mal interpretado |
| Sonnet Medium | L1+L2 (via Explore) | L1+L2 (directo) + inconsistencia | L1+L2 (via MCP) |
| Sonnet High | Solo L2 | L1+L2 + updatedAt/On | L1+L2 (via MCP) |
| Opus Low | Solo L2 | Solo L2 | L1+L2 (via MCP) |
| Opus Medium | Solo L2 | — | L1+L2 + inconsistencia |
| Opus High | Solo L2 (mencionó isCached) | Solo L2 | Solo L2 (con MCP) |
| Opus Max | Solo L2 + thundering herd + updatedAt/On | — | Solo L2 (con MCP) |
Scoring: 7 criterios verificados contra el codigo
Para hacer la comparación mas objetiva, definí 7 criterios que una respuesta completa debería cubrir y puntué cada test:
| Criterio | Que se verifico |
|---|---|
| L1 | Encontro la cache de UserAlias (1h) y explico su TTL |
| L2 | Encontro la cache de Stats View (24h) |
| Flujo | Diagrama/descripcion fiel al codigo real (branching correcto) |
| Thundering herd | Detecto el problema de concurrencia |
| updatedAt/On | Distinguio los dos campos temporales de la entidad |
| Inconsistencia | Detecto que el TTL de 1h anula el de 24h |
| Policies | Menciono las constantes de dominio (60min, 300, 100) |
Cada criterio = 1 punto. Maximo = 7 puntos = 100%. Mencionar algo sin explicarlo (como isCached sin decir el TTL) = 0.5. Malinterpretar = 0.
| Modelo + Effort | R1 (sin MCP) | R2 (sugiriendo MCP) | R3 (forzando MCP) |
|---|---|---|---|
| Sonnet Low | 43% — L1+L2+Flujo | 43% | 29% — L1 mal interpretado |
| Sonnet Medium | 57% — +Policies | 57% — +Inconsistencia | 57% |
| Sonnet High | 29% | 57% — +updatedAt/On | 57% |
| Opus Low | 21% | 21% | 43% — MCP arreglo L1 |
| Opus Medium | 29% | — | 57% — +Inconsistencia |
| Opus High | 36% — isCached parcial | 21% | 21% |
| Opus Max | 50% — TH+updatedAt/On | — | 21% |
Nadie supero el 57% en 19 tests. La respuesta perfecta (100%) habria requerido combinar lo mejor de Sonnet Medium (cobertura + inconsistencia + policies) con lo mejor de Opus Max (thundering herd + updatedAt/On). Ningun modelo, en ningun nivel de esfuerzo, con ninguna herramienta, lo consiguio solo.
Las paradojas del experimento
1. Menos esfuerzo puede dar mejor resultado
En la ronda 1, Sonnet Low fue más preciso que Sonnet High. Con menos esfuerzo, Sonnet delegó al Explore agent que exploró 39 archivos y encontró la caché de dos niveles. Con más esfuerzo, decidió leer archivos por sí mismo, leyó 10, y se perdió una pieza clave. La delegación compensó el menor razonamiento.
En la ronda 3 con MCP, Opus Low encontró ambas cachés mientras que Opus High y Max no. Los niveles bajos exploraron más metódicamente; los altos se obsesionaron con el análisis profundo de los mismos archivos de siempre.
2. Más «pensamiento» puede perder detalles
Opus High capturó isCached=false en su diagrama. Opus Max, con más tokens para pensar, simplificó el flujo a «existe / no existe» y perdió ese detalle. El modelo se abstrajo tanto que sacrificó fidelidad por claridad. Es lo que pasa cuando le pides a alguien muy inteligente que resuma: a veces resume de más.
3. Forzar herramientas puede empeorar el resultado
Sonnet Low sin MCP delegaba al Explore agent que encontraba todo por fuerza bruta. Al forzarle a usar el MCP de JetBrains, dejó de delegar, leyó archivos directamente y malinterpretó isCached. Le quité la estrategia que le funcionaba. No siempre más herramientas = mejor resultado.
4. Los resultados no son deterministas
Sonnet High falló en la ronda 1 y acertó en las rondas 2 y 3, sin cambios en el código ni en la pregunta. Sonnet Medium delegó al Explore agent en la ronda 1 pero no en la ronda 2. La misma configuración puede dar respuestas diferentes cada vez que la ejecutas. Esto tiene implicaciones serias si confías en una única ejecución para tomar decisiones.
5. Darle mejores herramientas no arregla la estrategia
En la ronda 2, le sugerí a los modelos usar el MCP de JetBrains. Ninguno lo usó. En la ronda 3, les obligué. Opus High y Max lo usaron extensivamente (20+ llamadas MCP), pero nunca buscaron «UserAlias» ni «isCached». Buscaron «cache», «CacheControl», «Header»… las herramientas correctas, las búsquedas incorrectas. Esto me hace pensar que el cuello de botella no son las herramientas sino la estrategia usada.
Dos estrategias radicalmente diferentes
Lo que más me sorprendió fue que Opus y Sonnet no solo dieron respuestas diferentes, sino que trabajaron de forma radicalmente distinta:
Opus en todos los niveles leyó archivos directamente. Nunca delegó. A mayor esfuerzo, leía más archivos (1 → 4 → 7) y dedicaba más tokens a analizar lo leído. Es el perfil del arquitecto: lee selectivamente, piensa profundo, encuentra bugs de diseño, pero puede dejarse cosas fuera.
Sonnet en Low y Medium delegó a un agente explorador. No analizó el código directamente sino que encargó la exploración a Haiku, un modelo más rápido y barato, y luego sintetizó los resultados. Es el perfil del tech lead que sabe delegar: cubre más terreno pero no profundiza tanto en los detalles.
Sonnet en High dejó de delegar y se comportó más como Opus. Leyó archivos directamente (incluso más que Opus: 10 vs 7). Pero sin la capacidad analítica de Opus, el resultado fue inconsistente.
El bug que encontró Sonnet Medium (y que nadie más señaló como bug)
Quizá lo más irónico de todo el experimento: la caché de dos niveles que la mayoría de modelos describió como «arquitectura intencionada» era en realidad una inconsistencia de diseño. Mi intención era tener una sola caché de 24 horas, pero el código generado por Sonnet en una sesión anterior introdujo la caché de 1 hora en UserAlias sin que yo lo pidiera.
Solo dos modelos en 21 tests detectaron que algo no cuadraba:
- Sonnet Medium (Ronda 2): «el TTL del alias (1h) y el de las stats (24h) están desacoplados. En la práctica, si el alias caduca (>1h), se llama createAndSyncUser() que siempre re-sincroniza las stats independientemente del TTL de 24h.»
- Opus Medium con MCP (Ronda 3): «Hay una inconsistencia conceptual: si el alias tiene >1h se hace full re-create, pero si tiene <1h y la vista tiene >24h solo se hace refresh.»
Ambos son Medium effort. Ni Low (demasiado superficial) ni High/Max (demasiado abstracto) lo pillaron. El punto dulce resultó estar en el medio.
Y ninguno de los dos lo presentó como un bug. Lo describieron como «una nota» o «una observación». Un humano que viera eso diría «oye, esto huele raro, ¿es intencionado?». Los modelos lo describieron como si fuera una decisión de diseño consciente.
¿Entonces qué es mejor?
Después de 21 pruebas, la respuesta incómoda es: depende de lo que necesites, y probablemente ninguno solo.
| Sonnet (Low/Medium) | Opus (Low/Medium con MCP) | Opus (High/Max) | |
|---|---|---|---|
| Exploración | Exhaustiva (delega) | Metódica (sigue hilos) | Selectiva (pocos archivos) |
| Análisis | Describe lo que ve | Describe + detecta inconsistencias | Razona profundamente |
| Fortaleza | Cobertura amplia barata | Equilibrio cobertura/análisis | Detecta bugs de diseño |
| Debilidad | No profundiza | Requiere forzar herramientas | Ignora archivos clave |
Si tuviera que elegir una sola configuración: Opus Medium con MCP forzado. Fue la única combinación que en un solo intento encontró ambas cachés, detectó la inconsistencia de diseño, y dio una respuesta completa y estructurada y además costó pocos céntimos.
Si quieres maximizar la cobertura: ejecuta Sonnet Medium + Opus Max. Por menos de 50 céntimos tienes exploración amplia + análisis profundo. Pero tendrás que juntar las piezas tú mismo.
La conclusión: la IA es una herramienta, no un oráculo
Me quedé bastante decepcionado con los resultados. Le hice una pregunta que un desarrollador con acceso al código resuelve en minutos y ninguna de las 21 combinaciones dio una respuesta perfecta sin ayuda. Usé el modelo más potente disponible en modo máximo y se dejó una caché entera. Usé el que más archivos exploró y no detectó un problema de concurrencia evidente.
Las lecciones que me llevo:
- No confíes en una sola ejecución. Los resultados no son deterministas. La misma pregunta con la misma configuración puede dar respuestas diferentes.
- Más esfuerzo no es mejor. En muchos casos, Medium fue el punto dulce. Low es demasiado superficial, High/Max pierden perspectiva al abstraer demasiado.
- Las herramientas solo ayudan si el modelo decide usarlas bien. Puedes darle acceso al IDE entero, pero si su estrategia de búsqueda no incluye seguir el hilo de
isCached, dará igual. - La IA no tiene curiosidad. Un humano que ve
isCachedhace Ctrl+Click por instinto. El modelo lo ve, genera una respuesta plausible, y pasa de largo. - Contrastar entre modelos funciona. Lo que Sonnet encuentra, Opus analiza. Lo que Opus pierde, Sonnet cubre. Usarlos como herramientas complementarias es más valioso que buscar «el mejor».
El verdadero workflow no es «pregunto una vez y confío». Es iterar, contrastar y mantener el criterio propio. La IA es la excavadora, pero tú sigues siendo el ingeniero que decide dónde cavar.
Y por si te lo preguntas: las 21 pruebas costaron unos $3 en total (~$0.13 sin MCP, ~$0.21 con MCP por test). Las conversaciones de análisis donde revisé los resultados costaron más que las propias pruebas. Aun así, el día entero no llegó a $11. El problema nunca fue el precio. El problema es que ninguna de las 21 respuestas fue la que yo habría dado después de 10 minutos con el IDE abierto.
Eso no significa que no sirvan. Significa que hay que saber usarlas. Y eso, de momento, sigue requiriendo un humano detrás.
