AI Agents 2026: El 90% de tu Agente Debería Ser Código Determinista
Deja de construir agents autónomos que se rompen. El 90% del código debe ser determinista. State machines, DAGs y LLM acotado en 3 pasos.
El 90% de los AI Agents en Producción Fallan por el Mismo Motivo: Demasiada Libertad
No es culpa del modelo. No es culpa del contexto. Es culpa de tu `while True: llm.invoke()`.
El 90% de los AI Agents que ves en producción se rompen en menos de 5 iteraciones. Loops infinitos. Tool calls alucinadas. Ventanas de contexto saturadas. Y el desarrollador mirando los logs pensando "pero si en el demo funcionaba perfecto".
*El problema no es que la IA no sea lo suficientemente inteligente. Es que le das demasiada libertad. *
La narrativa dominante dice: "haz los agentes más autónomos y serán más poderosos". La realidad es la opuesta: la autonomía es enemiga de la fiabilidad. El agente más efectivo no es el que más decide. Es el que menos decide, y lo que decide lo hace dentro de una cárcel de constraints bien diseñados.
Mira los datos: el asistente de código Devin, de Cognition, causó hype mundial. Su tasa de éxito real en SWE-bench: ~14%. Cursor, en cambio, que usa un pipeline multi-etapa con análisis estático determinista antes de cualquier llamada LLM, domina el mercado. No es casualidad.
---
El Gran Engaño: Autonomía vs Orquestación
1. El Fracaso de los "Agentes Autónomos" de 2023
AutoGPT alcanzó 160.000 estrellas en GitHub. BabyAGI corrió como la pólvora. Y luego la realidad: la mayoría de implementaciones colapsaban en 3-5 iteraciones.
No fue un fracaso de los LLMs. Fue un fracaso de arquitectura.
El patrón ReAct puro — `while True: llm.invoke()` — es frágil por diseño. Cada iteración añade tokens al contexto, cada tool call alucinada multiplica el espacio de errores, y no hay un mecanismo nativo para decir "para, esto no tiene sentido".
2. El Patrón que Sí Funciona: DAG + LLM Puntual
Los sistemas de agentes más robustos en producción usan orquestación determinista para el ~90% de la ejecución. El LLM solo decide en puntos concretos, acotados, con outputs estructurados y límites duros.
La evidencia es aplastante:
OpenAI y Anthropic recomiendan explícitamente límites de tool calls (10-25 máximo) y condiciones de parada explícitas. Una admisión tácita de que los loops autónomos sin cota degradan la fiabilidad.
LangChain añadió `max_iterations=15` por defecto en su `AgentExecutor` después de reportes masivos de runaway agents.
Los agentes basados en LangGraph (state machine sobre DAG) muestran un 20-30% más de tasa de finalización de tareas que los chains lineales con el mismo LLM.
El debate "agent vs chain" es un falso binario. La solución real son los *hybrid DAGs*: 80-90% de cableado fijo, 10-20% de decisiones LLM.
❌ Free-form loop: `while True: llm.invoke(tools=[...])` — sin estado, sin límites, sin fallbacks.
✅ State machine with LLM nodes: estados explícitos, transiciones válidas definidas, LLM solo decide en 1-2 nodos concretos con output estructurado.
---
Cómo Construir un Agent Híbrido: El Patrón de 3 Capas
Aquí va el framework. Lo llamo El Patrón de 3 Capas: DAG + State Machine + Circuit Breaker. Lo he aplicado en producción para sistemas de extracción de datos, orquestación de APIs y generación de contenido. Funciona.
Paso 1: Mapea tu Dominio como un DAG
Divide tu flujo de trabajo en nodos de ejecución. Identifica cuáles necesitan realmente razonamiento LLM y cuáles son transformaciones deterministas.
Target: menos del 30% de nodos LLM. Ideal: menos del 15%.
```python
from enum import Enum
from typing import Callable, Any
import networkx as nx
class NodeType(Enum):
TRANSFORM = "transform" # Pure Python: no LLM
MAP = "map" # Pure Python: no LLM
FILTER = "filter" # Pure Python: no LLM
LLM_DECIDE = "llm_decide" # LLM needed: semantic decision
class ExecutionNode:
def __init__(self, name: str, node_type: NodeType, fn: Callable):
self.name = name
self.node_type = node_type
self.fn = fn
self.max_retries = 1 if node_type == NodeType.LLM_DECIDE else 0
self.fallback = None
Build DAG: 8 nodes are deterministic, 2 are LLM
dag = nx.DiGraph()
dag.add_node("validate_input", node=ExecutionNode("validate_input", NodeType.TRANSFORM, validate_input_fn))
dag.add_node("classify_intent", node=ExecutionNode("classify_intent", NodeType.LLM_DECIDE, classify_intent_fn))
dag.add_node("extract_fields", node=ExecutionNode("extract_fields", NodeType.TRANSFORM, extract_fields_fn))
dag.add_node("generate_response", node=ExecutionNode("generate_response", NodeType.LLM_DECIDE, generate_response_fn))
dag.add_node("format_output", node=ExecutionNode("format_output", NodeType.TRANSFORM, format_output_fn))
Wire edges
dag.add_edge("validate_input", "classify_intent")
dag.add_edge("classify_intent", "extract_fields")
dag.add_edge("extract_fields", "generate_response")
dag.add_edge("generate_response", "format_output")
Execute DAG with topological sort
def execute_dag(dag: nx.DiGraph, initial_input: dict) -> Any:
results = {}
for node_name in nx.topological_sort(dag):
node = dag.nodes[node_name]["node"]
input_data = results.get(list(dag.predecessors(node_name))[-1]) if dag.predecessors(node_name) else initial_input
for attempt in range(node.max_retries + 1):
try:
results[node_name] = node.fn(input_data)
break
except Exception as e:
if attempt == node.max_retries and node.fallback:
results[node_name] = node.fallback(input_data)
elif attempt == node.max_retries:
raise
return results
```
Paso 2: State Machine Wrapper
Cada estado tiene transiciones explícitas. El LLM solo puede elegir entre un conjunto acotado de siguientes estados. Si intenta saltar a uno inválido, la máquina lanza error.
```python
from enum import Enum, auto
from typing import Dict, Set
class AgentState(Enum):
IDLE = auto()
VALIDATING = auto()
CLASSIFYING = auto()
EXTRACTING = auto()
GENERATING = auto()
FORMATTING = auto()
DONE = auto()
ERROR = auto()
Valid transitions matrix
VALID_TRANSITIONS: Dict[AgentState, Set[AgentState]] = {
AgentState.IDLE: {AgentState.VALIDATING},
AgentState.VALIDATING: {AgentState.CLASSIFYING, AgentState.ERROR},
AgentState.CLASSIFYING: {AgentState.EXTRACTING, AgentState.ERROR},
AgentState.EXTRACTING: {AgentState.GENERATING, AgentState.ERROR},
AgentState.GENERATING: {AgentState.FORMATTING, AgentState.ERROR},
AgentState.FORMATTING: {AgentState.DONE, AgentState.ERROR},
AgentState.DONE: set(),
AgentState.ERROR: {AgentState.IDLE}, # Reset only
}
class StateMachineAgent:
def __init__(self):
self.current_state = AgentState.IDLE
self.iteration_count = 0
self.max_iterations = 25
def transition_to(self, next_state: AgentState):
if next_state not in VALID_TRANSITIONS[self.current_state]:
raise ValueError(
f"Invalid transition: {self.current_state} -> {next_state}. "
f"Allowed: {VALID_TRANSITIONS[self.current_state]}"
)
self.iteration_count += 1
if self.iteration_count > self.max_iterations:
raise RuntimeError(f"Max iterations ({self.max_iterations}) exceeded. Forcing reset.")
self.current_state = next_state
def run(self, input_data: dict):
LLM only decides in CLASSIFYING & GENERATING states
Everything else is deterministic wiring
self.transition_to(AgentState.VALIDATING)
validated = validate(input_data)
if not validated["is_valid"]:
self.transition_to(AgentState.ERROR)
return {"error": "validation_failed", "details": validated["errors"]}
self.transition_to(AgentState.CLASSIFYING)
LLM call: only returns "extraction" | "generation" | "unknown"
intent = llm_classify_intent(validated["data"])
if intent == "unknown":
self.transition_to(AgentState.ERROR)
return {"error": "unrecognized_intent"}
self.transition_to(AgentState.EXTRACTING)
extracted = extract_fields(validated["data"])
self.transition_to(AgentState.GENERATING)
LLM call: only returns structured JSON matching Pydantic model
response = llm_generate_response(extracted)
self.transition_to(AgentState.FORMATTING)
formatted = format_output(response)
self.transition_to(AgentState.DONE)
return formatted
```
Paso 3: Circuit Breaker + Quota de Tool Calls
El error más común en agents es la ventana de contexto explotando por tool calls sin control. Implementa un `ToolCallManager` con cuota estricta.
```python
class MaxToolCallsExceeded(Exception):
pass
class ToolCallManager:
def __init__(self, max_calls_per_step: int = 5, max_total_calls: int = 25):
self.max_calls_per_step = max_calls_per_step
self.max_total_calls = max_total_calls
self.step_calls = 0
self.total_calls = 0
def reset_step(self):
self.step_calls = 0
def consume(self) -> bool:
"""Returns True if call is allowed, raises if exceeded."""
if self.step_calls >= self.max_calls_per_step:
raise MaxToolCallsExceeded(
f"Max calls per step ({self.max_calls_per_step}) exceeded. "
"Try reducing task complexity or breaking into smaller steps."
)
if self.total_calls >= self.max_total_calls:
raise MaxToolCallsExceeded(
f"Max total calls ({self.max_total_calls}) exceeded. "
"Circuit breaker triggered. Manual review required."
)
self.step_calls += 1
self.total_calls += 1
return True
```
Paso 4: Output Estructurado con Pydantic + JSON Mode
Nunca parsees texto libre de un LLM. Usa `response_model` de Instructor o `json_mode` nativo. Define modelos Pydantic para cada salida.
```python
from pydantic import BaseModel
from typing import Literal
from openai import OpenAI
import instructor
client = instructor.from_openai(OpenAI())
class ClassifyOutput(BaseModel):
intent: Literal["extraction", "generation", "summarization", "unknown"]
confidence: float # 0.0 to 1.0
reasoning: str
class GenerateOutput(BaseModel):
title: str
body: str
key_points: list[str]
tone: Literal["formal", "neutral", "casual"]
LLM call with structured output — never parse free text
def llm_classify_intent(text: str) -> ClassifyOutput:
return client.chat.completions.create(
model="gpt-4o",
response_model=ClassifyOutput,
messages=[{
"role": "system",
"content": "Classify the user request into one of four intents."
}, {
"role": "user",
"content": text
}]
)
def llm_generate_response(context: dict) -> GenerateOutput:
return client.chat.completions.create(
model="gpt-4o",
response_model=GenerateOutput,
messages=[{
"role": "system",
"content": "Generate a structured response based on the extracted context."
}, {
"role": "user",
"content": str(context)
}]
)
```
Paso 5: Monitorización de Divergencia
Loggea cada tool call, cada decisión, cada ruta de error. Cuando un agente excede el 80% de sus iteraciones permitidas, es señal de que la tarea no encaja en el patrón acotado. No añadas más capacidad al LLM. Rediseña el DAG.
```python
import logging
from datetime import datetime
class AgentMonitor:
def __init__(self, agent_name: str):
self.agent_name = agent_name
self.tool_calls = []
self.decisions = []
self.errors = []
def log_tool_call(self, tool_name: str, input_snapshot: dict, output_snapshot: dict):
self.tool_calls.append({
"timestamp": datetime.utcnow().isoformat(),
"tool": tool_name,
"input": input_snapshot,
"output": output_snapshot
})
def log_decision(self, state: str, choice: str, confidence: float):
self.decisions.append({
"timestamp": datetime.utcnow().isoformat(),
"state": state,
"choice": choice,
"confidence": confidence
})
def check_divergence(self, current_iteration: int, max_iterations: int):
if current_iteration > max_iterations * 0.8:
logging.warning(
f"[{self.agent_name}] Agent at {current_iteration}/{max_iterations} iterations "
f"({(current_iteration/max_iterations)*100:.1f}%). "
"This task may not fit the constrained pattern. Review DAG design."
)
```
---
Por Qué el Patrón de Multi-Agente No Escala
Añadir más agentes (CrewAI, sistemas multi-agente) multiplica el espacio de fallos exponencialmente. Cada agente extra añade latencia, ruido de contexto y posibilidad de desviación.
Los benchmarks lo confirman: *"one agent, well-constrained" supera a "five agents, loosely coupled"* en tareas estructuradas.
El paralelismo con la ingeniería de software tradicional es directo: no escribes código dejando que un proceso aleatorio decida la siguiente línea. Usas compiladores, type checkers, linters y tests — todos sistemas deterministas. El LLM es el generador de sugerencias, no el orquestador. El orquestador debe ser código determinista escrito por humanos.
Si tu sistema necesita LLM en más del 30% de los nodos, el dominio no está bien definido para agents. Vuelve al Paso 1.
---
La Objeción que te Estás Haciendo
"Si limito tanto al LLM, ¿para qué uso un agente? Podría hacerlo con código normal."
Tienes razón a medias. De hecho, muchas tareas no necesitan un agente. Los LLMs destacan exactamente en ese 10-20% de puntos de decisión donde la lógica determinista falla: desambiguación semántica, generación de variantes, análisis de lenguaje natural. El resto debe ser código.
La pregunta no es "¿cómo hago mi agente más autónomo?". Es "¿cuál es el mínimo de LLM que necesito para que este sistema funcione?".
La respuesta casi siempre es: menos del que crees.
---
Resumen y Siguiente Paso
Tres verdades incómodas sobre AI Agents en 2026:
1. La autonomía es enemiga de la fiabilidad. Cada grado de libertad que le das al LLM es un nuevo modo de fallo que no has visto aún.
2. El mejor agente es el que menos decide. Un DAG con 8 nodos deterministas y 2 nodos LLM bien acotados supera a cualquier free-form loop con el mismo modelo.
3. El orquestador debe ser código, no otro LLM. La orquestación determinista con state machines, circuit breakers y output estructurado no es opcional. Es la línea entre un demo bonito y un sistema que aguanta en producción.
El futuro de los AI Agents no son sistemas autónomos planeando y ejecutando sin supervisión. Esos no existen fuera de los demos controlados. El futuro son herramientas semiautónomas con guardarraíles duros, fallbacks deterministas y puntos de decisión LLM acotados como quién pone a un niño en una trona con barandillas.
Tu agente no necesita ser más autónomo. Necesita mejores esposas.
Lee el artículo completo en brianmenagomez.com
Más sobre mis servicios en brianmenagomez.com
Herramientas: Conversor IAE CNAE · Gestorias cerca de ti · Calculadora IRPF

