Inicio / Artículos

Los frameworks de agentes razonan: Temporal sobrevive gracias a su ejecución duradera para agentes.

LangGraph, CrewAI y AutoGen orquestan las llamadas a los LLM, pero evitan caídas o esperas de una semana. Cómo la ejecución duradera de Temporal llena esa brecha, y cuándo resulta excesiva.

Editor: REACTAPP.TOP11 min de lectura2460 palabras

Fuentes: Agentic AI Frameworks in 2026: The 3 Everyone’s Using and the 1 Nobody Talks About

Los ingenieros de encuestas que desarrollan sistemas agentes en 2026 hablan sobre sus frameworks y casi todos mencionan LangGraph, CrewAI, AutoGen o alguna combinación de ellos. Ninguna de esas herramientas es mala. El problema es que toda la conversación tiene lugar en el nivel incorrecto de la arquitectura.

Los sistemas agentes en producción presentan un patrón recurrente. Lo que despierta al ingeniero de guardia a las 3 a.m. rara vez es el framework que determina lo que piensa el agente. Casi siempre se trata de un mismo tipo de fallos: un proceso muere a mitad de un flujo de trabajo, un paso se ejecuta dos veces porque su intento de reintentar no fue idempotente, la expulsión de un pod termina con una tarea del agente en ejecución desde hace 40 minutos sin posibilidad de reanudarla, o el estado del flujo de trabajo se almacenaba en una variable de Python que desapareció cuando se recicló el contenedor.

Ninguno de LangGraph, CrewAI ni AutoGen maneja ese proceso de extremo a extremo, y eso es intencional, ya que existen para orquestar llamadas a modelos de lenguaje grande y el uso de herramientas. Los sistemas de agentes en producción realmente fracasan por la falta de una ejecución duradera, un campo mucho más antiguo y menos desarrollado que data aproximadamente de una década antes del término “agente”. Temporal, el sistema que lo desarrolla, apenas aparece en los debates sobre marcos de agentes, pero se ha convertido silenciosamente en la columna vertebral detrás de un número creciente de implementaciones serias de agentes. Lo que sigue explica qué hacen bien estos tres marcos populares, dónde falla cada abstracción y por qué la cuarta opción, menos popular, merece una inversión empresarial.

LangGraph

La contribución de LangGraph consiste en convertir a un agente en un grafo explícito basado en estados tipados, en lugar de una cadena implícita de instrucciones. Eso tiene un valor real, ya que el flujo de control se vuelve inspeccionable y verificable. Un flujo de trabajo de triaje, por ejemplo, podría verse así:

from typing import TypedDict, Literal
from langgraph.graph import StateGraph, END

class TriageState(TypedDict):
    ticket_id: str
    category: Literal["billing", "technical", "abuse", "unknown"]
    priority: int
    assigned_queue: str | None

def classify_node(state: TriageState) -> TriageState:
    category = llm_classify(state["ticket_id"])  # the only probabilistic step
    return {**state, "category": category}

def route_node(state: TriageState) -> str:
    # deterministic, testable, no LLM involved
    return {
        "billing": "billing_queue",
        "technical": "tech_queue",
        "abuse": "trust_and_safety_queue",
        "unknown": "human_review",
    }[state["category"]]

graph = StateGraph(TriageState)
graph.add_node("classify", classify_node)
graph.set_entry_point("classify")
graph.add_conditional_edges("classify", route_node)
app = graph.compile()

Esa es una buena idea, está bien redactada. Los problemas surgen cuando algo debe mantenerse activo más allá de una sola llamada a invoke() y su punto de control. Piense en la coordinación entre servicios, flujos de trabajo que duran días, o pasos que se ejecutan donde LangGraph no tiene influencia: un trabajo de Spark, una cola de aprobaciones que pasa por Slack y devuelve resultados días después, o un servicio que otro equipo despliega según su propio ritmo. El sistema de puntos de control funciona, pero solo abarca el modelo de ejecución propio de LangGraph. Tras la transferencia a infraestructura que no tiene conexión con los LLM, la durabilidad se convierte en algo que hay que implementar manualmente.

CrewAI

Se puede declarar un equipo multiagente en unas pocas líneas:

from crewai import Agent, Task, Crew

