← todos los posts
arquitectura·20 de septiembre de 2026·7 min

Cómo blindar los flujos críticos de tus agentes de IA con FSM

Aprendé a blindar tus flujos críticos con contratos tipados y FSM para evitar que tus agentes de IA ejecuten acciones ilegales en producción.

Tu agente de soporte recibe el reclamo de un cliente y tiene que arreglarle el servicio de internet. En el prompt le ponés “verificá la identidad del cliente antes de emitir un reembolso”. La demo funciona impecable. Pero un domingo a las tres de la mañana, con miles de ejecuciones encima, el modelo se saltea la autenticación y aplica un descuento directo.

En este post te muestro cómo blindar tus flujos críticos combinando contratos tipados y máquinas de estados finitos (FSM). Vas a ver cómo evitar mutaciones ilegales separando la planificación de la ejecución.

En pruebas aisladas, confiarle la lógica al prompt parece funcionar en el 98% de los casos. En producción con 50.000 ejecuciones diarias, ese 2% de degradación estadística son transacciones financieras sin autenticar y llamadas de guardia indeseadas.

El problema viene por la degradación de atención (attention drift) en contextos largos, tokens espurios en el historial y ambigüedades al resolver tool calls. Poner instrucciones en mayúsculas dentro del prompt no arregla esto. Necesitamos spec-driven development: contratos tipados, validaciones deterministas y ejecución durable.

Si el orden de ejecución de tu flujo crítico depende sólo de la atención de un modelo probabilístico, compraste un ticket de lotería.


El problema del loop reactivo en agentes de IA

Cuando dejás que el modelo decida el próximo paso en un loop ciego (while True: tool_call()), el sistema prioriza responderte algo coherente antes que cuidar tus datos.

El modelo razona en un espacio semántico y continuo. La lógica de negocio, en cambio, exige transiciones discretas en un grafo cerrado. Volvamos al agente de soporte técnico: si puede llamar indistintamente a fetch_telemetry(), apply_discount() o reboot_node(), tarde o temprano va a compensar con plata a un usuario antes de ver si su cuenta está suspendida por mora o fraude. Este riesgo ya lo vimos al analizar tres patrones de diseño para producción.

La solución es acotar el espacio de búsqueda desde el código. La navegación entre estados tiene que vivir en la infraestructura. El modelo sólo debe extraer datos, sintetizar texto y elegir herramientas dentro de una lista chica autorizada para cada fase puntual.

stateDiagram-v2
    [*] --> AUTHENTICATING
    AUTHENTICATING --> DIAGNOSING : Identidad validada
    AUTHENTICATING --> ESCALATED_TO_HUMAN : Fallo de auth (max 3 reintentos)
    
    DIAGNOSING --> REMEDIATING : Plan sintetizado y validado
    DIAGNOSING --> ESCALATED_TO_HUMAN : Diagnóstico inconcluso
    
    REMEDIATING --> COMPLETED : Postcondiciones verificadas
    REMEDIATING --> ROLLBACK : Fallo de ejecución en nodo
    
    ROLLBACK --> ESCALATED_TO_HUMAN : Compensación ejecutada
    ESCALATED_TO_HUMAN --> [*]
    COMPLETED --> [*]

FSMs como contratos estrictos y tipado de transiciones

Para proteger los flujos críticos unimos Máquinas de Estados Finitos (FSM) con autonomía local. La FSM fija qué transiciones son válidas por código y el modelo opera dentro del nodo activo con herramientas filtradas:

  1. Herramientas autorizadas por estado: En AUTHENTICATING, el esquema JSON de apply_discount ni siquiera viaja en el payload hacia la API del modelo.
  2. Precondiciones deterministas: Evaluás invariantes en código antes de aceptar la transición (por ejemplo, validar que auth_level == "MFA_VERIFIED").
  3. Postcondiciones de negocio: Chequeás aserciones sobre los cambios generados antes de avanzar al siguiente nodo.
  4. Circuit breakers y fallback: Si un nodo falla repetidamente (por ejemplo, tres esquemas inválidos seguidos), cortás la inferencia y pasás la sesión a ESCALATED_TO_HUMAN.

Acá tenés una implementación de referencia en Python. Modela el plan de soporte técnico como un Grafo Acíclico Dirigido (DAG) validado por una FSM:

from typing import List, Literal, Union
from pydantic import BaseModel, Field, model_validator

# 1. Definición formal de pasos atómicos con límites explícitos
class AuthStep(BaseModel):
    step_id: str
    action: Literal["request_mfa", "validate_pin"]
    user_id: str
    depends_on: List[str] = Field(default_factory=list)

class DiagnosisStep(BaseModel):
    step_id: str
    action: Literal["fetch_telemetry", "check_outages"]
    device_id: str
    depends_on: List[str] = Field(default_factory=list)

class RemediationStep(BaseModel):
    step_id: str
    action: Literal["reboot_node", "apply_discount"]
    target_id: str
    amount_cents: int = Field(default=0, le=5000)  # Límite infranqueable: max $50
    compensating_action: str = "revert_discount"   # Primitiva de rollback
    depends_on: List[str] = Field(default_factory=list)

StepType = Union[AuthStep, DiagnosisStep, RemediationStep]

