Inicio / Artículos

Elegir un marco de agentes según la forma de la carga de trabajo: seis ejes y una ruta de decisión

Elige un marco de agente según la forma de la carga de trabajo: LangGraph, CrewAI, AutoGen, Temporal o ninguno, evaluado en términos de determinismo, vida útil, idempotencia, auditoría y costo.

Editor: REACTAPP.TOP11 min de lectura2439 palabras

Fuentes: Your AI Agent Framework Is Probably the Wrong One Here’s How to Actually Choose

Preguntar qué framework de agente utilizar es la pregunta inicial incorrecta. Lo más adecuado es determinar la forma que tiene la carga de trabajo, ya que el framework se deriva de esa respuesta y no debe elegirse de forma aislada a partir de publicaciones de pruebas de rendimiento. Los equipos que toman una decisión en la primera semana basándose en un hilo de redes sociales suelen pasar de los meses tres a los seis descubriendo que las suposiciones fundamentales del framework no coinciden con lo que están desarrollando. Rara vez es el framework el culpable. Nadie definió las características que requería la carga de trabajo antes de elegir la herramienta destinada a proporcionarlas.

En lugar de comparar características, este enfoque identifica seis ejes que determinan la adecuación, muestra cómo se ven en el código y luego construye un camino de decisión. Un ejemplo en ejecución ilustra esto: un agente de triaje de alertas de infraestructura. Es sencillo, realista y abarca cada eje. El agente debe decidir si una alerta representa un problema real, actuar enviando una notificación a alguien o corrigiéndolo automáticamente, a veces esperar horas a que responda un humano, y nunca enviar una notificación a un ingeniero de guardia dos veces a las 4 a.m. solo porque se intentó nuevamente dos veces.

Eje uno: ¿hasta qué punto debe ser determinista la ramificación?

Este eje se omite por el momento y se aborda más adelante. La prueba adecuada es preguntarse si alguien resultará perjudicado si la misma entrada sigue un camino de código diferente el 2% de las veces. Algunas ramificaciones toleran variaciones, como elegir el tono de un correo de resumen.

# A branch where non-determinism is FINE — picking a tone for a summary email.
# If the agent occasionally phrases things slightly differently, nobody's paged.
def draft_summary_tone(context: dict) -> str:
    return llm_call(
        prompt=f"Summarize this incident in a {context['audience']}-appropriate tone.",
        temperature=0.7,  # variability here is a feature, not a bug
    )

Otros no lo hacen, como decidir si despertar a una persona a las 4 a.m. o automedicarse. Esa decisión debe estar en el código y no en un comando.

# A branch where non-determinism is NOT fine — deciding whether to page a human
# at 4am versus auto-remediating. This must be code, not a prompt.
def route_alert(alert: dict) -> str:
    if alert["severity"] == "critical" and alert["service"] in PAGE_ALWAYS_SERVICES:
        return "page_oncall"
    if alert["auto_remediation_available"] and alert["confidence"] > 0.9:
        return "auto_remediate"
    if alert["severity"] == "critical":
        return "page_oncall"
    return "log_and_monitor"

Cuando las decisiones importantes en un flujo de trabajo se presentan como una función determinista y auditable, algo que un revisor de cumplimiento querría leer como código simple, el marco debe mantener esa lógica alejada del LLM. Los bordes condicionales de LangGraph se adaptan bien a esto, ya que la función de enrutamiento es un Python ordinario y el modelo solo accede a las partes que se le entregan intencionadamente, como la clasificación o la redacción.

CrewAI y AutoGen están diseñados de forma inversa, permitiendo que el razonamiento propio del agente decida qué sucede a continuación. Eso es una ventaja cuando el objetivo es la flexibilidad creativa, pero representa un riesgo cuando esa flexibilidad implica omitir ocasionalmente las verificaciones de cumplimiento. Si las ramas más importantes deben ser deterministas, esa condición por sí sola descarta a una buena parte de las soluciones existentes.

Segundo eje: ¿cuánto tiempo dura una unidad de trabajo?

Una unidad de corta duración comienza y termina dentro de una sola solicitud HTTP. Esta es la zona donde no se necesita ningún framework, ya que su uso supondría un mero sobrecargo.

# Short-lived: starts and finishes inside one HTTP request.
# This is the "no framework needed" zone — a framework here is pure overhead.
async def handle_summarize_request(request: SummarizeRequest) -> SummarizeResponse:
    text = await fetch_document(request.doc_id)
    summary = await llm_summarize(text, max_tokens=300)
    return SummarizeResponse(summary=summary)