researcher = Agent(role="Research Analyst", goal="Find relevant market data", tools=[search_tool])
writer = Agent(role="Report Writer", goal="Turn research into a client-ready summary")

crew = Crew(agents=[researcher, writer], tasks=[
    Task(description="Research {company}'s Q2 earnings", agent=researcher),
    Task(description="Write a 3-paragraph summary for the client", agent=writer),
])
crew.kickoff(inputs={"company": "Acme Corp"})

Ninguna herramienta logra pasar de una idea inicial para un flujo de trabajo multiagente a una demostración funcional en veinte minutos más rápido que CrewAI. El costo aparece después. Los bloques kickoff() no ofrecen una forma adecuada de pausarlos durante horas o días, no cuentan con comportamiento de reintentos más allá de lo que está asociado a cada herramienta individual, y no mantienen un estado persistente salvo lo que el desarrollador guarda. Orquesta el razonamiento de los LLM pero no es un motor de flujos de trabajo, y nunca se promocionó como tal, aunque muchos equipos terminan tratándolo así por accidente.

AutoGen

Una configuración mínima de AutoGen combina a un asistente con un proxy de usuario:

import autogen

assistant = autogen.AssistantAgent(name="assistant", llm_config={"config_list": config_list})
user_proxy = autogen.UserProxyAgent(name="user_proxy", human_input_mode="NEVER")
user_proxy.initiate_chat(assistant, message="Summarize this quarter's incident reports.")

El estilo conversacional y emergente de AutoGen es adecuado para trabajos exploratorios. Su versión de 2026, con un entorno de trabajo más estructurado y patrones basados en eventos, incluso tiende a separar la orquestación de la conversación. Bajo todo esto, la base sigue siendo una transcripción de mensajes en lugar de un registro duradero y tipado de las ejecuciones. Cuando el proceso que alberga la conversación falla, lo que queda es un registro de chat parcial y no un cálculo que pueda reanudarse.

La brecha común

Todos los tres son eficaces en lo que realmente es nuevo: hacer que un LLM razoné, utilice herramientas y trabaje con otros agentes impulsados por LLM. La preocupación más antigua y sencilla, asegurarse de que un proceso de múltiples pasos se complete correctamente sin importar si hay fallos, reinicios, tiempos de espera o problemas en sus componentes subyacentes, no es algo que intenten abordar. Eso no es culpa suya; esperarlo de ellos es un error conceptual.

Piense en un flujo de trabajo real, no en una demostración: un agente que reconcilia facturas de proveedores. Este extrae los ítems de una factura en PDF, los compara con un pedido de compra en un sistema ERP, marca las discrepancias que superan un umbral para que las revise un humano y luego puede esperar días la respuesta antes de registrar la entrada aprobada en contabilidad. Si un paso falla, se debe intentar nuevamente sin realizar un registro doble. Una reconfiguración en medio del flujo de trabajo no debe causar que todo se reinicie desde cero ni desaparezca silenciosamente.

┌────────────┐     ┌───────────────┐     ┌─────────────────┐     ┌───────────────┐
│  Extract   │ --> │ Match against │ --> │  Human approval │ --> │  Post to ERP  │
│  line items│     │ purchase order│     │  (can take days)│     │  (idempotent) │
└────────────┘     └───────────────┘     └─────────────────┘     └───────────────┘
      ▲                                          │
      └───────────── retry on failure ───────────┘

Nadie diseñó LangGraph, CrewAI o AutoGen con la promesa de que este diagrama sobreviva a un reinicio del contenedor en el tercer día mientras alguien sigue evaluando el ítem 14. Eso no tiene que ver con la orquestación de LLMs. Se trata de una ejecución duradera, y los sistemas que lo hacen bien, con Temporal a la vanguardia, han impulsado tareas de backend poco llamativas durante años: procesamiento de pagos, cumplimiento de pedidos y microservicios basados en el patrón saga, mucho antes de que la palabra “agente” describiera a tales programas.

Cómo funciona Temporal

