← todos los posts
agentes·20 de septiembre de 2026·6 min

Tres patrones de diseño para que tu agente no explote en producción

Descubrí cómo ordenar tu agente con patrones de diseño para evitar loops infinitos y fallas en producción sin inflar costos.

Tu agente recibe el pedido de un usuario para migrar un schema de base de datos, llama a tres APIs distintas y revienta en la primera respuesta real. Intentar que un solo prompt resuelva todo en una pasada (zero-shot) es apostar plata de infraestructura al azar.

Para construir sistemas confiables en 2026, la salida no es esperar un modelo perfecto. La salida es aplicar patrones de agentes bien definidos. Con estos patrones ordenás el flujo, separás responsabilidades y usás test-time compute solo donde aporta valor.

Acá te explico cómo funcionan tres patrones de agentes clave, cómo combinarlos y qué costos tienen. Vamos a usar un caso concreto: un agente que analiza código, corre migraciones de base de datos y genera reportes.


Planner / Executor: separar la estrategia de la ejecución

El error más común con tool calling es dejar que el LLM decida sobre la marcha en cada paso. Si tu agente analiza código y corre migraciones, los JSON crudos y los logs llenan el context window. El modelo pierde de vista la instrucción inicial (attention drift), entra en bucles infinitos o inventa parámetros. Para estos casos conviene revisar cómo blindar el tool calling con esquemas estrictos e idempotencia.

El patrón Planner / Executor divide el trabajo en dos etapas claras:

  1. Planner (Razonamiento puro): Un modelo de frontera con soporte de thinking tokens recibe el objetivo. Devuelve un grafo acíclico dirigido (DAG) de pasos validado con Pydantic. No toca herramientas, solo proyecta dependencias y precondiciones.
  2. Executor (Determinismo y bajo costo): Un worker en código tradicional o un modelo compacto (sub-8B/sub-14B) toma cada nodo del grafo. Llama a las herramientas y guarda los resultados de forma aislada, sin ensuciar el contexto global.
from pydantic import BaseModel, Field
from typing import List, Optional

class Step(BaseModel):
    id: str = Field(..., description="Identificador único del paso")
    tool_name: str = Field(..., description="Herramienta a ejecutar")
    input_payload: dict = Field(..., description="Parámetros estructurados para la API")
    depends_on: List[str] = Field(default_factory=list, description="IDs de pasos previos requeridos")

class ExecutionPlan(BaseModel):
    objective: str
    steps: List[Step]
flowchart TD
    UserReq["User Request"] --> Planner["Planner (Tier de Razonamiento / Thinking Tokens)"]
    Planner --> DAG["Structured Plan (Pydantic / DAG)"]
    DAG --> ExecLoop["Executor Loop (Worker determinista / LLM compacto sub-8B)"]
    ExecLoop --> Tools["External Tools / APIs"]
    Tools --> ExecLoop
  • Cuándo usarlo: Procesos batch, migraciones de datos o pipelines con pasos predecibles pero composición dinámica (por ejemplo: análisis estático de código, extracción de dependencias y generación de reportes).
  • Cuándo NO usarlo: Entornos con mucha incertidumbre donde el resultado de la primera llamada suele cambiar todo el plan. Ahí necesitás un nodo de re-planning, no una ejecución fija.

Maker / Checker: validación asimétrica y desacople de sesgos

Si le pedís a un modelo que escriba una consulta SQL para la migración y en el mismo prompt le preguntás si está bien, te va a decir que sí. Tiende a defender lo que acaba de generar por pura coherencia interna.

El patrón Maker / Checker separa la creación del control en dos piezas distintas:

  • Maker: Un modelo generalista que redacta el código o estructura los datos.
  • Checker: Un validador que mezcla reglas fijas (linters, parsers de AST, validadores de esquemas) con un LLM evaluador (llm-as-a-judge). Ese LLM tiene un prompt antagónico pensado solo para encontrar fallas.
flowchart TD
    Start["User Request"] --> Maker["Maker (Generación)"]
    Maker --> Checker["Checker (Linter / Tests / Judge)"]
    Checker --> Decision{"¿Pasa validación?"}
    Decision -- "Sí" --> End["Output validado"]
    Decision -- "No (max_retries > 0)" --> Feedback["Feedback con stack trace / error exacto"]
    Feedback --> Maker
    Decision -- "No (límite alcanzado)" --> Fallback["Fallback / Human-in-the-loop"]

En producción, conviene usar las mismas validaciones de tu CI/CD como compuertas lógicas dentro del grafo. Podés mirar cómo armar un pipeline de evaluación en CI/CD para integrar estas reglas sin frenar el despliegue.


Reflection / Self-Critique: la trampa del loop cerrado

En el patrón de Reflection, el agente mira lo que hizo antes, evalúa los errores y prueba de nuevo con ese aprendizaje.