Una unidad de larga duración es diferente. Una alerta puede permanecer esperando la confirmación humana durante seis horas mientras el ingeniero de turno duerme, y luego reanudarse en otro proceso después de que una implementación haya reemplazado los pods subyacentes.

# Long-lived: this alert might sit in "awaiting human ack" for six hours
# while the on-call engineer is asleep, then resume on a completely
# different process after a deploy rotated the pods underneath it.
class AlertTriageWorkflow:
    async def run(self, alert: dict) -> dict:
        decision = await self.classify_and_route(alert)
        if decision == "page_oncall":
            await self.page(alert)
            await self.wait_for_ack(timeout_hours=1)  # this line is the whole ballgame
        ...

Si el trabajo se completa de manera fiable en segundos y permanece exclusivamente en la memoria de un proceso, es probable que la cuestión relacionada con el framework esté siendo sobreanalizada. Una función asíncrona sencilla con un decorador de intentos múltiples supera a cualquier framework en términos de mantenibilidad, ya que no hay nada que orquestar, y aprender el modelo de estado de LangGraph o ejecutar un clúster Temporal para un endpoint de resumen sin estado implica costos sin beneficios reales.

Una vez que una unidad de trabajo puede sobrevivir a un proceso, al esperar a una persona, a un trabajo por lotes o al SLA de otro equipo, se vuelve necesaria una ejecución duradera medida en horas. Ninguno de los tres frameworks populares de orquestación de LLM fue diseñado para funcionar como capa de durabilidad. Este enfoque apunta hacia soluciones como Temporal, o al menos hacia un sistema desarrollado manualmente que incluya estado persistente además de una cola de tareas con pasos reanudables.

Eje tres: ¿qué pasa si un paso se ejecuta dos veces?

Por lo general, los equipos encuentran este caso durante la revisión de incidentes. La primera versión mostrada parece estar bien en una demostración, pero representa un incidente que está a punto de ocurrir, ya que la reintentada actividad reinicia el servicio nuevamente.

# BEFORE — looks fine in a demo, is a live incident waiting to happen
async def auto_remediate(alert: dict):
    await restart_service(alert["service"])  # what if this activity gets retried?

La segunda versión es idempotente por diseño.

# AFTER — idempotent by construction
async def auto_remediate(alert: dict, idempotency_key: str):
    if await remediation_ledger.already_applied(idempotency_key):
        logger.info("remediation already applied, skipping", key=idempotency_key)
        return await remediation_ledger.get_result(idempotency_key)
    result = await restart_service(alert["service"])
    await remediation_ledger.record(idempotency_key, result)
    return result

La clave de idempotencia debe provenir de algo estable en los datos empresariales, como el ID de la alerta, y nunca de un UUID generado en cada intento, ya que de lo contrario las reintentos dejan de ser seguros. Esto corresponde a la función de la herramienta y no a ningún framework, pero afecta a qué frameworks son tolerables, ya que algunos facilitan olvidar que se realizó un reintento. La ejecución basada en chat de AutoGen no proporciona ninguna señal estructural que indique si una llamada a la herramienta es un reintento o una decisión nueva. Desde el punto de vista de la transcripción, el agente simplemente decidió llamar a la herramienta nuevamente, y si se trata de un reintento o de una acción duplicada no se registra.

Cuando los efectos secundarios son costosos, peligrosos o involucran cambios en dinero o en la infraestructura, la configuración necesita un framework, o una capa personalizada debajo de él, que trate si el efecto secundario ya ocurrió como un problema de primera importancia y no como algo deducido del registro de chat.

Eje cuatro: ¿quién lee la decisión más tarde y cómo?

Lo importante en un análisis posterior es un registro de auditoría independiente del marco utilizado, sea cual sea el proceso que haya orquestado los pasos.

# A framework-agnostic audit record — this is what actually matters
# in a postmortem, regardless of what orchestrated the steps.
@dataclass
class DecisionRecord:
    alert_id: str
    timestamp: float
    step: str
    reasoning: str        # what the LLM said, verbatim
    decision: str         # the structured outcome, not prose
    confidence: float | None
    human_override: bool

async def log_decision(record: DecisionRecord):
    await audit_store.insert(record)
    # Also emit as a structured log line — cheap insurance for when
    # the audit store itself is the thing that's down during an incident.
    logger.info("agent_decision", **asdict(record))

Cada framework permite registrar cierta información. La pregunta es si su estructura genera naturalmente un registro claro y consultable de las decisiones, o si para obtenerlo es necesario luchar contra la abstracción. Un objeto de estado de LangGraph pasa por nodos nombrados, por lo que preguntar cuál era el estado justo antes de una decisión de enrutamiento equivale a realizar una consulta directa en un TypedDict que nunca se convirtió en texto libre. Una conversación de AutoGen debe analizarse para reconstruir los mismos datos, ya que su formato de memoria nativo es prosa y no un estado estructurado. En cualquier entorno con obligaciones de cumplimiento, un equipo de auditoría o la costumbre de realizar análisis exhaustivos posteriores, este aspecto merece más importancia de la que sugiere el material de marketing.

