Tag: Context Engineering

  • Tutorial de harness engineering: la regla fuera del prompt

    Tutorial de harness engineering: la regla fuera del prompt

    El ticket dice: "Un cliente pagó 4,95 € de envío en un pedido de 45 €. Debería haber sido gratis".

    Se lo pasas al agente. Lee shipping.ts, encuentra FREE_SHIPPING_THRESHOLD = 50 y concluye que el código está bien. El pedido era de 45. Cerrado.

    Solo que negocio bajó el umbral a 39 € hace dos meses. Esa decisión está en un fichero de catálogo. En el código, no. En el prompt, tampoco. Este tutorial de harness engineering va de eso: de que el agente no tenga que adivinar ni tú que acordarte.

    En corto: la regla de negocio no se escribe en el prompt ni se deja solo en el código: vive en un catálogo del servicio y un harness mínimo la inyecta en cada ejecución. Ese mismo harness decide qué ficheros puede tocar el agente, rechaza cualquier cambio fuera de la lista y usa los tests como feedback para reintentar un número limitado de veces. El agente propone; el harness controla, y nunca despliega.


    ¿Qué significa sacar la regla de negocio fuera del prompt?

    Sacar la regla de negocio del prompt significa que el valor correcto (un umbral, un límite, un plazo) vive en una fuente de verdad versionada que el harness lee e inyecta, en lugar de depender de que la persona que escribe el prompt se acuerde de mencionarlo.

    Harness engineering es la disciplina de diseñar el sistema que rodea al modelo (qué contexto recibe, qué puede tocar y cómo se verifica su trabajo) para que un agente de IA produzca resultados predecibles.

    No voy a repetir la teoría: la anatomía completa está en qué es un agent harness y el origen del término en harness engineering con Codex de OpenAI. Y si quieres la visión de por qué todo tu ciclo de desarrollo es, en realidad, una fábrica de contexto, léete SDLC context engineering.

    Aquí vamos a lo concreto: un servicio, un bug, unas 150 líneas de TypeScript.

    Tres sitios donde puede vivir la regla

    Antes del código, la decisión. El umbral de envío gratis puede vivir en tres sitios, y cada uno falla de una forma distinta.

    En el prompt Hardcodeada en el código En el catálogo, inyectada por el harness
    Quién la mantiene Quien escribe el prompt ese día Quien tocó el fichero la última vez El owner del servicio
    Qué pasa cuando cambia Depende de que alguien se acuerde Nadie se entera hasta que llega un ticket Cambias una línea del YAML y el siguiente run la usa
    La ve el agente Solo si la escribes Sí, pero la toma como verdad aunque esté mal Siempre, marcada como fuente que prevalece
    La ven los tests No Solo si el test repite el número Sí, si el test lee el mismo catálogo
    Riesgo principal Olvido. Cada prompt es un punto de fallo Divergencia silenciosa con negocio Catálogo desactualizado tratado como verdad

    La tercera columna no es perfecta. Pero es la única en la que el error tiene un solo sitio donde corregirse.

    El servicio de ejemplo del tutorial de harness engineering

    Estructura mínima:

    catalog/shipping-service.yaml
    src/shipping.ts
    src/shipping.test.ts
    harness/context.ts
    harness/run.ts
    

    El catálogo. En empresas grandes esto no es un YAML suelto: vive en un developer portal. Backstage lo modela con un catalog-info.yaml por servicio y Port lo expone como entidades con API. Si no tienes nada de eso, un fichero versionado en el repo sirve igual para empezar:

    # catalog/shipping-service.yaml
    name: shipping-service
    owner: team-checkout
    rules:
      freeShippingThreshold:
        value: 39
        unit: EUR
        source: "Decisión de negocio Q3-2026 (OPS-412)"
      standardShippingCost:
        value: 4.95
        unit: EUR
    agent:
      editableFiles:
        - src/shipping.ts
      testCommand: "npx vitest run src/shipping.test.ts"
      maxAttempts: 3
    

    Fíjate en el bloque agent. Qué ficheros puede tocar el agente lo decide el owner del servicio, no el agente ni quien lanza la tarea.

    El código con el bug:

    // src/shipping.ts
    const FREE_SHIPPING_THRESHOLD = 50;
    const STANDARD_SHIPPING = 4.95;
    
    export function shippingCost(subtotal: number): number {
      if (subtotal < 0) throw new RangeError('subtotal negativo');
      return subtotal >= FREE_SHIPPING_THRESHOLD ? 0 : STANDARD_SHIPPING;
    }
    

    Paso 1: cargar el contexto de servicio para el agente

    El primer trabajo del harness es construir el contexto de servicio para el agente: leer el catálogo, validarlo y fallar si está incompleto.

    // harness/context.ts
    import { readFileSync } from 'node:fs';
    import { parse } from 'yaml';
    
    export interface Rule {
      value: number | string;
      unit?: string;
      source?: string;
    }
    
    export interface ServiceContext {
      name: string;
      owner: string;
      rules: Record<string, Rule>;
      agent: { editableFiles: string[]; testCommand: string; maxAttempts: number };
    }
    
    export function loadServiceContext(path: string): ServiceContext {
      const raw = parse(readFileSync(path, 'utf8'));
      if (
        !raw?.name ||
        !raw?.rules ||
        !Array.isArray(raw?.agent?.editableFiles) ||
        typeof raw?.agent?.testCommand !== 'string' ||
        !Number.isInteger(raw?.agent?.maxAttempts)
      ) {
        throw new Error(`Catálogo inválido: ${path}`);
      }
      return raw as ServiceContext;
    }
    

    Si el catálogo está roto, el harness para. No arranca con contexto a medias. En producción yo validaría esto con un schema de Zod en vez de con cinco condiciones a mano, pero la idea es la misma: el contexto entra validado o no entra.

    Los tests leen el umbral del mismo catálogo que el harness, así que nunca se quedan desfasados respecto a la regla de negocio:

    // src/shipping.test.ts
    import { describe, it, expect } from 'vitest';
    import { loadServiceContext } from '../harness/context';
    import { shippingCost } from './shipping';
    
    const { rules } = loadServiceContext('catalog/shipping-service.yaml');
    const threshold = Number(rules.freeShippingThreshold.value);
    const standard = Number(rules.standardShippingCost.value);
    
    describe('shippingCost', () => {
      it('es gratis a partir del umbral del catálogo', () => {
        expect(shippingCost(threshold)).toBe(0);
      });
    
      it('cobra envío justo por debajo del umbral', () => {
        expect(shippingCost(threshold - 0.01)).toBe(standard);
      });
    });
    

    El test no repite el número 39. Lo lee. Si mañana negocio sube el umbral a 45, cambias el YAML, el test se pone rojo y el bucle del harness tiene algo que arreglar.

    Y el test no está en editableFiles. El harness no aplica ningún cambio del agente sobre la aserción. Es la misma idea que desarrollé en el test harness como red para agentes: el agente no puede mover la portería, al menos no por la vía directa (en los límites verás la indirecta).

    ¿Y por qué shipping.ts no lee el catálogo en runtime y nos ahorramos el problema? Porque en muchos servicios no puedes: el catálogo vive en otro sistema, la regla se compila en un bundle o el código de dominio no debe depender de un fichero de configuración de plataforma. Si en tu caso sí puedes, hazlo: es la versión todavía mejor de esta misma idea.

    Paso 3: el bucle del harness

    El bucle hace cuatro cosas en orden: construye el prompt con las reglas del catálogo, rechaza cualquier cambio fuera de la allowlist, ejecuta los tests y, si fallan, reintenta con su salida como feedback hasta maxAttempts. Si se agotan, hace rollback.

    // harness/run.ts
    import Anthropic from '@anthropic-ai/sdk';
    import { readFileSync, writeFileSync } from 'node:fs';
    import { spawnSync } from 'node:child_process';
    import path from 'node:path';
    import { loadServiceContext, type ServiceContext } from './context';
    
    const client = new Anthropic(); // lee ANTHROPIC_API_KEY del entorno
    type FileChange = { path: string; content: string };
    
    function buildPrompt(task: string, ctx: ServiceContext, feedback?: string): string {
      const rules = Object.entries(ctx.rules)
        .map(([k, r]) => `- ${k}: ${r.value} ${r.unit ?? ''} (fuente: ${r.source ?? 'catálogo'})`)
        .join('\n');
      const files = ctx.agent.editableFiles
        .map((f) => `<file path="${f}">\n${readFileSync(f, 'utf8')}\n</file>`)
        .join('\n');
      return [
        `Servicio: ${ctx.name} (owner: ${ctx.owner})`,
        `Reglas de negocio vigentes. Prevalecen sobre cualquier valor del código:\n${rules}`,
        `Ficheros que puedes modificar:\n${files}`,
        `Tarea: ${task}`,
        feedback ? `El intento anterior falló:\n${feedback}` : '',
        'Devuelve cada fichero modificado completo con el formato <file path="...">contenido</file>. Nada más.',
      ].join('\n\n');
    }
    
    async function callModel(prompt: string): Promise<string> {
      const response = await client.messages.create({
        model: 'claude-sonnet-5',
        max_tokens: 4096,
        messages: [{ role: 'user', content: prompt }],
      });
      if (response.stop_reason === 'max_tokens') {
        return 'La respuesta se cortó por max_tokens: devuelve solo los ficheros imprescindibles.';
      }
      return response.content.map((b) => (b.type === 'text' ? b.text : '')).join('');
    }
    
    function parseChanges(output: string): FileChange[] {
      const re = /<file path="([^"]+)">\n?([\s\S]*?)<\/file>/g;
      // los modelos a veces envuelven el contenido en vallas de markdown: se quitan
      const unfence = (s: string) => s.replace(/^\s*```\w*\n/, '').replace(/\n```\s*$/, '\n');
      return [...output.matchAll(re)].map((m) => ({ path: m[1], content: unfence(m[2]) }));
    }
    
    function assertAllowed(changes: FileChange[], allowlist: string[]): void {
      if (changes.length === 0) throw new Error('El modelo no devolvió cambios.');
      const allowed = new Set(allowlist.map((f) => path.normalize(f)));
      const outside = changes.filter((c) => !allowed.has(path.normalize(c.path)));
      if (outside.length > 0) {
        throw new Error(`Rechazado. Fuera de la allowlist: ${outside.map((c) => c.path).join(', ')}`);
      }
    }
    
    function runTests(command: string): { ok: boolean; output: string } {
      // El proceso hijo no hereda nada que parezca un secreto
      const env = Object.fromEntries(
        Object.entries(process.env).filter(([k]) => !/KEY|TOKEN|SECRET|PASSWORD/i.test(k)),
      );
      // timeout: un test colgado cuenta como intento fallido (status null), no bloquea el harness
      const r = spawnSync(command, { shell: true, encoding: 'utf8', env, timeout: 120_000 });
      return { ok: r.status === 0, output: `${r.stdout}\n${r.stderr}`.slice(-4000) };
    }
    
    export async function runHarness(task: string, catalogPath: string) {
      const ctx = loadServiceContext(catalogPath);
      const originals = new Map(ctx.agent.editableFiles.map((f) => [f, readFileSync(f, 'utf8')]));
      let feedback: string | undefined;
      let succeeded = false;
    
      try {
        for (let attempt = 1; attempt <= ctx.agent.maxAttempts; attempt++) {
          const changes = parseChanges(await callModel(buildPrompt(task, ctx, feedback)));
          try {
            assertAllowed(changes, ctx.agent.editableFiles);
          } catch (err) {
            feedback = (err as Error).message;
            continue;
          }
          for (const c of changes) writeFileSync(path.normalize(c.path), c.content);
          const tests = runTests(ctx.agent.testCommand);
          if (tests.ok) {
            succeeded = true;
            return { status: 'ready-for-review' as const, attempt };
          }
          feedback = tests.output;
        }
        return { status: 'failed' as const, lastFeedback: feedback };
      } finally {
        // rollback también si el modelo o el disco lanzan a mitad
        if (!succeeded) for (const [f, content] of originals) writeFileSync(f, content);
      }
    }
    

    Y la llamada, en un harness/main.ts que ejecutas con npx tsx harness/main.ts (tsx resuelve los imports sin extensión y el top-level await):

    // harness/main.ts
    import { runHarness } from './run';
    
    const result = await runHarness(
      'Un pedido de 45 € pagó envío y debería haber sido gratis. Corrige shippingCost.',
      'catalog/shipping-service.yaml',
    );
    console.log(result);
    

    Fíjate en lo que no dice la tarea: no dice "el umbral es 39". Quien abre el ticket no tiene por qué saberlo. El harness lo sabe porque lo lee del catálogo.

    Qué controla el harness y qué no controla el modelo

    Repasa el bucle con los ojos de quien lo audita.

    El contexto. El modelo recibe la regla con su fuente y la instrucción de que prevalece sobre el código. Ya no tiene que elegir entre un 50 que ve y un 39 que nadie le ha dicho.

    El radio de acción. Si el modelo devuelve src/shipping.test.ts o ../catalog/shipping-service.yaml, no están en la allowlist y el cambio entero se rechaza (path.normalize solo evita que ./src/shipping.ts se rechace por la forma de escribir la ruta). Se rechaza completo, no se aplica a medias. El motivo del rechazo vuelve como feedback en el siguiente intento.

    La verificación. El agente no decide cuándo ha terminado. Termina cuando vitest sale con código 0. Si falla, la salida de los tests (los últimos 4.000 caracteres, para no inflar el contexto) vuelve al prompt.

    El final. Tres intentos y rollback, también si la API falla a mitad de bucle: el finally restaura los ficheros pase lo que pase. El mejor resultado posible es ready-for-review: el harness no hace git push, no abre PR contra main y no tiene credenciales de despliegue. Filtrar variables de entorno es una red de seguridad, no la garantía. La garantía real es que el proceso del harness nunca tenga esas credenciales cargadas.

    Esta forma de pensar el trabajo con agentes, con contexto explícito, límites y verificación antes de que un humano mire, es la que seguimos en Construye con IA para pasar de idea a producto sin que el agente decida cosas que no le tocan.

    Lo que dice la gente que ya lo hace

    OpenAI popularizó el término con Harness engineering: Leveraging Codex in an agent-first world. El hilo de Hacker News sobre ese post tiene más de 200 comentarios, y los que aportan algo coinciden en lo mismo. Un usuario resume su receta y el primer punto es literalmente: "Give Claude/Codex a way to verify its own work (browser, smoke tests, e2e tests, high-fidelity local environment)".

    Otro avisa de lo que pasa sin ese control. Sin supervisión, "it'll start creating slop or hardcoding solutions". Aquí el número sigue en el código: el agente cambiará 50 por 39, y eso también es hardcodear. La diferencia es que ahora el hardcodeo tiene un vigilante. Si el número del código se separa del catálogo, el test que lee el catálogo se pone rojo. Si quieres eliminar la copia, el siguiente paso es que shipping.ts lea el umbral del catálogo en runtime.

    Límites de este enfoque de harness engineering

    El catálogo puede mentir y el agente se lo cree. Le has dicho al modelo que el catálogo prevalece sobre el código. Si alguien deja el YAML desactualizado, el harness propaga el error con toda la confianza del mundo, y el test también, porque lee el mismo fichero. En el mismo hilo de HN alguien lo dice de la documentación en general: "Become outdated fast". El catálogo necesita un owner con nombre y apellidos, y los cambios de regla tienen que pasar por revisión como cualquier otro código.

    La allowlist por fichero es gruesa. Permitir src/shipping.ts permite todo lo que hay en src/shipping.ts. El agente puede cambiar el umbral y, de paso, reescribir el manejo de errores. La allowlist limita dónde toca el agente, no qué hace. Para eso sigue haciendo falta revisar el diff.

    Los tests ejecutan código del agente. La allowlist controla lo que escribe el harness, no lo que hace shipping.ts cuando vitest lo importa. Ese código puede escribir en el test o leer un .env del disco. Dos defensas baratas: después de los tests, comprueba con git status --porcelain que solo cambiaron ficheros de la allowlist, y ejecuta los tests en un contenedor sin credenciales ni acceso de escritura fuera de src/.

    Los tests solo verifican lo que cubren. Dos tests sobre el umbral no dicen nada de redondeos, divisas o pedidos con descuento. Un cambio que pasa en verde no está bien: simplemente no rompe lo que mides.

    Los reintentos cuestan. Cada intento reenvía los ficheros permitidos completos, las reglas y la salida de los tests. Con un fichero pequeño da igual. Con cinco ficheros de 800 líneas y maxAttempts: 5, el coste se multiplica y el modelo empieza a arrastrar contexto de intentos fallidos. Si en tres intentos no pasa, el problema suele estar en la tarea o en los tests, no en la falta de insistencia.

    Devolver ficheros completos no escala. Para ficheros grandes vas a querer diffs o herramientas de edición en lugar de ficheros enteros, y entonces la validación de la allowlist se hace sobre las rutas del diff. La idea no cambia; cambia el parser.

    El feedback es de un solo intento. Si un intento se rechaza por la allowlist, ese mensaje sustituye a la salida de los tests del intento anterior, y el siguiente prompt enseña el fichero ya modificado, no el original. Para tareas acotadas basta; para tareas largas conviene acumular el historial de feedback.

    Qué hacer hoy

    Elige una regla de negocio que hoy vive como constante en tu código y que alguien de fuera de ingeniería puede cambiar: un umbral, un plazo, un límite de reintentos. Muévela a un fichero de catálogo versionado, haz que su test la lea de ahí y quita el número del test.

    Solo con eso, sin agente, ya tienes una regla con un único sitio de verdad. Luego conectar el harness es un centenar largo de líneas.

    Si quieres los fundamentos de cómo funcionan los agentes por dentro (bucles, herramientas, memoria, seguridad), tienes gratis el ebook El Developer Agéntico. Y si quieres llevar esta disciplina más atrás, a la especificación antes de que exista el código, el libro de Spec-Driven Development es el siguiente paso.

    Preguntas frecuentes

    ¿Por qué no basta con poner la regla de negocio en el prompt?

    Porque depende de que quien escribe el prompt la conozca y se acuerde. Cada tarea nueva es una oportunidad de olvidarla. Si el harness la lee de una fuente de verdad, la regla llega siempre, aunque el ticket lo haya escrito alguien que no sabe que existe.

    ¿Necesito Backstage o Port para aplicar esto?

    No. Un fichero YAML o JSON versionado en el repo del servicio es suficiente para empezar. Backstage o Port tienen sentido cuando hay decenas de servicios y varios equipos y necesitas un catálogo centralizado con owners, API y búsqueda. El harness solo necesita una función que devuelva el contexto validado, venga de donde venga.

    ¿Qué pasa si el agente intenta modificar los tests para que pasen?

    El harness rechaza el cambio completo porque el fichero de test no está en la allowlist, y el motivo vuelve como feedback en el siguiente intento. Por eso los tests nunca deben estar en la lista de ficheros editables cuando el objetivo es corregir código contra ellos.

    ¿Por qué el código no lee directamente el catálogo en runtime?

    Si puedes, hazlo: es la versión más sólida de la idea, porque elimina la copia del valor. En muchos servicios no es viable (el catálogo vive en otro sistema, la regla se compila en un bundle o el dominio no debe depender de la configuración de plataforma). En esos casos, el test que lee el catálogo es lo que evita que el código y la regla se separen.

    ¿Cuántos reintentos debería permitir el harness?

    Entre dos y tres para tareas acotadas como esta. Más intentos rara vez arreglan lo que los primeros no arreglaron, y el coste en tokens crece con cada uno. Si falla de forma sistemática, revisa la tarea, el contexto o los tests antes de subir el límite.

    ¿Por qué el harness no despliega si los tests pasan?

    Porque unos tests verdes solo demuestran que no se ha roto lo que está cubierto. El despliegue necesita una revisión humana del diff y el pipeline de CI habitual. El harness entrega un cambio listo para revisar; quien tiene permisos de despliegue es otra persona, u otro sistema con sus propios controles.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • GPT-6 Sol y Luna: el precio por token es la mitad de la factura

    GPT-6 Sol y Luna: el precio por token es la mitad de la factura

    El martes 22 de septiembre de 2026 Anthropic sacó Claude Opus 5.5 con un recorte del 20%. Horas después, OpenAI respondió con GPT-6 Sol y Luna, dos modelos a mitad de precio que sus equivalentes GPT-5.6.

    Mi primer impulso fue el de todo el mundo: abrir el .env, cambiar el nombre del modelo y apuntarme el ahorro. Una línea, la mitad de factura.

    Luego me acordé de un post que escribí hace poco. Gemini 3.8 Flash mantuvo el precio por token y aun así la factura subió un 40%. El precio de la tarifa y lo que acabas pagando no son lo mismo.

    Con GPT-6 el recorte de los titulares te ahorra menos que dos decisiones de diseño que no salen en ninguna nota de prensa.

    En corto: GPT-6 Sol ($2/$10 por millón de tokens) y Luna ($0.10/$0.50) cuestan la mitad que GPT-5.6, pero en un agente lo que más mueve la factura es la caché: si el 90% del input de cada llamada sale de la caché, cada llamada a Sol cuesta un 71% menos que sin ella. La otra palanca es el routing: Luna cuesta exactamente 1/20 de Sol en cada tarifa, así que la extracción y la clasificación deberían ir a Luna.

    Precio de GPT-6 Sol y Luna frente a Claude Opus 5.5

    GPT-6 Sol cuesta $2 por millón de tokens de input y $10 de output; Luna, $0.10 y $0.50; Claude Opus 5.5, $4 y $20. Datos de las fichas oficiales de GPT-6 Sol, GPT-6 Luna y la página de precios de Anthropic.

    GPT-6 Sol GPT-6 Luna Claude Opus 5.5
    Input (por M) $2 $0.10 $4
    Input cacheado (por M) $0.20 $0.01 $0.20
    Escritura de caché (por M) $2.50 $0.125 (1,25× input) $5 (caché de 5 min)
    Output (por M) $10 $0.50 $20
    Para qué Coding, agentes, planificación Extracción, clasificación, resúmenes en volumen Briefs complejos donde la fidelidad manda
    Limitación / riesgo Menos fiel que Opus en briefs ricos; recargo por encima de 272K tokens de input Pierde ~45 Elo en AA-Briefcase: entregables que omiten partes de la rúbrica El doble que Sol por token; en tareas largas, mucho más lento

    Fíjate en la fila del input cacheado. Sol y Opus 5.5 cobran lo mismo: $0.20. En un agente donde casi todo el input sale de la caché, la diferencia de "la mitad de precio" se estrecha bastante.

    La fila de limitaciones no me la invento. Artificial Analysis midió que Luna cae unos 45 puntos Elo en su benchmark de entregables, mientras Sol se mantiene. Y en la prueba de Gekkode con la misma escena 3D, ambos cumplieron los 13 requisitos. Pero Opus 5.5 fue más fiel al brief y tardó 38 minutos ($6.96), frente a los 5 minutos ($0.29) de Sol.

    ¿Qué es el prompt caching en GPT-6 y por qué manda en un agente?

    El prompt caching es la reutilización del prefijo idéntico de un prompt (instrucciones, definiciones de tools, historial) entre llamadas: el proveedor no lo vuelve a procesar y te lo cobra con descuento, que en GPT-6 es del 90% sobre el input.

    Un agente reenvía todo el contexto en cada turno. Si empieza siempre igual, pagas tarifa de caché. Si cambia un token al principio, pagas todo otra vez. El mecanismo es el mismo que expliqué en prompt caching en la API de Claude; lo que cambia en GPT-6 son las tarifas y las reglas que invalidan la caché.

    En el hilo de Hacker News del lanzamiento (más de 850 comentarios) lo resumió alguien que tiene agentes de negocio en producción. El usuario MitziMoto escribió: "Cache reads are so heavy compared to anything else that it's the only price point that really matters, regular input and output are negligible." Para su carga, la lectura de caché es el único precio que importa.

    El cálculo: el mismo agente con y sin caché

    Supuestos, para que puedas rehacerlo tú. Cada llamada del agente lleva 50.000 tokens de input: 45.000 son prefijo estable (instrucciones, tools, historial anterior) y 5.000 son nuevos (mensaje del usuario y resultado de tools). Devuelve 1.000 tokens de output. Hace 30.000 llamadas al mes.

    Con el 90% del input de cada llamada en caché, los 45.000 se leen a tarifa de caché y los 5.000 nuevos se escriben (en modo implícito, la guía de OpenAI indica que se escribe hasta el último mensaje) a 1,25× el input.

    Escenario GPT-6 Sol Claude Opus 5.5
    Sin caché, por llamada $0.110 $0.220
    90% del input cacheado, por llamada $0.0315 $0.054
    Sin caché, 30.000 llamadas/mes $3300 $6600
    90% del input cacheado, 30.000 llamadas/mes $945 $1620

    Las cuentas de Sol con caché: 45.000 × $0.20/M = $0.009, más 5.000 × $2.50/M = $0.0125, más 1.000 × $10/M = $0.010. Total, $0.0315. Frente a $0.110 sin caché: un 71% menos.

    Sin caché, Opus 5.5 cuesta exactamente el doble que Sol. Con caché, 1,71 veces ($0.054 / $0.0315). El "50% más barato" depende de cómo esté hecho tu agente.

    Y lo que más duele: si metes un timestamp al principio del system prompt, el prefijo cambia en cada llamada. En modo implícito escribes los 50.000 tokens cada vez a $2.50/M: $0.135 por llamada, $4050 al mes. Pagas un 23% más que sin caché. Un new Date() mal colocado se come el recorte de precio entero: pagas más que con GPT-5.6 bien cacheado.

    Ojo: los tokenizadores de Anthropic y OpenAI no cuentan igual; la columna de Opus asume los mismos tokens. Mide los tuyos como explico en cómo medir el consumo de tokens de un agente.

    Routing de modelos para agentes: GPT-6 Sol para pensar, Luna para procesar

    El routing de modelos en dos niveles consiste en decidir el modelo por tipo de tarea antes de llamar: un modelo capaz para planificar, programar y revisar, y uno barato para extraer, clasificar y resumir en volumen.

    Luna cuesta 1/20 de Sol en input, en input cacheado y en output. Una extracción de un solo turno con 3.000 tokens de prefijo cacheado, 1.000 de documento nuevo y 300 de salida (en modo explícito, con el breakpoint al final del prefijo, el documento se cobra a tarifa normal sin recargo de escritura) cuesta $0.0056 en Sol y $0.00028 en Luna. Un millón de extracciones: $5600 contra $280.

    Una regla de la guía de prompt caching de OpenAI cambia el diseño del router: cambiar de model invalida el prefijo cacheado, igual que cambiar las tools, el reasoning.effort o el text.verbosity. No alternes Sol y Luna en la misma conversación: enruta por tarea, cada una en su hilo.

    Este código usa la Responses API. Los campos prompt_cache_key, prompt_cache_options y prompt_cache_breakpoint salen de los ejemplos de esa guía, consultada el 27 de septiembre de 2026. Si tu SDK todavía no los tipa, van en el cuerpo JSON tal cual.

    type Task = 'plan' | 'code' | 'review' | 'extract' | 'classify' | 'summarize';
    type Model = 'gpt-6-sol' | 'gpt-6-luna';
    
    const ROUTES: Record<Task, Model> = {
      plan: 'gpt-6-sol',
      code: 'gpt-6-sol',
      review: 'gpt-6-sol',
      extract: 'gpt-6-luna',
      classify: 'gpt-6-luna',
      summarize: 'gpt-6-luna',
    };
    
    const SINGLE_TURN = new Set<Task>(['extract', 'classify', 'summarize']);
    
    // Estable: sin fechas, sin nombre de usuario, sin nada que cambie entre llamadas.
    const INSTRUCTIONS: Record<Task, string> = { /* un bloque fijo por tarea */ } as Record<Task, string>;
    const TOOLS = Object.freeze([/* mismas tools, mismo orden, siempre */]);
    
    type InputItem = Record<string, unknown>;
    
    export function buildRequest(task: Task, history: InputItem[], turn: string, sessionId: string) {
      return {
        model: ROUTES[task],
        prompt_cache_key: SINGLE_TURN.has(task) ? `${task}_v1` : `${task}_v1:${sessionId}`,
        prompt_cache_options: { mode: SINGLE_TURN.has(task) ? 'explicit' : 'implicit' },
        tools: TOOLS,
        input: [
          {
            role: 'developer',
            content: [
              {
                type: 'input_text',
                text: INSTRUCTIONS[task],
                prompt_cache_breakpoint: { mode: 'explicit' },
              },
            ],
          },
          ...history, // append-only: nunca se edita, reordena ni resume a mitad de sesión
          { role: 'developer', content: `Fecha actual: ${new Date().toISOString()}` }, // lo dinámico, al final — guárdalo en history junto al turno o la próxima llamada no reutiliza el historial
          { role: 'user', content: turn },
        ],
      };
    }
    

    Tres decisiones que importan más que el código:

    1. Lo dinámico va al final. La guía lo dice literal: las marcas de tiempo y el contenido de usuario, al final o en mensajes posteriores.
    2. El historial es append-only. Si compactas o reescribes turnos antiguos, rompes el prefijo desde ese punto. Eso incluye el mensaje con la fecha: si lo envías pero no lo guardas en el historial, la siguiente llamada deja de coincidir justo ahí.
    3. Mide el hit rate en cada respuesta. Divide usage.input_tokens_details.cached_tokens entre usage.input_tokens y alerta si baja. Vigila también cache_write_tokens: si en cada llamada escribes casi todo el input, algo cambia al principio del prompt. OpenAI ha sacado un dashboard de caché y una herramienta de diagnóstico de fallos, pero tu propio log te avisa antes.

    La salida de Luna en extracción no te la creas sin más. Valídala con un schema y, si falla, reintenta esa tarea en Sol, en un hilo nuevo. Es el mismo patrón de fallback que conté en Opus 5.5, rechazos y fallback en producción. Para el schema uso Zod: tipas y validas en una sola pieza, como enseño en el curso de Zod.

    Límites: cuándo NO usar Luna, ni Sol, ni este router

    Luna no sirve para entregables largos. La caída de ~45 Elo en AA-Briefcase viene, según Artificial Analysis, de entregables que se saltan elementos de la rúbrica. Si la tarea es "redacta el informe completo", Luna ahorra en tokens y te lo cobra en revisiones.

    Sol y Luna tienen letra pequeña en Chat Completions. Sus fichas indican que ahí el function calling exige reasoning_effort en none. Si necesitas razonar y llamar tools a la vez, usa la Responses API.

    Sol no sustituye a Opus 5.5 cuando el brief es rico. Si un fallo de calidad te cuesta más que la diferencia de tokens, paga la diferencia.

    Contextos largos, tarifa distinta. Por encima de 272.000 tokens de input, Sol y Luna cobran el doble en input y caché y 1,5× en output, y lo aplican a toda la petición. Es el mismo asterisco de los 272.000 tokens de GPT-6 Astra.

    El router no arregla un agente mal medido. Si no sabes cuánto cuesta cada tarea terminada y aceptada, no sabes si Luna te ahorra o te obliga a repetir trabajo. Artificial Analysis lo mide por tarea, no por token: Sol a $1.06 por tarea en su índice y Luna a $0.07. Esa es la unidad que importa.

    Lo que puedes hacer hoy

    Abre el log de tu agente y calcula una sola cifra: tokens cacheados entre tokens de input totales, en la última semana. Si baja del 80%, busca qué cambia al principio del prompt antes de pensar en cambiar de modelo. Casi siempre es una fecha, un ID o unas tools que se reordenan.

    Con esa cifra alta, manda las tareas mecánicas a Luna con validación. En ese orden.

    Este diseño, en el que el programa decide qué va a cada modelo y el modelo solo decide dentro de su tarea, es el que desarrollo en El Developer Agéntico, el ebook gratuito de Dominicode. Y si quieres montar el agente completo, de la idea al producto, está en el curso Construye con IA.

    Preguntas frecuentes

    ¿Cuánto cuestan GPT-6 Sol y GPT-6 Luna?

    GPT-6 Sol cuesta $2 por millón de tokens de input, $0.20 de input cacheado y $10 de output. GPT-6 Luna cuesta $0.10, $0.01 y $0.50. Los dos son la mitad o menos que GPT-5.6, y por encima de 272.000 tokens de input se aplica recargo.

    ¿GPT-6 Sol es más barato que Claude Opus 5.5?

    Por token, sí: la mitad en input y output. Pero los dos cobran $0.20 por millón en input cacheado, así que en un agente con mucha caché la diferencia baja. En el cálculo del post, de 2× sin caché a 1,71× con el 90% del input cacheado.

    ¿Puedo cambiar entre Sol y Luna en la misma conversación?

    Puedes, pero pierdes la caché. La guía de OpenAI indica que cambiar el modelo invalida el prefijo cacheado. Enruta por tarea, con un hilo por tarea, en lugar de alternar modelos dentro del mismo hilo.

    ¿Qué rompe la caché de prompts en GPT-6?

    Cualquier cambio en el prefijo: un timestamp o un dato de usuario al principio, tools que cambian de orden o de descripción, o un cambio de reasoning.effort o text.verbosity en la petición. Para cambiar el esfuerzo de razonamiento sin romperla, la guía propone añadir un elemento configuration_update al input.

    ¿Para qué tareas conviene GPT-6 Luna?

    Para trabajo acotado y en volumen: extracción, clasificación, resúmenes cortos. Valida su salida con un schema y escala a Sol lo que no pase. Evítalo en entregables largos con muchos requisitos.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Zettelkasten para developers: ahora tus notas las lee un agente

    Zettelkasten para developers: ahora tus notas las lee un agente

    Hace unos meses le pedí a uno de mis agentes que redactara una sección concreta de una guía. Devolvió algo correcto, genérico y absolutamente vacío.

    Lo incómodo es que yo ya había escrito eso. Meses antes. Con el contexto, la decisión y el motivo por el que descarté la alternativa.

    El agente no lo encontró. Y cuando fui a buscarlo yo, tardé veinte minutos: estaba enterrado en la mitad de un markdown gigante sin título propio.

    Ahí entendí que mi problema con el Zettelkasten para developers no era de disciplina. Era de formato.

    La tesis de este post, después de un año largo escribiendo así: el método no ha cambiado nada. Lo que ha cambiado es quién lee las notas.

    En 2021 tomabas notas atómicas para tu yo futuro. En 2026 las tomas también para el agente que las va a recuperar, y ese lector es mucho menos indulgente que tú.

    Por si llegas sin contexto: el método Zettelkasten son las fichas que el sociólogo Niklas Luhmann usó durante décadas para escribir, con una caja de papel y ningún ordenador. Aplicado a programadores, el Zettelkasten para developers es esto: una idea por archivo, en markdown plano, con un título que afirma algo, autocontenida y enlazada a las demás. No es una carpeta de apuntes ordenada. Es un grafo consultable.

    Crédito: en esto me metió Tomas Vik, con su introducción al método y su retrospectiva un año largo después. Lo que viene es mi versión, con mis cicatrices.


    El conocimiento que solo entiendes tú ahora muere dos veces

    Durante años tomé notas como casi todos los programadores que conozco: capturas de pantalla, fragmentos copiados de la documentación, bullets sin verbo, enlaces con un "revisar esto" al lado.

    Eso no es una base de conocimiento, ni gestión del conocimiento personal (PKM). Es un vertedero ordenado alfabéticamente.

    El problema clásico ya era grave: un formato que solo entiendes tú, en el momento exacto en que lo escribiste, deja de entenderse a los tres meses.

    Lo nuevo es la segunda muerte. Si trabajas solo —yo opero Dominicode sin equipo— tus agentes son literalmente tu equipo, y ese equipo lee lo que dejaste escrito. Si lo que dejaste son bullets sin sujeto, tu equipo entero trabaja a ciegas.

    Una nota mal escrita ya no solo te penaliza a ti dentro de seis meses. Penaliza cada ejecución de cada agente que consulta ese repositorio, hoy.


    El Zettelkasten para developers ya no es productividad, es infraestructura

    La nota atómica pasó de consejo de productividad a requisito técnico el día en que dejó de leerte solo tú. Este es el giro que hace que esto merezca un post en 2026 y no en 2015.

    "Sé conciso" y "una idea por nota" eran consejos de higiene mental. Sonaban a gurú de productividad. Se podían ignorar sin consecuencias visibles.

    Hoy son requisitos técnicos de recuperación.

    Cuando indexas tu conocimiento para que un modelo lo consulte —lo que todo el mundo llama RAG— el texto se parte en fragmentos (chunking), esos fragmentos se convierten en embeddings y se recuperan por similitud semántica. Una nota de un solo tema, autocontenida y con un título que afirma algo es exactamente la unidad que ese índice recupera bien: cabe en muy pocos fragmentos y cada uno de esos fragmentos sigue significando algo por su cuenta.

    Una nota-cajón de sastre de cuatro mil palabras hace lo contrario. Se parte por la mitad de un razonamiento. El fragmento recuperado empieza con "por eso lo descartamos" y el sujeto de esa frase se quedó tres páginas más arriba. El modelo recibe un texto gramaticalmente correcto y semánticamente huérfano, y con eso improvisa. A eso lo llamamos alucinación, pero muchas veces es mala segmentación.

    Cómo se recuperan de verdad esos fragmentos —y por qué la búsqueda vectorial pura falla— lo desmonté en búsqueda híbrida y embeddings en producción.

    Nota-cajón Nota atómica
    Temas por archivo Varios Uno
    Título Una etiqueta: "Angular signals" Una afirmación: "Signals no sustituye a RxJS cuando necesitas cancelar una petición en vuelo"
    Al partirse en fragmentos El fragmento pierde el sujeto Cada fragmento sigue significando lo mismo
    Tú dentro de seis meses Hay que releer el archivo entero Decides si la abres sin abrirla
    Tu agente Recupera contexto huérfano e improvisa Recupera la unidad completa

    Mi ancla concreta: la base de conocimiento de Dominicode es un índice de búsqueda sobre markdown plano. Ahora mismo son 118 documentos y 455 fragmentos, repartidos en colecciones por ámbito —agents, _brand, _research, labs, videos, books, revenue— que los agentes consultan antes de escribir sobre cualquiera de esos ámbitos.

    Haz la división: sale por debajo de cuatro fragmentos por documento de media. No es un número mágico —los capítulos de libro tiran de esa media hacia arriba—, pero sí es el síntoma de la política editorial: documentos cortos y monotema. Cuando un archivo se dispara muy por encima de esa media, dentro hay dos o tres notas peleándose por el mismo sitio.

    La infraestructura de todo esto —el repo, la estructura, el gobierno vía CLAUDE.md— la conté en cómo montar un segundo cerebro con Claude Code. Este post va de la capa de encima: qué escribes dentro de cada archivo.


    El título es la nota

    Si me quedo con una sola regla de este año, es esta: el título tiene que afirmar algo, no nombrar un tema.

    No "Angular signals". Eso es una etiqueta.

    Sí "Signals no sustituye a RxJS cuando necesitas cancelar una petición en vuelo". Eso es una nota que puedes recuperar, contradecir o confirmar.

    Un título-afirmación hace tres cosas a la vez. Te obliga a tener una conclusión antes de escribir, en vez de acumular material. Le da al índice la señal más fuerte que va a recibir de ese documento. Y te permite decidir meses después si abres la nota sin abrirla.

    La segunda regla es más dura de lo que parece: la nota tiene que sobrevivir a ser leída sola. Sin la nota anterior. Sin la pestaña que tenías abierta ese día. Sin acordarte del proyecto.

    En la práctica eso significa desterrar los pronombres huérfanos. "Esto no funciona en producción" no es una nota. "El provideHttpClient con interceptores funcionales no captura errores lanzados dentro del resolver de una ruta" sí lo es.

    Así se ve el cambio en un archivo real:

    <!-- Antes: nota-cajón -->
    # Errores HTTP
    
    - esto no funciona en producción
    - revisar interceptores
    - preguntar a alguien
    
    <!-- Después: nota atómica -->
    # El interceptor funcional no captura errores lanzados dentro del resolver de una ruta
    
    `provideHttpClient(withInterceptors([...]))` solo ve lo que pasa por
    `HttpClient`. Si el resolver falla antes de lanzar la petición, el error
    sale por el router y el interceptor nunca se entera.
    
    Alternativa descartada: mover el try/catch a cada componente. Funciona,
    pero hay que repetirlo en cada ruta.
    

    El de abajo es más largo de escribir. Es el único de los dos que sigue sirviendo dentro de un año, y el único que un agente puede recuperar suelto.

    La autocontención es la misma propiedad que hace útil a un fragmento recuperado. Escribes para ti dentro de un año y, sin proponértelo, escribes para un recuperador vectorial. Es el mismo requisito con dos nombres.

    Es la lógica que aplicamos en el curso Construye con IA al montar la capa de contexto de un producto: el modelo no falla por falta de inteligencia, falla porque le llega texto sin sujeto.


    Los enlaces son el trabajo que no puedes delegar

    Enlazar es la única parte del Zettelkasten que no puedes automatizar, y es donde está todo el retorno. Aquí es donde mucha gente se baja.

    Enlazar una nota nueva con las que ya tienes te obliga a responder dos preguntas que no se contestan en piloto automático: ¿dónde encaja esto? y ¿contradice algo que ya escribí?

    La primera es de arquitectura. La segunda es la buena.

    Cuando una nota nueva choca de frente con una de hace ocho meses, ha pasado algo real: o aprendiste, o una de las dos estaba mal, o —lo más habitual— las dos son ciertas en contextos distintos y nunca habías delimitado cuál era cuál. Resolver ese choque suele producir una tercera nota, y esa tercera nota es la única de las tres que vale dinero.

    Ese momento no te lo da un agente. Un modelo te sugiere enlaces plausibles todo el día, y como sugerencia sirven. Lo que no tiene es la sensación de "un momento, esto no cuadra con lo que decidí en marzo". Esa fricción es el aprendizaje. Externalizarla es quedarte con el grafo y perder el motivo por el que existe.

    Lo que sale de ahí es un grafo, con las relaciones como dato de primera clase. Es la idea detrás de qué es graph engineering: la estructura entre las piezas transporta tanta información como las piezas.

    Con un efecto secundario que no esperaba: es el mejor antídoto contra el context drift que he encontrado. Cuando la conversación con el agente lleva dos horas derivando, las notas enlazadas son el punto fijo al que volver. No lo que el modelo cree recordar, sino lo que tú decidiste y escribiste.

    Y si viven en markdown, acaban siendo consultables como datos: notas huérfanas, enlaces rotos, temas que lo acaparan todo. Salud del grafo con una consulta, como conté en DuckDB + Obsidian.


    El coste honesto: esto es lento

    El coste real del método es el tiempo, y toca la parte que casi nadie escribe.

    Escribir así es lento. Mucho más lento que guardar el enlace y seguir. Leer para destilar una nota es más lento que leer, y bastante menos placentero. Dejé libros y documentación técnica a medias porque procesarlos a ese ritmo se volvió un trabajo, no una lectura.

    Y algo dejó de funcionar del todo: la ambición de capturarlo todo. Durante meses convertí en nota cosas que no lo merecían y el grafo se llenó de ruido que empeoraba la recuperación. Más notas no es mejor.

    Ahora tengo tres filtros. Si la respuesta a cualquiera de los tres es sí, no hay nota:

    • ¿Caduca en menos de dos semanas? Es un recordatorio, no conocimiento. Va a un issue, no al grafo.
    • ¿Existe upstream, es estable y está bien escrito? Enlázalo. Reescribir la documentación oficial de una librería es trabajo perdido que además envejece mal.
    • ¿Podrías reconstruirlo en cinco minutos buscando? No es tuyo todavía. Es información disponible, no conocimiento propio.

    Lo que sí merece nota casi siempre es lo mismo: la decisión, la alternativa que descartaste y el porqué. Eso no existe upstream, no está en la documentación de nadie, y es exactamente lo que te vuelven a preguntar dentro de un año.

    Es la misma forma de pensar que sostiene el libro de Spec-Driven Development: escribir la decisión antes que el código, porque la decisión es la parte cara.

    Y un último coste, más traicionero: optimizar el sistema en vez de usarlo. Reorganizar carpetas, cambiar de herramienta, diseñar la taxonomía perfecta. Todo eso se siente productivo y no produce nada.


    Qué cambiaría de mi Zettelkasten si empezara hoy de cero

    Cuatro cosas, y ninguna es instalar nada.

    1. Títulos-afirmación desde la primera nota. Renombrar cientos de archivos después es un trabajo horrible, y hasta que no lo haces la recuperación no mejora.
    2. Escribir pensando en el fragmento, no en el documento. Antes de guardar, pregúntate si un trozo suelto de esa nota, leído sin nada alrededor, sigue significando lo mismo. Si no, pártela.
    3. Colecciones por ámbito, no carpetas por tema. Los temas se solapan siempre y acabas con la misma nota en tres sitios. El ámbito casi nunca es ambiguo, y es lo que te permite filtrar la búsqueda.
    4. La nota de decisión antes que la nota de resumen. Los resúmenes de lo que leí envejecen. Las decisiones que tomé, con su alternativa descartada, siguen valiendo años después.

    Y una cosa que no cambiaría: no delegar los enlaces.


    Empieza por la siguiente nota

    No migres nada. No reorganices el vault. No elijas herramienta.

    Coge la última decisión técnica que tomaste esta semana —esa que discutiste contigo mismo durante veinte minutos— y escríbela como una sola nota, con un título que afirme algo y sin un solo pronombre sin sujeto. Después enlázala con lo más parecido que ya tengas escrito.

    Eso es todo. Repítelo cuando vuelva a pasar.

    Dentro de un año esa nota la vas a leer tú, medio dormido, buscando por qué hiciste lo que hiciste. Y la va a leer un agente que no tiene tu memoria, ni tu contexto, ni tu paciencia. Escríbela para el segundo: el primero sale beneficiado gratis.

    Si quieres el paso siguiente —cómo hacer que un agente use ese conocimiento sin inventarse la mitad— te lo dejé en el ebook gratuito Revisión por Contrato. Y si prefieres verlo montado sobre proyectos reales, con el repo y los agentes funcionando, está en Dominicode Labs.


    Preguntas frecuentes

    ¿Qué es exactamente el Zettelkasten para developers y en qué se diferencia de tomar apuntes?

    El Zettelkasten para developers es un sistema de notas atómicas en markdown: una idea por archivo, con un título que afirma algo, autocontenida y enlazada con las demás. La diferencia con tomar apuntes es el enlace. Los apuntes se acumulan en carpetas y se consultan por nombre de archivo; el Zettelkasten forma un grafo donde la relación entre dos notas transporta tanta información como las notas.

    En 2026 hay una segunda diferencia: una nota atómica es también la unidad que un índice vectorial recupera entera. Un apunte largo no.

    ¿Sigue teniendo sentido el Zettelkasten ahora que la IA me resume cualquier cosa?

    Tiene más sentido que antes, y por un motivo distinto al de siempre. La IA resume bien lo que está publicado; no puede resumir la decisión que tomaste tú, con la alternativa que descartaste y el motivo. Eso no existe en ningún corpus.

    Lo que sí cambió es el destinatario. Antes escribías notas atómicas para tu yo futuro. Ahora las escribes también para el agente que las va a recuperar, y ese lector no perdona un pronombre sin sujeto.

    ¿Necesito una herramienta de Zettelkasten o me vale markdown en un repo?

    Te vale markdown en un repo, y es lo que uso. La única condición innegociable es que los archivos sean texto plano y tuyos: eso te da git, búsqueda y la posibilidad de indexarlos para que los lea un agente.

    Las herramientas específicas —Obsidian, Logseq, Roam— aportan comodidad al enlazar y visualizar el grafo. Pero elegir herramienta antes de tener notas es la forma más elegante de no empezar nunca.

    ¿Cuántas notas hacen falta para que esto empiece a servir?

    Menos de las que imaginas para el uso individual, y bastantes más para el efecto de red, que es cuando el grafo te sugiere conexiones que no habías visto.

    No te fijes un número, fíjate una señal: el sistema funciona el día que buscas algo, lo encuentras, y esa nota te resuelve el problema sin abrir nada más.

    ¿No puede el agente resumir mis notas largas y ahorrarme el trabajo?

    Puede resumir. El problema es cuándo. En el momento de la recuperación el agente ya no ve la nota entera: ve los fragmentos que el índice le devolvió —salvo que montes recuperación por documento padre, que es trabajo aparte—, y si están mal cortados el resumen es una reconstrucción sobre material incompleto.

    Usar un modelo para partir notas viejas en notas atómicas sí es buena idea. Lo que no puedes delegar es decidir qué idea va en cada nota, porque esa decisión es el contenido.

    ¿Esto sirve para documentación de equipo o solo para notas personales?

    Sirve para ambas, pero no son lo mismo. La documentación de equipo describe cómo funciona el sistema hoy y se actualiza cuando el sistema cambia. Las notas atómicas capturan por qué se decidió algo y se acumulan sin borrarse.

    Si trabajas en solitario la frontera casi desaparece: tu grafo de decisiones acaba siendo el onboarding de tus propios agentes.

    ¿Qué hago con las notas largas que ya tengo escritas?

    Nada, hasta que las necesites. Migrar el archivo entero es el clásico proyecto que se abandona a la tercera tarde.

    Aplica la regla al vuelo: la próxima vez que abras una nota vieja y te cueste encontrar dentro lo que buscabas, ese es el momento de partirla en dos o tres notas con título propio. Migras solo lo que demuestra que se usa.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Claude Code en monorepos: dale solo la rebanada que necesita

    Claude Code en monorepos: dale solo la rebanada que necesita

    Un cliente me pasó su monorepo el mes pasado. Nueve paquetes, pnpm workspaces, Turborepo por encima. Le pedí a Claude Code algo ridículamente pequeño: cambiar el tipo de una prop en packages/ui.

    Tres respuestas después me estaba proponiendo tocar el cliente HTTP del backend.

    No era un modelo tonto. Era yo. Había arrancado la sesión desde la raíz del repo, y trabajar con Claude Code en monorepos desde la raíz significa una cosa muy concreta: le has dado nueve paquetes de superficie para una tarea que vive en uno.

    Esto no es el problema del que ya escribí en Context Drift. Aquel es temporal: la sesión se alarga, el historial se pudre, el agente se olvida de la instrucción de la iteración 3. Este es espacial. Se degrada en el minuto uno, con la ventana medio vacía, porque el repo es grande y nadie le ha dicho qué parte del repo importa.

    La tesis del post es esta: en un monorepo, la decisión más importante que tomas no es qué prompt escribes. Es desde qué directorio arrancas el agente.

    Smart context slicing es la práctica de arrancar el agente en el subárbol mínimo del monorepo que la tarea necesita, calculado a partir del grafo de dependencias en vez de a ojo. Son tres decisiones concretas: desde qué directorio lanzas claude, qué paquetes vecinos añades con --add-dir y qué rutas bloqueas con reglas de denegación. Las tres, en ese orden, son el resto del post.

    Claude Code en monorepos: un CLAUDE.md en la raíz no escala

    La documentación de Anthropic recomienda mantener cada CLAUDE.md por debajo de 200 líneas, y lo justifica: los archivos largos consumen más contexto y reducen la adherencia a las instrucciones.

    Ahora divide. Nueve paquetes, 200 líneas: 22 líneas por paquete para explicar su stack, sus convenciones y sus trampas.

    Así que solo hay dos finales, y he visto los dos.

    O el CLAUDE.md crece hasta las 600 líneas y el agente ignora la mitad — incluidas las reglas que importaban. O se queda genérico ("usa TypeScript estricto", "escribe tests"), que es una forma elegante de no decir nada.

    Si todavía estás montando el tuyo, el punto de partida lo dejé en CLAUDE.md: el system prompt de tu proyecto. Aquí doy por hecho que ya lo tienes y se te ha quedado pequeño.

    La solución es partirlo: raíz para lo global, un archivo por paquete para lo local.

    monorepo/
      CLAUDE.md                 # reglas globales: commits, estilo, "corre los scripts desde el paquete"
      packages/
        ui/CLAUDE.md            # convenciones de componentes, tokens de diseño
        api/CLAUDE.md           # Knex, migraciones, .env obligatorio
        web/CLAUDE.md           # rutas, data fetching
    

    Pero partirlo no sirve de nada si no entiendes cuándo se carga cada trozo.

    La regla de carga que casi nadie ha leído

    Claude Code no trata igual a los CLAUDE.md que están por encima de ti y a los que están por debajo.

    Dónde vive el CLAUDE.md Cuándo entra en contexto
    Tu directorio de trabajo y todos sus ancestros Al arrancar la sesión, siempre
    Subdirectorios por debajo de ti Bajo demanda, solo cuando el agente lee un archivo de esa carpeta

    Si arrancas desde la raíz, cargas solo el CLAUDE.md raíz — y vas acumulando el de cada paquete que el agente toque. Toca muchos, porque no sabe dónde está el límite.

    Si arrancas con cd packages/ui && claude, cargas raíz + packages/ui de golpe, y los de api y web no existen para esa sesión mientras no los pises. Además, solo puede leer y editar dentro de ese subárbol hasta que le concedas más.

    Eso es una rebanada. Y te ha costado un cd.

    Compruébalo: lanza /context y mira la lista de Memory files. Ahí está lo que se cargó de verdad.

    El slice no lo decides tú: lo decide el grafo de dependencias

    "Trabaja desde el paquete" está bien hasta que la tarea toca de verdad a los vecinos. Cambiar un tipo exportado de ui puede romper a quien lo consume, y si el agente no ve a esos consumidores, te entrega algo que compila en su rebanada y revienta en CI.

    La pregunta correcta no es qué paquetes te apetece abrir, sino qué paquetes toca esta tarea de verdad. Y esa respuesta ya está en tu repo: en el grafo de dependencias.

    Monté un workspace de cinco paquetes para verlo, con pnpm 11.1.3 y Turborepo 2.10.12. @acme/api y @acme/web dependen de @acme/ui; @acme/ui depende de @acme/config; @acme/jobs va por libre.

    Inventario primero:

    pnpm ls -r --depth -1
    

    Ahora el blast radius hacia arriba — qué se rompe si toco @acme/ui. En la sintaxis de filtros de pnpm, los tres puntos delante del nombre significan "y todo lo que depende de él":

    pnpm --filter "...@acme/ui" ls --depth -1
    # (salida recortada al nombre de cada paquete)
    # @acme/ui
    # @acme/api
    # @acme/web
    

    Y hacia abajo, con los puntos detrás, "y todo aquello de lo que depende":

    pnpm --filter "@acme/ui..." ls --depth -1
    # (salida recortada)
    # @acme/ui
    # @acme/config
    

    Si quieres el cierre completo en los dos sentidos, pones los puntos a ambos lados: "...@acme/ui...". Y si te sobra el propio paquete, el circunflejo lo excluye: "...^@acme/ui" devuelve solo api y web.

    Turborepo lo da con un matiz. --dry enseña el plan sin ejecutar nada:

    turbo run build --filter="...@acme/ui" --dry
    
    • Packages in scope: @acme/api, @acme/ui, @acme/web
    • Running build in 3 packages
    

    El detalle que solo ves ejecutándolo: "Packages in scope" son 3, pero si sacas el JSON aparecen 4 tareas:

    turbo run build --filter="...@acme/ui" --dry=json | jq -r '.tasks[].directory' | sort -u
    # packages/api
    # packages/config
    # packages/ui
    # packages/web
    

    @acme/config no está en el scope de edición, pero entra en el grafo de build porque ui lo necesita compilado. Son dos rebanadas distintas y conviene no confundirlas:

    Rebanada Paquetes % del repo
    Repo completo 5 100%
    Slice de edición (ui + dependientes) 3 60%
    Slice de build (añade config) 4 80%
    Nunca entra (@acme/jobs) 1 20%

    En un repo de cinco paquetes, dejar fuera un paquete suena a poco. En el del cliente, con nueve, el slice real de la tarea eran tres paquetes: dos tercios del repo que no tenían por qué abrirse nunca.

    Con esa lista en la mano, el arranque deja de ser una corazonada:

    cd packages/ui
    claude --add-dir ../api --add-dir ../web
    

    Si el equipo entero trabaja así, lo fijas en packages/ui/.claude/settings.json:

    {
      "permissions": {
        "additionalDirectories": ["../api", "../web"]
      }
    }
    

    Ojo con una diferencia que muerde: additionalDirectories da acceso a los ficheros pero no carga nunca el CLAUDE.md ni las skills de esos directorios. Con --add-dir sí cargan las skills, y el CLAUDE.md solo si arrancas con CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1. Si escribiste un CLAUDE.md en packages/api y no sale en /context, es por esto.

    Y como el slice te dice qué se puede romper, también sabes qué verificar antes de dar la tarea por buena: los tests de api y web, no los de ui. Convertir "parece que funciona" en un veredicto ejecutable lo desarrollé entero en el ebook gratuito Revisión por Contrato, sobre cómo revisar lo que te entrega un agente sin leértelo línea a línea.

    Lo que no debe entrar en la ventana bajo ningún concepto

    Las búsquedas de contenido de Claude Code respetan tu .gitignore por defecto, así que node_modules/, dist/ y build/ ya están fuera de los resultados de un grep.

    El problema es lo que sí está commiteado: código generado, un SDK vendorizado, snapshots enormes. Para eso hay reglas de denegación:

    {
      "permissions": {
        "deny": [
          "Read(./**/dist/**)",
          "Read(./**/*.generated.*)",
          "Read(./vendor/**)"
        ]
      }
    }
    

    Un detalle que rompe esto sin avisar: los patrones relativos anclan en el directorio desde el que arrancas la sesión, no en la raíz del repo. Si guardas estas reglas en la raíz pero lanzas la sesión desde packages/ui, Read(./vendor/**) está apuntando a packages/ui/vendor/. Para que apliquen en todo el repo las escribes absolutas, con doble barra: Read(//ruta/absoluta/al/repo/vendor/**).

    Y si arrancando desde la raíz se te cuelan los CLAUDE.md de equipos con los que no trabajas, existe claudeMdExcludes, en el .claude/settings.local.json de la raíz. Los patrones se comparan contra rutas absolutas, así que empiezan por **/ para que casen en cualquier punto del árbol:

    {
      "claudeMdExcludes": ["**/packages/legacy-*/**"]
    }
    

    Con un aviso honesto: esa lista es estática, no un interruptor por tarea. Para alternar de paquete cada día la herramienta sigue siendo el cd.

    Cuando de verdad no sabes dónde está, delega la búsqueda

    Todo lo anterior asume que sabes qué paquete tocar. A veces no lo sabes, y ahí es donde la gente destroza la sesión: "busca en el repo dónde se genera el token de refresco". El agente lee doscientos archivos y te devuelve una frase. Los doscientos archivos se quedan en tu ventana. La frase también, pero ya da igual.

    Delégalo a un subagente. Corre en su propia ventana de contexto y te devuelve el resumen, no los archivos:

    Usa un subagente para localizar en qué paquetes se genera y se valida
    el token de refresco. Devuélveme solo la lista de rutas y una línea
    por cada una. No propongas cambios todavía.
    

    El resultado es una lista de paquetes. Cierras la sesión, haces cd al correcto y empiezas la tarea real con la ventana limpia. La exploración se paga una vez y se tira.

    Es el mismo principio que conté en Context Engineering: lo caro no es el token, es el token irrelevante que se queda mirándote el resto de la sesión.

    Lo que puedes hacer hoy en tu monorepo

    Una sola cosa, y es gratis: deja de arrancar el agente desde la raíz del monorepo.

    Antes de la próxima tarea, corre pnpm --filter "...<tu-paquete>" ls --depth -1, mira los tres o cuatro nombres que salen, y arranca así:

    cd packages/<tu-paquete>
    claude --add-dir ../<vecino>
    

    No hace falta que escribas ni un CLAUDE.md nuevo para notar la diferencia. Eso viene después, cuando ya sepas qué reglas son globales y cuáles de un paquete — y eso solo se ve claro tras unos días trabajando por rebanadas.

    Si quieres el flujo completo, de la idea al producto con estas decisiones tomadas antes de escribir código, es lo que montamos en el curso Construye con IA.

    Preguntas frecuentes

    ¿Es mejor arrancar Claude Code desde la raíz del monorepo o desde el paquete?

    Desde el paquete, salvo que la tarea cruce varios subsistemas de verdad. Arrancando desde packages/ui cargas el CLAUDE.md raíz más el de ui, y el agente solo puede leer y editar ese subárbol. Desde la raíz tienes acceso a todo: útil para refactors transversales, caro para cualquier otra cosa. Si necesitas un vecino puntual, --add-dir te lo añade sin romper el aislamiento.

    ¿Los CLAUDE.md de los subdirectorios se cargan siempre?

    No, y esta es la confusión más habitual. Los de tu directorio de trabajo y de todos sus ancestros se cargan al arrancar la sesión. Los de subdirectorios por debajo de ti se cargan bajo demanda, solo cuando el agente lee un archivo de esa carpeta. Para ver qué se cargó de verdad en una sesión, lanza /context.

    ¿Qué hago si la tarea toca varios paquetes a la vez?

    Dásela entera en una sola sesión, con el slice completo delante. Partirla en una sesión por paquete es peor: cada sesión redecide el diseño desde cero y acabas con tres criterios distintos. Calcula el slice con el filtro de dependientes, añade esos directorios y trabaja en plan mode antes de editar: el plan se escribe a un archivo que Claude Code reinyecta tras cada compactación.

    ¿Esto sirve si uso Nx o si mi repo es un solo árbol grande sin paquetes?

    Sí. En Nx el equivalente es nx graph para ver el grafo y nx show projects --affected para saber qué proyectos toca un cambio: cambia el comando, no la idea. Y en un repo de un solo árbol sustituyes "paquete" por "subsistema" — src/billing/, src/auth/, lib/core/. Un CLAUDE.md por subsistema y un cd hacen el mismo trabajo.

    ¿No basta con el .gitignore para que el agente no lea dist?

    Para las búsquedas de contenido sí: Claude Code respeta el .gitignore por defecto, así que dist/, build/ y node_modules/ no aparecen cuando busca texto. Lo que no cubre es lo commiteado — código generado, SDKs vendorizados, fixtures gigantes. Para eso necesitas reglas Read(...) en permissions.deny. Con un límite: cubren las herramientas de fichero y los comandos de Bash que Claude Code reconoce, pero un grep -r sobre una carpeta con ficheros denegados sigue sacándolos por pantalla.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • SQL agéntico local con Qwen3.8-27B y DuckDB: el 98,6 % es contexto

    SQL agéntico local con Qwen3.8-27B y DuckDB: el 98,6 % es contexto

    En una formación de empresa, en julio, un equipo me enseñó un problema que parecía de modelo.

    Su agente de datos —SQL agéntico local sobre DuckDB, un harness sencillo— fallaba una de cada tres preguntas. Habían probado tres modelos, cada uno más caro que el anterior. La precisión se movió tres puntos.

    Miré el prompt. El agente recibía el DESCRIBE de las tablas y nada más. Ni qué significa cada columna, ni el dialecto, ni las trampas del dominio.

    Ese es el punto ciego del debate sobre SQL agéntico local: discutimos qué modelo poner cuando el problema casi nunca está en el modelo.

    Aclaremos el término antes de seguir. SQL agéntico es dejar que un modelo de lenguaje, en vez de devolver una única consulta, itere en bucle: inspecciona el esquema, escribe SQL, lo ejecuta contra la base de datos, lee el resultado o el error y corrige hasta responder la pregunta de negocio. SQL agéntico local es hacer exactamente eso con un modelo abierto corriendo en tu máquina: sin coste por token y sin que los datos salgan del equipo.

    En su tabla de ventas, las devoluciones son filas con importe negativo que nadie borra. Ningún modelo, por caro que sea, adivina eso. Se lo tienes que decir.


    El titular viral del SQL agéntico local y lo que se salta

    MotherDuck publicó un artículo montando un agente SQL con Qwen3.8-27B corriendo en local sobre DuckDB. Lo pasaron por DABstep, un benchmark de análisis de datos cuyo test set completo tiene más de 400 preguntas de negocio reales.

    Según su benchmark, el modelo local en cuantización 4-bit sacó un 98,6 % de accuracy. Gratis, o menos de 0,50 $ si cuentas la electricidad. GPT 5.6 Luna Max gastó más de 8 $ en el mismo benchmark: 17 veces más caro según su cálculo, y con peor resultado.

    Los otros datos que reportan en la misma pasada, para situar:

    Modelo Precisión en DABstep Coste de la pasada Tiempo por pregunta
    Qwen3.8-27B local, 4-bit (MacBook Air M5) 98,6 % < 0,50 $ de electricidad 5-6 min
    Qwen3.8-27B local, 3-bit IQ3_XXS (M1 Pro, 16 GB) 96,4 % < 0,50 $ de electricidad 5-6 min
    GPT 5.6 Luna Max Por debajo del Qwen local > 8 $ ~40 s
    Gemini-3-Flash El más preciso del test 2,3× el precio de Luna Max ~25 s
    Sonnet 5 Significativamente menos preciso Más caro, sin cifra publicada —

    Dos puntos de precisión por la mitad de RAM.

    Fuente: benchmark de MotherDuck sobre DABstep. MotherDuck no publica el porcentaje exacto de los modelos en la nube, solo su posición relativa — por eso esas celdas van en cualitativo.

    El titular escribe solo: un modelo abierto en tu portátil empata a los frontier en SQL. Pero hay una frase enterrada en el artículo que cambia por completo la lectura.

    La capa de contexto que usó el agente la construyeron con un modelo frontier. Textualmente: "general documentation (including some SQL snippets) is fed into Claude Fable 5 and converted into MotherDuck Guides". Claude Fable 5 destiló la documentación; el modelo local solo consumió el resultado.

    Ahí está la historia real.


    El modelo frontier no desaparece del agente SQL: se mueve de sitio

    El modelo caro no se ha quedado sin trabajo. Ha cambiado de turno.

    Antes lo llamabas mil veces, una por pregunta, y pagabas mil veces. Ahora lo llamas una vez para destilar tus esquemas, tu documentación y tus reglas de negocio en un fichero de contexto, y luego infieres gratis en local todas las veces que quieras.

    Es un cambio de CAPEX por OPEX. Pagas una vez por construir el contexto y amortizas esa inversión en cada consulta posterior.

    Lo cual deja el corolario más útil del artículo, y es uno que el titular no da:

    Si tu agente de datos falla, no cambies de modelo. Arregla el contexto. Y si con contexto bueno ya funciona, entonces sí baja a un modelo local y deja de pagar por token.

    En ese orden. Al revés te sale caro y encima no funciona.

    El trabajo difícil migró del prompt al contexto. Quien no se entera sigue comprando inteligencia que no necesita.


    Qué contiene la capa de contexto de un agente SQL sobre DuckDB

    Una capa de contexto útil para un agente SQL tiene cuatro bloques: el esquema anotado columna a columna, las reglas de negocio que no están en el esquema, las reglas del dialecto SQL concreto y un puñado de queries doradas. Esta es la parte que no encontrarás en el original: qué escribes exactamente en ese fichero.

    No es el DESCRIBE. Eso ya lo consigue el modelo con una tool. Lo que no tiene es la semántica, el dialecto y los precedentes.

    Este es el esqueleto que uso para un agente SQL sobre nuestros datos de Dominicode —ventas de cursos y eventos de vídeo— en agent/context/ventas.md:

    # Contexto: analítica de ventas y vídeo (DuckDB)
    
    ## Datos disponibles
    
    Los ficheros son Parquet locales. Cárgalos siempre con read_parquet, nunca
    asumas que existe una tabla con ese nombre en el catálogo.
    
      read_parquet('data/ventas_cursos/*.parquet')
      read_parquet('data/eventos_video/*.parquet')
    
    ## ventas_cursos — una fila por transacción
    
    - id_venta      VARCHAR    Único. Las devoluciones NO comparten id con la venta.
    - fecha_utc     TIMESTAMP  Naive, siempre en UTC. El negocio reporta en Madrid.
    - curso_slug    VARCHAR    Clave de negocio del curso. Une por aquí, no por título.
    - plataforma    VARCHAR    'udemy' | 'kursar'. Kursar no tiene filas antes de 2026-03.
    - canal         VARCHAR    'organico' | 'referido' | 'udemy_business'.
    - precio_bruto  DOUBLE     0.0 cuando el cupón es del 100 %. No es un error.
    - neto_usd      DOUBLE     Ingreso YA repartido con la plataforma.
    - pais          VARCHAR    ISO-2. Puede ser NULL en Udemy Business.
    
    ## Reglas de negocio que no están en el esquema
    
    1. Las devoluciones son filas con neto_usd < 0. No se borran nunca.
       Para facturación real: SUM(neto_usd) sobre TODAS las filas.
       Nunca filtres con WHERE neto_usd > 0 salvo que pidan ventas brutas.
    2. No calcules el neto multiplicando el bruto por el reparto de la
       plataforma. Ese cálculo ya viene hecho en neto_usd y el porcentaje
       cambia por canal.
    3. "Mes de agosto" significa mes natural en Europe/Madrid, no en UTC.
    4. Una venta con precio_bruto = 0 sigue contando como unidad vendida.
    
    ## Dialecto DuckDB — reglas obligatorias
    
    - GROUP BY ALL y ORDER BY ALL existen. Úsalos en vez de repetir columnas.
    - SELECT * EXCLUDE (col) y SELECT * REPLACE (expr AS col) son válidos.
    - QUALIFY filtra sobre window functions sin subconsulta. Prefiérelo.
    - QUALIFY no se puede combinar con GROUP BY ALL: el binder lo rechaza.
      Con QUALIFY usa GROUP BY explícito. Y dentro de la window repite la
      agregación —ORDER BY SUM(x) DESC—, nunca el alias del SELECT: si el
      alias se llama igual que la columna, resuelve a la columna cruda y falla.
    - La división / devuelve DOUBLE. Para división entera usa //.
    - No existe TOP n. Usa LIMIT.
    - Zona horaria: la columna es naive UTC, así que la conversión correcta es
      fecha_utc AT TIME ZONE 'UTC' AT TIME ZONE 'Europe/Madrid'
      La conversión la aporta ICU, ya incluida en las builds oficiales: no hace
      falta INSTALL ni LOAD. Una sola llamada AT TIME ZONE da mal resultado.
    - Antes de una pregunta abierta, ejecuta SUMMARIZE sobre la tabla
      para ver rangos y nulos reales antes de escribir la query final.
    

    Y al final del mismo fichero, la sección que más cambia el resultado: las queries doradas. Pares de pregunta y SQL correcto, escritas por alguien que conoce los datos.

    -- P: "¿Cuánto facturamos neto en agosto de 2026?"
    SELECT ROUND(SUM(neto_usd), 2) AS neto_usd
    FROM read_parquet('data/ventas_cursos/*.parquet')
    WHERE date_trunc(
            'month',
            fecha_utc AT TIME ZONE 'UTC' AT TIME ZONE 'Europe/Madrid'
          ) = DATE '2026-08-01';
    
    -- P: "Top 3 cursos por ingreso neto en cada plataforma este año"
    -- Ojo: GROUP BY explícito (QUALIFY no admite GROUP BY ALL) y SUM(neto_usd)
    -- dentro de la window, no el alias.
    SELECT plataforma, curso_slug, SUM(neto_usd) AS neto_usd
    FROM read_parquet('data/ventas_cursos/*.parquet')
    WHERE fecha_utc AT TIME ZONE 'UTC' AT TIME ZONE 'Europe/Madrid'
          >= TIMESTAMP '2026-01-01'
    GROUP BY plataforma, curso_slug
    QUALIFY row_number() OVER (
              PARTITION BY plataforma ORDER BY SUM(neto_usd) DESC
            ) <= 3
    ORDER BY plataforma, neto_usd DESC;
    

    Ese fichero son cuatro pantallas y vale más que cambiar de modelo tres veces.

    Cada bloque mata un fallo distinto. El esquema anotado mata las columnas alucinadas. Las reglas de negocio matan las respuestas plausibles pero falsas, las más caras de todas.

    Y el dialecto mata dos cosas: el SQL de PostgreSQL que el modelo escribe por defecto, y trampas como la de QUALIFY que ningún modelo adivina porque solo las conoces si te han explotado en la cara. Es la misma idea que en preparar datos para agentes de IA con Python: el agente no necesita más inteligencia, necesita menos ambigüedad.

    Y antes de dejar que ese agente escriba algo que no sea un SELECT, monta el contrato de revisión. Escribí un ebook gratuito sobre eso, Revisión por Contrato: cómo revisar el código que genera un modelo sin leerlo línea a línea.


    El coste del SQL agéntico local no es el precio: es el tiempo

    El dato que decide más que el precio es la latencia.

    El agente local tarda 5-6 minutos por pregunta. Gemini-3-Flash tarda unos 25 segundos. GPT 5.6 Luna Max, unos 40.

    No es un 20 % más lento. Es un orden de magnitud. Y eso no se arregla con contexto.

    El rendimiento observado ronda los 5-7 tokens por segundo en un MacBook Air M5 y unos 5 en un MacBook Pro M1 Pro de 16 GB. Un agente que da cuatro o cinco pasos quema miles de tokens antes de devolver la primera fila.

    Eso convierte la decisión en algo binario:

    • Sí a local: batch nocturno, informes recurrentes, datos que no pueden salir de la máquina, exploración sin prisa, entornos sin conectividad.
    • No a local: dashboard interactivo, chat de datos para negocio, cualquier flujo donde alguien esté mirando un spinner.

    Y hay una restricción estructural que se cuenta poco: en local corres un prompt a la vez. En cloud lanzas 15 preguntas en paralelo sin pensarlo. Para una suite de evals nocturna eso es la diferencia entre veinte minutos y seis horas.

    Si estás decidiendo qué modelo abierto meter en tu máquina, ya comparé opciones en los mejores modelos de IA local en 2026, y de la familia Qwen hablé en el análisis de benchmarks de Qwen3.8 Max.


    La letra pequeña del "gratis": qué cuesta Qwen3.8-27B en local

    Correr Qwen3.8-27B en local no sale gratis: cuesta unos 6 $ por cada 1.000 preguntas amortizando el hardware, y pide 14-16 GB de VRAM en 4 bits. Cuatro matices antes de que pidas presupuesto para una GPU.

    No es gratis. Contando amortización del hardware, el coste real ronda 6 $ por cada 1.000 preguntas. La comparación honesta no es "gratis contra 8 $", sino esos 6 $ por mil preguntas contra lo que te cobre tu proveedor por esas mismas mil. En volumen alto sigue ganando el local por goleada, pero "gratis" es marketing.

    Puede que no te quepa. Son 27B de parámetros densos —sin MoE—, atención híbrida, encoder de visión, contexto nativo de 262.144 tokens y licencia Apache 2.0, según la ficha oficial del modelo. En 4-bit son ~18 GB de descarga y 14-16 GB de VRAM; FP8 sube a ~28 GB y BF16 a ~56 GB. Y MotherDuck estima que solo alrededor de un tercio de los portátiles pasa de 16 GB de RAM, así que la mayoría se queda en la cuantización de 3 bits.

    El acelerador puede frenarte. El multi-token predictor está pensado para ir más rápido, pero en hardware antiguo puede ralentizar. Mide antes de dejarlo activado.

    Un benchmark no es tu base de datos. DABstep tiene esquema limpio y preguntas bien formuladas. Tu warehouse tiene tres columnas llamadas status y una tabla que solo entiende alguien que se fue en 2023.

    Por eso el paso siguiente no es "probarlo", es medirlo con tus preguntas: evals deterministas sobre veinte consultas reales tuyas, comparando el resultado de la query y no el texto de la respuesta.


    Cómo montar un agente SQL local con Qwen3.8-27B y DuckDB en 7 pasos

    Tal como lo describe MotherDuck, con LM Studio —no Ollama:

    1. Instala DuckDB.
    2. Instala LM Studio.
    3. Descarga el modelo cuantizado: Qwen3.8-27B-MLX-4bit si tienes 32 GB; el IQ3_XXS de unsloth si tienes 16 GB.
    4. Opcionalmente añade el acelerador MTP, y mide si te ayuda.
    5. Levanta el endpoint compatible con OpenAI de LM Studio, con 16.384 tokens de contexto y el reasoning en low u off.
    6. Conecta tu harness de agente —OpenCode o el que uses— a ese endpoint.
    7. Apunta DuckDB a tus datos.

    Si el agente va a consultar mucho o desde varios procesos, monta bien la parte de acceso: lo cubrí en conexión eficiente a DuckDB.


    Lo que haría yo hoy con tu agente de datos

    Abre el prompt de tu agente de datos y cuenta cuántas líneas hablan de tu negocio. Si la respuesta es cero, no tienes un problema de modelo.

    Coge la tabla que más consultas, escribe el fichero de contexto de arriba para ella —esquema anotado, reglas de negocio, dialecto y tres queries doradas— y vuelve a lanzar las mismas preguntas con el mismo modelo que ya pagas. Esa es la medición que importa. Si con contexto sube, ya sabes que puedes bajar de modelo. Si no sube, cambiar de modelo tampoco te habría salvado.

    Y si quieres construir el agente completo, esta forma de trabajar —contexto primero, modelo después— es la que enseño en el curso Construye con IA: de la idea al producto con Claude Code. El harness, los contratos y las evals que hacen que un agente sea fiable, no impresionante en una demo.

    En Dominicode Labs tenemos las plantillas de contexto que usamos en producción, incluida esta de DuckDB.


    Preguntas frecuentes

    ¿Qué es el SQL agéntico y en qué se diferencia del text-to-SQL?

    El text-to-SQL clásico traduce una pregunta en lenguaje natural a una consulta y ahí termina: si falla o devuelve algo absurdo, el problema es tuyo. El SQL agéntico mete al modelo en un bucle con herramientas: inspecciona el esquema, escribe la consulta, la ejecuta, lee el error o el resultado y corrige hasta responder la pregunta de negocio. El SQL agéntico local es ese mismo bucle con un modelo abierto en tu máquina, sin coste por token y sin que los datos salgan del equipo.

    ¿Qwen3.8-27B es mejor que GPT 5.6 Luna Max para SQL?

    En el benchmark de MotherDuck sobre DABstep, sí: Qwen3.8-27B en cuantización de 4 bits alcanzó un 98,6 % de precisión y superó a GPT 5.6 Luna Max, que costó más de 8 $ en la misma pasada. Pero ese resultado se midió con una capa de contexto construida a mano para ese conjunto de datos, y tardando 5-6 minutos por pregunta frente a unos 40 segundos del modelo en la nube. Sin esa capa de contexto y con un humano esperando, la comparación se da la vuelta.

    ¿Qué hardware necesito para correr Qwen3.8-27B en local?

    En cuantización de 4 bits son ~18 GB de descarga y necesitas entre 14 y 16 GB de VRAM, así que en la práctica hablamos de una máquina con 32 GB de RAM unificada o una GPU dedicada equivalente. Con 16 GB puedes tirar de la cuantización de 3 bits IQ3_XXS de unsloth, que según el benchmark de MotherDuck baja la precisión de 98,6 % a 96,4 %. En FP8 el modelo pide ~28 GB y en BF16 ~56 GB, que ya es territorio de servidor.

    ¿De verdad sale gratis?

    No literalmente. La inferencia no tiene precio por token, y el coste de electricidad de la pasada completa del benchmark quedó por debajo de 0,50 $. Pero si amortizas el hardware, el coste real ronda los 6 $ por cada 1.000 preguntas. La comparación honesta no es "gratis contra 8 $", es "6 $ por mil preguntas contra lo que te cobre tu proveedor por esas mil". Sigue ganando el local por goleada en volumen alto.

    ¿Sirve esto para un chat de datos en producción?

    Para un dashboard interactivo, no. Cinco o seis minutos por pregunta con una sola petición en curso a la vez descarta cualquier caso donde haya un humano esperando. Donde sí encaja es en batch nocturno, informes recurrentes, entornos sin conectividad y datos sensibles que no pueden salir de la máquina. Ese último caso, por sí solo, ya justifica el montaje en más empresas de las que parece.

    ¿Puedo usar Ollama en lugar de LM Studio?

    El setup que describe MotherDuck usa LM Studio y su endpoint compatible con la API de OpenAI, configurado con 16.384 tokens de contexto y el reasoning en bajo o desactivado. Cualquier runtime que exponga un endpoint compatible te vale para conectar el harness, pero comprueba dos cosas antes de comparar resultados: que estás cargando exactamente la misma cuantización y que la ventana de contexto configurada es la misma. Cambiar cualquiera de las dos cambia los números.

    ¿Es seguro dejar que un agente ejecute SQL sobre mi base de datos?

    Solo si le pones los límites antes, no después. Lo mínimo: conexión de solo lectura, un usuario con permisos únicamente sobre las tablas que necesita, un LIMIT por defecto y un timeout de query. Con DuckDB sobre ficheros Parquet el riesgo baja mucho, porque el agente lee ficheros y no toca el warehouse de producción. Nunca le des credenciales de escritura a un agente para ahorrarte un paso.

    Tengo 200 tablas. ¿Escribo el contexto de todas?

    No. Empieza por las cinco que concentran el 80 % de las preguntas y documenta esas a fondo. La capa de contexto no se escribe entera de golpe: crece cada vez que el agente falla. Cuando una respuesta salga mal, no reescribas el prompt del sistema —añade la regla de negocio que faltaba y la query dorada correspondiente. Ese fichero acaba siendo el activo más valioso del sistema, y es el que sobrevive cuando cambies de modelo.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • SDLC context engineering: arregla el ciclo, no el prompt

    SDLC context engineering: arregla el ciclo, no el prompt

    El mismo agente. El mismo modelo. Prácticamente el mismo prompt.

    En uno de mis repositorios la tarea salió a la primera. En el otro, el agente se inventó un helper que no existía y escribió los tests con una librería que ese proyecto abandonó hace más de un año.

    No falló el modelo. Falló todo lo que había alrededor del modelo.

    Y eso que hay alrededor tiene nombre: SDLC context engineering. Tu ciclo de desarrollo es la fábrica del contexto que consume el agente, y si la fábrica va mal, da igual cómo escribas el prompt.

    El primer repositorio tiene un CLAUDE.md con las convenciones escritas, una carpeta de decisiones de arquitectura y un índice del contenido previo que el agente puede consultar. El segundo tiene un README viejo y el resto vive en mi cabeza.

    Y ahí está el problema: cuando el contexto vive en tu cabeza, el agente tiene que adivinarlo. Adivinar, en un modelo de lenguaje, se llama alucinar.

    Por eso llevo meses insistiendo en lo mismo: el prompt no es la unidad de contexto. Puedes escribir el prompt más elaborado del mundo, con sus tres adjetivos y su frase en mayúsculas, que si la información que necesita el agente no existe en ningún sitio legible, no va a aparecer porque tú se lo pidas con más énfasis.

    El contexto no se escribe en el prompt. Se fabrica antes, en tu ciclo de desarrollo.

    Opero Dominicode solo: cursos, libros, una plataforma y un canal. No tengo un equipo que rellene los huecos por mí, así que los huecos los tengo que cerrar en el proceso. De ahí sale la idea que más ha cambiado mi forma de trabajar en el último año:

    Tu SDLC no es un proceso para humanos. Es la cadena de montaje que fabrica el contexto que consumen tus agentes.


    Qué es el SDLC context engineering

    El SDLC context engineering es tratar tu ciclo de vida del software como el sistema que fabrica el contexto que consumen tus agentes de IA.

    Cada una de las cinco fases —requisitos, diseño, implementación, code review y documentación— deja de producir artefactos para humanos y pasa a producir artefactos que una máquina puede leer, verificar y ejecutar: una spec con límites, un esquema de validación, una suite de tests como gate, un diff acotado y contexto versionado en el repositorio.

    La diferencia con el prompt engineering es de capa. El prompt engineering optimiza la instrucción de un turno. El SDLC context engineering optimiza la información que esa instrucción tiene disponible, y esa información la produce tu proceso, no tú en el momento de escribir.


    Qué cambia cuando el que lee el proceso es una máquina

    Cuando el que lee tu proceso es una máquina cambia el destinatario de cada artefacto: deja de valer lo que un humano completa con conocimiento implícito y solo cuenta lo que cabe en la ventana de contexto.

    El ciclo de vida clásico producía artefactos para personas: un ticket de tres líneas, la foto de una pizarra, un hilo de Slack, una reunión de refinamiento.

    Todo eso funciona con humanos por una razón que casi nunca decimos en voz alta: una persona rellena los huecos con conocimiento implícito. Sabe que en este proyecto los servicios van en esa carpeta. Sabe que ese campo del modelo está deprecado aunque siga ahí. Y, sobre todo, sabe a quién preguntar cuando algo no cuadra.

    Un agente no tiene a quién preguntar. Solo tiene lo que le entre por la ventana de contexto.

    Así que cada fase de tu ciclo tiene dos versiones posibles: la que produce algo para un humano y la que produce algo que una máquina puede leer, verificar y ejecutar.

    # Fase del ciclo Artefacto para humanos Artefacto para agentes
    1 Requisitos Ticket de 3 líneas spec.md con límites explícitos
    2 Diseño Diagrama en una pizarra Contratos ejecutables que validan
    3 Implementación "En mi máquina funciona" Tests como gate de salida
    4 Code review "A mí me parece bien" Diff acotado + auditor automático
    5 Documentación Wiki de hace tres años Contexto versionado en el repositorio

    La columna de la derecha es tu context engineering. No es un documento aparte que escribes el viernes por la tarde: es el residuo natural de un ciclo bien montado.

    Vamos fase por fase.


    1. Requisitos: del ticket de tres líneas al spec.md

    Qué falla: un ticket ambiguo no le da al agente lo único que de verdad necesita. Y no es la descripción de la funcionalidad: son los límites.

    Esta es la frase que más repito y la que más discusión genera: el alcance no es lo que el agente tiene que hacer, es lo que el agente no puede tocar.

    Un agente al que le pides "añade validación al formulario de registro" y no le dices nada más, se expande. Toca el modelo de datos porque le pareció que hacía falta. Refactoriza el componente de al lado porque estaba feo. Añade una dependencia. Y cuando abres el diff, tienes once archivos modificados y ninguna forma rápida de saber cuáles querías.

    Una spec no necesita ser larga. Una página con cuatro bloques:

    • Contratos de datos. La forma exacta de lo que entra y lo que sale.
    • Archivos afectados. Las rutas concretas que se pueden tocar.
    • Fuera de alcance. Lo que no se toca, escrito explícitamente.
    • Criterio de terminado. Qué comando tiene que pasar en verde.

    Con esos cuatro bloques, una spec entera te cabe en la pantalla:

    # Spec — Validación del formulario de registro
    
    ## Contratos
    - Entrada: { email: string, password: string, acceptedTerms: boolean }
    - Salida: { ok: true } | { ok: false, errors: FieldError[] }
    
    ## Archivos afectados
    - src/features/auth/register-form.tsx
    - src/features/auth/register.schema.ts
    
    ## Fuera de alcance
    - No tocar el modelo de usuario ni las migraciones
    - No añadir dependencias nuevas
    - No refactorizar componentes vecinos
    
    ## Terminado cuando
    - `bun test src/features/auth` pasa en verde
    - `tsc --noEmit` sin errores
    

    Ese tercer bloque es el que mejor retorno da de todo el documento, y es el que casi nunca veo escrito.

    Improvisar aquí no sale gratis, y el coste se puede calcular turno a turno: lo hice en la factura del vibe coding. Si tus specs ya existen pero el agente sigue desviándose, el problema suele estar en uno de estos 7 fallos. Y si quieres saber hasta dónde llevar el enfoque, están los tres niveles de Spec-Driven Development.

    La metodología completa, con las plantillas que uso a diario, está en el libro de Spec-Driven Development.


    2. Diseño: del diagrama en la pizarra a contratos ejecutables

    Qué falla: si la forma de tus datos vive dispersa por el código, el agente inventa propiedades. Y las inventa con una seguridad absoluta, porque estadísticamente user.email es un campo muy razonable aunque en tu proyecto se llame user.contactAddress.

    La solución no es documentar los tipos en un wiki. Es que la definición y la verificación sean el mismo artefacto.

    Un esquema de validación —Zod 4 en el ecosistema TypeScript, pero el principio vale para cualquier stack— hace tres cosas a la vez:

    • Describe la forma de los datos en un sitio único y localizable.
    • La comprueba en ejecución, así que si la descripción miente, algo se rompe y te enteras.
    • Genera el tipo con z.infer, así que la definición y la verificación salen del mismo artefacto y se actualizan a la vez.

    Esa segunda parte es la que lo convierte en contexto fiable. Un diagrama puede quedarse obsoleto en silencio durante dos años. Un esquema que se ejecuta, no: cuando un campo obligatorio cambia de tipo o desaparece, revienta y te enteras.

    Con un matiz que conviene saber, porque es donde la gente se confía: por defecto z.object() descarta las claves que no conoce en lugar de fallar. Si la API empieza a devolver campos nuevos, tu esquema los tira sin decir nada. Para que esa deriva también haga ruido necesitas z.strictObject(). El esquema te protege del campo que falta; del campo que sobra, solo si se lo pides.

    Y no confundas una cosa con la otra: los tipos de TypeScript desaparecen al compilar y no validan nada en ejecución. Evitan bugs antes de desplegar, que no es poco, pero el que comprueba lo que entra de verdad por la API es el esquema.

    Estos patrones —esquemas como contrato, inferencia de tipos y validación en los bordes— son los que desarrollo en el curso de Zod para TypeScript.

    La otra mitad del diseño es el acceso. En vez de pegar el esquema de tu base de datos dentro del prompt cada mañana, expones la fuente y dejas que el agente la consulte cuando la necesite. Eso es lo que resuelven los servidores de Model Context Protocol: el contexto deja de ser algo que copias y pasa a ser algo que se consulta.

    Eso sí, cada servidor que conectas mete sus definiciones de herramientas en la ventana. MCP cambia copiar por consultar, no elimina el coste de contexto: conecta los que uses, no los que tengas.


    3. Implementación: del "en mi máquina funciona" al gate de salida

    Qué falla: preguntarle al agente si ha terminado.

    Te va a decir que sí. No porque mienta, sino porque no tiene forma de saberlo: está evaluando su propio trabajo con exactamente el mismo contexto con el que lo escribió. Si le faltaba una pieza para escribirlo bien, le sigue faltando para revisarlo.

    Necesitas una señal que venga de fuera del modelo. Y la señal más barata que existe es un código de salida.

    El bucle que uso:

    1. El agente escribe primero el test que falla.
    2. Escribe el código mínimo para que pase.
    3. El pipeline ejecuta tipado, tests y lint. Si sale 0, la tarea entra en la cola de revisión. Si no, el agente recibe el error y corrige sin que yo intervenga.

    El gate no tiene que ser un pipeline entero. Un script que encadene los tres comandos ya sirve: si devuelve 0, la tarea pasa; si no, el agente recibe el error y sigue solo.

    {
      "scripts": {
        "gate": "tsc --noEmit && bun test && bun run lint"
      }
    }
    

    Lo importante no es que sea TDD de manual. Es que la condición de parada la decide un proceso externo y no una frase del agente. Mientras la puerta de calidad seas tú leyendo la terminal, no has automatizado nada: solo has cambiado de sitio el cuello de botella.

    El flujo completo de validar código generado antes de mergear lo desarrollé en TDD con IA. Y si lo que quieres es probar al propio agente en CI —no solo al código que produce— eso es un test harness, que es una pieza distinta.

    Y ojo con el nivel de la suite, porque aquí hay un efecto perverso: unos tests flojos no son neutros. Le dan al agente permiso para dar por terminado un trabajo a medias, con la ventaja de que ahora el sello de aprobado es automático.


    4. Code review: del "a mí me parece bien" al diff acotado

    Qué falla: el volumen. Un agente produce en veinte minutos más código del que puedes revisar con atención en una tarde.

    Y aquí hay una trampa que cuesta ver: la calidad de tu code review se decide en la fase 1, no en la fase 4. Un diff de once archivos es muy difícil de auditar bien, y la razón por la que toca once archivos es que la spec no dijo cuáles no tocar. Cuando el alcance está escrito, el diff sale acotado solo, y revisarlo pasa de ser una tarde a ser un rato.

    Con el diff ya acotado, la revisión se reparte en dos filtros:

    • El automático, primero. Tipado, tests, lint y un auditor que mire el diff antes que tú. Lo que no pasa esos gates no llega a tus ojos. Cómo montarlo en el pipeline lo detallé en revisiones de código con IA en CI/CD.
    • El tuyo, después, y solo para lo que la máquina no puede ver. Que la abstracción elegida sea la correcta. Que no haya duplicado algo que ya existía. Que el error se maneje donde tiene sentido y no donde resultaba cómodo.

    Esa segunda lista es más larga de lo que parece, y hay fallos del código generado por IA que un code review directamente no ve. Los tests cubren la corrección. Tú cubres el criterio.

    El checklist que uso para auditar diffs generados por IA antes de mergear está en el ebook gratuito Revisión por Contrato.


    5. Documentación: del wiki muerto al contexto versionado

    Qué falla: guardar la arquitectura en herramientas que el agente no puede abrir.

    El contexto tiene que vivir en el repositorio, al lado del código y bajo control de versiones, en cuatro capas de artefactos de contexto para agentes que hacen cosas distintas:

    • CLAUDE.md o AGENTS.md en la raíz. Convenciones, comandos de build, qué no se toca. AGENTS.md es un formato abierto supervisado por la Agentic AI Foundation, bajo la Linux Foundation, y lo usan ya más de 60.000 proyectos open source. Es lo primero que lee el agente al arrancar y lo que evita la mayoría de los "esto no va aquí".
    • docs/adr/ con decisiones de arquitectura. Markdown ligero que explica por qué se decidió algo, no solo qué se decidió. Sin el porqué, el agente deshace tus decisiones creyendo que mejora el código.
    • Un índice consultable del conocimiento previo. Para que pueda buscar en lo que ya existe sin que le metas el proyecto entero en la ventana.
    • Un mapa de dependencias del repositorio. Qué depende de qué. Es la diferencia entre un agente que cambia una función y otro que sabe qué se rompe al cambiarla, y va de graph engineering.

    Con una advertencia importante, porque es el error clásico de quien descubre esto: más contexto no es mejor contexto. Llenar la ventana de documentación irrelevante degrada las respuestas igual que no tener nada, solo que gastando más. Cómo estructurar esa memoria para que sume está en context engineering aplicado a agentes, y qué pasa cuando la conversación se alarga demasiado, en context drift.


    La regla del eslabón más débil de tu SDLC

    La regla del eslabón más débil dice que tu ciclo rinde lo que rinda su fase peor: da igual lo bien que hagas las otras cuatro, el resultado del agente lo marca la fase rota.

    Por eso esta es la parte práctica, la que decide por dónde empezar mañana:

    • Specs impecables sin gate de tests: el agente escribe muy rápido algo que nadie valida.
    • Tests excelentes con tickets ambiguos: validas a la perfección la funcionalidad equivocada.
    • Todo bien montado y el conocimiento en tu cabeza: cada mañana empiezas de cero.

    Así que no empieces por la fase que más te apetece, que suele ser la que ya haces bien. Empieza por la que te está costando dinero ahora mismo. Este diagnóstico lo resuelve en un minuto:

    Lo que te pasa con el agente Fase que tienes rota
    Se sale del alcance y toca archivos que no debía 1. Requisitos
    Inventa campos, funciones o rutas que no existen 2. Diseño
    Dice que ha terminado y no funciona 3. Implementación
    Los diffs son tan grandes que no los revisas 4. Code review
    Repite errores que ya corregiste la semana pasada 5. Documentación

    Ese último síntoma es el más frecuente y el que más gente confunde con un problema de memoria del modelo. No lo es. Es que la corrección se quedó en el chat en vez de acabar en un archivo del repositorio.


    Lo que no debes hacer

    Documentarlo todo.

    Es la reacción típica cuando alguien entiende esta idea: se pasa un fin de semana escribiendo un CLAUDE.md de cuarenta secciones y una carpeta de ADRs preciosa. Tres meses después, la mitad ya no es verdad.

    Y contexto desactualizado es peor que no tener contexto, porque el agente lo obedece. Un archivo que dice que los servicios van en una carpeta que ya no existe no es un documento inútil: es una instrucción activa para hacerlo mal.

    La regla que aplico: si no lo vas a mantener, no lo escribas. Es preferible un archivo de quince líneas verdaderas que uno de doscientas donde no sabes cuáles siguen siéndolo.

    Tampoco todo proyecto necesita este aparato montado. Hay casos concretos en los que el enfoque de spec te frena, y conviene reconocerlos antes de meter ceremonia donde no hace falta.


    Los 3 cambios para tu próximo ticket

    No hace falta rehacer la metodología de tu equipo. En la próxima tarea que delegues:

    1. Escribe el "fuera de alcance". Una línea diciendo qué archivos no debe tocar el agente. Es el cambio con mejor retorno de esta lista.
    2. Pon un gate automático. Aunque sea solo tsc --noEmit y los tests. Que la respuesta a "¿ha terminado?" la dé un código de salida y no una frase.
    3. Mueve una convención de tu cabeza al repositorio. Una. La que más veces has tenido que repetirle al agente esta semana.

    Con esos tres, el siguiente prompt que escribas tiene muchas más probabilidades de salir a la primera sin que le cambies ni una palabra. Porque no habrás mejorado el prompt: habrás mejorado la fábrica que lo alimenta. Eso es SDLC context engineering.

    El flujo completo, de la idea al producto con herramientas agénticas, lo enseño paso a paso en el curso Construye con IA con Claude Code.

    Y si quieres ver los artefactos reales —specs, gates y archivos de contexto de proyectos que están en producción— eso es lo que compartimos cada semana en Dominicode Labs.

    Deja de buscar el prompt mágico. Arregla la fase que tienes rota y el contexto se arregla solo.


    Preguntas frecuentes

    ¿Qué es exactamente el SDLC context engineering?

    Es tratar tu ciclo de vida del software como el sistema que fabrica el contexto de tus agentes de IA. En lugar de escribir prompts cada vez más largos, haces que cada fase del ciclo —requisitos, diseño, implementación, revisión y documentación— deje un artefacto que una máquina pueda leer y verificar: una spec con límites, un esquema de validación, una suite de tests, un diff acotado y contexto versionado en el repositorio.

    ¿Esto no es lo mismo que el prompt engineering?

    No, y la diferencia es de escala. El prompt engineering trabaja sobre la instrucción concreta que escribes en un turno. El context engineering trabaja sobre la información que esa instrucción tiene disponible, y esa información la produce tu proceso, no tú en el momento de escribir. Un buen prompt sobre un ciclo roto sigue dando resultados malos, solo que con mejor redacción.

    ¿Por dónde empiezo si tengo las cinco fases mal?

    Por el síntoma que estés sufriendo ahora, no por el orden numérico. Si el agente se sale del alcance, empieza por la spec. Si inventa campos que no existen, por los contratos de datos. Si dice que ha terminado y no funciona, por el gate de tests. La cadena rinde lo que rinda su fase peor, así que arreglar la que más te está costando da más retorno que mejorar la que ya funciona.

    ¿Hace falta usar Spec-Driven Development para esto?

    No es obligatorio, pero la fase de requisitos es la que más impacto tiene sobre las otras cuatro, y SDD es la forma más ordenada de resolverla. Puedes empezar con algo mucho más ligero: una línea de "fuera de alcance" en el ticket ya cambia el comportamiento del agente. Y hay casos concretos en los que el enfoque de spec te frena en vez de ayudarte, así que conviene reconocerlos antes de montar ceremonia.

    ¿Cuánto contexto es demasiado contexto?

    El que no puedas mantener actualizado. Un archivo de contexto que ya no refleja la realidad no es neutro: el agente lo obedece y hace las cosas mal con total seguridad. La medida correcta no es cuántas páginas tienes, sino cuántas líneas puedes garantizar que siguen siendo ciertas hoy. Además, llenar la ventana de contexto irrelevante degrada las respuestas y encarece cada turno.

    ¿Qué documentación para agentes de IA hace falta de verdad en un repositorio?

    Cuatro capas y nada más: un CLAUDE.md o AGENTS.md en la raíz con convenciones y comandos, una carpeta docs/adr/ con el porqué de las decisiones de arquitectura, un índice consultable del conocimiento previo y un mapa de dependencias del repositorio. Todo versionado junto al código. Lo que no esté en el repositorio, el agente no lo puede abrir.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Claude Fable 5.1: los 3 breaking changes que rompen tu agente

    Claude Fable 5.1: los 3 breaking changes que rompen tu agente

    Ayer por la tarde cambié una línea en un agente que llevaba once semanas en producción sin que nadie lo tocara.

    claude-fable-5 → claude-fable-5-1. Eso es todo lo que pide la guía de migración a Claude Fable 5.1. Deploy, café, a otra cosa.

    En mi cuenta no pasó nada. En la del cliente, abierta el lunes, las conversaciones largas empezaron a devolver un 400 con un mensaje que no había visto nunca: The block is bound to a different conversation.

    El modelo no tenía la culpa. La tenía una función mía que reconstruía el system prompt en cada petición para inyectar la fecha de hoy. Con Fable 5 era invisible. Ahora invalida todos los thinking blocks posteriores y la API rechaza la petición entera.

    Esa es la tesis de este post: actualizar el model ID es una línea; lo que rompe es cómo construyes el array de messages.


    Qué es Claude Fable 5.1 y qué no cambia respecto a Fable 5

    Claude Fable 5.1 es el modelo de razonamiento de gama alta de Anthropic, lanzado el 1 de septiembre de 2026 junto a Claude Mythos 5.1. Mantiene la ficha técnica de Fable 5 —ventana de 1M de tokens, 128K de output, $10 de input y $50 de output por millón— y su retirada no será antes del 1 de septiembre de 2027. Lo único que cambia de precio es el cache read. Lo único que rompe es cómo construyes el array de messages.

    El resto de la ficha tampoco se mueve: mismo tokenizer que Fable 5, thinking adaptativo siempre activo con effort por defecto en high y knowledge cutoff en junio de 2026. La ventana de 1M es a la vez el valor por defecto y el máximo, a precio estándar de principio a fin.

    Dónde puedes llamarlo: la API de Claude como claude-fable-5-1, Amazon Bedrock como anthropic.claude-fable-5-1, Google Cloud y Microsoft Foundry, además de Claude Code, Claude Enterprise y la Claude Platform. Mythos 5.1 (claude-mythos-5-1) es solo por invitación.

    Si vienes de Fable 5, en la ficha técnica no hay nada nuevo que aprender. Lo nuevo está en dos sitios: tres cosas que dejan de funcionar y un precio que cambia la economía de los agentes largos.


    Breaking change 1: tool_choice forzado devuelve 400 en Claude Fable 5.1

    tool_choice: {"type": "any"} y {"type": "tool", "name": "..."} devuelven un 400 invalid_request_error con este mensaje literal:

    tool_choice: type "tool" and "any" are not supported for this model.
    

    Siguen funcionando {"type": "auto"} (el default) y {"type": "none"}. La misma validación se aplica al endpoint de token counting: si tenías un estimador de costes que replicaba el payload, también se cae.

    El motivo tiene sentido. Con el thinking siempre activo, forzar la tool se salta el bloque de razonamiento y el modelo acaba metiéndolo en los argumentos.

    El fix son dos cambios y ninguno es dramático:

    // Antes (Fable 5): forzabas la tool para garantizar JSON válido
    const res = await client.messages.create({
      model: "claude-fable-5",
      max_tokens: 4096,
      tools: [extractInvoice],
      tool_choice: { type: "tool", name: "extract_invoice" }, // 400 en Fable 5.1
      messages,
    });
    
    // Después (Fable 5.1): auto + strict, y la orden va en el prompt
    const res = await client.messages.create({
      model: "claude-fable-5-1",
      max_tokens: 4096,
      tools: [{ ...extractInvoice, strict: true }],
      tool_choice: { type: "auto" },
      messages: [
        ...messages,
        {
          role: "user",
          content: "Usa la tool `extract_invoice` para responder.",
        },
      ],
    });
    

    Un aviso antes de que lo copies: ese mensaje se queda en el historial. Si lo inyectas en cada petición y lo borras en la siguiente, acabas de reproducir el breaking change 3. O lo dejas fijo, o lo mandas como system message de un solo turno, que lo verás más abajo.

    Si lo que buscabas con tool_choice era JSON conforme a un esquema y no una herramienta de verdad, mueve el esquema a structured outputs y quítate la tool de en medio. El esquema pasa a ser el contrato, y un contrato hay que validarlo también en tu lado. Si lo que devuelve el modelo lo compruebas con un if (typeof x === "string"), en el curso de Zod para TypeScript está la versión que no se rompe cuando el esquema crece.


    Breaking change 2: los thinking blocks están atados al modelo que los produjo

    Cada thinking block registra qué modelo lo generó, y la compatibilidad es unidireccional. Fable 5.1 lee los bloques de modelos anteriores. Ningún modelo anterior lee los de Fable 5.1.

    Traducción para quien tiene un router: si tu fallback salta de Fable 5.1 a Opus 5 a mitad de conversación, la API descarta el bloque antes de que el modelo lo vea. No cuenta como input_tokens, no se factura y no te avisa.

    Ese silencio es el problema: tu agente sigue respondiendo, pero razona con menos contexto del que crees y en la traza no hay un error que investigar. Con el header beta thinking-binding-controls-2026-08-01 el descarte se reporta en input_transformations. Enciéndelo en staging antes de migrar.


    Breaking change 3: editar turnos anteriores invalida los thinking blocks

    Este es el que me mordió a mí.

    Modificar cualquier cosa antes de un thinking block —el system, el array de tools o un mensaje anterior— provoca un 400 en la siguiente petición: The block is bound to a different conversation.

    Patrones que invalidan todos los bloques posteriores:

    • Editar, reordenar o eliminar un turno anterior conservando los siguientes.
    • Inyectar texto por petición en un turno anterior (un recordatorio, una línea de estado) que borras en la siguiente.
    • Reconstruir el system prompt o el array de tools entre peticiones de la misma conversación.
    • Servir bytes distintos para la misma imagen o documento en una petición posterior. La comprobación mira los bytes, no la URL, así que una signed URL rotatoria del mismo fichero es válida.

    Patrones que no invalidan nada:

    • Eliminar una racha inicial de thinking blocks, del más viejo primero.
    • Dejar que la compactación o el context editing server-side recorten el historial.
    • Mover marcadores cache_control.
    • Cambiar el effort entre peticiones.

    La regla mental cabe en tres palabras: trata la conversación como append-only.

    Y aquí el detalle que explica por qué a unos les explota y a otros no: la comprobación se aplica a cuentas creadas a partir del 31 de agosto de 2026. En cuentas anteriores la API registra el desajuste pero solo actúa si mandas thinking.block_binding.prefix_mismatch_behavior, y Mythos 5.1 no la aplica nunca. Tu código puede estar roto hoy y no enterarte hasta que un cliente nuevo abra su cuenta.

    Claude Code, claude.ai, Claude Managed Agents y el Agent SDK ya mantienen ese prefijo intacto por ti. El problema es tuyo solo si construyes el array messages a mano, que es lo que hacemos casi todos los que tenemos agentes en producción.

    Los 3 breaking changes de Claude Fable 5.1, en una tabla

    Qué se rompe Cómo se manifiesta Fix
    tool_choice de tipo any o tool 400 invalid_request_error inmediato, también en token counting tool_choice: auto + strict: true, o mover el esquema a structured outputs; la orden de usar la tool, en el prompt
    Thinking blocks de Fable 5.1 enviados a un modelo anterior Silencio: el bloque se descarta, no se factura, nadie avisa Que el router no cambie de modelo a mitad de conversación; header thinking-binding-controls-2026-08-01 para verlo en input_transformations
    Editar system, tools o un turno anterior 400 The block is bound to a different conversation, solo en cuentas creadas desde el 31/08/2026 Historial append-only; recordatorios con system messages de un solo turno; recortes con context editing server-side

    No es la primera vez: ya conté los 2 breaking changes de Claude Opus 5. El patrón se repite en cada release y siempre pilla al mismo tipo de código, el que trata el historial como un array mutable.


    Precio de Claude Fable 5.1: el cache read baja un 75 %

    El cache read de Claude Fable 5.1 cuesta $0,25 por millón de tokens, un 75 % menos que los $1,00 de Fable 5. El resto de precios no se mueve.

    Concepto Fable 5.1 Fable 5
    Input $10 / MTok $10 / MTok
    Output $50 / MTok $50 / MTok
    Cache write 5 min $12,50 / MTok $12,50 / MTok
    Cache write 1 h $20 / MTok $20 / MTok
    Cache read $0,25 / MTok $1,00 / MTok

    El mínimo cacheable sigue en 512 tokens y el Batch API mantiene su 50 % de descuento: $5 de input y $25 de output.

    Pero el número que cambia la arquitectura es otro: en Fable 5.1 el cache read cuesta 0,025× el input base, cuando en el resto de modelos Claude es 0,1×. Es la excepción, no la norma.

    Eso convierte un prefijo grande y estable en algo casi gratis de releer: Anthropic declara un ahorro en torno al 25 % en cargas típicas y hasta el 45 % en trabajo muy agéntico.

    Fíjate en la simetría: los mismos patrones que invalidan los thinking blocks son los que te tiran la caché. Reconstruir el system en cada llamada no era un bug latente; era una factura que llevabas pagando desde antes de migrar.

    Si nunca lo has medido en serio, empieza por cómo funciona el prompt caching en la API de Claude. Después, cómo medir el consumo real de tokens de un agente. Sin esas dos métricas, elegir modelo es una corazonada.


    Tres novedades de Claude Fable 5.1 que sí vale la pena adoptar

    Effort por mensaje (beta, header mid-conversation-output-config-2026-07-01). Cambias el nivel de effort a mitad de conversación sin invalidar la caché de prompt. Súbelo para los pasos difíciles, bájalo para los rutinarios:

    const response = await client.beta.messages.create({
      model: "claude-fable-5-1",
      max_tokens: 4096,
      output_config: { effort: "high" },
      messages: [
        { role: "user", content: "Planifica la migración de SQLite a PostgreSQL." },
        { role: "assistant", content: "1. Exporta los datos. 2. Crea el esquema. 3. Importa y verifica." },
        // El nuevo nivel entra en vigor desde el siguiente turno de usuario
        { role: "system", content: [], output_config: { effort: "low" } }, // sin texto: es un mensaje de control
        { role: "user", content: "Resume el plan en una frase." },
      ],
      betas: ["mid-conversation-output-config-2026-07-01"],
    });
    

    Si tu editor subraya el role: "system" dentro de messages, es que el tipado de tu SDK todavía no lo incluye: actualiza el paquete antes de pelearte con TypeScript.

    System messages de un solo turno (beta, header mid-conversation-system-clear-at-2026-08-21). Es literalmente el sustituto del patrón que ahora rompe. Va dentro del array messages, como un turno más:

    {
      "role": "system",
      "clear_at": "next_user_message",
      "content": "Los resultados están en tu bandeja. Revísala antes de ejecutar más código."
    }
    

    Tiene autoridad de system prompt durante el turno actual y deja de inyectarse en el prompt cuando llega un mensaje de usuario posterior. Se queda en messages y lo sigues mandando igual, así que nada anterior cambia: la caché sigue casando, los thinking blocks siguen válidos y un mensaje limpiado cuesta 0 tokens de input.

    Progress updates entre tool calls (beta, header thinking-display-updates-2026-08-18). Con thinking.display: "updates" recibes como texto los avisos de progreso que el modelo escribe entre llamadas a tools, manteniendo el razonamiento oculto. Con el default "omitted" no llega nada y un turno agéntico de tres minutos parece un cuelgue.


    Dos cambios de comportamiento de Fable 5.1 que verás sin tocar código

    El parallel tool calling es más variable. Fable 5.1 puede hacer una llamada por turno donde Fable 5 agrupaba varias. No baja la calidad de la respuesta, pero cuesta tokens, round trips y tiempo de reloj. Se arregla con una instrucción de una línea pidiendo agrupar las lecturas independientes.

    Y Fable 5.1 reescribe ficheros enteros para cambios pequeños. Donde esperas una edición quirúrgica, te devuelve el fichero entero. Mismo resultado, más tokens de output.


    Claude Fable 5.1 vs Opus 5: la parte incómoda es que no es tu default

    Lo dicen los propios docs de Anthropic: "For most workloads, start with Claude Opus 5". Fable 5.1 se reserva para razonamiento exigente y trabajo agéntico de largo recorrido, o para cuando tus evals con Opus 5 a effort alto se quedan cortas. Opus 5 cuesta $5 de input y $25 de output. La mitad exacta.

    Benchmark Fable 5.1 Fable 5 Opus 5
    Terminal-Bench 4.0 55,8 % 42,0 % 52,3 %
    Terminal-Bench-Science 0.1 52,6 % 24,7 % 29,0 %
    OSWorld 2.0 (partial) 77,9 % 72,9 % 75,4 %
    OSWorld 2.0 (strict) 41,7 % 36,1 % 39,6 %

    Mira Terminal-Bench 4.0: 55,8 % contra 52,3 %. Tres puntos y medio por el doble de precio. En Terminal-Bench-Science son 52,6 % contra 29,0 %, casi veinticuatro puntos, y ahí la decisión se toma sola.

    La elección de modelo dejó de ser global. Se decide por tarea y con tus evals delante. Y si lo montas como router, recuerda el breaking change 2 y no cambies de modelo a mitad de conversación.


    Qué es Claude Mythos 5.1 y quién puede usarlo

    Claude Mythos 5.1 (claude-mythos-5-1) es el mismo modelo subyacente que Fable 5.1 con salvaguardas más permisivas. Comparte specs y precios. Solo por invitación, para participantes de Project Glasswing: organizaciones verificadas de ciberseguridad y ciencias de la vida. Con esas salvaguardas relajadas rinde más, 60,9 % en Terminal-Bench 4.0 frente al 55,8 % de Fable 5.1.

    La noticia no es el modelo, es la separación. Por primera vez Anthropic distingue de forma tan explícita "el modelo que puedes usar" de "el modelo que rinde más si te verifican". Tiene lógica. Parte del rendimiento que pierde un generalista se va en negarse a cosas que en un laboratorio de biología son trabajo normal, como se veía venir en los agentes autónomos aplicados a proteínas.

    Pero tenlo delante antes de comparar capturas de benchmarks: los números de un modelo con salvaguardas relajadas y los del modelo que tú puedes llamar no son comparables.


    Cómo migrar de Claude Fable 5 a Fable 5.1, en 4 pasos

    1. Busca tool_choice en tu repo. Si aparece any o tool, cámbialo a auto con strict: true y mueve la orden al prompt. Media hora.
    2. Busca dónde reconstruyes el system o el array de tools. Cualquier new Date() ahí dentro es una bomba: sácalo a un mensaje de usuario o a un system message de un solo turno.
    3. Audita tu router. Si puede cambiar de modelo a mitad de conversación, activa thinking-binding-controls-2026-08-01 en staging y mira input_transformations.
    4. Mide antes y después. Con el cache read a $0,25 tu arquitectura puede cambiar, pero solo si tienes el número delante.

    El resto está en los docs de novedades de Fable 5.1; el trabajo de verdad está en tus evals.

    Si construyes agentes con Claude y quieres el flujo completo de idea a producto, es lo que enseño en Construye con IA. Y en Dominicode Labs probamos estos patrones de historial append-only y routing por tarea sobre proyectos reales.

    Una release de modelo no se mide por lo que el modelo hace mejor. Se mide por cuántas suposiciones tuyas deja de sostener.


    Preguntas frecuentes

    ¿Tengo que migrar ya de Claude Fable 5 a Claude Fable 5.1?

    No hay urgencia. Fable 5.1 no se retirará antes del 1 de septiembre de 2027. La razón para migrar es económica, no de soporte. Si tu carga es muy agéntica y relee un prefijo grande en cada turno, el cache read a $0,25 justifica la migración por sí solo.

    ¿Por qué Claude Fable 5.1 me devuelve un 400 con tool_choice?

    Porque tool_choice: {"type": "any"} y {"type": "tool", "name": "..."} dejaron de estar soportados en Fable 5.1 y en Mythos 5.1. La API responde un invalid_request_error con el mensaje literal tool_choice: type "tool" and "any" are not supported for this model, y la misma validación se aplica al endpoint de token counting. Solo siguen siendo válidos {"type": "auto"} (el default) y {"type": "none"}. El fix es tool_choice: {"type": "auto"} con strict: true en la definición de la tool y la orden de usarla escrita en el prompt.

    ¿Cómo fuerzo ahora que el modelo llame a una tool concreta?

    No puedes forzarla. Díselo en el prompt —"Usa la tool get_weather para responder"— y comprueba que la haya llamado antes de seguir. Sin tool_choice no hay garantía, solo una instrucción que el modelo cumple casi siempre. Si lo que necesitabas era JSON conforme a un esquema, usa strict: true con tool_choice: auto o mueve el esquema a structured outputs.

    ¿Por qué a mi compañero le da 400 y a mí no, con el mismo código?

    Por la fecha de creación de la cuenta: la comprobación solo se aplica a las creadas a partir del 31 de agosto de 2026. En cuentas anteriores la API registra el desajuste pero no actúa salvo que mandes thinking.block_binding.prefix_mismatch_behavior. Tu código está igual de roto en ambos casos; solo cambia quién se entera.

    ¿Merece la pena Fable 5.1 frente a Opus 5?

    Los propios docs recomiendan empezar por Opus 5, que cuesta la mitad. Fable 5.1 saca tres puntos y medio más en Terminal-Bench 4.0, pero casi veinticuatro en Terminal-Bench-Science. Si tu tarea es razonamiento exigente y agéntico de largo recorrido, la diferencia paga; para el resto, no.

    ¿Puedo usar Claude Mythos 5.1?

    Solo si tu organización está aprobada en Project Glasswing, el programa por invitación para entidades verificadas de ciberseguridad y ciencias de la vida. No hay lista de espera pública: el acceso se pide a través de tu equipo de cuenta de Anthropic, AWS o Google Cloud. Es el mismo modelo con salvaguardas más permisivas y el mismo precio; si no estás dentro, tu referencia es Fable 5.1.

    ¿Puedo mezclar Claude Fable 5.1 y Opus 5 en el mismo agente?

    Sí, pero no dentro de la misma conversación. Los thinking blocks de Fable 5.1 no los lee ningún modelo anterior, así que un router que salte a Opus 5 a mitad de hilo pierde ese razonamiento sin avisarte. Reparte por tarea y arranca conversación nueva al cambiar de modelo, o activa el header thinking-binding-controls-2026-08-01 para ver los descartes en input_transformations.

    ¿Dónde puedo usar Claude Fable 5.1?

    En la API de Claude con el model ID claude-fable-5-1, en Amazon Bedrock como anthropic.claude-fable-5-1, en Google Cloud y en Microsoft Foundry, además de Claude Code, Claude Enterprise y la Claude Platform. No necesitas ningún header beta para llamarlo: los headers solo hacen falta para las funciones nuevas como el effort por mensaje o los system messages de un solo turno.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Agentic code review: el 64,7% de los PRs se aprueba sin leerlo

    Agentic code review: el 64,7% de los PRs se aprueba sin leerlo

    Esta semana has aprobado al menos un PR sin leerlo entero. Has mirado el diff en diagonal, has visto que el CI estaba en verde y has escrito "LGTM".

    No te estoy juzgando. Te estoy describiendo. Y no lo digo yo. Lo dice un estudio sobre cinco proyectos de gran escala (Gon et al.), recogido en un paper académico sobre agentic code review que acabo de leer entero: el 64,7% de los PRs se aprueban sin un solo comentario. Y esos reviews silenciosos presentan el "LGTM smell" —aprobar sin revisar de verdad— 3,5 veces más que los reviews con conversación.

    El paper se llama Rethinking Code Review in the Age of AI: A Vision for Agentic Code Review (arXiv:2605.17548). Es un vision paper: propone un framework, no un sistema implementado. Pero la radiografía que hace del review actual es tan incómoda que he cambiado cómo revisan código mis dos herramientas open source.

    Te cuento por qué.

    Los números que describen tu equipo

    El paper recopila estudios empíricos de la última década. Léelos pensando en tu repo, no en el de otros:

    • El 34% de 333.001 descripciones de PR analizadas en GitHub estaban vacías. Ni una línea de contexto (Liu et al.).
    • El 34,3% de los PRs no enlazan con ningún issue. En commits de bugfix, el 52,4% van sin enlazar (Dogan et al.; Bachmann et al.).
    • En Mozilla, el 54% de los code reviews no detectaron bugs que estaban presentes en commits aprobados (Kononenko et al.).
    • En Microsoft, solo el 15% de los comentarios de review señalaban defectos potenciales (Czerwonka et al.). Y entre un 34,5% y un 44,47% de los comentarios se clasifican directamente como "no útiles".
    • Un 19,1% de los comentarios de review de un dataset estudiado eran, literalmente, tóxicos (Sarker et al.).

    La etapa que llamamos "control de calidad" dejó pasar bugs en más de la mitad de los reviews medidos en Mozilla, genera ruido en un tercio de los comentarios y a veces hasta hace daño.

    Y ahora métele IA.

    El code review con IA no arregla el problema. Lo desborda

    Los asistentes de IA aceleran las tareas individuales de código en más de un 50%, según los estudios que recopila el paper. Escribimos más código que nunca. Pero hay dos datos que deberían quitarte la sonrisa.

    Uno: las contribuciones generadas por IA requieren más iteraciones de review que las escritas por humanos.

    Dos: cuando la IA asiste al reviewer, este encuentra más issues de severidad baja… pero no más defectos graves. La automatización arrastra tu atención hacia los problemas fáciles. El naming, el estilo, el typo. Mientras, el bug de concurrencia pasa de largo con su "LGTM".

    El paper lo dice sin rodeos: el code review ya no es solo un cuello de botella de productividad, es "la superficie de control primaria de la calidad y la responsabilidad del código producido por IA".

    Piensa en lo que eso significa. Si un agente escribe el 60% de tu código, el review es el único punto donde un humano responde por él. Y ese punto, según los datos de arriba, está roto.

    Hay una capa del problema que el review ni siquiera puede tocar, y la desarrollé aparte en los 5 fallos del código generado por IA que un code review no puede ver. Este post va de la otra mitad: arreglar lo que el review sí puede hacer y no hace.

    Qué es el agentic code review: el review no es una etapa, es un ciclo

    El agentic code review es un modelo de revisión en el que agentes de IA especializados cubren las cinco etapas del ciclo de vida del PR, mientras el humano actúa como supervisor con capacidad de veto en cada punto de decisión. La diferencia con "un bot que comenta el diff" es que el contexto cruza las fronteras entre etapas en lugar de perderse en cada salto.

    Y esa es la propuesta central del paper: la efectividad del review no es el resultado de una etapa aislada, sino de todo el ciclo de vida del PR.

    Un comentario de review útil depende de que el PR tenga una descripción con rationale. La descripción depende de que exista un issue enlazado. Y los reviews futuros dependen de que las lecciones de los reviews pasados queden escritas en algún sitio. Ninguna herramienta que optimice una sola etapa puede resolver esas dependencias.

    El framework tiene cinco etapas con agentes especializados y puertas humanas en cada punto de decisión: PR Creation → PR Augmentation → Reviewer Selection → AI-Assisted Code Review → PR Retrospective. El reviewer deja de ser un inspector manual y pasa a ser un operador supervisor de agentes.

    De todo el framework, hay dos piezas que me parecen oro. Y son las dos que he implementado hoy.

    Qué es el veredicto de alineación: Exact, Tangling y Missing

    El paper recoge una taxonomía de Isik et al. que formaliza algo que todos intuimos pero nadie mide: ¿el PR hace lo que se pidió?

    Categoría Qué significa Cómo se manifiesta con agentes Dato del paper
    Exact Cubre lo pedido, sin extras El caso que quieres —
    Tangling Incluye código que nadie pidió Le pides un fix y refactoriza tres ficheros "de paso" 7-20% de los changesets
    Missing No cubre todo lo pedido Marca la tarea como hecha sin implementar el criterio 16,5% de los PRs
    Missing and Tangling Ambas a la vez Se deja lo pedido y añade lo que no —

    Un review que solo busca bugs responde a la pregunta equivocada. La primera pregunta no es "¿este código tiene errores?". Es "¿este código es el que se pidió?".

    Por eso el skill /ak:review de ai-workflow-kit y la fase de Code Review del plugin sdd-creator ya no cierran el review con una lista de bugs. Cuando encuentran una spec que cubre el cambio, abren el review con una capa de cumplimiento y lo cierran con un veredicto de alineación explícito, contrastado criterio a criterio contra esa spec:

    ## Review: [feature slug]
    
    Status: PASS | CHANGES REQUIRED
    Alignment: Exact | Tangling | Missing | Missing and Tangling
    
    ### Requirements compliance
    - [AC-XX]: implemented / missing / diverges — [evidence]
    - Tasks marked done without a matching implementation: [list or none]
    - Out of scope: [code no criterion asks for, or none]
    

    La regla que lo hace útil es la última: un veredicto distinto de Exact no puede ser PASS salvo que tú aceptes la desviación por escrito. El código fuera de alcance se quita o se especifica; el trabajo que falta se completa o se saca del alcance. Es un veredicto que puedes verificar en dos minutos, en lugar de un "se ve bien" que no compromete a nadie.

    Si el repo no tiene specs/, no hay contra qué contrastar y el review vuelve al formato de severidades de siempre. Que es, en sí mismo, el argumento del paper.

    Qué es la retrospectiva de PR y por qué un review sin memoria se repite

    La quinta etapa del framework es la que casi todo el mundo se salta: el PR Retrospective. Cuando el PR se aprueba o se rechaza, un agente resume qué se decidió, qué se descartó y por qué, y lo guarda en la memoria del repositorio para que los agentes (y los humanos) del siguiente review partan de ahí.

    Aquí el paper suelta un detalle que valida algo que llevo tiempo defendiendo. Al explicar por qué los modelos no generalizan entre proyectos distintos, dice que inyectar reglas específicas del repositorio vía archivos de configuración tipo "Agents.MD" directamente en la ventana de contexto del agente es una alternativa computacionalmente barata al fine-tuning. No necesitas reentrenar un modelo para que entienda tu proyecto. Necesitas escribir las decisiones en un fichero que viaje con el repo.

    Eso también lo he incorporado: los dos productos ahora cierran el review proponiendo qué promocionar a la memoria del proyecto — decisión confirmada, alternativa rechazada, riesgo que se materializó. En el flujo SDD va a specs/INDEX.md; en el kit, a memory/decisions/. Los arreglos de código se quedan en el review; solo sube el conocimiento duradero. El siguiente review no redescubre lo mismo. Acumula.

    El paper valida SDD sin saberlo

    Y hay una frase del paper que me hizo reírme solo: "el contexto debe cruzar las fronteras entre etapas". Porque eso es exactamente Spec-Driven Development: el spec.md, el plan.md y el tasks.md no se quedan en la fase de diseño. Viajan hasta el review y hasta el PR. El reviewer no reconstruye la intención desde el diff — la tiene delante, escrita antes de la primera línea de código.

    El 34% de descripciones de PR vacías no es un problema de disciplina. Es un problema de flujo: si el contexto no existe antes de codificar, nadie lo va a escribir después. SDD lo resuelve por diseño — siempre que la spec esté bien planteada, porque una spec mal escrita rompe al agente igual que no tener ninguna.

    Lo que el paper admite que puede salir mal

    No te vendo humo: los propios autores dedican una sección entera a los riesgos, y son serios.

    Las alucinaciones se propagan en cascada entre agentes. Si el agente de review inventa una vulnerabilidad de concurrencia, el agente de fixes genera locks innecesarios. Para cuando el humano detecta el error, ya has pagado los tokens de tres agentes resolviendo un problema que nunca existió.

    Súmale la degradación de contexto en PRs grandes y el sesgo de automatización: aceptar el output del agente sin verificarlo, que es el LGTM smell con esteroides.

    Y el más silencioso de todos: el deterioro del mentoring implícito. Si el chatbot le explica el PR al junior, el senior ya no se lo explica.

    La respuesta a todos esos riesgos es la misma: puertas humanas con veredictos verificables. No "confía en el agente". Tampoco "desconfía de todo". Sino: exige al agente un output que un humano pueda comprobar en minutos.

    Cómo aplicar el agentic code review hoy en 3 pasos

    No necesitas esperar a que alguien implemente el framework completo del paper. Las tres piezas con más retorno caben en tu flujo actual:

    1. Cierra cada review con un veredicto de alineación. Exact, Tangling, Missing o ambas, contra el issue o la spec. Si no puedes emitirlo, no tenías contexto para revisar — y ese es el verdadero hallazgo del review.
    2. Escribe una retrospectiva de tres líneas por PR relevante. Qué se confirmó, qué se rechazó, qué riesgo apareció. Guárdala en el repo, donde el siguiente agente la pueda leer.
    3. Haz que el contexto viaje. Spec antes del código, spec enlazada en el PR, spec delante del reviewer.

    Si además quieres que esto corra solo en cada push, ya escribí cómo integrar revisiones de código automáticas con IA en el pipeline de CI/CD — el veredicto de alineación encaja ahí como un check más.

    Y si prefieres verlo funcionando en lugar de montarlo desde cero, tanto sdd-creator como ai-workflow-kit son open source y ya incorporan las dos piezas. Si quieres montarlo guiado y de principio a fin, el curso Construye con IA recorre justo este flujo: de la spec al PR revisado. Y si lo que buscas es trabajarlo sobre proyectos completos y en directo, eso es Dominicode Labs.

    El code review no va a desaparecer. Va a convertirse en el trabajo más importante que hagas. Mejor llegar con el contexto puesto.

    Preguntas frecuentes

    ¿Qué es el agentic code review?

    Es un modelo de revisión de código en el que agentes de IA especializados cubren las cinco etapas del ciclo de vida del PR —creación, enriquecimiento, selección de reviewer, revisión y retrospectiva— mientras el humano actúa como supervisor con capacidad de veto en cada punto de decisión. La diferencia con "un bot que comenta el diff" es que el contexto cruza las fronteras entre etapas en lugar de perderse en cada salto.

    ¿Cómo emito un veredicto de alineación en un PR?

    Compara el PR contra el issue o la spec y clasifícalo en una de cuatro categorías: Exact si cubre lo pedido sin extras, Tangling si trae cambios que nadie pidió, Missing si deja algo fuera, o Missing and Tangling si ocurren ambas. Escribe la categoría explícitamente en el PR con una frase de justificación. Si no puedes clasificarlo, el problema no es el PR: es que no tenías contexto suficiente para revisarlo.

    ¿No basta con poner un agente de IA a comentar los pull requests?

    No. Cuando la IA asiste al reviewer aparecen más issues de severidad baja, pero no más defectos graves: la herramienta desplaza la atención hacia lo fácil de detectar. Y un agente que solo comenta diffs no puede saber si el PR hace lo que se pidió, porque nadie le pasó la spec ni el issue.

    ¿En qué se diferencia esto de automatizar el code review en CI/CD?

    En el alcance. Automatizar en CI/CD resuelve la ejecución: que la revisión corra sola en cada push. El enfoque agéntico resuelve el contexto: que la revisión sepa qué se pidió, quién debe revisarlo y qué se aprendió en los PRs anteriores. Son complementarios — el veredicto de alineación se puede publicar como un check más del pipeline.

    ¿El framework del paper ya se puede usar en producción?

    El framework completo no: es un vision paper, una propuesta arquitectónica sin implementación ni evaluación empírica. Pero dos de sus piezas —el veredicto de alineación y la retrospectiva escrita en el repo— no dependen de ninguna infraestructura nueva y las puedes adoptar hoy con las herramientas que ya usas.


    Referencia: Kamalı, H. Ö., Tuna, E., Haratian, V., Tüzün, E. (2026). Rethinking Code Review in the Age of AI: A Vision for Agentic Code Review. Ankara University, Microsoft y Bilkent University. arXiv:2605.17548, mayo de 2026. Vision paper — propuesta de framework, no sistema implementado.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Claude diseñó proteínas solo: manual de agentes de IA autónomos

    Claude diseñó proteínas solo: manual de agentes de IA autónomos

    Casi todo lo que leo sobre IA cabe en tres cajones: autocompletar código, sacar un gráfico de un Excel y vídeos de gente que no existe bailando en una playa.

    Ese es el techo mental de la conversación. El mío también, muchos días.

    Y mientras discutimos si Cursor gestiona el contexto mejor que Claude Code, los mismos agentes de IA autónomos que tú y yo soltamos dentro de un repo llevaban 48 horas seguidas diseñando proteínas que no existían. Proteínas que después alguien sintetizó de verdad, en un laboratorio de verdad, y midió con un aparato de verdad.

    Ahí el error no se arregla con git revert.

    El 18 de agosto de 2026 Anthropic publicó How Claude is accelerating protein design and analytical chemistry y, debajo, un informe técnico con el detalle fino: 1.320 diseños generados, 354 binders confirmados en laboratorio, 14 de 15 dianas con al menos un acierto.

    Ese titular corrió por todas partes. Y es el trozo menos interesante de la historia.

    Porque lo que a ti y a mí nos sirve el lunes por la mañana no son los 354 binders. Es cómo estaba escrito el documento que le dieron al agente antes de arrancar.


    Primero, qué hizo exactamente

    Un binder es una proteína pequeña que se pega a una diana concreta. Es el paso cero de medio catálogo de fármacos. No es un fármaco: por delante queda todo el recorrido preclínico y regulatorio, que se mide en años.

    Claude no inventó ninguna herramienta. Usó las que ya existen y son públicas: diez generadores de estructura distintos, con PXDesign (358 diseños), RFdiffusion3 (267) y Genie 3 (185) a la cabeza; SolubleMPNN para el diseño de secuencia (1.133 de los diseños testeados) y un ensemble de ESMFold2, ESMFold2-Fast y Protenix v2 para rankear. Ninguna venía preinstalada: el protocolo le obliga a compilar cada una desde su repositorio público y validarla en la primera hora. Todo dentro de Claude Science, el entorno de investigación de Anthropic.

    Lee esa lista otra vez. Ninguna herramienta es suya.

    El modelo no aportó capacidad generativa nueva al campo. Aportó criterio: qué herramienta usar, en qué orden y qué candidatos tirar a la basura. Es la diferencia entre IA generativa e IA agéntica llevada a un dominio donde el resultado se mide con un sensor.

    Y se nota en el ranking: su diseño número uno acertó el 49 % de las veces, el top cinco un 44 %, el top diez un 39 %, frente al 28 % del conjunto de treinta. El criterio estaba en el orden.

    Y se midió fuera de casa. Adaptyv Bio convirtió las secuencias en ADN, sintetizó las proteínas con síntesis libre de células y robots, y midió afinidad por resonancia de plasmón superficial. Twist Bioscience también participó. Esto no es una simulación puntuándose a sí misma.

    Los números por brazo del experimento, que mucha gente ha contado mal mezclándolos:

    Configuración Binders / diseños Tasa de acierto
    Baseline de la industria hoy — 10–15 %
    Opus 4.8 · 14 dianas a la vez, una sola sesión · 48 h 88 / 390 22,6 %
    Mythos Preview · 14 dianas a la vez, una sola sesión · 48 h 104 / 390 26,7 %
    Mythos Preview · una sesión de 24 h por diana 158 / 450 35,1 %

    Del formato multi-diana se analizan 13 de las 14 dianas; la campaña de diana única cubrió las 15.

    El 26,8 % global sale de dividir 354 entre 1.320. El 95 % de los diseños se expresó correctamente. Y hubo dos casos que se salen de la media:

    • RBX1: 28 binders de 90 diseños sumando las tres campañas, un 31 %. Y un 40 % en la campaña de diana única, la mejor configuración. En la competición abierta previa sobre esta misma diana solo pegaron 9 de 245 diseños: un 3,7 %. El mejor diseño del agente se midió en 3,9 nM, por delante del que ganó aquella competición.
    • TREM2: 72 binders de 90 diseños. Un 80 %, frente al 38,3 % de la competición previa de Adaptyv.

    Con un asterisco que pone el propio informe: cuatro de las seis competiciones con las que se compara ya estaban publicadas y accesibles para el agente mientras diseñaba.

    Impresionante igual. Ahora la parte que de verdad importa.


    Dos tercios del prompt no eran de biología

    Antes de arrancar, un grupo de expertos escribió un protocolo. Unos 30.000 tokens. Y después —cita textual del informe— "no dimos ninguna guía científica, técnica ni operativa adicional después de iniciar las campañas".

    Ni una corrección. Ni un "prueba mejor por aquí". Los únicos mensajes humanos que entraron fueron instrucciones cortas y no técnicas para reanudar cuando una sesión se caía por infraestructura. El resto de incidencias las detectó y las sorteó el agente solo.

    Lo interesante es cómo se repartía ese documento:

    PROTOCOLO ENTREGADO AL AGENTE - ~16.000 palabras (~30.000 tokens)
    
      Ciencia y herramientas      ################   34,2 %
      Orquestacion y validacion   ################   34,7 %
      Operaciones                 ##############     31,1 %
                                                     -------
      Todo lo que NO es dominio                      65,8 %
    

    Un 34,2 % de ciencia. Y un 65,8 % de cosas que no tienen nada que ver con proteínas: cómo trabajar, cómo decidir, cómo validar, cuándo parar, qué hacer cuando algo se rompe.

    Piensa ahora en tu último system prompt.

    Si el 90 % es "eres un ingeniero senior experto en X con 20 años de experiencia", ya sabes qué te falta. No te falta dominio. Te falta procedimiento.

    Y un detalle remata la idea: probaron el protocolo en campañas piloto y, antes de las corridas finales, revisaron justo las secciones de orquestación y operaciones. No cambiaron el modelo. No añadieron más ciencia. Iteraron sobre el harness.

    Eso es Spec-Driven Development sin llamarlo por su nombre: escribir la especificación antes de dejar que nada se ejecute, y corregir la especificación en lugar de corregir la ejecución. La misma disciplina que desarrollo en el libro de SDD, solo que aquí el precio de improvisar no era un sprint perdido, eran 50.000 dólares de GPU.


    "¿No habíamos quedado en que los mega-prompts son mala idea?"

    Sí. Y este experimento no me desmiente. Me da la razón, aunque de lejos parezca lo contrario.

    Escribí Arquitectura de subagentes vs. mega-prompt defendiendo que un contexto único cargado de responsabilidades se degrada. Aquí hay un documento de 30.000 tokens que funcionó. Toca mirar el detalle.

    Primero: eso no es un prompt, es un protocolo compartido. Y el informe lo dice sin ambigüedad: cada agente de la campaña lo recibe como system prompt. En plural. Uno de los bloques de orquestación explica cómo delegar el trabajo en un equipo de subagentes de dos capas y cómo supervisarlo.

    Especificación larga, ejecución repartida. Que es justo lo que defendía aquel post.

    Segundo, el dato que lo remata. La campaña que atacó las 14 dianas a la vez dentro de una sola sesión se quedó en el 26,7 %. Darle a cada diana su propia sesión de 24 horas subió al 35,1 %: 143 binders frente a 104 sobre las mismas 13 dianas, con una p de 0,003.

    Anthropic avisa de que esa sesión dedicada también tuvo 2,8 veces más cómputo por diana, así que foco y presupuesto no se pueden separar del todo. Pero la dirección es la de siempre: cuantas menos cosas metes en un contexto, mejor sale.

    Un mega-prompt de los malos es sedimento. Instrucciones de dominio acumuladas, ejemplos pegados a mano y reglas contradictorias que alguien fue añadiendo cada vez que algo petó en producción. Esto es un manual de operaciones escrito una vez y repartido entre varios agentes.

    La lección no es "escríbelo todo más largo". Es qué metes dentro de cada contexto y cómo lo estructuras.


    Qué es un agente de IA autónomo (y qué no lo es)

    Un agente de IA autónomo es un sistema que recibe un objetivo y un protocolo escritos por una persona y, a partir de ahí, decide solo qué herramientas usar, en qué orden y qué resultados descartar, sin intervención humana durante la ejecución. No es un modelo más listo: es un modelo con un carril bien escrito.

    En esta campaña la autonomía duró 48 horas. Lo que la hizo posible no fue el modelo, fue el documento que alguien escribió antes de pulsar enter.


    La frontera real de los agentes de IA autónomos

    La palabra "autónomo" ha vendido muchos titulares estos días. Merece un asterisco grande.

    Lo decidió el agente Lo fijó el humano
    Qué investigar de cada diana Qué dianas
    Qué epítopo atacar El protocolo
    Qué herramientas usar y en qué orden Los antígenos del ensayo
    Qué candidatos descartar Los pedidos de síntesis
    Cómo rankear las secuencias finales La lectura de los datos

    Nadie tocó al agente durante la corrida. Cierto. Pero un humano eligió el problema, escribió las reglas, definió el ensayo y leyó los resultados.

    Ese es el patrón que veo funcionar una y otra vez en producción: autonomía total dentro de un carril que alguien dibujó antes, con mucho cuidado.

    El trabajo del ingeniero se ha movido del bucle al carril. Es justo lo que trabajo en el curso Construye con IA: el resultado depende mucho más de lo que escribes antes de lanzar el agente que del modelo que elijas.


    Por qué la química tardó minutos y esto semanas

    En la misma publicación hay un segundo experimento que casi nadie ha citado. Claude Opus 5 procesó ficheros de NMR y LC-MS en 23 y 19 minutos, y calculó una pureza del 96,4 % frente al 96,33 % que había medido el laboratorio.

    Minutos.

    Los binders necesitaron semanas de laboratorio húmedo para saber si el agente había acertado.

    Misma tecnología, misma calidad de razonamiento, velocidades incomparables. ¿La variable? Lo que cuesta comprobar la respuesta.

    Donde verificar es barato y rápido, el agente itera, se corrige y avanza. Donde verificar cuesta semanas y dinero, el agente dispara a ciegas y espera.

    Tu código está en el primer grupo. O debería estarlo. Un test que corre en 200 milisegundos es tu resonancia de plasmón superficial: la señal barata que le dice al agente si va bien o va mal. Por eso insisto tanto con el test harness. Sin él, tu agente vive en el mundo de las proteínas: dispara y reza.

    Y un detalle que deberías tatuarte: las puntuaciones de confianza del propio agente no avisaron de ninguno de los fallos. Los diseños contra MBP puntuaban casi igual que los que sí funcionaron. La confianza del modelo no es una señal de verificación.


    Los fallos, que Anthropic no escondió

    Contra MBP (maltose binding protein), una superficie grande, convexa y polar, sin un bolsillo donde agarrarse: 0 binders de 90 diseños. Cero.

    Contra TNFα, Opus 4.8 sacó 12 binders de 150 diseños y Mythos Preview ninguno de 60. El modelo mejor en la media, a cero en esa diana concreta. Y el informe no lo vende como victoria de un modelo: dice que cada campaña corrió una sola vez y usó generadores distintos, así que no pueden atribuir la diferencia a los modelos.

    Y hubo una diana 16 (GDF-8 mature) excluida del análisis porque el ensayo dio mediciones de mala calidad: la proteína se agregaba consigo misma. Ahí no falló el agente, falló el ensayo.

    Estos tres datos me dan más confianza que los 354 binders. Un informe que solo cuenta aciertos es marketing.

    Tampoco esconden el coste: 50.000 dólares de GPU en la corrida de 48 horas contra todas las dianas a la vez (hasta 12.500 horas de NVIDIA H100) y 10.000 por cada sesión de 24 horas contra una sola. La configuración más precisa fue también la más cara por diana.


    Lo que esto no es

    No es peer review. Es un estudio autopublicado por Anthropic sobre sus propios modelos. El trabajo de laboratorio lo hicieron terceros, que es lo que lo salva de ser una nota de prensa, pero nadie externo ha revisado la metodología.

    Y hay una línea que Anthropic no ha cruzado: el diseño de proteínas y otras capacidades de biología de uso dual siguen sin acceso general en Claude Fable 5, su modelo más capaz, por riesgo de armas biológicas. Los modelos clase Opus mantienen acceso limitado.

    La empresa que publica el estudio ha decidido no ofrecer esa capacidad en su mejor modelo. Ese freno también es un resultado del experimento.


    Qué haces el lunes con tus agentes de IA autónomos

    Abre el system prompt del agente que tengas en producción ahora mismo. Son quince minutos:

    1. Etiqueta cada bloque con una de estas tres palabras: dominio, orquestación, operaciones.
    2. Saca porcentajes. Divide las líneas de cada etiqueta entre el total.
    3. Compara con el 34/35/31 de Anthropic. Si te sale algo parecido a 90/5/5, ya sabes qué te falta: no te falta dominio, te falta procedimiento.
    4. Escribe lo que falta: el procedimiento paso a paso, los criterios para descartar, las señales de que va por buen camino, cuándo debe pararse y a quién avisa cuando no sabe seguir.

    Ese fue el 65,8 % del documento que le dieron a Claude. Y es la parte que casi nadie escribe, porque es aburrida y no luce en un tuit.

    Si te llevas una sola frase de todo esto, que sea esta: si tu agente no tiene una forma barata de saber si acertó, no tienes un agente. Tienes un generador de texto con acceso a tu terminal.

    En Dominicode Labs desmontamos este tipo de arquitecturas con proyectos reales. Pero el ejercicio de los quince minutos hazlo hoy.


    Preguntas frecuentes

    ¿Claude ha creado un fármaco?

    No. Diseñó binders: proteínas pequeñas que se pegan a una diana. Es el paso cero de muchos programas farmacológicos, y por delante queda todo el recorrido preclínico y regulatorio, que se mide en años.

    ¿El estudio está revisado por pares?

    No. Es un estudio autopublicado por Anthropic sobre sus propios modelos, sin peer review. Lo que sí es externo es la validación: Adaptyv Bio y Twist Bioscience sintetizaron las proteínas y midieron afinidad por resonancia de plasmón superficial. Nadie de fuera ha revisado la metodología, pero los resultados no salen de una simulación.

    ¿Qué significa una tasa de acierto del 26,8 %?

    Que de 1.320 diseños generados, 354 se confirmaron como binders en el laboratorio. El baseline actual de la industria está entre el 10 % y el 15 %. Conviene no mezclar brazos del experimento: el 26,8 % es el dato global y, por configuración, va del 22,6 % al 35,1 %.

    ¿Por qué acierta más si trabaja contra una sola diana?

    Porque la sesión dedicada de 24 horas concentra todo el presupuesto de razonamiento y cómputo en un único problema: sube del 26,7 % al 35,1 %. También sale más cara por diana, y Anthropic avisa de que no puede separar el efecto del foco del de un presupuesto 2,8 veces mayor por diana. Es tu mismo dilema entre lanzar un agente contra quince tickets a la vez o dedicarle una sesión completa al que importa.

    ¿Puedo usar Claude para diseñar proteínas?

    No con acceso general. El diseño de proteínas y otras capacidades de biología de uso dual siguen restringidas en Claude Fable 5, el modelo más capaz, por riesgo de armas biológicas. Los modelos clase Opus mantienen acceso limitado.

    ¿Qué me llevo de esto para mis agentes de IA autónomos si no toco biología?

    El reparto del protocolo: 34,2 % dominio, 34,7 % orquestación y validación, 31,1 % operaciones. Es la plantilla que yo usaría para escribir el contexto de un agente. Y la consecuencia práctica: iteraron sobre el harness, no sobre el modelo.


    Fuentes


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Context Drift: por qué tu agente se vuelve tonto en la iteración 15

    Context Drift: por qué tu agente se vuelve tonto en la iteración 15

    El agente empieza como un ingeniero senior.

    Analiza el problema con precisión quirúrgica. Propone una arquitectura impecable. Modifica los primeros módulos con código limpio y bien tipado.

    Pero llega la iteración 14.

    De repente olvida la regla que le pusiste en el primer mensaje. Inventa funciones auxiliares que ya existen. Borra código que él mismo escribió hace cinco minutos. Y para resolver un fallo de compilación, entra en pánico y sugiere reescribir la mitad del proyecto.

    No se ha vuelto tonto de golpe. Está sufriendo Context Drift: el mismo modelo, con las mismas instrucciones, deja de prestarles atención porque el historial las ha sepultado bajo miles de tokens de basura.

    Y aquí va lo importante: esto no se arregla con un modelo mejor ni con una ventana más grande. Se arregla en el bucle.


    Por qué el fallo aparece en la iteración 15 y no en la 3

    Que un modelo reparte su atención de forma desigual —mucha al principio del prompt, mucha al final, poca en el medio— ya lo conoces. Y si no, lo tienes explicado con detalle en Context Engineering: cómo estructurar la memoria de tus agentes de IA. Este post no va de eso.

    Va de lo que pasa cuando ese efecto se combina con un bucle que se ejecuta solo.

    En una conversación normal tú controlas lo que entra. En un agentic loop no: cada iteración inyecta la salida de una herramienta sin que nadie la lea. Y esas salidas son enormes. Un npm test, un git diff, un grep sobre un monorepo o un schema de base de datos rondan fácil los 4.000 tokens cada uno.

    Supón un agente con 2.000 tokens de system prompt y 3.000 de especificación. Eso es lo que de verdad tiene que respetar: 5.000 tokens fijos de instrucciones.

    Iteración Salidas de herramientas acumuladas Total en ventana Peso de tus instrucciones
    3 12.000 tokens 17.000 29 %
    8 32.000 tokens 37.000 14 %
    15 60.000 tokens 65.000 8 %

    Tus reglas no han desaparecido: siguen ahí, íntegras, en el token 1. Lo que ha cambiado es que ahora compiten contra sesenta mil tokens de ruido reciente que el modelo también considera relevante.

    [System prompt + spec: 5.000 tokens] ──► Alta atención
    
    [Log 1: salida de grep]        ──┐
    [Log 2: test runner completo]    ├── Zona de dilución
    [Log 3: schema SQL entero]      ──┘
    
    [Instrucción final] ──────────────────► Alta atención
    

    El 92 % de lo que el modelo está leyendo en la iteración 15 es material que ya no sirve para nada. Ahí es donde empieza a alucinar.


    Las 3 técnicas para eliminar el Context Drift

    1. Poda de salidas de herramientas

    Nunca devuelvas al contexto la salida completa de un comando.

    Si un test runner ejecutó 120 tests y falló uno, el agente necesita saber tres cosas:

    • El estado general (FAILED).
    • El nombre del test que falló.
    • Las 10 líneas relevantes del stack trace.

    Los otros 3.000 tokens de tests en verde son ruido puro: encarecen la factura y compiten por la atención del modelo. Poda en el tool handler, antes de que ese texto llegue al historial. No le pidas al modelo que lo ignore, porque no puede.

    Si quieres ver cuánto te está costando esto de verdad, en medir el consumo de tokens de un agente desgloso en qué se te van realmente.

    2. El patrón scratchpad: memoria en disco, no en el chat

    El peor sitio para guardar el estado de un proyecto es el historial del chat. Es volátil, crece sin control y no puedes consultarlo sin arrastrarlo entero.

    El patrón robusto es obligar al agente a mantener un archivo en disco —scratch/state.md— como única fuente de la verdad. En cada paso clave lo actualiza:

    • Tareas completadas.
    • Tareas pendientes.
    • Decisiones técnicas tomadas y descartadas.

    Cuando el contexto conversacional se ensucia, tiras la conversación entera y arrancas otra pidiéndole que lea ese archivo. El agente recupera todo su foco por unos cientos de tokens en vez de sesenta mil.

    Esta es la versión mínima y sin dependencias del asunto. Si necesitas memoria entre sesiones, perfiles de usuario o recuperación semántica, el terreno está mapeado en implementación de memoria en agentes de IA. Pero empieza por el archivo de texto: resuelve más de lo que parece.

    3. Compactación rodante del historial

    En lugar de acumular treinta mensajes, sustituye el tramo intermedio por un resumen sintético y conserva intactos el system prompt y los últimos turnos.

    Suena trivial, y tiene dos trampas que revientan la petición en producción: cortar un mensaje de resultado de herramienta separándolo de la llamada que lo produjo, y romper la alternancia de roles que exigen algunas APIs.

    export type Role = "system" | "user" | "assistant" | "tool";
    
    export interface AgentMessage {
      role: Role;
      content: string;
    }
    
    /**
     * Sustituye el tramo intermedio del historial por un resumen.
     * Preserva el system prompt y los ultimos `keepLastTurns` mensajes.
     */
    export function compactContext(
      messages: AgentMessage[],
      keepLastTurns = 6,
    ): AgentMessage[] {
      if (keepLastTurns < 1) {
        throw new RangeError("keepLastTurns debe ser >= 1");
      }
    
      const hasSystem = messages[0]?.role === "system";
      const head = hasSystem ? messages.slice(0, 1) : [];
      const body = hasSystem ? messages.slice(1) : messages;
    
      if (body.length <= keepLastTurns) return messages;
    
      // Un mensaje `tool` sin la llamada que lo genero es un 400 en la API.
      // Retrocedemos el corte hasta que deje de apuntar a un resultado huerfano.
      let cut = body.length - keepLastTurns;
      while (cut > 0 && body[cut].role === "tool") cut--;
    
      const middle = body.slice(0, cut);
      if (middle.length === 0) return messages;
    
      const summary: AgentMessage = {
        role: "user",
        content:
          `[RESUMEN DE PASOS ANTERIORES] Se ejecutaron ${middle.length} operaciones ` +
          `de inspeccion y validacion. El estado real esta en scratch/state.md; ` +
          `los mensajes siguientes son la fase activa.`,
      };
    
      return [...head, summary, ...body.slice(cut)];
    }
    

    Dos notas para llevarlo a producción:

    • Si tu proveedor exige alternancia estricta de roles, comprueba que body[cut] no sea otro mensaje de usuario. Si lo es, fusiona el resumen con él en vez de insertarlo aparte.
    • El resumen genérico es un punto de partida, no el final. En cuanto puedas, genera ese texto con una llamada barata a un modelo pequeño que resuma los pasos reales: qué se intentó, qué falló y por qué. Un resumen que dice "se ejecutaron 12 operaciones" evita la saturación, pero no conserva el aprendizaje.

    El coste oculto de compactar (y por qué no debes hacerlo cada turno)

    Aquí es donde mucha gente se pega el tiro en el pie.

    La caché de prompts de los proveedores funciona por prefijo: se reutiliza el principio del prompt mientras siga siendo idéntico. Cuando compactas, reescribes justo esa parte del historial, así que la petición siguiente se paga entera a precio completo, sin descuento de caché.

    Si compactas cada turno, pierdes más de lo que ahorras: tendrás menos tokens, pero pagados todos a tarifa plena y con la caché reconstruyéndose sin parar.

    La regla práctica que uso:

    1. Poda siempre, en cada llamada a herramienta. Eso no toca el prefijo cacheado, porque afecta a lo que todavía no ha entrado.
    2. Compacta por lotes, cuando cruzas un umbral (por ejemplo, el 60 % de la ventana) o cuando termina una fase completa de trabajo.
    3. Nunca compactes a mitad de una subtarea. Espera al cambio de fase: es cuando el resumen sale bien y cuando el corte de caché duele menos.

    Por qué Spec-Driven Development resuelve el 80 % del problema

    El Context Drift no es solo un problema de memoria; es un problema de ambigüedad inicial.

    Si no le das al agente una especificación cerrada en un spec.md, tiene que deducir qué hacer sobre la marcha. Y deducir cuesta tokens: exploración, preguntas, archivos abiertos por si acaso, callejones sin salida. Todo eso acaba en el historial y satura la ventana con material que ni siquiera hacía falta.

    Con Spec-Driven Development el alcance está delimitado desde el primer segundo: el agente sabe qué archivos puede tocar y qué queda fuera. Menos exploración es, literalmente, menos drift.

    Es el método que enseño en profundidad en el curso Construye con IA: de la idea al producto con Claude Code y que tienes explicado paso a paso en el libro de Spec-Driven Development.

    Y si además validas con esquemas de Zod todo lo que entra y sale de tus herramientas, cortas el otro vector de degradación: datos deformados que el agente arrastra durante veinte iteraciones sin que nadie los detecte. Cómo blindar esos contratos lo tienes en el curso de Zod para TypeScript.


    Lo que puedes aplicar hoy

    1. Poda los logs. Limita la salida de cada herramienta y resume los errores antes de inyectarlos.
    2. Externaliza la memoria. Guarda el progreso en un archivo en disco en lugar de confiar en el historial.
    3. Compacta por fases, no por turnos. Y mira el impacto en la caché antes de darlo por bueno.

    En Dominicode Labs diseñamos arquitecturas de agentes que ejecutan tareas de horas sin desviarse del objetivo.

    El punto fijo al que volver cuando el contexto ya derivó son tus propias notas, siempre que estén escritas para recuperarse sueltas: Zettelkasten para developers.

    Tener una ventana de contexto gigante no es una excusa para ser descuidado con lo que metes dentro. La diferencia entre un prototipo frágil y un sistema agéntico de producción no está en el modelo: está en qué le dejas leer en la iteración 15.


    Preguntas frecuentes

    ¿Cada cuántas iteraciones conviene compactar el historial de un agente?

    No lo ates a un número de iteraciones, átalo a un umbral de ocupación de la ventana y a los cambios de fase. Compactar al cruzar el 60 % de la ventana, o al terminar una subtarea completa, funciona mejor que hacerlo cada N turnos: el resumen sale más limpio y no partes el trabajo por la mitad.

    ¿Compactar el contexto invalida la caché de prompts y encarece la factura?

    Sí. La caché funciona por prefijo idéntico, así que al reescribir el tramo intermedio del historial la siguiente petición se paga completa. Por eso conviene compactar por lotes en lugar de cada turno: podar la salida de las herramientas antes de que entren al historial reduce tokens sin tocar el prefijo ya cacheado.

    ¿Qué diferencia hay entre compactar el historial y reiniciar la conversación?

    Compactar conserva el hilo conversacional y sustituye lo antiguo por un resumen; reiniciar tira todo y arranca de cero leyendo el archivo de estado. Compactar es más fácil de aplicar en mitad de una tarea. Reiniciar limpia mejor, pero solo es viable si el estado real vive en disco y no en el chat.

    ¿Cómo distingo un context drift de un fallo del modelo?

    Por la reproducibilidad. Coge la petición que falló, arranca una conversación nueva con el system prompt, el estado actual y esa única instrucción, y vuelve a lanzarla. Si con el contexto limpio sale bien, no era el modelo: era el historial. Si falla igual, el problema está en tus instrucciones o en la propia tarea.

    ¿Sirve de algo esto si uso un modelo con ventana de un millón de tokens?

    Sirve más, no menos. La ventana grande solo amplía cuánta basura cabe antes de que la petición reviente, pero la atención se sigue diluyendo y el coste por llamada crece con todo lo que arrastras. Una ventana grande es margen de maniobra, no un sustituto de la gestión del contexto.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.