Category: Blog

Your blog category

  • LangGraph TypeScript: cuándo un grafo gana al while loop

    LangGraph TypeScript: cuándo un grafo gana al while loop

    Tenía un agente que revisaba pull requests. Cincuenta líneas de TypeScript, un while, tres tools. Funcionaba.

    Hasta que un PR tocó el módulo de autenticación y el agente hizo lo correcto: parar y pedir aprobación humana. El problema es que "parar" significaba dejar un proceso de Node vivo esperando un webhook que llegó dieciocho horas después. El proceso ya no existía. El contexto tampoco.

    Reinicié. Volvió a analizar el PR desde cero, volvió a gastar tokens, volvió a pedir aprobación. Mi loop no tenía un bug: tenía un límite arquitectónico.

    LangGraph TypeScript existe para ese límite exacto. Y lo adoptas sin comprar la casa entera para usar el garaje.

    LangGraph es la librería de orquestación de agentes de LangChain, disponible para TypeScript y Python, que modela un agente como un grafo de estados: los nodos son funciones que reciben y devuelven estado, las aristas deciden qué nodo va después, y un checkpointer persiste el estado tras cada paso. Ese checkpointer es lo que permite pausar una ejecución hoy y reanudarla dentro de tres días, en otra máquina.

    El loop explícito resuelve la mayoría de los agentes

    Empecemos por lo incómodo: en los proyectos que he tocado, la gran mayoría de los agentes no necesitan un framework de orquestación. Necesitan esto.

    type ToolCall = { id: string; name: string; args: unknown };
    type Msg =
      | { role: "user" | "assistant" | "system"; content: string }
      | { role: "assistant"; content: string; toolCalls: ToolCall[] }
      | { role: "tool"; toolCallId: string; content: string };
    
    export async function agente(prompt: string): Promise<string> {
      const messages: Msg[] = [{ role: "user", content: prompt }];
      let turno = 0;
    
      while (turno++ < 10) {
        const res = await llm.complete(messages);
    
        if (!res.toolCalls?.length) return res.content;
    
        messages.push({
          role: "assistant",
          content: res.content,
          toolCalls: res.toolCalls,
        });
    
        for (const call of res.toolCalls) {
          const tool = tools[call.name];
          // El nombre de la tool lo elige el modelo. Si alucina uno, se lo devuelves
          // como error para que se corrija, en vez de reventar a mitad de ejecución.
          const salida = tool
            ? await tool(call.args)
            : `Error: la herramienta "${call.name}" no existe.`;
          messages.push({ role: "tool", toolCallId: call.id, content: salida });
        }
      }
    
      throw new Error("Límite de turnos alcanzado");
    }
    

    Eso es un agente. Lo depuras con console.log, lo entiendes entero en treinta segundos y no tiene una capa de orquestación que pueda romperte en la siguiente minor.

    Yo defiendo este loop, y lo he defendido por escrito: en multi-agente sin orquestador explico por qué la mayoría de los sistemas "multi-agente" son un for con buen marketing. Sigo pensando lo mismo.

    El loop no falla por complejidad. Falla por duración.

    Cuándo usar LangGraph en vez de un while loop

    Usa LangGraph cuando tu agente cumpla al menos uno de estos tres síntomas. Si no cumple ninguno, quédate con el while. El punto de inflexión no es "mi agente hace muchas cosas": es uno de estos tres, y basta con uno.

    1. El estado tiene que sobrevivir al proceso. Si tu agente vive más que un request HTTP —minutos, horas, días— el array messages en memoria es una bomba de relojería. Un deploy, un reinicio, un pod que se recicla, y perdiste la ejecución.
    2. La ramificación es real, no cosmética. Un if dentro del loop está bien. Pero cuando el camino A y el camino B tienen pasos distintos, reintentos distintos y puntos de salida distintos, el loop se convierte en un árbol de condicionales que nadie quiere tocar.
    3. Un humano tiene que decidir en mitad de la ejecución. No al principio ni al final. En mitad. Y puede tardar un día en contestar.
    while loop explícito LangGraph (StateGraph)
    Estado entre pasos Array en memoria Campos tipados con reducers
    Sobrevive a un reinicio No Sí, con checkpointer
    Pausar y reanudar Lo escribes tú interrupt() + Command({ resume })
    Ramificación if anidados addConditionalEdges tipado
    Depuración console.log Inspección del grafo y del checkpoint
    Dependencias Ninguna @langchain/langgraph + @langchain/core
    Coste de entrada Cero Una tarde por tramo migrado

    Si no tienes ninguno de los tres, cierra esta pestaña y quédate con tu loop. Hablo en serio. Ese es justamente el argumento que defiendo en el stack de IA agéntica que uso: la capa de orquestación es la última que deberías añadir, no la primera.

    Un apunte de vocabulario, porque genera confusión real: aquí "grafo" significa grafo de orquestación —qué nodo se ejecuta después de cuál—. No tiene nada que ver con el grafo de recuperación del que hablé en qué es graph engineering, que va de qué código llega al contexto del modelo. Misma palabra, dos capas distintas del sistema.

    El estado primero, el grafo después

    En LangGraph el estado no es un detalle de implementación: es el contrato. Y en la v1 —@langchain/langgraph 1.4.10, agosto de 2026— se declara con StateSchema, aceptando esquemas de Zod campo a campo.

    import {
      StateSchema,
      MessagesValue,
      ReducedValue,
    } from "@langchain/langgraph";
    import { z } from "zod";
    
    const RevisionState = new StateSchema({
      messages: MessagesValue,
      pr: z.string(),
      riesgo: z.enum(["bajo", "alto"]).default("bajo"),
      hallazgos: new ReducedValue(z.array(z.string()).default(() => []), {
        reducer: (actual: string[], nuevo: string[]) => actual.concat(nuevo),
      }),
      aprobado: z.boolean().default(false),
    });
    
    type Revision = typeof RevisionState.State;
    

    Fíjate en ReducedValue. Esa es la pieza que no tiene equivalente limpio en el loop: define cómo se combinan las actualizaciones de un campo. Los nodos devuelven trozos de estado y el reducer decide si se sobrescriben o se acumulan. Sin eso, dos nodos que escriben en hallazgos se pisan.

    Y sí, es Zod de verdad: .default(), .enum(), refinamientos. El estado del agente es un contrato de datos como cualquier otro, y aquí es donde se nota tenerlos bien tipados — es el mismo músculo que entreno en el curso de Zod para TypeScript.

    Verás mucho tutorial con Annotation.Root({ ... }). Sigue funcionando y sigue compilando, pero es la sintaxis anterior. Si empiezas hoy, empieza con StateSchema.

    Nodos, aristas y la decisión que el loop no sabe expresar

    Un nodo es una función que recibe el estado y devuelve un trozo de estado. Nada más.

    import {
      StateGraph, START, END, MemorySaver, interrupt, Command,
    } from "@langchain/langgraph";
    
    async function analizar(state: Revision) {
      const tocaAuth = state.pr.includes("auth");
      return {
        hallazgos: ["Cobertura de tests: 62%"],
        riesgo: tocaAuth ? ("alto" as const) : ("bajo" as const),
      };
    }
    
    async function aprobarAuto(_state: Revision) {
      return { aprobado: true };
    }
    
    function enrutar(state: Revision): "aprobarAuto" | "revisionHumana" {
      return state.riesgo === "alto" ? "revisionHumana" : "aprobarAuto";
    }
    
    const grafo = new StateGraph(RevisionState)
      .addNode("analizar", analizar)
      .addNode("aprobarAuto", aprobarAuto)
      .addNode("revisionHumana", revisionHumana)
      .addEdge(START, "analizar")
      .addConditionalEdges("analizar", enrutar, ["aprobarAuto", "revisionHumana"])
      .addEdge("aprobarAuto", END)
      .addEdge("revisionHumana", END)
      .compile({ checkpointer: new MemorySaver() });
    

    addConditionalEdges recibe el nodo origen, la función que decide y la lista de destinos posibles. Esa lista no es decorativa: es lo que hace que el enrutado sea tipado y que el grafo sea inspeccionable antes de ejecutarlo.

    Y ahí está compile({ checkpointer }). Esa línea es la que justifica todo lo demás. Sin checkpointer tienes un runner de funciones con sintaxis rara. Con checkpointer tienes una máquina de estados que se puede pausar y reanudar.

    MemorySaver es para desarrollo —vive en RAM y muere con el proceso—. Para producción usa los checkpointers persistentes, que van en paquetes aparte: @langchain/langgraph-checkpoint-postgres (1.0.4) o @langchain/langgraph-checkpoint-sqlite (1.0.3).

    Human-in-the-loop en LangGraph: la funcionalidad que justifica el cambio

    El human-in-the-loop en LangGraph se implementa con interrupt(): el nodo lanza la pausa, el grafo guarda el estado en el checkpointer y la ejecución termina. Cuando llega la respuesta humana, se reanuda con Command({ resume }) sobre el mismo thread_id.

    Aquí es donde mi PR de las dieciocho horas deja de ser un problema.

    type PeticionRevision = {
      pr: string;
      hallazgos: string[];
      pregunta: string;
    };
    
    async function revisionHumana(state: Revision) {
      const decision = interrupt<PeticionRevision, { aprobado: boolean }>({
        pr: state.pr,
        hallazgos: state.hallazgos,
        pregunta: "Este PR toca auth. ¿Lo apruebas?",
      });
    
      return { aprobado: decision.aprobado };
    }
    

    Cuando la ejecución llega a interrupt(), el grafo persiste el estado exacto y para. No bloquea un proceso: termina. El estado queda guardado bajo un thread_id.

    const config = { configurable: { thread_id: "pr-482" } };
    
    await grafo.invoke({ pr: "feat/auth-refresh-token" }, config);
    
    const pausa = await grafo.getState(config);
    console.log(pausa.next);                          // [ 'revisionHumana' ]
    console.log(pausa.tasks[0]?.interrupts[0]?.value); // el payload de la pregunta
    
    // Horas o días después, otro proceso, otro deploy:
    const final = await grafo.invoke(
      new Command({ resume: { aprobado: true } }),
      config
    );
    console.log(final.aprobado); // true
    

    Léelo otra vez. El segundo invoke puede ocurrir en otra máquina, la semana siguiente, después de tres despliegues. El grafo continúa donde estaba: analizar no se vuelve a ejecutar y no gastas otra vez esos tokens.

    Eso, en el loop, no lo montas en un rato. Escribes tu propio serializador de estado, tu propio registro de "en qué paso iba" y tu propia lógica de reanudación. Es decir: escribes un checkpointer peor.

    Tres avisos que cuestan tiempo:

    1. El nodo que contiene el interrupt() sí se re-ejecuta entero al reanudar. El grafo no repite los nodos anteriores, pero este arranca otra vez desde su primera línea. Si pones un INSERT, un webhook o un cobro antes de la llamada, ocurre dos veces. Todo efecto secundario va después del interrupt(), nunca antes.
    2. No envuelvas interrupt() en un try/catch: señaliza la pausa con una excepción y te la comerías.
    3. Lo que le pasas debe ser serializable a JSON.

    Cuándo NO montar el grafo de estados en LangGraph

    No lo montes porque el proyecto "va a crecer". Móntalo cuando tengas uno de los tres síntomas delante.

    Y no confundas esto con adoptar el ecosistema entero. Ya dejé claro mi veredicto sobre la librería base en el stack de IA agéntica que uso: demasiada abstracción sobre abstracciones. LangGraph es otra cosa. Es una máquina de estados con persistencia, y puedes usarla sin tocar el resto.

    Un detalle actual que evita un error frecuente: el prebuilt createReactAgent de @langchain/langgraph/prebuilt está marcado como deprecado. Se movió al paquete langchain como createAgent. Si sigues un tutorial de hace un año, vas a copiar un import obsoleto.

    Y antes de dibujar un solo nodo, escribe qué estados existen y qué transiciones son legales. Un grafo mal pensado es peor que un loop, porque además parece serio. Esa disciplina de definir el contrato antes de escribir la implementación es la misma que defiendo en el libro de Spec-Driven Development, y aquí paga doble.

    Qué hacer con esto hoy

    Abre tu agente y busca una sola cosa: un punto donde la ejecución tenga que sobrevivir a un reinicio. Una aprobación, una espera larga, un proceso por lotes que tarda horas.

    Si no lo encuentras, tu loop está bien. Cierra el editor.

    Si lo encuentras, no reescribas el agente entero. Extrae solo ese tramo a un StateGraph con checkpointer, deja el resto como está y quédate con las tools intactas. Migrar un tramo cuesta una tarde. Migrar por moda cuesta un trimestre.

    Si quieres ver este tipo de decisiones tomadas en proyectos reales —cuándo meter un framework y cuándo no—, es justo lo que trabajo en Construye con IA, y en Dominicode Labs montamos estos grafos con el código completo delante.

    Preguntas frecuentes

    ¿Cuándo usar LangGraph en vez de un while loop?

    Cuando la ejecución tenga que sobrevivir al proceso, cuando la ramificación tenga pasos y salidas realmente distintas, o cuando un humano deba decidir en mitad del flujo. Con uno solo de esos tres basta. Si tu agente empieza y termina dentro del mismo request, el while explícito es mejor opción: se depura con console.log y no tiene una capa de orquestación que se rompa en la siguiente minor. LangGraph no gana por complejidad, gana por duración.

    ¿Necesito un checkpointer para usar interrupt() en LangGraph?

    Sí, y no es opcional. interrupt() funciona persistiendo el estado del grafo y terminando la ejecución. Sin un checkpointer en compile() no hay dónde guardar ese estado, así que no hay nada que reanudar. En desarrollo te vale MemorySaver; en producción necesitas uno con almacenamiento real, o perderás las pausas en cada despliegue.

    ¿StateSchema con Zod sustituye a Annotation.Root en LangGraph TypeScript?

    Es la forma actual de declarar el estado y la que deberías usar en código nuevo. Annotation.Root sigue exportándose y sigue compilando, así que no tienes que migrar nada con prisa. La diferencia práctica es que con StateSchema reutilizas esquemas de Zod que probablemente ya tienes en el proyecto, con sus default() y sus validaciones, en lugar de aprender una segunda sintaxis solo para el estado del grafo.

    ¿Qué checkpointer uso en producción con LangGraph JS?

    MemorySaver viene en el paquete principal pero guarda en RAM: sirve para tests y ejemplos, no para producción. Los checkpointers persistentes van en paquetes aparte, @langchain/langgraph-checkpoint-postgres y @langchain/langgraph-checkpoint-sqlite. Si ya tienes Postgres en el stack, esa es la respuesta fácil, porque el estado del agente pasa a ser una tabla más que respaldas y auditas como cualquier otra.

    ¿Puedo migrar mi while loop a LangGraph sin reescribir las tools?

    Sí, y es la vía que recomiendo. Las tools son funciones con un esquema de entrada; no les afecta quién las llama. Lo que cambia es el orquestador: el bucle pasa a ser nodos y aristas, y el array de mensajes pasa a ser un campo del estado. Puedes migrar un solo tramo del flujo —el que necesita pausarse— y dejar el resto del agente exactamente como está.

    ¿Necesito LangChain para usar LangGraph en TypeScript?

    No. @langchain/langgraph declara @langchain/core como peer dependency —de ahí salen los tipos de mensajes y modelos— pero no requiere el paquete langchain ni sus cadenas y abstracciones: una instalación limpia trae @langchain/core, langgraph-checkpoint y langgraph-sdk, y ahí se acaba. Puedes montar un StateGraph llamando dentro de tus nodos al SDK del proveedor que ya uses. Un nodo es una función asíncrona: lo que hagas dentro es cosa tuya.


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

  • Arquitectura de subagentes IA: por qué falla el mega-prompt

    Arquitectura de subagentes IA: por qué falla el mega-prompt

    El año pasado construí un agente que pretendía ser el ingeniero de software definitivo. Fue mi primer intento serio de arquitectura de subagentes IA — y la forma en la que fallé me enseñó por qué un solo agente nunca debería hacerlo todo.

    Su System Prompt ocupaba casi 3.000 palabras. Le instruí para ser arquitecto de software, experto en seguridad, programador senior de TypeScript, tester meticuloso y redactor técnico. Además, le configuré 28 herramientas distintas: leer archivos, escribir código, ejecutar comandos bash, consultar 3 bases de datos y hacer peticiones HTTP.

    Al principio parecía impresionante. En la primera tarea sencilla respondió bien.

    Pero a la cuarta tarea compleja, el sistema colapsó por completo:

    • Confundía las reglas de testing con las de documentación.
    • Para cambiar una sola línea de CSS llamaba a herramientas de base de datos.
    • Consumía 100.000 tokens en cada paso solo leyendo la lista gigante de herramientas disponibles.

    Ese día entendí una verdad fundamental del desarrollo con IA: en lugar de construir un único agente que intente hacerlo todo, necesitas un equipo de subagentes con tareas concretas y contextos aislados.


    Por qué los Mega-Prompts fallan en la práctica

    No es una limitación de que el modelo "sea tonto"; es el resultado de cómo funcionan los Transformers:

    1. Degradación por ruido de herramientas (Tool Noise)

    Cuantas más herramientas (tools) le expones a un modelo en un único turno, mayor es la probabilidad de que elija la herramienta equivocada o invente parámetros incompatibles.

    Con pocas herramientas bien definidas, la precisión de selección se mantiene alta. Cuando la lista crece a decenas de herramientas mezclando responsabilidades distintas (leer archivos, escribir código, consultar bases de datos, hacer peticiones HTTP), esa precisión cae de forma abrupta — es el mismo problema que un desarrollador tendría memorizando 30 comandos de CLI casi idénticos.

    2. Dispersión de atención (Attention Drift)

    Si tu prompt contiene 50 reglas diferentes ("no uses any", "usa el prefijo on en eventos", "escribe tests en Vitest", "documenta en JSDoc"), el mecanismo de atención del LLM diluye la importancia de cada una.

    Cuando el contexto se llena de logs y código, las reglas del medio del prompt simplemente dejan de tener peso estadístico.

    3. Contaminación de contexto

    Si un agente pasa 20 minutos investigando archivos y leyendo logs de error, esos 80.000 tokens de "ruido exploratorio" se quedan atascados en la memoria para siempre.

    Cuando luego le pides que escriba la solución final, su respuesta estará condicionada por todo ese texto basura previo.


    La solución: Arquitectura de Subagentes en 3 capas

    La solución no es hacer prompts más largos ni añadir más mayúsculas al texto. Es aplicar el principio de Responsabilidad Única que llevamos décadas usando en ingeniería de software.

    En Dominicode organizamos el trabajo en tres tipos de subagentes especializados:

                      ┌───────────────────────┐
                      │    AGENTE ORQUESTADOR │
                      │  (Planifica y delega) │
                      └──────────┬────────────┘
                                 │
                ┌────────────────┼────────────────┐
                ▼                ▼                ▼
         ┌─────────────┐  ┌─────────────┐  ┌─────────────┐
         │ RESEARCHER  │  │ IMPLEMENTER │  │  REVIEWER   │
         │ (Read-only) │  │(Write + TDD)│  │ (Auditoría) │
         └─────────────┘  └─────────────┘  └─────────────┘
    

    1. El Investigador (Researcher — Solo Lectura)

    • Herramientas permitidas: Búsqueda en archivos, lectura de código, búsqueda web.
    • Herramientas prohibidas: Edición de archivos, ejecución de comandos destructivos.
    • Misión: Explora el codebase, localiza las funciones relevantes y devuelve un resumen limpio de 50 líneas con los hallazgos. Su memoria sucia de 60.000 tokens se descarta al terminar; solo el resumen pasa al siguiente agente.

    2. El Implementador (Implementer — Escritura + TDD)

    • Herramientas permitidas: Edición precisa de archivos, ejecución de tests.
    • Misión: Recibe el resumen del Researcher y la especificación técnica. Su único objetivo es crear el test, escribir el código mínimo para pasarlo y verificar que compila. No pierde tiempo buscando archivos porque el Researcher ya le dio las rutas exactas.

    3. El Revisor (Reviewer — Auditor de Calidad)

    • Herramientas permitidas: Lectura de diffs de git, linter.
    • Misión: Revisa los cambios antes de hacer commit. Evalúa si se respetan los estándares de tipado, si hay regresiones de rendimiento y si se cumplió la especificación original.

    Cómo se comunican los subagentes sin saturar tokens: El patrón Artifact

    El error habitual al montar sistemas multiagente es hacer que el Agente A le hable al Agente B en un chat conversacional interminable ("Hola Agente B, ¿cómo estás? He encontrado esto…"). Eso gasta tokens en cortesías inútiles.

    El patrón más eficiente es la comunicación mediante artefactos en disco:

    1. El Researcher escribe sus hallazgos en un archivo local: scratch/research_findings.md.
    2. El Orchestrator lee ese archivo y lanza al Implementer pasándole únicamente la ruta del archivo.
    3. El Implementer ejecuta los cambios y escribe el resumen de modificaciones en scratch/changes_summary.md.

    Cada subagente arranca con una ventana de contexto limpia, consumiendo solo los tokens necesarios para su tarea concreta.

    Este flujo de trabajo desacoplado es el que explicamos a fondo en el curso Construye con IA: de la idea al producto con Claude Code, donde mostramos cómo estructurar entornos reales multiagente que no se degradan con el tiempo.


    Ejemplo práctico: Definiendo un subagente en TypeScript

    Si estás creando tus propios agentes con código propio, no necesitas frameworks gigantes. Puedes instanciar agentes especializados restringiendo las tools y el system prompt:

    import { generateText, tool, stepCountIs } from "ai";
    import { anthropic } from "@ai-sdk/anthropic";
    import { z } from "zod";
    
    // Agente especializado solo en investigación.
    // Modelo de ejemplo: sustituye por la versión vigente de Claude en tu build.
    export async function spawnResearcherAgent(query: string, codebaseDir: string) {
      const result = await generateText({
        model: anthropic("claude-sonnet-5"),
        system: `Eres un agente de investigación técnica de solo lectura.
        Tu objetivo es explorar el código en "${codebaseDir}", localizar las funciones clave
        y responder con un informe conciso. NUNCA propongas escribir código ni modificar archivos.`,
        prompt: `Investiga: ${query}`,
        tools: {
          searchFiles: tool({
            description: "Busca patrones en el repositorio",
            inputSchema: z.object({ pattern: z.string() }),
            execute: async ({ pattern }) => {
              // Lógica de búsqueda grep/ripgrep
              return { matches: ["src/auth/service.ts:45", "src/auth/jwt.ts:12"] };
            },
          }),
          readFile: tool({
            description: "Lee un archivo específico",
            inputSchema: z.object({ path: z.string() }),
            execute: async ({ path }) => Bun.file(path).text(),
          }),
        },
        stopWhen: stepCountIs(8),
      });
    
      return result.text; // Salida limpia lista para pasar al Implementador
    }
    

    Al limitar el rol a lectura y 2 herramientas, la tasa de error baja notablemente y el coste por ejecución se reduce al mínimo.


    Qué hacer hoy con tu proyecto

    Si tienes un archivo de prompt de 5 páginas o un agente que intenta resolver todo el ciclo de vida de tu software:

    1. Separa la lectura de la escritura: Crea un agente explorador con herramientas de solo lectura y un agente constructor que solo toque archivos cuando ya sabe exactamente qué cambiar.
    2. Usa especificaciones previas: Antes de lanzar a los subagentes, asegúrate de tener una base firme con Spec-Driven Development (SDD) para que ningún agente tenga que improvisar requisitos sobre la marcha.
    3. Pasa datos, no conversaciones: Haz que tus subagentes se comuniquen a través de archivos estructurados en lugar de historiales de chat kilométricos.

    En Dominicode Labs compartimos arquitecturas reales de subagentes que usamos a diario para automatizar la creación de cursos, la refactorización de código y el mantenimiento de proyectos en producción.

    Deja de pedirle milagros a un mega-prompt. Diseña un sistema de subagentes donde cada uno haga una sola cosa, pero la haga con precisión quirúrgica.

  • 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.

    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.

  • Test harness para agentes de IA: el banco de pruebas que te falta en CI

    Test harness para agentes de IA: el banco de pruebas que te falta en CI

    Nadie prueba un motor de avión montándolo en un aparato con pasajeros. Lo amarran a un banco de pruebas, le conectan sensores, le inducen fallos y miden qué aguanta. Si revienta, revienta en tierra.

    Con software tenemos el equivalente desde hace décadas y se llama test harness: el andamiaje que rodea al código bajo prueba, le inyecta entradas controladas y comprueba las salidas.

    Con agentes de IA, en cambio, la mayoría probamos en caliente. Lanzamos el agente contra una API real, miramos si el resultado "parece bien" y lo damos por bueno.

    El problema no es la pereza. Es que un agente rompe los tres supuestos sobre los que se construyó todo tu testing:

    • No es determinista: la misma entrada da salidas distintas.
    • Tiene efectos secundarios reales: escribe archivos, llama a APIs, toca bases de datos.
    • No tiene garantía de terminar: puede quedarse en bucle gastando dinero.

    Ya expliqué por qué un LLM por sí solo no es un producto y qué capas necesita alrededor para funcionar en producción. Este post va de la otra mitad del problema, la que casi nadie monta: el arnés que se ejecuta en CI, antes del deploy. Con código.


    Por qué un test unitario normal no sirve aquí

    Un test clásico es un contrato de tres líneas: preparas la entrada, ejecutas, comparas con el valor esperado.

    Con un agente, ese toEqual no existe. La respuesta correcta no es una cadena concreta, es cualquiera de un conjunto amplio de cadenas aceptables. Y si aun así escribes la aserción exacta, tendrás un test que pasa hoy y falla el martes sin que nadie haya tocado nada.

    De ahí sale la reacción habitual, que es la equivocada: dejar de testear el agente y testear solo las funciones puras que lo rodean. Los parsers, los formateadores, los validadores. Cosas que ya sabías hacer.

    Mientras tanto, lo que de verdad puede costarte dinero —el bucle, las llamadas a herramientas, el gasto— viaja a producción sin una sola comprobación.

    El arnés cambia la pregunta. En lugar de "¿ha respondido lo correcto?", que es un problema de evals, pregunta cosas que sí tienen respuesta binaria:

    • ¿Ha llamado a alguna herramienta que no tenía permitida?
    • ¿Se ha pasado del presupuesto de tokens que le di?
    • ¿Ha terminado dentro del tiempo límite?
    • ¿Ha intentado escribir fuera de su directorio temporal?
    • ¿Ha llamado 14 veces a la misma herramienta con los mismos argumentos?

    Eso son tests de verdad: deterministas, rápidos y rojos cuando algo se rompe.

       caso de prueba              TEST HARNESS                  veredicto
      ┌──────────────┐    ┌──────────────────────────────┐    ┌────────────┐
      │ entrada fija │───►│  tools falsas (sin red)      │───►│ PASS/FAIL  │
      │ estado fijo  │    │  presupuesto de tokens       │    │ trace.json │
      └──────────────┘    │  timeout + AbortSignal       │    └────────────┘
                          │  directorio efimero          │
                          └──────────────────────────────┘
    

    Las 3 piezas que hacen testeable a un agente

    1. Herramientas falsas, no red

    La regla es simple: en modo test, el agente no toca nada real. Ni base de datos, ni API de pagos, ni sistema de archivos fuera de un directorio temporal que destruyes al terminar.

    Y no basta con mockear la implementación. Hay que no exponer las herramientas no autorizadas: si el agente ve deleteUser en su lista de tools, tarde o temprano la llamará, y el error que quieres detectar en CI es precisamente ese. Un arnés que expone la herramienta y luego lanza una excepción llega tarde para razonar sobre el diseño, aunque salve los datos.

    Si necesitas ejecutar código generado de verdad —no simularlo—, ahí el aislamiento sube un nivel y toca contenedor: lo conté en Docker sandboxing para ejecutar código de IA de forma segura.

    2. Presupuesto de tokens y timeout que cortan de verdad

    Esta es la pieza que casi todo el mundo escribe mal.

    He visto docenas de arneses con un campo maxTokens en la configuración que no se comprueba en ningún sitio. Y timeouts implementados con Promise.race que devuelven el control al test pero dejan la ejecución corriendo por detrás, gastando tokens contra la API mientras el test ya ha dado verde.

    Un límite que no corta no es un límite: es un comentario.

    3. Traza reproducible

    El arnés graba cada paso: qué herramienta, con qué argumentos, cuánto tardó, cuánto costó. Un array de objetos serializado a JSON.

    Sirve para dos cosas. Para que un fallo en CI sea depurable sin volver a lanzar el agente. Y para escribir aserciones sobre el proceso, no sobre el texto final, que es donde está la señal útil: si el agente llegó al resultado correcto llamando siete veces a la misma consulta, eso es un bug aunque la salida sea perfecta.


    El arnés en TypeScript

    Vamos al código. Un arnés mínimo con presupuesto real, cancelación real y traza, sin dependencias más allá de Zod para validar los argumentos que el modelo envía a cada herramienta.

    Primero, los tipos y el registro de herramientas:

    import { z } from "zod";
    
    export interface HarnessConfig {
      maxTokens: number;
      timeoutMs: number;
      allowedTools: string[];
    }
    
    export interface TraceEntry {
      tool: string;
      args: unknown;
      durationMs: number;
      tokens: number;
    }
    
    /** Herramienta ya validada: el schema queda encapsulado dentro de `run`. */
    export interface HarnessTool {
      cost: number;
      run: (rawArgs: unknown) => Promise<unknown>;
    }
    
    export class BudgetExceededError extends Error {}
    
    /**
     * En el punto de definicion conservas el tipado completo del schema.
     * En el registro todas las tools comparten la misma firma, que es lo
     * que permite recorrerlas en bucle sin castings.
     */
    export function defineTool<S extends z.ZodType>(
      schema: S,
      cost: number,
      run: (args: z.infer<S>) => Promise<unknown>,
    ): HarnessTool {
      return { cost, run: (rawArgs) => run(schema.parse(rawArgs)) };
    }
    
    // Fixtures: nada de esto sale a la red.
    export const testTools: Record<string, HarnessTool> = {
      queryDatabase: defineTool(
        z.object({ table: z.string(), limit: z.number().max(100) }),
        320,
        async ({ table }) => ({ rows: [{ id: 1, table, name: "Fixture User" }] }),
      ),
      sendEmail: defineTool(
        z.object({ to: z.string().email(), body: z.string() }),
        90,
        async () => ({ delivered: true }),
      ),
    };
    

    Ahora el arnés. Fíjate en tres detalles: solo se construyen las herramientas permitidas, el presupuesto se comprueba antes de ejecutar cada llamada, y el temporizador se limpia siempre.

    export type HarnessStatus = "SUCCESS" | "TIMEOUT" | "BUDGET_EXCEEDED" | "FAILED";
    
    export interface HarnessResult {
      status: HarnessStatus;
      tokensUsed: number;
      durationMs: number;
      output: string | null;
      trace: TraceEntry[];
    }
    
    type ToolBox = Record<string, (args: unknown) => Promise<unknown>>;
    
    export async function runWithHarness(
      task: (tools: ToolBox, signal: AbortSignal) => Promise<string>,
      config: HarnessConfig,
    ): Promise<HarnessResult> {
      const startedAt = performance.now();
      const trace: TraceEntry[] = [];
      let tokensUsed = 0;
    
      // 1. Solo existen las tools autorizadas. El resto no se expone.
      const tools: ToolBox = {};
      for (const name of config.allowedTools) {
        const tool = testTools[name];
        // Un nombre desconocido es un error de configuracion del test: que reviente ya.
        if (!tool) throw new Error(`Tool desconocida en allowedTools: ${name}`);
    
        tools[name] = async (rawArgs: unknown) => {
          // 2. El presupuesto se comprueba ANTES de gastar.
          if (tokensUsed + tool.cost > config.maxTokens) {
            throw new BudgetExceededError(
              `Presupuesto agotado: ${tokensUsed} + ${tool.cost} > ${config.maxTokens}`,
            );
          }
          const t0 = performance.now();
          const result = await tool.run(rawArgs); // Zod valida dentro: si no cuadra, revienta
          tokensUsed += tool.cost;
          trace.push({
            tool: name,
            args: rawArgs,
            durationMs: Math.round(performance.now() - t0),
            tokens: tool.cost,
          });
          return result;
        };
      }
    
      // 3. Cancelacion real: la tarea recibe el signal y debe propagarlo al SDK.
      const controller = new AbortController();
      const timer = setTimeout(() => controller.abort(), config.timeoutMs);
    
      const finish = (status: HarnessStatus, output: string | null): HarnessResult => ({
        status,
        tokensUsed,
        durationMs: Math.round(performance.now() - startedAt),
        output,
        trace,
      });
    
      try {
        const output = await task(tools, controller.signal);
        return finish("SUCCESS", output);
      } catch (error) {
        if (controller.signal.aborted) return finish("TIMEOUT", null);
        if (error instanceof BudgetExceededError) return finish("BUDGET_EXCEEDED", null);
        return finish("FAILED", error instanceof Error ? error.message : String(error));
      } finally {
        clearTimeout(timer); // sin esto, el timer mantiene vivo el proceso al terminar
      }
    }
    

    Un aviso honesto sobre el punto 3: el AbortSignal solo cancela de verdad si tu tarea lo propaga al SDK del modelo y a cada fetch. Si lo ignoras, el arnés dará TIMEOUT y devolverá el control al test, pero la llamada seguirá viva por detrás y te la cobrarán igual. El signal no es decorativo: es el único mecanismo que corta el gasto.

    Y ahora sí, un test

    Con esto, probar el bucle del agente vuelve a ser testing normal:

    import { describe, expect, it } from "vitest";
    import { runWithHarness } from "./harness";
    
    describe("agente de facturación", () => {
      it("corta la ejecución al agotar el presupuesto", async () => {
        const result = await runWithHarness(
          async (tools) => {
            // Un agente en bucle: consulta la misma tabla sin parar.
            for (let i = 0; i < 20; i++) {
              await tools.queryDatabase({ table: "invoices", limit: 10 });
            }
            return "listo";
          },
          { maxTokens: 1_000, timeoutMs: 5_000, allowedTools: ["queryDatabase"] },
        );
    
        expect(result.status).toBe("BUDGET_EXCEEDED");
        expect(result.tokensUsed).toBeLessThanOrEqual(1_000);
        expect(result.trace).toHaveLength(3); // 3 × 320 = 960; la cuarta no cabe
      });
    
      it("no expone las herramientas fuera del allowlist", async () => {
        const result = await runWithHarness(
          async (tools) => {
            if ("sendEmail" in tools) return "PELIGRO: tool disponible";
            return "ok";
          },
          { maxTokens: 5_000, timeoutMs: 5_000, allowedTools: ["queryDatabase"] },
        );
    
        expect(result.output).toBe("ok");
      });
    });
    

    Deterministas, sin red, en milisegundos. Se pueden ejecutar en cada push sin pensar en la factura.

    Ese expect(result.trace).toHaveLength(3) es el tipo de aserción que solo puedes escribir si grabas la traza: comprueba el comportamiento del bucle, no el texto de salida.

    Si quieres afinar el diseño de tests y el aislamiento de dependencias externas —que es exactamente el músculo que necesitas aquí—, lo trabajo a fondo en el curso de Testing en Angular con Jest y Testing Library. Y el uso de Zod para validar los argumentos que envía el modelo, con transformaciones y errores tipados, lo tienes en el curso de Zod para TypeScript.


    Qué encaja arriba y qué encaja abajo

    Tres piezas que se confunden todo el rato y conviene separar:

    Pieza Cuándo corre Qué responde
    Test harness En CI, en cada push ¿Se sale de los límites, del allowlist o del tiempo?
    Evals Por lotes, con casos reales ¿La calidad de las respuestas sube o baja?
    Agentic harness En producción, en cada ejecución ¿Cómo lo mantengo controlado con usuarios reales?

    El arnés de pruebas es el más barato de los tres y el que casi nadie tiene. Cuestión de horas montarlo, y atrapa la clase de fallo que más caro sale.

    Sobre el reparto de trabajo entre los tests que escribes tú y los que genera el agente, ya hay un post entero: adopta TDD para implementar pruebas efectivas con agentes de IA. Y sobre por qué la spec y la arquitectura no bastan sin esta capa debajo, también. Este post es la parte que faltaba: el código.

    Si trabajas con Spec-Driven Development, el encaje es directo. Los límites que escribes en la sección de NFRs del spec.md —presupuesto, latencia, herramientas permitidas— dejan de ser un párrafo y pasan a ser los argumentos de HarnessConfig. La especificación se vuelve ejecutable, que es de lo que va el libro de Spec-Driven Development.


    Lo que puedes montar esta semana

    1. Una lista blanca de herramientas por entorno. Que en test solo existan las que necesita el caso.
    2. Un presupuesto que corte. Comprobado antes de cada llamada, no después. Si tu maxTokens no aparece en ningún if, no existe.
    3. Una traza en JSON por ejecución. Y al menos un test que asierte sobre ella, no sobre el texto de salida.

    En Dominicode Labs montamos este tipo de arneses sobre agentes que corren horas sin supervisión.

    Deja de probar tus motores en pleno vuelo. Amárralos al banco, súbeles la presión hasta que rompan y arréglalos en tierra, que es donde sale barato.


    Preguntas frecuentes

    ¿Cómo se testea algo que no es determinista?

    No asertando sobre el texto de salida, sino sobre el comportamiento observable: qué herramientas llamó, con qué argumentos, cuántas veces, cuánto gastó y si terminó a tiempo. Todo eso sí es determinista y da un rojo claro cuando se rompe. La calidad de la respuesta es otra disciplina y se mide por lotes, no en cada push.

    ¿El test harness sustituye a los mocks de toda la vida?

    No, los usa. La diferencia es el alcance: un mock reemplaza una dependencia concreta, mientras que el arnés controla el entorno completo de la ejecución —qué herramientas existen, cuánto puede gastar, cuánto puede tardar y qué queda grabado—. Un mock por sí solo no impide que el agente entre en bucle.

    ¿Hay que llamar al modelo real en estos tests?

    No en los que corren en cada push: se ejecuta el bucle del agente con respuestas fijas, y eso vale para verificar límites, allowlist y control de flujo. Las ejecuciones con modelo real cuestan dinero y tardan, así que van en un job aparte, programado y sobre un conjunto reducido de casos.

    ¿Qué hago si el timeout salta pero el agente sigue gastando dinero?

    Es que estás cortando en el sitio equivocado. Promise.race devuelve el control al test pero no cancela nada: hay que crear un AbortController, pasar su signal a la tarea y propagarlo al SDK del modelo y a cada fetch. Si el SDK que usas no acepta señal de cancelación, el único corte real es aislar la ejecución en un proceso o contenedor aparte y matarlo.

    ¿Merece la pena montarlo si mi agente solo lee datos?

    Sí, por el gasto y por los bucles. Un agente de solo lectura no borra nada, pero puede repetir la misma consulta cuarenta veces y facturarte la broma entera. El presupuesto y la traza detectan ese patrón en CI, que es donde cuesta cero arreglarlo.


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

  • Agentic Loop en TypeScript: cómo evitar el bucle infinito

    Agentic Loop en TypeScript: cómo evitar el bucle infinito

    Un viernes a las 11 de la noche dejé corriendo un script experimental con un agente para refactorizar unos modelos de datos.

    A la mañana siguiente abrí la consola de Anthropic y vi que había ejecutado más de 300 llamadas a herramientas seguidas. ¿El motivo? Intentó leer un archivo que no existía, la herramienta devolvió un error genérico, el modelo interpretó que la ruta estaba mal escrita, probó otra ruta inexistente, y así durante horas.

    Un agente de IA no es un modelo inteligente: es un Agentic Loop, un bucle while en TypeScript que tú controlas (o que te controla a ti). La factura de tokens dolió, pero la lección técnica valió cada céntimo.

    La mayoría de tutoriales te enseñan a pasarle tres herramientas a un LLM y llamar a generateText. Nadie te explica qué pasa cuando el modelo entra en pánico lógico, se niega a terminar o repite la misma llamada una y otra vez.

    Aquí está cómo funciona de verdad el Agentic Loop en producción y cómo implementarlo en TypeScript sin caer en trampas de novato.


    Qué es realmente un Agentic Loop

    La diferencia entre un chatbot clásico y un agente autónomo no es el modelo de lenguaje. Ambos pueden usar Claude Sonnet 5, Opus 5 o el GPT vigente. La diferencia es el patrón de ejecución.

    Un chatbot recibe un mensaje y devuelve texto. Fin del ciclo.

    Un agente recibe una instrucción, razona qué herramienta necesita, la ejecuta, observa el resultado y vuelve a razonar con esa nueva información hasta que considera que la tarea está terminada.

    El flujo básico sigue siempre este ciclo:

    1. Reasoning (Razonamiento): El modelo evalúa el historial y decide si responder o invocar una herramienta (Tool Call).
    2. Action (Acción): Tu servidor ejecuta la función requerida (leer un archivo, consultar una base de datos, llamar a una API).
    3. Observation (Observación): Inyectas el resultado de la función de vuelta al contexto como un mensaje de rol tool.
    4. Evaluation (Evaluación): El modelo analiza el nuevo estado. Si el objetivo está cumplido, entrega la respuesta final; si no, repite desde el paso 1.

    Si este ciclo te suena, es porque es la formalización del patrón ReAct: Thought → Action → Observation. Ahí explico el razonamiento; aquí vamos a lo que nadie cuenta: qué defensas necesita ese bucle para sobrevivir a producción.

    Parece sencillo en un diagrama. En producción, si dejas este bucle sin defensas activas, tienes una bomba de tiempo.


    Los 3 errores fatales que rompen tus agentes en producción

    1. No tener un límite de pasos rígido

    Si no defines un tope máximo de iteraciones, un fallo imprevisto en una API externa convertirá a tu agente en un generador infinito de facturación. En cualquier sistema agéntico serio, cada tarea debe tener un presupuesto máximo de pasos (usualmente entre 5 y 15 para tareas estándar).

    2. Tratar los errores de las herramientas como excepciones no controladas

    Si una tool lanza un throw new Error("File not found") y tu código revienta el proceso de Node/Bun, tu agente se cae. Pero si capturas el error y le devuelves un string vacío, el agente asumirá que el archivo está en blanco y tomará decisiones erróneas.

    Devuelve los errores de herramientas formateados como datos descriptivos para que el modelo sepa exactamente qué falló y pueda autocorregirse.

    3. Falta de detección de estancamiento (Loop Fingerprinting)

    El modelo a veces se obsesiona. Ejecuta readFile({ path: "config.json" }), falla, y en el siguiente paso vuelve a llamar exactamente a readFile({ path: "config.json" }) esperando un milagro.

    Si no comparas la huella de la llamada actual con las anteriores, tu agente se quedará atascado en un ciclo ciego. Y ojo: un límite de pasos no te salva de esto, solo te acota el gasto. El agente sigue quemando el presupuesto entero sin avanzar un milímetro.


    Implementando un Agentic Loop robusto en TypeScript

    Para evitar dependencias pesadas que oscurezcan lo que pasa por debajo, el estándar más limpio hoy es usar el Vercel AI SDK con schemas tipados mediante Zod.

    La clave está en stopWhen: acepta un array de condiciones y detiene el bucle cuando cualquiera de ellas se cumple. Eso te permite combinar el techo de gasto con tu propio detector de bucles.

    Aquí tienes una implementación defensiva lista para producción:

    import { generateText, tool, isStepCount, type StopCondition } from "ai";
    import { anthropic } from "@ai-sdk/anthropic";
    import { z } from "zod";
    
    // 1. Definición estricta de herramientas con Zod
    const tools = {
      readFile: tool({
        description: "Lee el contenido de un archivo del proyecto",
        inputSchema: z.object({
          filePath: z.string().describe("Ruta relativa del archivo a leer"),
        }),
        execute: async ({ filePath }) => {
          try {
            const content = await Bun.file(filePath).text();
            return { success: true, data: content };
          } catch (err: any) {
            // Devolvemos el error como información útil para el agente
            return {
              success: false,
              error: `No se pudo leer el archivo "${filePath}": ${err.message}`,
            };
          }
        },
      }),
    };
    
    // 2. Huella de cada tool call: nombre + argumentos exactos
    const fingerprint = (call: { toolName: string; input: unknown }) =>
      `${call.toolName}:${JSON.stringify(call.input)}`;
    
    // 3. Condición de parada propia: corta a la tercera llamada idéntica
    //    (la segunda es un reintento legítimo; la tercera ya es obsesión)
    const noRepeatedCalls: StopCondition<typeof tools> = ({ steps }) => {
      const calls = steps.flatMap((step) => step.toolCalls.map(fingerprint));
    
      return calls.some(
        (f) => calls.filter((other) => other === f).length >= 3
      );
    };
    
    // 4. Ejecutor agéntico con las dos defensas activas
    export async function runAgentTask(prompt: string, maxIterations = 10) {
      const result = await generateText({
        model: anthropic("claude-sonnet-5"),
        system: `Eres un asistente de desarrollo autónomo.
        Usa las herramientas disponibles para inspeccionar el entorno.
        Si una herramienta falla, analiza el motivo antes de reintentar.
        Cuando termines la tarea, responde con el resumen final sin invocar más tools.`,
        prompt,
        tools,
        // Para en cuanto se cumpla una: presupuesto agotado o bucle detectado
        stopWhen: [isStepCount(maxIterations), noRepeatedCalls],
        onStepFinish: ({ toolCalls }) => {
          for (const call of toolCalls) {
            console.log(`[STEP] ${fingerprint(call)}`);
          }
        },
      });
    
      return { text: result.text, steps: result.steps.length };
    }
    

    Fíjate en lo que hace este patrón:

    1. Schemas Zod estrictos: Si el LLM intenta inventarse un parámetro que no existe, la librería lo rechaza antes de tocar el sistema de archivos. Si quieres profundizar en cómo validar estructuras complejas, en nuestro curso de Zod para TypeScript vemos cómo blindar estas entradas.
    2. Resultados estructurados: Las herramientas devuelven { success: boolean, data?: string, error?: string }. El modelo sabe interpretar este formato a la primera.
    3. Dos condiciones de parada independientes: isStepCount es el techo de gasto; noRepeatedCalls es el que de verdad mata el bucle infinito, porque detecta el estancamiento aunque queden pasos de presupuesto.
    4. Trazabilidad paso a paso: onStepFinish te da el log de cada decisión del agente. No puede abortar el bucle —eso es trabajo de stopWhen—, pero es lo que te permite reconstruir después por qué se atascó.

    Un detalle que se pasa por alto: la parada devuelve el control a tu código, no lanza una excepción. Comprueba steps al recibir el resultado — si el agente terminó en el paso 10 de 10, probablemente no completó la tarea y toca escalarla, no darla por buena.

    Es el mismo cambio de mentalidad del que hablo en Loop Engineering: el valor ya no está en el prompt, está en el bucle que lo envuelve.


    La regla de oro: Antes de escribir el loop, define el Spec

    El mejor código de loop no compensa una instrucción ambigua. Si le dices a tu agente "arregla el bug del login", gastará 8 iteraciones solo descubriendo dónde está el archivo de login.

    Esta es la razón por la que en Dominicode insistimos tanto en la metodología Spec-First. Antes de soltar a un agente a modificar código, generamos una especificación técnica clara que acota el alcance exacto de la tarea. Es exactamente el flujo que enseñamos en el curso Construye con IA: de la idea al producto y en el libro de Spec-Driven Development.


    Qué puedes cambiar hoy en tu arquitectura

    Si ya tienes agentes corriendo en tus proyectos locales o en servidores de staging, haz esta revisión técnica hoy mismo:

    1. Revisa tu límite de pasos (stopWhen): Ninguna llamada agéntica debería correr sin un límite superior finito.
    2. Audita el retorno de tus tools: Asegúrate de que las excepciones devuelvan mensajes de error útiles en lugar de romper el runtime o devolver respuestas vacías.
    3. Implementa fingerprinting: Añade una condición de parada propia que compare la huella de cada tool call con las anteriores y corte a la tercera repetición exacta.
    4. Comprueba en qué paso terminó: Si el agente agota el presupuesto, trátalo como fallo y escala la tarea. Un resultado entregado en el último paso casi nunca es un resultado bueno.

    En Dominicode Labs estamos probando patrones avanzados de orquestación agéntica con subagentes independientes y guardrails de seguridad en proyectos reales.

    Construir con IA no consiste en maravillarse con lo que genera el modelo en el primer prompt, sino en diseñar la arquitectura que hace que el sistema sea predecible y seguro cuando nadie está mirando la pantalla.


    Preguntas frecuentes

    ¿Cuántos pasos como máximo debe tener un Agentic Loop?

    Entre 5 y 15 para tareas estándar. Con menos de 5 el agente no llega a explorar el entorno; por encima de 15 el problema casi nunca es el presupuesto, sino que la instrucción era ambigua. Si necesitas 30 pasos, parte la tarea en dos.

    ¿Qué diferencia hay entre un Agentic Loop y el patrón ReAct?

    ReAct describe el razonamiento del modelo: pensar, actuar, observar. El Agentic Loop es la implementación real de ese ciclo en tu código, con sus límites, su manejo de errores y sus condiciones de parada. ReAct es el qué; el Agentic Loop es el cómo lo ejecutas sin que se te vaya la factura.

    ¿Debo lanzar una excepción cuando falla una tool?

    No. Captura el error dentro de execute y devuélvelo como dato estructurado ({ success: false, error: "..." }). Si lanzas la excepción, rompes el proceso; si devuelves vacío, el modelo asume que el resultado era vacío y decide mal. El error descriptivo es lo único que le permite autocorregirse.

    ¿No basta con poner un límite de pasos para evitar el bucle infinito?

    El límite evita que el gasto sea infinito, pero no evita el bucle. Un agente atascado consumirá los 10 pasos repitiendo la misma llamada y te devolverá un resultado inútil habiendo pagado el presupuesto completo. El límite de pasos es el airbag; el fingerprinting es el freno.

    ¿Necesito un framework de orquestación para implementar esto?

    No para un bucle de una sola tarea: el Vercel AI SDK con stopWhen te da control de pasos y parada personalizada en unas 40 líneas. Los frameworks de grafos empiezan a compensar cuando necesitas ramificaciones, estado persistente entre sesiones o varios agentes coordinados.


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

  • Patrón ReAct en Agentes de IA: Guía del Bucle Thought-Action

    Patrón ReAct en Agentes de IA: Guía del Bucle Thought-Action

    Si le pides a un modelo de lenguaje que resuelva un problema complejo solo con texto, alucina. Si le das herramientas pero no lo dejas razonar, ejecuta acciones a ciegas y rompe tu base de datos.

    En 2022, un paper de investigadores de Princeton y Google (Yao et al.) demostró una verdad incómoda:

    Un LLM que solo "piensa" (Chain-of-Thought) vive desconectado del mundo real y acumula errores lógicos. Un LLM que solo "actúa" (Action-only) carece de plan y ejecuta llamadas a APIs sin evaluar si la respuesta previa tuvo sentido.

    La solución fue combinarlos en un ciclo continuo: ReAct (Reasoning + Acting).

    Cualquier agente moderno que uses hoy —desde Claude Code hasta un asistente con LangGraph o el SDK de OpenAI— desciende directamente de este patrón.

    Aquí te explico la mecánica interna del patrón ReAct en agentes de IA, cómo implementarlo en TypeScript puro sin librerías hinchadas y los tres fallos críticos que debes evitar en producción.


    Anatomía del patrón ReAct en agentes de IA: Thought → Action → Observation

    El patrón ReAct descompone la resolución de cualquier tarea en tres fases iterativas:

                      ┌───────────────────────────────┐
                      ▼                               │
              ┌───────────────┐                       │
              │   1. THOUGHT  │  Razona el estado     │
              └───────┬───────┘                       │
                      │                               │
                      ▼                               │
              ┌───────────────┐                       │
              │   2. ACTION   │  Ejecuta herramienta  │
              └───────┬───────┘                       │
                      │                               │
                      ▼                               │
              ┌───────────────┐                       │
              │ 3.OBSERVATION │  Recibe resultado     │
              └───────┬───────┘                       │
                      │                               │
                      └───────── ¿Finalizado? ────────┘
                                 │           │
                                [No]        [Sí]
                                             │
                                             ▼
                                       [Respuesta Final]
    
    1. Thought (Pensamiento): El modelo analiza el historial de la conversación, identifica qué sub-objetivo tiene pendiente y decide qué información le falta.
    2. Action (Acción): El modelo emite una llamada estructurada a una herramienta externa (una consulta SQL, un comando de terminal, una llamada HTTP o una búsqueda en vector store).
    3. Observation (Observación): El entorno ejecuta la herramienta y devuelve el resultado crudo al contexto del modelo.
    4. Evaluación: El modelo lee la observación, actualiza su pensamiento y decide si ya puede responder al usuario o si necesita otra iteración.

    Implementación mínima en TypeScript (sin frameworks)

    Para entender ReAct no necesitas instalar un framework de 40 dependencias. El núcleo del patrón es un bucle while que evalúa las llamadas a funciones devueltas por el LLM:

    import Anthropic from "@anthropic-ai/sdk";
    
    const client = new Anthropic();
    
    // 1. Definición de herramientas
    const tools: Anthropic.Tool[] = [{
      name: "search_database",
      description: "Busca registros de clientes por email",
      input_schema: {
        type: "object",
        properties: { email: { type: "string" } },
        required: ["email"]
      }
    }];
    
    // 2. Ejecutor de herramientas del entorno
    async function executeTool(name: string, input: any): Promise<string> {
      if (name === "search_database") {
        // Simulación de consulta real
        return JSON.stringify({ id: "usr_402", status: "active", plan: "pro" });
      }
      throw new Error(`Herramienta ${name} no encontrada`);
    }
    
    // 3. El bucle ReAct
    export async function runReActAgent(userGoal: string) {
      const messages: Anthropic.MessageParam[] = [
        { role: "user", content: userGoal }
      ];
    
      const MAX_ITERATIONS = 5;
      let iterations = 0;
    
      while (iterations < MAX_ITERATIONS) {
        iterations++;
    
        // Fase THOUGHT + ACTION (generación del modelo)
        const response = await client.messages.create({
          model: "claude-sonnet-5",
          max_tokens: 1024,
          tools,
          messages
        });
    
        // Si el modelo decide responder directamente, salimos
        if (response.stop_reason === "end_turn") {
          const finalReply = response.content.find(c => c.type === "text");
          return finalReply?.type === "text" ? finalReply.text : "";
        }
    
        // Si el modelo solicita ejecutar una herramienta (ACTION)
        if (response.stop_reason === "tool_use") {
          messages.push({ role: "assistant", content: response.content });
    
          const toolUse = response.content.find(c => c.type === "tool_use");
          if (toolUse && toolUse.type === "tool_use") {
            // Fase OBSERVATION (ejecución real en tu infraestructura)
            const observation = await executeTool(toolUse.name, toolUse.input);
    
            // Se inyecta la observación al historial para la siguiente vuelta
            messages.push({
              role: "user",
              content: [{
                type: "tool_result",
                tool_use_id: toolUse.id,
                content: observation
              }]
            });
          }
        }
      }
    
      throw new Error("El agente alcanzó el límite máximo de iteraciones sin resolver.");
    }
    

    Fíjate en lo que ocurre aquí: el LLM no tiene acceso a tu base de datos. El LLM solo emite la intención de consultar. Tu backend ejecuta la acción, captura la salida y se la devuelve como una Observation.


    ReAct con Tool Calling nativo vs. ReAct por texto plano

    En los primeros papers de 2022, el modelo generaba texto plano con prefijos:

    Thought: Necesito consultar el saldo del usuario con ID 102.
    Action: get_balance[102]
    Observation: 450.00 EUR
    Thought: El usuario tiene 450 euros. Ya puedo responder.
    Final Answer: Tu saldo disponible es de 450,00 €.
    

    Ese enfoque por texto requería expresiones regulares frágiles para parsear qué herramienta quería invocar el modelo.

    Hoy, los proveedores (Anthropic, OpenAI) integran Tool Calling nativo a nivel de tokens. El modelo garantiza JSON válido para los argumentos y separa el bloque de razonamiento del bloque de ejecución mediante bloques tipados (tool_use y tool_result). El principio conceptual sigue siendo 100% ReAct.


    Los 3 problemas del patrón ReAct en producción

    Cuando pasas de una demo a un sistema con usuarios reales, te enfrentas a tres trampas:

    1. Bucles infinitos (Infinite Loop Trap)

    Si una herramienta devuelve un error que el modelo no sabe interpretar (por ejemplo, 500 Internal Server Error), un agente ingenuo intentará llamar a la misma herramienta con los mismos parámetros una y otra vez.

    Solución: Límite estricto de iteraciones (MAX_ITERATIONS = 5-8) y formatear las excepciones dentro de la Observation explicando qué falló para que el modelo cambie de estrategia.

    2. Explosión del context window

    En cada ciclo, la Observation completa se añade al historial. Si una herramienta devuelve un JSON de 4.000 líneas con logs o registros de base de datos, el consumo de tokens y el coste se disparan exponencialmente en la siguiente iteración.

    Solución: Truncar, filtrar y resumir las salidas de las herramientas antes de entregárselas al modelo. Si quieres profundizar en cómo estructurar esa memoria sin que se dispare el contexto, lo cubrimos en context engineering: cómo estructurar la memoria de tus agentes de IA.

    3. Falta de determinismo en pipelines críticos

    ReAct es ideal para exploración y tareas no lineales. Pero si tu flujo de negocio tiene pasos rígidos (Paso 1: Validar DNI → Paso 2: Cobrar en Stripe → Paso 3: Enviar email), dejar que un agente ReAct decida el orden en tiempo real es una irresponsabilidad.

    Para procesos estructurados, la combinación óptima es un grafo determinista (como LangGraph o flujos guiados por especificaciones) donde ReAct se use solo dentro de nodos específicos que requieran adaptabilidad.

    Esta metodología de ingeniería de agentes con límites estrictos es el núcleo del curso Construye con IA: De la Idea al Producto con Claude y Specs, donde mostramos cómo gobernar agentes sin perder el control de la ejecución.


    ReAct vs. Plan-and-Solve: cuándo elegir cuál

    Criterio Patrón ReAct Patrón Plan-and-Solve
    Estrategia Decide el siguiente paso sobre la marcha tras cada observación Genera un plan completo de $N$ pasos antes de actuar
    Adaptabilidad Alta: Si un paso falla, recalcula inmediatamente Baja: Si el entorno cambia, el plan inicial queda obsoleto
    Latencia Mayor (un round-trip al LLM por cada herramienta) Menor (menos llamadas al modelo si el plan es estático)
    Caso de uso ideal Búsqueda interactiva, debugging, navegación web, soporte Generación de informes largos, migraciones batch

    Para patrones avanzados de observabilidad y control de memoria en arquitecturas de agentes, en Dominicode Labs construimos y testeamos implementaciones en producción semana a semana.


    Preguntas frecuentes

    ¿Qué significa ReAct en inteligencia artificial?

    ReAct es el acrónimo de Reasoning and Acting. Es un patrón de diseño para agentes de IA que combina el razonamiento paso a paso (Thought) con la ejecución de herramientas externas (Action) y la lectura de resultados (Observation).

    ¿ReAct es una librería o un concepto arquitectónico?

    Es un concepto arquitectónico. LangGraph tiene un factory prebuilt, create_react_agent (aunque ya está marcado como deprecado a favor de create_agent), y otros frameworks como CrewAI ofrecen sus propias implementaciones de referencia. Pero el patrón en sí no depende de ninguna librería: se implementa en cualquier lenguaje con llamadas nativas a la API del LLM.

    ¿Cuál es la diferencia entre Chain-of-Thought (CoT) y ReAct?

    Chain-of-Thought solo produce razonamiento interno sin interactuar con el entorno exterior, lo que suele derivar en alucinaciones cuando faltan datos. ReAct conecta ese razonamiento con herramientas externas reales (APIs, bases de datos, código).

    ¿Cómo evitar que un agente ReAct gaste demasiados tokens?

    Estableciendo un límite máximo de pasos por sesión, filtrando el contenido de las observaciones antes de inyectarlas al contexto y utilizando modelos más pequeños y rápidos para las llamadas intermedias. Antes de optimizar a ciegas, mide en qué parte del bucle se te va el presupuesto.


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

  • Certificación LangChain: Guía del Examen y Preparación

    Certificación LangChain: Guía del Examen y Preparación

    La semana pasada un desarrollador me preguntó si valía la pena pagar por la certificación oficial de LangChain para conseguir trabajo como AI Engineer.

    Mi respuesta fue directa: el badge digital en LinkedIn te abre la puerta de la entrevista con recursos humanos. Pero en la prueba técnica te van a pedir que depures un grafo cíclico en LangGraph o que arregles un desbordamiento de tokens en producción.

    Si intentas preparar el examen memorizando tutoriales de hace un año, vas a suspender.

    El ecosistema de LangChain ha cambiado de raíz: las cadenas monolíticas (LLMChain, ConversationalRetrievalChain) están formalmente obsoletas. La certificación oficial se llama LangChain Certified Agent Engineer (LCAE) y evalúa el ciclo de vida completo de un agente: construirlo con LCEL y LangGraph, probarlo y monitorizarlo con LangSmith, y desplegarlo en producción.

    Aquí tienes el desglose exacto de lo que entra, los errores que más suspensos provocan y la ruta de estudio para aprobar sin rodeos.


    Qué evalúa LangChain Certified Agent Engineer

    La certificación —oficial de LangChain Academy, no un curso con diploma de cortesía— valida que eres capaz de construir, probar, desplegar y monitorizar agentes de producción. Se organiza en cuatro dominios de peso idéntico, el ciclo de vida completo del agente (lo que LangChain llama ADLC, Agent Development Life Cycle):

            [ LANGCHAIN CERTIFIED AGENT ENGINEER ]
                            │
       ┌──────────┬─────────┼─────────┬──────────┐
       ▼          ▼         ▼         ▼
    Building   Testing   Deploying  Monitoring
     Agents     Agents     Agents     Agents
      25%        25%        25%        25%
    (LCEL,    (LangSmith  (LangGraph  (LangSmith
    LangGraph,  evals,     Platform,   tracing,
    RAG, Tool   datasets)  runtime,    coste,
    Calling)               versionado) latencia)
    

    Formato del examen:

    • Tipo de preguntas: 40 preguntas de opción múltiple, entregadas online con supervisión (proctored).
    • Duración: 120 minutos (~135 minutos contando el tiempo de la plataforma).
    • Puntuación para aprobar: 28 de 40 (70%).
    • Precio: $99 USD.
    • Validez: 24 meses.
    • Idioma: Inglés.
    • Requisito recomendado (no obligatorio): completar los cinco cursos gratuitos fundamentales de LangChain Academy antes de presentarte.

    Los 4 dominios del examen (y las técnicas que necesitas dominar)

    Para aprobar LangChain Certified Agent Engineer, tu preparación debe cubrir los cuatro dominios oficiales, con el mismo peso cada uno:

    Dominio 1: Building Agents (25%)

    Es donde más código nuevo vas a tocar, aunque en el examen pese lo mismo que los otros tres. Reúne LCEL, LangGraph, RAG y Tool Calling.

    LCEL (LangChain Expression Language) y Runnables. Columna vertebral de todo el código moderno de LangChain. Domina la composición con el operador pipe (|) y la interfaz Runnable:

    from langchain_core.runnables import RunnablePassthrough, RunnableParallel
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    
    # Composición declarativa con LCEL
    chain = (
        {"context": retriever, "question": RunnablePassthrough()}
        | prompt
        | model
        | StrOutputParser()
    )
    

    Conceptos clave: métodos estándar (.invoke(), .stream(), .batch(), .ainvoke(), .astream_events()), primitivas auxiliares (RunnableLambda, RunnableParallel, .bind(), .with_fallbacks(), .with_retry()) y streaming asíncrono granular con astream_events(version="v2").

    LangGraph: grafos de estado y agentes cíclicos. LangChain dejó atrás el concepto de "agentes lineales". Hoy los flujos complejos se modelan como grafos dirigidos con estado — si el concepto de grafo te resulta nuevo, cubrimos la base en qué es el graph engineering:

    from langgraph.graph import StateGraph, END
    from typing import TypedDict, Annotated
    import operator
    
    class AgentState(TypedDict):
        messages: Annotated[list, operator.add]
        next_step: str
    
    # Construcción del grafo
    workflow = StateGraph(AgentState)
    workflow.add_node("agent", call_model)
    workflow.add_node("tools", execute_tools)
    
    workflow.add_conditional_edges(
        "agent",
        should_continue,
        {"continue": "tools", "end": END}
    )
    

    Conceptos clave: esquemas de estado con TypedDict y reducers (operator.add), nodos (add_node), aristas fijas (add_edge) y condicionales (add_conditional_edges), persistencia con MemorySaver / SqliteSaver (Checkpointers), y control Human-in-the-loop con interrupt().

    RAG avanzado (Retrieval-Augmented Generation). No basta con saber qué es un VectorStore. Entran estrategias de Chunking (Recursive Character Text Splitter vs Semantic Chunking), búsqueda híbrida (BM25 léxico + Dense Embeddings vectorial vía Reciprocal Rank Fusion) y compresión contextual/Reranking (Cohere Rerank u otros).

    Tool Calling y validación de schemas. Integración de herramientas tipadas con @tool de langchain_core.tools, validación de argumentos con Pydantic (Python) o Zod (TypeScript), y manejo de excepciones con handle_tool_error.

    Dominio 2: Testing Agents (25%)

    Aquí LangSmith deja de ser una herramienta de debugging manual y se convierte en infraestructura de evaluación: creación de datasets de prueba, evaluadores automáticos (LLM-as-a-judge) y comparación de versiones de un agente contra un mismo dataset antes de promoverlo a producción.

    Dominio 3: Deploying Agents (25%)

    El dominio que más se salta la gente porque no hay un notebook bonito que enseñe esto. Cubre llevar el grafo de LangGraph a un runtime de producción (LangGraph Platform u otro hosting propio), gestionar configuración por entorno (variables, secretos, versión del grafo desplegado) y sustituir el checkpointer de desarrollo (SqliteSaver) por uno apto para producción con concurrencia real (por ejemplo, respaldado en Postgres).

    Dominio 4: Monitoring Agents (25%)

    LangSmith otra vez, pero mirando hacia atrás en vez de hacia adelante: trazas de ejecución en producción, latencia por nodo, consumo y coste de tokens, y detección de tendencias de calidad a partir de esas trazas. Activas el tracing con la variable de entorno LANGCHAIN_TRACING_V2=true. Si no tienes claro en qué parte del pipeline se te va el presupuesto, empieza por medir el consumo de tokens de un agente.


    Los 4 errores que hacen suspender a los desarrolladores

    1. Estudiar código de LangChain v0.0.x / v0.1.x: Si en tus prácticas ves from langchain.chains import LLMChain o initialize_agent, estás usando sintaxis deprecada. El examen exige langchain_core y langgraph.
    2. Ignorar el flujo de Streaming: Confundir .stream() con .astream_events() o no saber cómo extraer tokens en tiempo real dentro de un grafo.
    3. No practicar con Checkpointers: Suspender preguntas sobre cómo recuperar el estado anterior de un hilo (thread_id) en LangGraph.
    4. Estudiar solo "Building" e ignorar Deploying y Monitoring: Entre los dos suman la mitad del examen, y son los dominios donde menos contenido gratuito hay. Si tu preparación se queda en notebooks locales con SQLite, vas justo de la mitad de las preguntas.

    Esta disciplina de no depender de tutoriales viejos y estructurar el software antes de escribir código es lo que trabajamos a fondo en el curso Construye con IA: De la Idea al Producto con Claude y Specs, donde aprendes a gobernar agentes con especificaciones formales.


    Plan de estudio de 4 semanas

    Si le dedicas entre 6 y 8 horas semanales, este es el calendario recomendado:

    Semana Dominio Foco de estudio Práctica obligatoria
    Semana 1 Building Agents (I) LCEL y Runnables Construir 3 pipelines usando RunnableParallel, .bind() y .with_fallbacks()
    Semana 2 Building Agents (II) LangGraph, RAG y Tool Calling Montar un agente con ciclo de reflexión, Hybrid Search con Reranking y tool calling
    Semana 3 Testing + Deploying Evals con LangSmith y despliegue en LangGraph Platform Crear un dataset de evaluación, correr LLM-as-a-judge y desplegar el agente con un checkpointer de producción
    Semana 4 Monitoring + Simulacros Trazas, coste y latencia en LangSmith Configurar tracing en producción y hacer tests tipo examen (40 preguntas, 120 minutos)

    ¿Vale la pena obtener la certificación?

    Si eres freelance, consultor técnico o trabajas en una agencia que vende proyectos de IA a clientes corporativos, . En licitaciones y contratos B2B, las certificaciones oficiales son un filtro de confianza rápido.

    Si eres un desarrollador de producto en una startup, la certificación es secundaria: lo que te dará el puesto es tu capacidad para demostrar proyectos en producción con arquitecturas limpias, costes controlados y cero alucinaciones críticas.

    Para ver casos de estudio reales de agentes y arquitecturas con grafos en producción, en Dominicode Labs documentamos experimentos y benchmarks semanales con el ecosistema de IA.


    Preguntas frecuentes

    ¿En qué idioma se presenta el examen de LangChain Certified Agent Engineer?

    El examen se entrega íntegramente en inglés. Los fragmentos de código de las preguntas son mayoritariamente Python, que es donde LangChain y LangGraph tienen más ejemplos y documentación oficial.

    ¿Es necesario saber LangGraph para aprobar la certificación?

    Sí. LangGraph es hoy el componente central del examen para todo lo relativo a agentes, bucles de decisión, memoria conversacional y workflows multi-agente.

    ¿Cuánto tiempo dura la validez del certificado?

    24 meses (2 años) desde la fecha en que apruebas. Pasado ese plazo hay que recertificar, algo razonable dada la velocidad a la que cambian LangChain y LangGraph.

    ¿Cuánto cuesta la certificación LangChain Certified Agent Engineer?

    $99 USD por intento. El examen tiene 40 preguntas de opción múltiple, dura 120 minutos y necesitas acertar 28 de 40 (70%) para aprobar.

    ¿Dónde puedo practicar antes de presentarme?

    La mejor preparación es la documentación oficial de LangChain (v0.3+), los tutoriales de LangGraph Academy y la creación de trazas reales en LangSmith.


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

  • Fetching de Datos en Paralelo en Next.js con Suspense

    Fetching de Datos en Paralelo en Next.js con Suspense

    Hace unos meses audité el dashboard de un SaaS construido con el App Router de Next.js. El usuario iniciaba sesión y la pantalla tardaba 4,2 segundos en mostrar el primer píxel interactivo.

    El equipo pensaba que el cuello de botella estaba en los índices de PostgreSQL o en la memoria de la máquina en Vercel.

    Abrí el archivo page.tsx del dashboard. Había cuatro llamadas con await consecutivas:

    // ❌ El clásico waterfall en Server Components
    const user = await getUser();
    const stats = await getStats(user.id);
    const notifications = await getNotifications(user.id);
    const recommendations = await getExternalRecommendations();
    

    Cada petición esperaba a que la anterior terminara: 400ms + 1.200ms + 600ms + 2.000ms. Un waterfall secuencial de 4,2 segundos. Y peor aún: si el microservicio de recomendaciones caía con un error 504, toda la página devolvía un error 500 al cliente.

    Next.js te ofrece React Server Components por defecto, pero si no estructuras el fetching de datos en paralelo en Next.js, conviertes tu servidor en una fila india de bloqueos innecesarios.

    Aquí te muestro la arquitectura en tres capas para paralelizar llamadas, tolerar caídas de microservicios y hacer streaming instantáneo hacia el navegador.


    1. El antipatrón del Waterfall secuencial

    Cuando colocas llamadas asíncronas consecutivas en el cuerpo de una función de Server Component, la ejecución en Node.js se detiene en cada línea.

    TIEMPO (ms)  0ms       400ms                1600ms          2200ms                    4200ms
                 ├─────────┼────────────────────┼───────────────┼─────────────────────────┤
    Llamada 1:   [getUser]
    Llamada 2:             [getStats]
    Llamada 3:                                  [getNotifs]
    Llamada 4:                                                  [getRecommendations]
                                                                                          ▲
                                                                               Primer render (4.2s)
    

    Las peticiones no arrancan a la vez; arrancan en cascada. Salvo que una llamada requiera obligatoriamente el resultado de la anterior para construir su consulta, ejecutar esto en serie es desperdiciar los hilos de red del servidor.


    2. Nivel 1: fetching de datos en paralelo con Promise.all

    Si necesitas varios bloques de datos indispensables para renderizar la vista y ninguno depende de otro, la primera optimización es disparar todas las promesas al mismo tiempo con Promise.all:

    // app/dashboard/page.tsx
    interface DashboardData {
      user: User;
      stats: UserStats;
      notifications: Notification[];
    }
    
    export default async function DashboardPage() {
      // Inicia todas las promesas en paralelo
      const [user, stats, notifications] = await Promise.all([
        getUser(),
        getStats(),
        getNotifications(),
      ]);
    
      return (
        <main className="p-6">
          <UserProfile user={user} />
          <StatsOverview stats={stats} />
          <NotificationList items={notifications} />
        </main>
      );
    }
    

    La ganancia:

    El tiempo total de espera ya no es la suma de todas las llamadas, sino el tiempo de la más lenta. Si la más lenta tarda 1.200ms, la página resuelve en 1.200ms en lugar de 2.200ms.

    TIEMPO (ms)  0ms             1200ms
                 ├───────────────┤
    getUser:     [==== 400ms ====]
    getStats:    [======== 1200ms =======]
    getNotifs:   [====== 600ms ======]
                                 ▲
                      Resuelve en paralelo (1.2s)
    

    La trampa de Promise.all:

    Promise.all tiene comportamiento de rechazo rápido (fail-fast). Si dos promesas resuelven con éxito pero una falla, toda la llamada lanza una excepción. Úsalo exclusivamente para datos que son 100% obligatorios para la vista.


    3. Nivel 2: Promise.allSettled para tolerancia a fallos

    ¿Qué ocurre cuando una página incluye datos secundarios, como recomendaciones de productos, widgets del clima o analíticas de terceros?

    No tiene sentido romper el perfil del usuario porque una API externa esté caída. Para peticiones secundarias o no bloqueantes, la solución nativa es Promise.allSettled:

    // app/dashboard/page.tsx
    export default async function DashboardPage() {
      const [profileResult, recommendationsResult] = await Promise.allSettled([
        getUserProfile(),
        getThirdPartyRecommendations(),
      ]);
    
      // Si el perfil falla, cortamos porque es crítico
      if (profileResult.status === "rejected") {
        throw new Error("No se pudo cargar el perfil del usuario.");
      }
    
      const profile = profileResult.value;
    
      // Si las recomendaciones fallan, degradamos elegantemente sin romper la UI
      const recommendations =
        recommendationsResult.status === "fulfilled"
          ? recommendationsResult.value
          : [];
    
      return (
        <section>
          <UserProfile user={profile} />
          {recommendations.length > 0 ? (
            <RecommendationCarousel items={recommendations} />
          ) : (
            <p className="text-sm text-gray-500">Recomendaciones no disponibles hoy.</p>
          )}
        </section>
      );
    }
    

    Promise.allSettled garantiza que Node.js esperará a que todas las promesas finalicen, devolviendo un objeto con { status: 'fulfilled', value } o { status: 'rejected', reason }. Tu interfaz resiste caídas parciales sin tirar el servidor.


    4. Nivel 3: React <Suspense> y Streaming para pulverizar el TTFB

    Incluso con Promise.all, si un componente tarda 2,5 segundos, el usuario mirará una pantalla en blanco durante 2,5 segundos antes de ver el primer byte HTML.

    Con React Suspense y Streaming en Next.js, desacoplas la carga de la página del componente más lento.

    La regla de oro: mueve el await dentro del componente que realmente consume los datos.

    // app/dashboard/page.tsx
    import { Suspense } from "react";
    import { UserHeader } from "./components/UserHeader";
    import { HeavyAnalyticsWidget } from "./components/HeavyAnalyticsWidget";
    import { SkeletonWidget } from "./components/SkeletonWidget";
    
    export default function DashboardPage() {
      return (
        <div className="space-y-6">
          {/* Carga inmediata (rápido) */}
          <Suspense fallback={<p>Cargando cabecera...</p>}>
            <UserHeader />
          </Suspense>
    
          {/* Widget pesado: no bloquea el resto de la página */}
          <Suspense fallback={<SkeletonWidget />}>
            <HeavyAnalyticsWidget />
          </Suspense>
        </div>
      );
    }
    
    // app/dashboard/components/HeavyAnalyticsWidget.tsx
    // Este Server Component hace su propio fetching asíncrono
    export async function HeavyAnalyticsWidget() {
      const data = await getHeavyMetrics(); // Tarda 2.5s
      return <MetricsChart data={data} />;
    }
    

    Qué experimenta el usuario:

    1. En 80 milisegundos, el servidor envía la estructura HTML principal, el menú, la cabecera y el skeleton del gráfico.
    2. La página es interactiva de inmediato.
    3. A los 2,5 segundos, Next.js envía por streaming el fragmento HTML del gráfico y React lo reemplaza en el DOM sin recargar la página.

    Si este patrón te suena a fricción con Server Components mal migrados, cubrimos los fallos más comunes en Errores comunes al migrar a React Server Components en producción.

    Esta mentalidad de arquitectura modular y control de estados asíncronos es exactamente lo que desarrollamos en el curso Construye con IA: De la Idea al Producto con Claude y Specs, donde conectamos interfaces modernas en Next.js con servicios backend de alta concurrencia.


    Comparativa: ¿Cuándo usar cada técnica?

    Escenario Técnica recomendada Beneficio clave
    Múltiples datos obligatorios para el layout principal Promise.all Máxima velocidad paralela (tiempo = el de la llamada más lenta)
    Widgets externos o servicios propensos a timeouts Promise.allSettled Resiliencia y degradación elegante
    Componentes lentos o dashboards con múltiples secciones React <Suspense> + Streaming TTFB mínimo y UX instantánea
    Datos con dependencias en cadena (A depende de B) await secuencial estricto Integridad en la cadena de datos

    Arquitectura de fetching de datos en paralelo en producción

    El patrón profesional más sólido en aplicaciones Next.js de gran escala combina los tres enfoques:

    1. El layout principal y la vista estructural no bloquean con await globales innecesarios.
    2. Cada bloque funcional vive en su propio Server Component envuelto en <Suspense>.
    3. Dentro de cada bloque funcional, si se requieren múltiples recursos, se utiliza Promise.all (si son obligatorios) o Promise.allSettled (si toleran fallo).

    Si además necesitas exprimir memoria y builds en producción, ya cubrimos ese terreno en Optimización extrema de rendimiento y consumo de memoria en Next.js 16.

    Para profundizar en optimizaciones avanzadas de memoria, caching y patrones de Server Components en producción, en Dominicode Labs revisamos arquitecturas reales y analizamos benchmarks de rendimiento semana a semana.


    Preguntas frecuentes

    ¿Promise.all en Next.js Server Components ejecuta en el cliente o en el servidor?

    En Server Components (archivos sin 'use client'), Promise.all se ejecuta íntegramente en el entorno de Node.js o Edge del servidor antes de generar o transmitir el HTML al cliente.

    ¿Qué diferencia hay entre Promise.all y Promise.allSettled?

    Promise.all rechaza inmediatamente si cualquiera de las promesas falla (comportamiento todo o nada), mientras que Promise.allSettled espera a que todas concluyan y entrega el estado individual (fulfilled o rejected) de cada una.

    ¿Suspense sustituye por completo a Promise.all?

    No. Se complementan. <Suspense> gestiona el streaming y la interfaz de carga progresiva entre diferentes componentes, mientras que Promise.all gestiona la concurrencia de datos dentro de un mismo componente.

    ¿El uso de Suspense afecta al SEO en Google?

    No. Los rastreadores web modernos de Google esperan la resolución del stream de Server Components y reciben el HTML final renderizado con el contenido completo.


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

  • OpenSpec y Claude Code: integración paso a paso del flujo OPSX

    OpenSpec y Claude Code: integración paso a paso del flujo OPSX

    El lunes le pedí a Claude Code que añadiera paginación a un listado. Lo hizo bien.

    El miércoles abrí una sesión nueva en el mismo proyecto y le pedí un filtro. Se inventó otra forma de paginar, distinta a la del lunes, y reescribió la que ya funcionaba.

    No fue culpa del modelo. El contexto de Claude Code vive en la sesión: cierras la terminal y se evapora.

    OpenSpec con Claude Code resuelve eso: lo acordado vive en archivos versionados dentro del repo y el agente los lee antes de tocar código.

    Tutorial de integración OpenSpec Claude Code, paso a paso: instalación, inicialización y el flujo OPSX de principio a fin.


    Aviso rápido: OpenSpec no es OpenAPI

    Comparten cuatro letras y nada más.

    OpenSpec es un framework open source de spec-driven development para asistentes de código, de Fission-AI. No describe endpoints REST. Si has llegado buscando Swagger, este no es tu post.

    Lo que hace es meter una capa de especificación entre tú y el agente: propuesta, diseño, tareas y spec del cambio. Todo en Markdown, todo dentro del repo, todo bajo control de versiones.


    Paso 1: instalar (y el error de scope que arrastran los tutoriales viejos)

    npm install -g @fission-ai/openspec@latest
    

    Fíjate bien en el scope, porque esto:

    # ❌ NO es OpenSpec
    npm install -g openspec
    

    instala otro paquete distinto, sin relación con el framework. Es el fallo más repetido en tutoriales de hace unos meses, y luego pasas media hora preguntándote por qué openspec init no hace lo que dice la documentación.

    Instala siempre el paquete con scope @fission-ai/.


    Paso 2: inicializar OpenSpec en Claude Code

    Desde la raíz del repo:

    cd tu-proyecto
    openspec init
    

    El init te pregunta qué herramienta usas. Selecciona Claude Code.

    Y aquí el detalle que casi nadie explica bien: para Claude Code te crea las dos cosas.

    .claude/skills/openspec-*/SKILL.md    ← una skill por cada acción del flujo
    .claude/commands/opsx/<id>.md         ← los slash commands
    openspec/config.yaml                  ← la configuración del proyecto
    

    Las skills las carga Claude Code solo, sin que tú hagas nada. Los comandos son la puerta de entrada manual cuando quieres disparar una fase concreta. No eliges entre unas y otros: conviven.

    El config.yaml guarda además tus preferencias entre ejecuciones de init y update. Si mañana actualizas OpenSpec, no te vuelve a preguntar todo.


    Paso 3: llena el config.yaml antes de pedir nada

    Este paso parece opcional. No lo es.

    openspec/config.yaml no es un README que el agente abre si le apetece. Su contenido se inyecta en cada petición de planificación. Va dentro del prompt, siempre.

    Dedica cinco minutos a describir de verdad tres cosas: el stack real con sus versiones, las convenciones que sigues (naming, estructura de carpetas, patrón de tests) y lo que está prohibido en el proyecto — esa librería que ya migraste, ese patrón que odias.

    La diferencia se nota en la primera propuesta. Con el config vacío recibes una propuesta genérica de manual. Con el config bien puesto recibes una que usa tus carpetas, tus nombres y tu forma de testear.

    Un apunte honesto: no inventes claves en el YAML. Completa las que el propio init deja generadas y, si necesitas un campo que no existe, mira la doc oficial.

    Es la misma lógica que trabajamos en el curso Construye con IA: el resultado de un agente depende mucho menos del prompt del momento que del contexto estable que le dejaste montado antes.


    Paso 4: el flujo OPSX de principio a fin

    En Claude Code los comandos van con dos puntos: /opsx:<id>. Este detalle importa y ahora verás por qué.

    /opsx:explore — pensar sin comprometerte

    /opsx:explore
    

    Fase de planificación pura. Exploras el problema, discutes enfoques, descartas caminos. No genera artefactos ni te ata a nada.

    Es el comando que más se omite en los tutoriales y el que más rentabilidad da. Cuando saltas directo a propose, el agente propone algo — y lo propone bien argumentado, con lo cual te lo crees. En explore es donde descubres que el problema real era otro, antes de tener cuatro archivos que revisar.

    /opsx:propose — generar la propuesta

    /opsx:propose añadir filtros por categoría al listado de productos
    

    Aquí se materializa el trabajo:

    openspec/changes/<nombre-del-cambio>/
    ├── proposal.md    ← qué se va a hacer y por qué
    ├── design.md      ← cómo, a nivel técnico
    ├── tasks.md       ← el desglose ejecutable
    └── specs/         ← la delta spec del cambio
    

    Y ahora tu parte: leerlo. Este es el punto exacto donde el flujo funciona o no funciona. Corriges asunciones, ajustas el diseño, partes tareas demasiado grandes. Cuesta minutos ahora y ahorra horas después.

    /opsx:apply — implementar contra la spec

    /opsx:apply
    

    El agente implementa tarea por tarea, referenciando la spec acordada. La diferencia con pedirle código a pelo es que ya no hay margen de interpretación.

    /opsx:update y /opsx:sync — mantener la spec viva

    Antes de archivar, el perfil por defecto trae dos comandos más que casi nadie menciona: /opsx:update revisa los artefactos de un cambio si algo se movió a mitad de camino, y /opsx:sync fusiona la delta spec del cambio dentro de las specs generales del proyecto, para que la spec principal quede al día sin tocarla a mano.

    /opsx:archive — cerrar el cambio

    /opsx:archive
    

    Mueve el cambio a openspec/changes/archive/ y actualiza la fuente de verdad del proyecto. A partir de ahí eso ya no es un cambio pendiente: es cómo funciona tu sistema.

    El perfil extendido (y dónde vive /opsx:verify)

    El perfil por defecto (core) trae los seis comandos que acabas de ver: explore, propose, apply, update, sync y archive. Si necesitas control más granular, cambias de perfil:

    openspec config profile
    openspec update
    

    Eso desbloquea /opsx:new, /opsx:continue, /opsx:ff, /opsx:bulk-archive, /opsx:onboard y, el que más se echa en falta, /opsx:verify: contrasta la implementación contra la spec acordada. No es un test runner, es la comprobación de que no se coló nada que nadie pidió y que no falta nada que sí se pidió.

    Empieza sin el perfil extendido. Actívalo cuando el ciclo base (explore → propose → apply → archive) te sepa corto.


    Delta specs: por qué esto sirve en un proyecto que ya existe

    Aquí está la decisión de diseño que hace a OpenSpec usable en el mundo real.

    La spec de un cambio no describe tu sistema entero. Describe solo lo que se mueve:

    ## ADDED Requirements
    
    ### Requirement: Filtrado por categoría
    El listado DEBE permitir filtrar productos por categoría.
    
    #### Scenario: Usuario selecciona una categoría
    - WHEN el usuario selecciona la categoría "Audio"
    - THEN el listado muestra solo productos de esa categoría
    
    ## MODIFIED Requirements
    
    ## REMOVED Requirements
    

    Escenarios en Markdown plano con sintaxis WHEN/THEN. Sin Gherkin, sin herramientas extra, sin plugins.

    Piensa en la alternativa: especificar entera una aplicación con tres años de historia para poder añadir un filtro. No lo hace nadie, y por eso la mayoría de intentos de SDD en brownfield mueren en la segunda semana. Con deltas, la unidad de trabajo es el cambio, no el sistema.

    Si quieres el marco completo detrás de esto — cómo se escribe una spec que un agente pueda ejecutar sin rellenar huecos por su cuenta — lo desarrollo en el libro de Spec-Driven Development. Y para el reverso de la moneda, ya escribí sobre por qué una spec falla con un agente de IA.


    La sintaxis cambia según la herramienta

    Dato práctico que ahorra confusión cuando copias comandos de un tutorial grabado con otro editor:

    Herramienta Sintaxis
    Claude Code /opsx:propose
    Cursor /opsx-propose
    GitHub Copilot /opsx-propose
    Amazon Q @opsx-propose
    Codex $openspec-propose

    Mismo flujo, distinto prefijo. Si el comando no autocompleta en tu editor, casi siempre es esto.


    Si vienes de un tutorial de hace unos meses

    El flujo pre-OPSX está muerto. Pasó de fases cerradas a acciones, y la traducción es esta:

    Antes Ahora
    /openspec:proposal /opsx:propose
    openspec/project.md openspec/config.yaml
    changes/active/ openspec/changes/

    Si tienes un proyecto con la estructura antigua, no lo migres a mano. Vuelve a ejecutar openspec init y deja que la herramienta reconstruya lo suyo.


    Qué hacer hoy con esto

    Abre un proyecto que ya tengas — uno real, con código feo dentro — y no empieces por una feature grande.

    Instala, ejecuta openspec init, dedica cinco minutos de verdad al config.yaml y lanza un /opsx:explore sobre el próximo cambio pequeño que tenías pendiente. Sigue hasta /opsx:archive. Media hora, un ciclo completo.

    Lo que vas a notar no es velocidad. Es que la siguiente sesión de Claude Code arranca sabiendo lo que se decidió en la anterior. Eso es lo que compras aquí.

    Dos avisos para terminar. Esto no sustituye a revisar el código: sustituye a discutir el mismo diseño tres veces. Y no todo cambio merece el ciclo completo — un fix de dos líneas no necesita una propuesta, y sobre eso escribí en cuándo NO usar spec-driven development.

    Si quieres ver este flujo aplicado a proyectos completos, con los casos donde se rompe y cómo se arregla, lo trabajamos dentro de Dominicode Labs.


    Preguntas frecuentes

    ¿OpenSpec es lo mismo que OpenAPI?

    No. OpenSpec es un framework open source de spec-driven development para asistentes de código, creado por Fission-AI. OpenAPI es una especificación para describir APIs REST. Comparten cuatro letras y nada más.

    ¿Por qué mi instalación de OpenSpec no funciona?

    Lo más probable es que hayas instalado el paquete equivocado. El comando correcto es npm install -g @fission-ai/openspec@latest, con el scope @fission-ai. El paquete llamado openspec a secas es otro proyecto distinto, y es el error que arrastran muchos tutoriales antiguos.

    ¿OpenSpec sirve en un proyecto que ya existe o solo en proyectos nuevos?

    Sirve en proyectos existentes, y esa es su mejor característica. La spec de cada cambio es una delta: solo describe lo que se añade, se modifica o se elimina, con las secciones ADDED, MODIFIED y REMOVED Requirements. No necesitas especificar tu sistema entero para empezar.

    ¿Se puede usar OpenSpec con Cursor o Copilot en vez de Claude Code?

    Sí. El flujo es el mismo y lo que cambia es el prefijo de los comandos. Claude Code usa /opsx:propose con dos puntos, Cursor y Copilot usan /opsx-propose con guion, Amazon Q usa @opsx-propose y Codex usa $openspec-propose. Lo seleccionas al ejecutar openspec init.

    ¿Puedo saltarme el comando explore e ir directo a propose?

    Puedes, pero es donde más gente pierde tiempo. El comando explore es la fase de planificación sin compromiso y sirve para descartar enfoques antes de generar propuesta, diseño, tareas y spec. Si vas directo a propose, acabas revisando cuatro artefactos de una solución que quizá resuelve el problema equivocado.

    ¿Qué hago si seguí un tutorial con el flujo antiguo de OpenSpec?

    Ese flujo ya no es válido. El comando /openspec:proposal pasó a /opsx:propose, el archivo openspec/project.md pasó a openspec/config.yaml y la carpeta changes/active/ pasó a openspec/changes/. Lo más limpio es volver a ejecutar openspec init en el proyecto en lugar de renombrar archivos a mano.


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

  • ChatGPT Business Premium Seats: Precio Real y Cuándo Conviene

    ChatGPT Business Premium Seats: Precio Real y Cuándo Conviene

    Hace dos meses hablé con el CTO de una empresa de 25 personas. Tenían a 14 empleados pagando cuentas individuales de ChatGPT Plus con la tarjeta de la empresa.

    Cada uno con su login personal. Cada uno compartiendo fragmentos de código, contratos y datos de clientes en chats que OpenAI, por defecto en planes personales, podía usar para entrenar sus modelos. Cero control de accesos, cero logs de auditoría y $280 al mes tirados en cuentas aisladas.

    Cuando OpenAI renombró su plan Team a ChatGPT Business y lanzó ChatGPT Business Premium seats como upgrade dentro de ese mismo workspace —junto al contrato Enterprise para volumen alto— la promesa fue sencilla: orden corporativo, privacidad estricta y capacidad computacional dedicada.

    La pregunta que todo líder técnico se hace es otra: ¿te conviene pagar una suscripción mensual fija por cada asiento o es más rentable montar herramientas internas sobre la API?

    Vamos a desglosar los precios reales, las novedades técnicas de estos asientos y el criterio exacto para decidir sin quemar presupuesto.


    La estructura de precios de ChatGPT para empresas

    Para entender dónde encajan los asientos Premium en entornos de trabajo, primero hay que ubicar la pirámide de planes actual de OpenAI:

    Nivel Precio por usuario Compromiso mínimo Privacidad de datos Modelos y Capacidad
    ChatGPT Plus $20 / mes 1 usuario ❌ Opt-out manual necesario Límites estándar de mensajes
    ChatGPT Business (asiento estándar) $20 / mes (anual) / $25 (mensual) Mínimo 2 usuarios ✅ SOC 2 Tipo II + cero entrenamiento Límite de uso de 5h + Workspace compartido
    ChatGPT Business — Asiento Premium $100 / mes (anual) / $125 (mensual) Add-on dentro del mismo workspace ✅ SOC 2 Tipo II + cero entrenamiento 5x más uso, sin límite de 5h
    ChatGPT Enterprise Cotización a medida (mercado: ~$45-75/asiento) Mínimo ~150 licencias ✅ SLA dedicado + BAA HIPAA Ventana 128K-196K + Computación dedicada

    Nota: ChatGPT Team se renombró a ChatGPT Business en agosto de 2025 — no son dos planes distintos, es el mismo workspace con precio actualizado.

    El asiento Premium —lanzado el 10 de agosto de 2026 como upgrade dentro del mismo workspace de Business— cubre la brecha que existía entre el límite de 5 horas del asiento estándar, que a veces frena a los usuarios más intensivos en picos de trabajo, y un contrato Enterprise con comerciales y un mínimo de ~150 licencias.


    Qué trae de nuevo ChatGPT Business Premium seats

    No estás pagando solo "más mensajes por hora". La diferencia técnica está en el aislamiento de infraestructura y las herramientas de gobierno corporativo.

           [ Workspace Corporativo ]
                      │
      ┌───────────────┼───────────────┐
      ▼               ▼               ▼
    SSO / SCIM    Retención Cero  GPTs Internos
    (Okta, Azure)  (No Training)  (Conectores RBAC)
      │               │               │
      └───────────────┬───────────────┘
                      ▼
          Capa de Cómputo Prioritario
    (Modelos de razonamiento sin throttling)
    

    1. Garantía contractual de no entrenamiento y cumplimiento

    En las cuentas personales de ChatGPT Plus, los datos se usan para entrenar futuros modelos a menos que el usuario desactive el historial manualmente.

    En los asientos de empresa (Business, estándar o Premium, y Enterprise), la cláusula es contractual:

    • Cero entrenamiento con tus prompts, código o archivos subidos.
    • Cumplimiento SOC 2 Tipo II, RGPD y opciones de acuerdos de procesamiento de datos (DPA).
    • Retención de datos configurable por política de la empresa (por ejemplo, purgado automático a los 30 o 90 días).

    2. Capacidad dedicada para modelos de razonamiento y Canvas

    Los modelos con cadena de pensamiento profunda y las interfaces interactivas como Canvas consumen órdenes de magnitud más cómputo que un modelo estándar.

    El asiento estándar de Business tiene un límite de uso de 5 horas que frena a los usuarios más intensivos en picos de trabajo. El asiento Premium lo elimina: otorga 5 veces más capacidad de uso y ejecución prioritaria incluso en horas punta globales.

    3. Directorio interno de Custom GPTs y conectores seguros

    En lugar de que cada miembro cree asistentes aislados, el espacio corporativo permite:

    • Desplegar GPTs internos accesibles solo para correos bajo el dominio de la empresa.
    • Asignar permisos basados en roles (RBAC) a fuentes de conocimiento internas.
    • Evitar que un empleado comparta accidentalmente un asistente con datos confidenciales hacia el exterior.

    4. Aprovisionamiento centralizado (SSO y SCIM)

    Si alguien abandona la compañía, revocar su cuenta de Google Workspace o Microsoft Entra ID (Azure AD) cancela automáticamente su acceso a los chats y documentos del workspace de IA, evitando fugas de información.


    Asientos por suscripción vs. Consumo por API: la comparativa real

    Aquí es donde los equipos de desarrollo cometen el error más costoso: asumir que toda la empresa necesita el mismo tipo de acceso.

    Un desarrollador senior no interactúa con la IA igual que un responsable de marketing o un analista financiero.

    PERFIL DE USUARIO          HERRAMIENTA ÓPTIMA              MODELO DE COSTE
    ─────────────────────────────────────────────────────────────────────────────
    Desarrollador              Claude Code / Cursor / CLI      API / Tokens
    Product Manager / Copy     ChatGPT Business / Premium      Tarifa fija ($20-125/mes)
    Automatizaciones Backend   Scripts propios + SDK           Pago por token puro
    

    Si compras un asiento Premium de $125/mes para un developer que pasa el 90% de su día dentro de la terminal o el editor de código, estás pagando un sobreprecio injustificado. Ese desarrollador obtendrá diez veces más valor con herramientas especializadas conectadas a claves de API, donde solo pagas por los tokens reales que entran y salen.

    Si quieres el desglose técnico de cuándo conviene saltar directo a la API en vez de la interfaz de chat, lo cubrimos con ejemplos reales en GPT-5.6 vía API: guía práctica para developers.

    Esta es la base metodológica que trabajamos en el curso Construye con IA: De la Idea al Producto con Claude y Specs: separar las interfaces genéricas de chat de los flujos de ingeniería guiados por especificaciones y agentes en terminal.


    Cuándo conviene contratar ChatGPT Business Premium seats

    Para tomar una decisión clara sin perder semanas en comités internos, aplica esta matriz:

    ✅ Contrata ChatGPT Business (estándar o Premium) si:

    1. Tienes perfiles no técnicos: Producto, marketing, recursos humanos, soporte y ventas que necesitan una interfaz web lista para usar sin configurar entornos ni gestionar claves de API.
    2. Manejas datos confidenciales: Necesitas un acuerdo DPA y la garantía legal de que la información corporativa no alimentará los modelos públicos de OpenAI.
    3. Quieres centralizar costes: Es más fácil aprobar una factura única consolidada con IVA desglosado que reembolsar 20 notas de gastos individuales de $20 cada mes.

    ❌ NO compres asientos Premium si:

    1. Tu equipo son exclusivamente programadores: Para escribir código, refactorizar arquitecturas y revisar pull requests, herramientas como Claude Code, Cursor o plugins con claves de API dedicadas son infinitamente superiores a una ventana de chat en el navegador.
    2. Tu volumen de uso es esporádico: Si un usuario hace 5 consultas a la semana, pagar $20-$125 al mes por asiento es quemar dinero cuando esa misma interacción en la API costaría menos de 50 céntimos.
    3. Buscas automatizaciones masivas: Para procesar miles de tickets o clasificar bases de datos, el chat web no sirve; necesitas pipelines backend con llamadas estructuradas y, antes de escalarlas, medir cuánto estás gastando realmente en tokens.

    Si quieres analizar patrones de costes y arquitecturas eficientes para equipos que integran agentes en producción, en Dominicode Labs evaluamos mensualmente los números reales de operar con modelos propietarios y locales.


    Cómo hacer la migración en 3 pasos

    Si decides unificar tu equipo bajo un workspace corporativo:

    1. Haz inventario de cuentas personales: Identifica cuántos miembros ya tienen ChatGPT Plus pagado por la empresa y fija una fecha límite para cancelarlas.
    2. Arranca con el asiento estándar de ChatGPT Business: No saltes directo a contratos Enterprise a menos que tengas más de 100-150 usuarios y requisitos estrictos de compliance (SLA dedicado, BAA HIPAA). El asiento estándar a $20-25/usuario/mes ofrece la misma garantía SOC 2 y suele ser más que suficiente; sube a Premium solo para quien tope el límite de 5 horas.
    3. Establece directrices de uso: Define qué perfiles operan en el chat corporativo y qué perfiles técnicos deben usar herramientas de terminal conectadas a la API con presupuestos monitorizados.

    Preguntas frecuentes

    ¿Los planes ChatGPT Business y Enterprise usan mis datos para entrenar a la IA?

    No. A diferencia de las cuentas gratuitas y de ChatGPT Plus, en ChatGPT Business (asiento estándar o Premium) y en Enterprise OpenAI tiene el compromiso explícito de no utilizar las conversaciones, archivos ni código para entrenar sus modelos.

    ¿Cuál es el número mínimo de usuarios para contratar ChatGPT Business?

    ChatGPT Business (el plan que hasta agosto de 2025 se llamaba Team) requiere un mínimo de 2 asientos. Si eres un único profesional independiente, la opción estándar sigue siendo ChatGPT Plus o el consumo directo mediante API.

    ¿Puedo tener usuarios con asientos estándar y usuarios con asientos Premium en el mismo workspace?

    Sí. Dentro de un mismo workspace de Business puedes mezclar, asignar y reasignar asientos estándar y Premium según el rol de cada empleado desde el panel de administración, sin necesidad de separar equipos en distintos workspaces ni saltar a Enterprise.

    ¿Qué diferencia hay entre pagar ChatGPT Plus y usar la API?

    ChatGPT Plus es una tarifa plana con interfaz de chat, navegación web y herramientas visuales. La API es un servicio para desarrolladores con cobro exacto por token consumido, mayor flexibilidad de integración y parámetros técnicos de control (temperatura, function calling, structured outputs).


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