# 2. Artefacto del Plan estructurado como DAG de ejecución
class ExecutionPlan(BaseModel):
    workflow_id: str
    target_state: Literal["AUTHENTICATING", "DIAGNOSING", "REMEDIATING"]
    steps: List[StepType]

    @model_validator(mode="after")
    def validate_dag_integrity(self) -> "ExecutionPlan":
        step_ids = {s.step_id for s in self.steps}
        state_rules = {
            "AUTHENTICATING": AuthStep,
            "DIAGNOSING": DiagnosisStep,
            "REMEDIATING": RemediationStep,
        }
        allowed_type = state_rules[self.target_state]

        for step in self.steps:
            if not isinstance(step, allowed_type):
                raise ValueError(
                    f"Violación de Spec: El paso '{step.action}' no está permitido en '{self.target_state}'."
                )
            for dep in step.depends_on:
                if dep not in step_ids:
                    raise ValueError(f"Dependencia rota: '{dep}' no existe en el plan.")
        return self

Patrón Plan-then-Execute y mitigación de fallas

El patrón Plan-then-Execute separa la fase de planificación de la mutación real en tus sistemas, como se plantea en Plan-and-Solve Prompting. Dividir el flujo te permite usar modelos distintos según el costo y la dificultad de cada etapa:

  • Fase de planificación (Reasoning Tier): Usás un modelo de razonamiento con presupuesto de cómputo en inferencia (test-time compute). El modelo piensa el problema del cliente, resuelve dependencias y devuelve un DAG de pasos con depends_on.
  • Validación sintáctica y semántica: Tu backend pasa ese plan por un linter determinista contra la spec. Si hay un error, le devolvés la falla estructurada al modelo con un límite de dos reintentos. Si no lo resuelve, derivás a una persona.
  • Fase de ejecución y orquestación (Utility/Execution Tier): Con el plan validado, un motor determinista corre los pasos en orden topológico. Para formatear texto o extraer campos puntuales usás modelos rápidos y económicos.
[Entrada de Usuario]
        │
        ▼
[Planificación: Reasoning Tier] ───► Emite DAG de ejecución
        │
        ▼
[Validador Determinista de Spec] ──► ¿Plan válido? ──► NO ──► [Loop corrección (Max 2)] ──► Fallo ──► [Escalamiento Humano]
        │
       SÍ
        ▼
[Motor de Ejecución Durable] ─────► Despacho a APIs / Utility Tier
        │
     ¿Fallo en runtime?
     ┌──┴──┐
    SÍ     NO
     │      │
     ▼      ▼
[Saga Rollback] ──► [Compensación completada]

Manejo de fallas parciales (Patrón Saga)

Imaginate que el plan tiene cuatro pasos: reiniciar el módem en el paso 2 y aplicar un descuento en el 3. Si el paso 3 falla habiendo alterado el módem antes, el runtime no tiene que improvisar. Cada acción tipada declara su compensación (compensating_action). El orquestador recorre en reversa lo ejecutado y corre los rollbacks antes de frenar el estado.


Análisis de tradeoffs

Sumar especificaciones formales a tus agentes te pide balances claros de arquitectura:

Dimensión Loop Reactivo Tradicional FSM + Spec-Driven Architecture
Punto de Control Implícito en el system prompt del modelo Explícito en código, contratos y FSM
Garantías Transaccionales Nulas (sujeto a desvíos de atención) Forzadas por el runtime, dentro del alcance de la spec
Latencia añadida Baja: inferencia directa sin fases Media-Alta: una fase de planning extra (del orden de cientos de ms a segundos, según modelo y tamaño del plan)
Costo por Inferencia Variable y propenso a loops no acotados Mayor costo inicial de planning, pero predecible
Tolerancia a Regresiones Muy baja (cambiar un prompt rompe tools) Alta (la spec actúa como suite de tests)

Yo prefiero asumir la latencia extra de la fase de planning. Te ahorrás lidiar con transacciones inválidas y datos rotos en tu base de datos.


La spec como base para evaluaciones y guardrails

Meter reglas de negocio en prompts gigantes genera regresiones continuas: tocás una frase para arreglar una herramienta y rompés otra que no tenía nada que ver.

Tener especificaciones formales te da tres ventajas claras:

  1. Evals como tests unitarios e integrales: Tus benchmarks dejan de depender del criterio difuso de un LLM-as-a-judge. Pasan a ser aserciones booleanas, algo clave cuando montás un pipeline de evaluación de LLMs en CI/CD. Frente a tu dataset de prueba validás en código: ¿el plan emitido violó la FSM?, ¿respetó los topes de plata?
  2. Guardrails en la capa de transporte: En vez de rezarle al prompt, frenás los problemas con un middleware. Si los parámetros de una llamada superan los límites del esquema, bloqueás la petición a la API en la red.
  3. Control de versiones auditable: Los contratos y esquemas se guardan en Git con el resto del sistema. Cualquier cambio en las reglas de negocio pasa por un Pull Request con tests de regresión automáticos.

Plan de acción para ingeniería

  1. Auditá el grafo de herramientas: Revisá qué funciones exponés hoy. Si el modelo puede escribir, borrar o mover plata sin que el sistema valide llamadas previas, separá esa herramienta ya mismo.
  2. Implementá una FSM con ejecución durable: Sumá una herramienta de durable execution como Temporal o grafos persistidos. Te asegura que los estados sobrevivan a caídas de red manteniendo la consistencia.
  3. Desacoplá planificación de ejecución: Pasá tus flujos críticos a dos fases. Usá un tier de razonamiento para armar el DAG y un runner determinista para ejecutar las acciones aprobadas.
  4. Definí primitivas de compensación: Asignale a cada acción mutable un rollback explícito para evitar estados corruptos si algo falla en el medio.
  5. Construí un dataset de evals adversarios: Creá tests diseñados para forzar saltos de estado prohibidos en tu soporte técnico. El validador tiene que frenar el intento antes de tocar cualquier servicio interno.

El determinismo en el software no le saca autonomía a tus agentes: es lo único que hace posible usarlos en producción en serio.