Eje cinco: ¿qué limita la velocidad del equipo?

Esto rara vez aparece en un gráfico de comparación técnica y, sin embargo, a menudo decide el resultado. Una startup formada por cuatro personas que verifica si los agentes pueden llevar a cabo el trabajo en absoluto no debería pasar tres semanas aprendiendo las restricciones de determinismo de Temporal antes de escribir cualquier lógica de negocio.

# Week-one prototype: prove the concept fast, accept the debt knowingly.
from crewai import Agent, Task, Crew

triage_agent = Agent(role="Alert Triage", goal="Decide how to handle infra alerts")
crew = Crew(agents=[triage_agent], tasks=[Task(description="Triage: {alert}", agent=triage_agent)])
crew.kickoff(inputs={"alert": alert_payload})

Ese es un efecto completamente legítimo de la primera semana. El error no radica en haber elegido CrewAI para el prototipo, sino en que el equipo, seis meses después, nunca volvió a revisar esa decisión cuando estaban en juego tareas reales y cambios reales en la infraestructura, ya que el prototipo ya funcionaba. La elección de un marco técnico tiene una fecha de vencimiento relacionada con la cantidad de operaciones que se realicen en el sistema, y anotar esa fecha al momento de elegirlo evita que se establezca por defecto.

Eje seis: ¿cuál es la latencia y el presupuesto de costos por decisión?

Cada decisión de enrutamiento que implica una llamada al LLM se multiplica en una conversación entre varios agentes con varias interacciones, y con volúmenes de alertas de cientos al día, e incluso miles durante un incidente, la factura crece rápidamente.

# Expensive pattern: every routing decision is its own LLM call,
# multiplied across a multi-agent conversation with several turns.
# At alert volumes (hundreds/day, sometimes bursts of thousands during
# a real incident), this is a real line item, not a rounding error.
async def route_via_llm(alert: dict) -> str:
    return await llm_call(f"How should we handle this alert? {alert}")

# Cheaper, faster, and more auditable: cheap deterministic pre-filtering
# in code, LLM reserved for genuinely ambiguous cases.
async def route_alert_efficiently(alert: dict) -> str:
    if alert["service"] in KNOWN_NOISY_SERVICES and alert["severity"] == "low":
        return "log_and_monitor"          # zero LLM calls for the common case
    if alert["signature"] in KNOWN_REMEDIATION_PLAYBOOK:
        return "auto_remediate"           # deterministic lookup, zero LLM calls
    return await llm_call(f"Novel alert, needs judgment: {alert}")  # LLM only when genuinely needed

Se trata más bien de una disciplina arquitectónica que de un eje de framework que ayude a cualquiera de ellos. Los frameworks que facilitan la combinación de código determinista económico con llamadas a modelos costosas permiten ahorrar dinero real a gran escala. LangGraph lo hace, al igual que un motor de flujos de trabajo como Temporal, que encapsula una llamada a un LLM como una sola actividad. La alternativa es un patrón nativo que dirige todo a través de otro turno de conversación mediado por un modelo, que es la configuración predeterminada de AutoGen cuando speaker_selection_method está establecido en auto.

Un camino de decisión, no un árbol de decisiones

Un diagrama de flujo que genere una sola respuesta sería engañoso, ya que las regiones se superponen. El camino que se muestra a continuación se acerca más a la forma en que realmente se toma la decisión.

Is this unit of work stateless and finishes in seconds?
  └─ YES → skip the framework entirely. Plain functions + retries. Ship it.
  └─ NO, continue.

Does it need to survive process restarts / wait on humans for hours-to-days?
  └─ YES → you need durable execution (Temporal or equivalent) as the backbone,
           regardless of what else you pick for the reasoning layer.
  └─ NO, continue.

Are the important branches safety- or compliance-critical
(money, infra changes, irreversible external actions)?
  └─ YES → LangGraph-style explicit graphs, keep LLM scoped to narrow nodes.
  └─ NO, mostly exploratory/creative → CrewAI or AutoGen are legitimate defaults.

Is this still a prototype whose findings might get thrown away?
  └─ YES → optimize for speed of iteration over long-term correctness,
           but write down when you'll revisit that tradeoff.