El problema es que la autocrítica probabilística sin datos reales no mejora el resultado y muchas veces lo empeora (Huang et al., 2023). Si el modelo inventa un método del SDK al armar el script de migración, pedirle que “reflexione sobre su código” no sirve. Vuelve a cometer el mismo error. Para que funcione necesitás señales externas: el código de salida de un compilador, un error HTTP 404 real o el fallo de un test unitario.

from pydantic import BaseModel
from typing import Optional

class CodeGenerationState(BaseModel):
    task: str
    code: str = ""
    error_feedback: Optional[str] = None
    iterations: int = 0
    max_iterations: int = 3
    is_valid: bool = False

def run_in_sandbox(code: str) -> tuple[int, str, str]:
    # Trade-off de infraestructura:
    # Aislamiento seguro (gVisor/Wasm/MicroVM) añade latencia por iteración según el entorno.
    # Evitar siempre el uso de exec()/eval() directos en el proceso host.
    ... # Implementación de ejecución aislada
    return exit_code, stdout, stderr

def checker_node(state: CodeGenerationState) -> CodeGenerationState:
    exit_code, stdout, stderr = run_in_sandbox(state.code)
    
    if exit_code == 0:
        state.is_valid = True
        state.error_feedback = None
    else:
        state.is_valid = False
        state.error_feedback = f"Exit code {exit_code}.\nStderr: {stderr}\nStdout: {stdout}"
    
    state.iterations += 1
    return state

def maker_node(state: CodeGenerationState, llm_client) -> CodeGenerationState:
    prompt = f"Task: {state.task}\n"
    if state.error_feedback:
        prompt += f"\nPrevious attempt failed:\n{state.error_feedback}\nFix the root cause."
    
    state.code = llm_client.generate(prompt=prompt, system="Output pure Python code only.")
    return state

El trade-off central: test-time compute vs latencia y costos

Sumar control cuesta plata y tiempo. Si cambiás una sola llamada por grafos con validaciones, gastás más test-time compute: consumís más tokens, hacés más llamadas en paralelo y sube la latencia.

Patrón Tier de Modelo Típico Latencia Costo Token Nivel de Determinismo Caso de Uso Óptimo
Zero-shot Prompt Generalista económico Base (1x) Base (1x) Mínimo Extracción y clasificación simple
Planner / Executor Planner: Reasoning tier<br>Executor: Compacto (sub-8B) 1.5x – 2x 1.5x – 2x Alto Orquestación de APIs y flujos multi-herramienta
Maker / Checker Maker: Generalista<br>Checker: AST / LLM Judge 2x – 3x 2x – 3x Muy Alto Generación de SQL crítico, migraciones, finanzas
Reflection Loop Generalista / Code-specialized Variable (Nx) Proporcional (Nx) Alto (con sandbox) Reparación de tests, refactorización de código

Multiplicadores orientativos (no medidos), relativos a una sola llamada sin patrón.


Cómo componer estos patrones de agentes

En producción los patrones se combinan por capas para balancear costos y fallas. Un planificador arma las tareas y cada paso crítico corre dentro de su propio circuito de validación:

flowchart TD
    User["User Request"] --> Planner["Planner Node (Reasoning Tier)"]
    Planner --> SubDAG["Sub-task DAG"]
    
    subgraph SubAgent["Maker-Checker Execution Sub-Graph"]
        SubDAG --> Maker["Maker (Genera artefacto)"]
        Maker --> Checker["Checker (Linter + Sandbox)"]
        Checker --> CheckResult{"¿Tests OK?"}
        CheckResult -- "No" --> Reflect{"¿Iteraciones < Max?"}
        Reflect -- "Sí" --> Feedback["Inyectar Stderr"] --> Maker
    end
    
    CheckResult -- "Sí" --> Aggregate["Aggregate Results"]
    Reflect -- "No" --> Fallback["Human-in-the-loop / Dead Letter Queue"]
    Aggregate --> Output["Final Response"]

Si querés profundizar en la estructura de control, podés ver cómo comparar bucles y grafos en agentes para definir tus transiciones de estado.


Qué hacer el lunes

  1. Buscá puntos de falla única: Encontrá los mega-prompts que resuelven negocio, ejecutan herramientas y se evalúan a sí mismos en una sola pasada.
  2. Definí contratos de salida: Obligá a tus modelos a responder con esquemas validados (Pydantic o JSON Schema) antes de pasarle datos a otro servicio.
  3. Meté validaciones deterministas en el hot-path: Usá chequeos rápidos (linters, parsers o regex) en el medio del flujo. Dejá las evaluaciones pesadas con LLM fuera de las llamadas sincrónicas del usuario.
  4. Fijá límites duros: Todo loop de corrección tiene que tener un tope de reintentos (max_retries <= 3) y un límite de tokens. Si no resuelve en esa ventana, mandalo a un fallback o a una persona.

Que un agente funcione en producción no depende de la fuerza bruta del modelo. Depende de armar una arquitectura que le marque límites claros y cuide cada llamada.