El modelo de Temporal parece sorprendentemente simple. El flujo de trabajo se escribe como una función en código ordinario, con bucles, condicionales y llamadas a funciones, y Temporal hace que el historial de ejecución de dicha función sea duradero. Si el proceso se cae en cualquier momento, Temporal vuelve a ejecutar la función a partir de su historial de eventos persistido hasta el punto del fallo y continúa de manera determinista, sin volver a ejecutar los efectos secundarios. La magia oculta radica en que los efectos secundarios, como llamar a un LLM, acceder a una API o escribir en una base de datos, se encuentran en funciones separadas llamadas actividades. Temporal vuelve a intentar ejecutarlas de forma independiente con un retardo configurable, mientras que la función del flujo de trabajo solo las coordina.

El agente de conciliación de facturas que se muestra a continuación trata la llamada al LLM como una actividad entre varias. Primero, las actividades, que son las partes que interactúan con el mundo exterior:

# activities.py — the parts that touch the outside world (LLMs, APIs, DBs)
from temporalio import activity
from dataclasses import dataclass

@dataclass
class Invoice:
    invoice_id: str
    vendor: str
    line_items: list[dict]

@activity.defn
async def extract_line_items(pdf_url: str) -> list[dict]:
    # calls a vision-capable LLM, or a dedicated OCR model — doesn't matter to the workflow
    text = await download_and_ocr(pdf_url)
    return await llm_extract_structured(text, schema=LINE_ITEM_SCHEMA)

@activity.defn
async def match_purchase_order(invoice: Invoice) -> dict:
    po = await erp_client.get_purchase_order(invoice.vendor)
    discrepancies = diff_line_items(invoice.line_items, po["line_items"])
    return {"po_id": po["id"], "discrepancies": discrepancies}
@activity.defn
async def post_to_erp(invoice_id: str, po_id: str) -> dict:
    # idempotency is enforced with a deterministic key derived from workflow inputs,
    # not a random UUID generated per attempt — this is the same lesson as always
    return await erp_client.post_journal_entry(
        idempotency_key=f"invoice-post-{invoice_id}",
        invoice_id=invoice_id,
        po_id=po_id,
    )

@activity.defn
async def notify_human_reviewer(invoice_id: str, discrepancies: list[dict]) -> None:
    await slack_client.post_message(
        channel="#ap-review",
        text=f"Invoice {invoice_id} has {len(discrepancies)} discrepancies needing review.",
    )

A continuación viene el flujo de trabajo, que asume el papel de orquestación que normalmente desempeñarían LangGraph o CrewAI, con la diferencia de que su durabilidad está integrada y no depende de activar un checkpointer:

# workflows.py
from temporalio import workflow
from datetime import timedelta

@workflow.defn
class InvoiceReconciliationWorkflow:
    def __init__(self) -> None:
        self._approval_received = False
        self._approved = False

    @workflow.signal
    async def submit_human_decision(self, approved: bool) -> None:
        # A signal is how the outside world talks to a running, possibly-days-old workflow
        self._approval_received = True
        self._approved = approved

    @workflow.run
    async def run(self, pdf_url: str, invoice_id: str, vendor: str) -> dict:
        line_items = await workflow.execute_activity(
            extract_line_items, pdf_url,
            start_to_close_timeout=timedelta(minutes=2),
            retry_policy=workflow.RetryPolicy(maximum_attempts=3),
        )

        invoice = Invoice(invoice_id=invoice_id, vendor=vendor, line_items=line_items)
        match_result = await workflow.execute_activity(
            match_purchase_order, invoice,
            start_to_close_timeout=timedelta(seconds=30),
            retry_policy=workflow.RetryPolicy(maximum_attempts=5),
        )

        if match_result["discrepancies"]:
            await workflow.execute_activity(
                notify_human_reviewer, invoice_id, match_result["discrepancies"],
                start_to_close_timeout=timedelta(seconds=10),
            )
            # This is the line that would be genuinely hard to write safely in the
            # other three frameworks: wait, durably, for up to 7 days, for a signal
            # that might arrive from a Slack button, a web form, anything —
            # while the workflow's entire state sits safely in Temporal's history,
            # costing zero compute while it waits.
            await workflow.wait_condition(
                lambda: self._approval_received,
                timeout=timedelta(days=7),
            )
            if not self._approved:
                return {"status": "rejected", "invoice_id": invoice_id}

        result = await workflow.execute_activity(
            post_to_erp, invoice_id, match_result["po_id"],
            start_to_close_timeout=timedelta(seconds=30),
            retry_policy=workflow.RetryPolicy(maximum_attempts=5),
        )
        return {"status": "posted", "invoice_id": invoice_id, "erp_result": result}