Al aplicarlo al agente de triaje de alertas, el resultado es un diseño en capas. Este sobrevive a los reinicios y espera a que intervengan los humanos, por lo que se necesita una capa de ejecución duradera. Su rama más importante es crítica para la seguridad, ya que se trata de la opción de notificación versus la de autoremediación; por eso se requiere un enrutamiento explícito al estilo de LangGraph dentro de cada paso. Además, ya está muy avanzado más allá de la fase de prototipo una vez que cuenta con un sistema de notificación. El aspecto temporal se convierte en la columna vertebral, con un paso de orquestación de LLMs dentro de un ámbito específico como una de las actividades que lo componen.

@activity.defn
async def classify_alert_activity(alert: dict) -> dict:
    # LangGraph-style graph runs here — bounded reasoning, deterministic routing —
    # inside an activity Temporal will retry and time-box like any other side effect.
    result = alert_triage_graph.invoke({"alert": alert, "audit_log": []})
    return {"decision": result["decision"], "confidence": result["confidence"]}

@workflow.defn
class AlertTriageWorkflow:
    def __init__(self):
        self._acked = False

    @workflow.signal
    async def acknowledge(self):
        self._acked = True

    @workflow.run
    async def run(self, alert: dict) -> dict:
        classification = await workflow.execute_activity(
            classify_alert_activity, alert,
            start_to_close_timeout=timedelta(seconds=20),
            retry_policy=workflow.RetryPolicy(maximum_attempts=3),
        )
        if classification["decision"] == "page_oncall":
            await workflow.execute_activity(page_oncall, alert, start_to_close_timeout=timedelta(seconds=10))
            await workflow.wait_condition(lambda: self._acked, timeout=timedelta(hours=1))
            if not self._acked:
                await workflow.execute_activity(escalate_to_secondary, alert, start_to_close_timeout=timedelta(seconds=10))
        elif classification["decision"] == "auto_remediate":
            await workflow.execute_activity(
                auto_remediate, alert, f"remediate-{alert['id']}",
                start_to_close_timeout=timedelta(minutes=2),
                retry_policy=workflow.RetryPolicy(maximum_attempts=2),
            )
        return {"alert_id": alert["id"], "decision": classification["decision"]}

La elección real de un framework se ve así en la práctica, y no es una respuesta satisfactoria que se pueda dar en una sola línea. Se trata de una decisión de composición, y esa capacidad de composición es la verdadera habilidad.

Errores que siguen repitiéndose

  • Elegir según el número de estrellas en GitHub. La popularidad indica el apoyo de la comunidad y cuántas soluciones existen en línea. No dice nada sobre si el modelo de ejecución principal cumple con los requisitos de durabilidad y determinismo que necesita la carga de trabajo.
  • Dejar que el framework del prototipo se convierta en permanente. El prototipo que funcionaba bien de forma sencilla se transforma en el sistema de producción, porque reescribir la orquestación parece no añadir nuevas funcionalidades. En realidad, añade fiabilidad, algo que permanece invisible hasta que falta.
  • Comenzar con un motor de flujos de trabajo de alto rendimiento. El fracaso opuesto es igualmente real: un equipo lee demasiados artículos sobre ejecución duradera y despliega Temporal detrás de un bot de respuesta a preguntas de uso único que nunca sobreviviría a un reinicio del proceso. La herramienta debe adaptarse a la carga de trabajo en ambos sentidos.
  • Tratar un marco de trabajo de agentes como la solución definitiva. Este abarca la capa de razonamiento y uso de herramientas, pero rara vez las capas de durabilidad, idempotencia y observabilidad, y por eso los aspectos mencionados anteriormente suelen quedar fuera del material de marketing de cualquier marco de trabajo.
  • La verdadera respuesta

    No existe un marco de trabajo de agentes óptimo, y pensar que sí lo hay es la razón por la cual muchos de estos sistemas deben ser reestructurados ya después de dieciocho meses. Sí existe un marco adecuado para una carga de trabajo con un conjunto específico de seis propiedades. La disciplina necesaria para trabajar en equipo consiste en identificar esas propiedades antes incluso de tocar un marco de trabajo, y una vez hecho eso, la elección suele quedar clara por sí sola. Cuando se omite este paso, la capa de orquestación debe ser reescrita dentro de un año, independientemente del marco elegido, ya que el problema nunca radicó en el marco en sí, sino en no haber preguntado primero qué se estaba construyendo antes de decidir cómo hacerlo.

    Leer más

  • El ajuste fino ahora es económico, pero los agentes siguen necesitando un mecanismo para los pesos — Cómo convertir las lecciones almacenadas de un agente en pesos mediante auto-distilación dentro de la política, cuatro criterios de promoción, y por qué repetir el ciclo tres veces lo daña.