Varios detalles merecen atención, ya que son precisamente los modos de fallo que hacen que los sistemas de agentes sean inestables, y aquí se trata de propiedades del entorno de ejecución y no de características que alguien deba recordar agregar.

Una espera de siete días es gratuita y resiste todo tipo de situaciones. workflow.wait_condition no mantiene abierto ningún hilo, proceso ni contenedor durante una semana. Temporal almacena el estado del flujo de trabajo en su propio almacenamiento duradero, por lo que el trabajador puede ser redeployado, escalar a cero o incluso colapsar por completo mientras tanto. Cuando finalmente llega la señal, Temporal reproduce el historial y reanuda exactamente en ese await. Hacer lo mismo con un checkpointer de LangGraph o una crew de CrewAI significaría, en esencia, reconstruir el modelo basado en eventos de Temporal.

Las reintentos son automáticos, tienen un alcance limitado y nunca repiten el trabajo ya completado. Supongamos que post_to_erp falla después de que match_purchase_order tenga éxito. Temporal no volverá a ejecutar match_purchase_order, ya que su resultado ya se encuentra en el historial duradero. Comparemos esto con la visión simplista de la misma idea:

# BEFORE — naive retry loop, the kind you write at 11pm under deadline pressure
async def reconcile_invoice_naive(pdf_url, invoice_id, vendor):
    for attempt in range(3):
        try:
            line_items = await extract_line_items(pdf_url)
            match_result = await match_purchase_order(Invoice(invoice_id, vendor, line_items))
            if match_result["discrepancies"]:
                await notify_human_reviewer(invoice_id, match_result["discrepancies"])
                approved = await wait_for_approval_somehow(invoice_id)  # good luck
                if not approved:
                    return {"status": "rejected"}
            return await post_to_erp(invoice_id, match_result["po_id"])
        except Exception:
            continue  # retries EVERYTHING from the top, including re-extracting the PDF

Esa versión ingenua vuelve a realizar la extracción con el LLM en cada intento, gastando tokens y dinero en un paso que ya funcionó, y wait_for_approval_somehow no ofrece una solución adecuada si debe sobrevivir a un redeploy. La versión de Temporal no aborda estos problemas mediante código adicional; gracias a la estructura de ejecución, esos problemas nunca surgen.

Python puro implica que es posible probarlo. Temporal incluye un marco de pruebas que permite saltarse una espera de timedelta(days=7) en milisegundos dentro de una prueba unitaria, y las actividades pueden simularse sin necesidad de conectar con la red:

import pytest
from temporalio.testing import WorkflowEnvironment
from temporalio.worker import Worker

@pytest.mark.asyncio
async def test_rejected_invoice_does_not_post_to_erp():
    async with await WorkflowEnvironment.start_time_skipping() as env:
        async with Worker(
            env.client, task_queue="test-queue",
            workflows=[InvoiceReconciliationWorkflow],
            activities=[extract_line_items, match_purchase_order, notify_human_reviewer, post_to_erp],
        ):
            handle = await env.client.start_workflow(
                InvoiceReconciliationWorkflow.run,
                args=["fake.pdf", "INV-001", "Acme"],
                id="test-invoice-001",
                task_queue="test-queue",
            )
            await handle.signal(InvoiceReconciliationWorkflow.submit_human_decision, False)
            result = await handle.result()
            assert result["status"] == "rejected"

Observe las ausencias: no hay un sueño real de siete días, no se insiste en los mecanismos internos de los puntos de verificación, ni se realiza razonamiento manual sobre los reinicios. La durabilidad es una infraestructura de soporte, y no algo que cada prueba deba demostrar una y otra vez.

Uso de ambas capas

La respuesta equilibrada no consiste en descartar LangGraph en favor de Temporal. Funcionan en capas diferentes, y las mejores configuraciones de producción de 2026 las combinan: LangGraph o CrewAI para el razonamiento dentro de un paso, y Temporal para la capa de flujo de trabajo que vincula el proceso de varios días y servicios. Una actividad puede contener un paso de razonamiento delimitado de esta manera:

@activity.defn
async def run_agent_reasoning_step(ticket_text: str) -> dict:
    # LangGraph handles the bounded, single-turn reasoning —
    # classify, decide, draft — inside an activity that Temporal
    # retries and times out like any other side effect.
    result = triage_graph.invoke({"ticket_id": ticket_text, "audit_log": []})
    return {"category": result["category"], "priority": result["priority"]}

@workflow.defn
class SupportTicketWorkflow:
    @workflow.run
    async def run(self, ticket_id: str, ticket_text: str) -> dict:
        triage = await workflow.execute_activity(
            run_agent_reasoning_step, ticket_text,
            start_to_close_timeout=timedelta(seconds=30),
            retry_policy=workflow.RetryPolicy(maximum_attempts=3),
        )
        if triage["priority"] >= 8:
            await workflow.execute_activity(page_oncall_engineer, ticket_id, ...)
            await workflow.wait_condition(lambda: self._acknowledged, timeout=timedelta(minutes=15))
        # ... routing, SLA timers, escalation chains — all durable, all testable

Esta es la arquitectura que se recomienda para un equipo cuyo sistema de agentes debe resistir el entorno de producción. El marco nativo para LLMs se encarga de lo que sabe hacer mejor: razonar dentro de pasos limitados. Un motor de ejecución duradero se ocupa de lo para lo que fue diseñado, asegurando que todo el proceso, ya sea que dure minutos o semanas y ya sea que incluya llamadas a LLMs o aprobaciones humanas, finalice correctamente o falle de manera evidente. Ninguno de los dos tiene que asumir el papel del otro.

Donde los costos temporales son demasiado elevados

Temporal tiene desventajas que lo convierten en una opción deficiente para muchos trabajos de agentes. Se trata de otro sistema distribuido que hay que ejecutar, ya sea un servidor autohospedado o una factura de Temporal Cloud, además de workers y colas de tareas; es decir, mucha más infraestructura que un proceso en Python que ejecuta LangGraph. El código de los flujos de trabajo también debe seguir reglas estrictas de determinismo. Está prohibido llamar directamente a datetime.now(), utilizar aleatoriedad sin control o realizar operaciones de E/S directas dentro de la función del flujo de trabajo. Los equipos necesitan tiempo para asimilar esto, y el incumplimiento de las reglas genera errores de no determinismo difíciles de resolver al reproducir el proceso. Para tareas cortas con un solo proceso, como responder a una pregunta o resumir un documento en una sola llamada al LLM, la ejecución duradera representa simplemente una carga adicional, ya que no existe un proceso de múltiples pasos que pueda dañarse por una caída.

Regla de decisión

Elija una ejecución duradera como pilar fundamental siempre que un flujo de trabajo pueda superar la vida útil de un proceso único, necesite coordinarse entre servicios o esperar a una persona, o maneje elementos que no deben procesarse dos veces, como dinero, inventario o un correo electrónico que no debe enviarse en repetidas ocasiones. Deje que el marco LLM preferido funcione como un componente dentro de uno de sus pasos. Cuando todo comienza y termina dentro de un único ciclo de solicitud-respuesta, ómprelo, ya que agregarlo resolvería un problema que aún no existe.

Muchas discusiones sobre “¿qué framework de agentes debo usar?” pasan por alto una pregunta más importante: ¿qué capa del sistema realmente necesita reforzamiento? LangGraph, CrewAI y AutoGen mantienen un debate valioso sobre cómo estructurar el razonamiento de los LLM y la coordinación entre múltiples agentes. Pero la cuestión de si un sistema de agentes sobrevive a un despliegue defectuoso, a una partición de red o a que una persona tarde tres días en hacer clic en “aprobar” es otra distinta, y los motores de flujo de trabajo ya la resolvieron en silencio, al gestionar ejecuciones duraderas mucho antes de que se hablara de IA de agentes. Los frameworks populares ajustan lo que se muestra en las demostraciones. El que se pasa por alto decide si algo seguirá en pie un mes después.

Leer más

  • Identidad para agentes en ejecución prolongada: tokens de delegación y un bucle de rehidratación — Por qué las claves estáticas fallan para los agentes autónomos y cómo la delegación OBO, un plano de auditoría y un bucle de rehidratación permiten que los agentes con estado funcionen de forma segura en entornos serverless de corta duración.