Inicio / Artículos / De “Continuar” a completado: Estado, aprobación e idempotencia para agentes que actúan

De “Continuar” a completado: Estado, aprobación e idempotencia para agentes que actúan

Aprenda cómo las propuestas explícitas, las aprobaciones vinculantes, la revalidación, las claves de idempotencia y la verificación de resultados convierten un flujo de trabajo de reembolso con múltiples agentes en uno en el que se puede confiar.

3113 palabras

Una demostración con múltiples agentes suele terminar con una respuesta convincente. Un sistema de producción comienza su trabajo más difícil justo después, cuando el usuario responde “Genial, continúa” y el software debe transformar una sugerencia conversacional en un cambio real en otro sistema. Este artículo sigue el proceso de una sola solicitud de reembolso a través de un coordinador, un agente especialista, propuestas almacenadas, aprobación y ejecución por parte de humanos, y muestra qué responsabilidades corresponden al modelo y cuáles deben quedar en el código de la aplicación. Al final, dispondrá de una lista de verificación concreta para decidir si un flujo de trabajo basado en agentes está listo para actuar en el mundo real.

¿Qué cambia cuando un asistente debe actuar?

Hasta el momento de la aprobación, el asistente solo tenía que interpretar una solicitud y generar una respuesta útil. Una vez que el usuario dice que sí, surgen un conjunto diferente de obligaciones. El sistema debe recordar con precisión lo que ofreció, localizar el registro correcto, seleccionar el componente responsable de la tarea, confirmar que la acción sigue teniendo sentido, verificar si este usuario puede autorizarla, llamar a un servicio externo, lidiar con fallos y, finalmente, determinar algo que suena sencillo pero rara vez lo es: si la acción realmente tuvo lugar.

La arquitectura de demostración habitual abarca solo la primera parte. Un orquestador recibe la solicitud, la delega a especialistas, estos últimos llaman a las herramientas correspondientes y luego llega una respuesta. Esa estructura es valiosa, pero para acciones en el mundo real constituye únicamente el punto de entrada. Actuar con seguridad requiere un estado explícito, autorización, flujos de trabajo que permitan soportar tiempos de espera y procesos de revalidación, idempotencia, recuperación ante fallos ambiguos, además de pruebas de que la tarea se ha completado. En ese momento el diseño de los agentes pasa a ser un problema arquitectónico en lugar de uno relacionado con la formulación de instrucciones.

Rastreando una sola solicitud de reembolso

Tomemos una solicitud que un cliente podría enviar a un asistente de soporte: revisar el pedido n.º 4821, determinar si cumple los requisitos para un reembolso y, de ser así, elaborar uno para su aprobación. Un agente humano dividiría ese proceso en aproximadamente diez pasos:

  1. Averiguar qué es lo que el cliente está solicitando.
  2. Obtener información del pedido.
  • Encuentre la política de reembolso aplicable.
  • Compare el pedido con dicha política.
  • Resuelva cualquier aspecto que aún esté sin aclarar.
  • Calcule el monto del reembolso.
  • Elabore una propuesta.
  • Solicite la aprobación.
  • Ejecute el reembolso.
  • Confirme que se ha realizado con éxito.
  • En el software, cada una de esas transiciones necesita un responsable y un lugar donde desarrollarse. Una secuencia funcional lleva la solicitud desde el usuario hasta un orquestador, luego a un especialista que recopila pruebas y elabora una propuesta, la cual es aprobada, ejecutada y finalmente verificada.

    • Dirigir la solicitud es tarea del orquestador.
    • El especialista realiza la investigación en el área correspondiente.
    • Las herramientas conectan el sistema con la documentación y los registros en tiempo real.
    • El estado explícito refleja la acción propuesta.
    • El usuario aprueba una propuesta específica.
  • El código de la aplicación realiza la operación.
  • La verificación determina si se produjo el resultado esperado.
  • Un único principio une todo esto:

    El modelo puede decidir qué debería suceder; la aplicación decide qué está permitido que suceda.

    Los fragmentos que siguen son bocetos arquitectónicos en Python utilizando LlamaIndex. Se han omitido la configuración del modelo, las integraciones de almacenamiento y servicios, y nombres como llm y policy_retriever representan los componentes que proporcionaría su aplicación. Algunos fragmentos también tienen saltos de línea eliminados (por ejemplo, las instrucciones unidas en una sola línea), por lo que deben leerse como esquemas y no como código listo para copiar y pegar.

    Separar el enrutamiento del trabajo especializado

    La forma más rápida de crear un agente es proporcionarle todas las herramientas de una sola vez: búsqueda de documentación, consulta de pedidos, verificación de pagos, reembolsos, correo electrónico, generación de informes. Para una aplicación pequeña, esto es perfectamente razonable. Sin embargo, a medida que se acumulan las responsabilidades, resulta difícil describir la tarea del agente en una sola oración, y una solicitud relacionada con los plazos de entrega termina compartiendo la misma herramienta que una operación encargada de mover dinero.

    Un límite más claro separa dos aspectos: decidir a dónde pertenece una solicitud y realizar el trabajo especializado en ella. Para el asistente de soporte, eso significa contar con un coordinador que responde a las preguntas generales y remite las solicitudes de reembolso a un especialista dedicado. Dicho especialista se crea a partir de FunctionAgent de LlamaIndex:

    from llama_index.core.agent.workflow import FunctionAgent
    

    Su definición le otorga un mandato limitado: tres herramientas y la autorización para devolver el control al coordinador. Obsérvese que el prompt del sistema le pide que explique sus pruebas antes de proponer algo:

    refund_specialist = FunctionAgent(
        name="refund_specialist",
        description="Checks refund eligibility and prepares proposals.",
        system_prompt=(
            "Check the order and the applicable policy. "
            "Explain your evidence before proposing a refund. "
            "Hand back unrelated requests to the coordinator."
        ),
        tools=[get_order, search_policy, prepare_refund],
        llm=llm,
        can_handoff_to=["coordinator"],
    )
    

    El campo description no es una decoración; forma parte de la arquitectura. Una etiqueta como “asistente útil” no comunica nada. “Verifica la elegibilidad para reembolsos y prepara propuestas” define una responsabilidad que puede ser probada y auditada. En LlamaIndex, las clases FunctionAgent y AgentWorkflow están diseñadas precisamente para este tipo de transferencia explícita. Si aún está eligiendo un framework, la comparación entre LangChain y LlamaIndex aborda los trade-offs más amplios.

    Añadir agentes no implica necesariamente una mejora. Cada especialista genera costos adicionales por coordinación, por lo que cada uno debe justificar su existencia. Aquí, una pareja de agentes —un coordinador y un especialista en reembolsos— establece un límite claro sin costos adicionales.

    Vinculación del coordinador con el especialista

    Tanto el coordinador como el flujo de trabajo que conecta a ambos agentes provienen del mismo módulo:

    from llama_index.core.agent.workflow import AgentWorkflow
    

    El coordinador tiene acceso a la búsqueda de políticas para preguntas generales y puede transferir las tareas al especialista en reembolsos. AgentWorkflow registra a ambos agentes y hace que el coordinador sea el nodo raíz que recibe todas las solicitudes. En este fragmento, el paréntesis de cierre del coordinador y la asignación del flujo de trabajo están en la misma línea; se trata de dos instrucciones separadas:

    coordinator = FunctionAgent(
        name="coordinator",
        description="Answers general questions and routes refund requests.",
        system_prompt=(
            "Use documentation for general questions. "
            "Hand refund requests to the refund specialist."
        ),
        tools=[search_policy],
        llm=llm,
        can_handoff_to=["refund_specialist"],
    )workflow = AgentWorkflow(
        agents=[coordinator, refund_specialist],
        root_agent="coordinator",
    )
    

    Con esto en su lugar, la solicitud cuenta con una ruta. El coordinador reconoce que se trata de una pregunta sobre reembolso y la transfiere; el especialista obtiene el pedido, busca la política correspondiente, anota qué falta aún y, una vez que hay pruebas suficientes, elabora una propuesta.

    La secuencia en la conversación variará. Un cliente indica la fecha de entrega de antemano; otro pregunta primero por las reglas de reembolso y solo menciona el pedido tres mensajes después. Los agentes pueden adaptarse a esa variabilidad, mientras la aplicación sigue definiendo qué capacidades existen y los límites para usarlas.

    Las conversaciones pueden mantenerse flexibles mientras las operaciones críticas permanecen controladas.

    Pruebas antes de la elegibilidad

    Enviar la solicitud al agente adecuado no garantiza que su conclusión sea correcta. El especialista aún debe recopilar pruebas.

    Supongamos que la política permite la devolución de productos sin abrir dentro de un plazo de 30 días, mientras que los registros del pedido indican que el pedido #4821 llegó hace 12 días. Estos datos provienen de dos sistemas diferentes y ambos son necesarios: la política establece la regla, el pedido describe la situación, y la elegibilidad solo se determina al combinarlos. Por lo tanto, la recuperación de información se convierte en un paso dentro del flujo de trabajo. El especialista busca los documentos de la política mientras carga por separado el registro actual del pedido.

    Una herramienta mínima para buscar políticas comienza con una firma asíncrona y una documentación que explica al agente para qué sirve la herramienta:

    async def search_policy(question: str) -> list[dict]:
        """Find policy passages relevant to a customer request."""
    

    El cuerpo de la función recupera los fragmentos correspondientes y los devuelve junto con su título de origen y sección. Al igual que en el fragmento anterior, la llamada a aretrieve y la instrucción return deben estar en líneas separadas:

        matches = await policy_retriever.aretrieve(question)    return [
            {
                "text": match.node.get_content(),
                "source": match.node.metadata.get("title"),
                "section": match.node.metadata.get("section"),
            }
            for match in matches
        ]
    

    Lo importante es lo que viaja junto con el texto: su origen. Si más adelante el asistente afirma que el pedido está dentro del plazo de reembolso, la explicación debe poder citar el pasaje de la política que define dicho plazo. Sin embargo, el origen tiene sus limitaciones:

    Una cita no puede demostrar que una interpretación es correcta; lo que hace es permitir que alguien la verifique.

    La condición de no haber abierto el paquete revela una laguna: en el sistema de pedidos no se registra si el paquete fue abierto. La medida correcta no es adivinar, sino preguntarle al cliente. Un diseño sólido muestra explícitamente la información faltante en lugar de permitir que la incertidumbre se convierta en una recomendación categórica.

    El historial de conversaciones no es el estado de la aplicación

    Se da por concluida la investigación. El especialista informa que el pedido #4821 califica para un reembolso de 79 € y pregunta si proceder. El usuario responde que sí.

    La ruta de procesamiento funcionó, la investigación también y las herramientas proporcionaron sus pruebas, pero sigue habiendo una laguna. El sistema no puede indicar con precisión qué fue aprobado: el pedido, la cantidad y la moneda, así como la versión de la oferta, están todos implícitos en lugar de registrados. Si la conversación abordó dos pedidos, o si la cantidad se volvió a calcular después de la oferta, la palabra “sí” se vuelve ambigua.

    Por esta razón, las acciones consecuentes no deben quedar únicamente en la transcripción del chat. La aplicación necesita un registro explícito de la acción pendiente, como este:

    pending_action = {
        "proposal_id": "refund-proposal-17",
        "order_id": "4821",
        "amount_minor": 7900,
        "currency": "EUR",
        "status": "awaiting_approval",
        "evidence": [
            "returns-policy:section-3",
            "order:4821",
        ],
    }
    

    El campo status es útil, pero proposal_id es el que tiene mayor importancia. La aprobación debe referirse exactamente a la propuesta que vio el usuario. Si cambia la cantidad, se trata de una nueva propuesta. Si cambia el orden, también es una nueva propuesta. Si pruebas nuevas modifican la recomendación, sigue siendo una nueva propuesta. El dinero se almacena como amount_minor en centavos enteros, lo que evita sorpresas por redondeo de punto flotante, y la lista evidence mantiene registrada qué sección de la política y qué registro de orden justificaron la oferta.

    La transcripción existe para comprender; el estado estructurado existe para actuar.

    La conversación ayuda al modelo a interpretar a qué se refiere “proceder”. La propuesta almacenada proporciona a la aplicación algo inequívoco para ejecutar.

    Aprobación como datos y no como palabras

    La forma ingenua de registrar el consentimiento es mediante un único hecho:

    user said yes
    

    Un modelo más seguro une la identidad, la propuesta, la acción y el contexto:

    user X approved proposal Y,
    containing action Z,
    under the current conditions.
    

    Esa distinción se vuelve decisiva en el momento en que un agente puede afectar sistemas externos. El consentimiento debe vincular a una persona específica con una acción propuesta concreta, nunca con algo que el modelo reconstruya posteriormente. Por lo tanto, la propuesta necesita incluir todos los detalles operativos relevantes:

    • el registro objetivo
    • la cantidad
    • la moneda
    • las pruebas de respaldo
    • el estado actual
    • un identificador único de la propuesta

    El lenguaje natural explica la acción al usuario; la propuesta estructurada la define para el sistema.

    Esperar forma parte del flujo de trabajo

    La aprobación humana también introduce un factor de tiempo. El usuario puede responder de inmediato, después del almuerzo o al día siguiente tras cerrar el navegador. Cuando un flujo de trabajo depende de una persona, la pausa no es un caso excepcional sino una fase normal, por lo que el trabajo pendiente debe guardarse y reanudarse más tarde.

    LlamaIndex proporciona objetos de contexto para los flujos de trabajo y formas de pausar a la espera de la entrada humana, lo que da estructura a esta interacción. Sin embargo, el almacenamiento duradero, la autorización, las reglas de vencimiento y el ciclo de vida de la aplicación en su conjunto siguen siendo su responsabilidad.

    En asuntos tan importantes como un reembolso, una división adecuada del trabajo consiste en dejar que el agente prepare la propuesta y que el código de aplicación habitual ejecute la operación aprobada. El procesador de aprobaciones comienza cargando la propuesta almacenada:

    async def approve_refund(proposal_id, user):
        proposal = await proposals.load(proposal_id)
    

    Luego verifica los permisos, confirma que la propuesta sigue pendiente, la vuelve a validar, llama al servicio de pago utilizando el ID de la propuesta como clave de idempotencia y registra el comprobante. En este fragmento, estas llamadas asincrónicas se ejecutan juntas en líneas compartidas; cada await constituye una instrucción independiente:

        await permissions.require(
            user,
            "approve_refund",
            proposal,
        )    await proposals.require_pending(proposal)    await refunds.revalidate(proposal)    receipt = await payments.refund(
            order_id=proposal.order_id,
            amount_minor=proposal.amount_minor,
            idempotency_key=proposal.id,
        )    await proposals.mark_completed(
            proposal.id,
            receipt,
        )    return receipt
    

    Varios aspectos importantes están ocultos en esa función breve:

    • La propuesta proviene del estado almacenado, no de la conversación.
    • La autorización se aplica fuera del modelo.
    • El código confirma que la propuesta sigue esperando aprobación, lo cual evita una segunda ejecución por este camino (bajo condiciones de concurrencia, esta verificación debe ser atómica con la actualización de estado).
    • La elegibilidad se vuelve a validar justo antes de tomar acción.
    • La llamada de pago utiliza una clave de idempotencia estable.
    • El resultado se guarda permanentemente.

    La secuencia abarca desde una propuesta guardada, pasando por una aprobación autorizada, una validación nueva y la ejecución en sí, hasta un resultado almacenado. Esa secuencia es mucho más importante que el hecho de si el agente formuló su mensaje perfectamente.

    Vuelve a validar inmediatamente antes de actuar

    ¿Por qué verificarlo de nuevo después de que el usuario ya haya dicho que sí? Porque el mundo sigue avanzando mientras el sistema espera. Es posible que el pedido haya sido reembolsado a través de otro canal de soporte, que el estado del pago haya cambiado, que alguien haya editado el pedido o que un flujo de trabajo paralelo ya haya realizado la operación.

    Una propuesta es una instantánea de lo que parecía correcto en el momento en que se redactó. La ejecución ocurre después, a veces mucho más tarde. Antes de realizar una operación con consecuencias, la aplicación debe confirmar que las suposiciones en las que se basa la propuesta siguen siendo válidas.

    Un “sí” otorga permiso para actuar; no impide que el mundo cambie.

    Cuanto más tiempo permanece una propuesta en espera, más importante se vuelve esto. Combinar la revalidación con un plazo de vencimiento para las propuestas mantiene limitado el período de suposiciones obsoletas.

    Los intentos de repetición no deben realizar la misma acción

    Considere otro fallo: el usuario aprueba, la aplicación envía el reembolso, el proveedor de pagos lo procesa, pero la red se interrumpe antes de que llegue la respuesta. El cliente intenta nuevamente. ¿Debería el cliente recibir un segundo pago de 79 euros? Claramente no.

    La idempotencia evita eso. Un identificador de operación estable permite que cualquier servicio externo que soporte la idempotencia reconozca dos solicitudes como una sola operación lógica. En lugar de interpretar la repetición como “emitir otro reembolso”, el servicio la trata como una solicitud del estado o resultado de la operación ya asociada a la propuesta n.º 17. Utilizar el ID de la propuesta como clave funciona porque cada propuesta aprobada debería generar como máximo un reembolso. Los mecanismos del lado del servidor se explican con más detalle en entendiendo las claves de idempotencia en los endpoints POST de Node.js.

    Esto rara vez se muestra en demostraciones pulidas de múltiples agentes. Pero tan pronto como un sistema puede transferir dinero, enviar mensajes, modificar registros, abrir tickets o provocar cualquier otro efecto en el mundo real, las repeticiones forman parte de su arquitectura.

    "Desconocido" es un resultado legítimo

    Ahora, el caso más problemático: la llamada de pago supera el tiempo de espera. Quizás no ocurrió nada, o quizás el dinero fue transferido un momento antes de que se interrumpiera la conexión. Para el cliente, la diferencia es de 79 euros.

    Estaría mal informar al usuario de que el reembolso falló y se está intentando nuevamente, ya que no hay nada que respalde esa afirmación. Todo lo que tiene el sistema es la falta de confirmación, lo cual es un hecho distinto. Un flujo de trabajo fiable mantiene visible esa incertidumbre: en lugar de repetir la operación, consulta el estado de la transacción existente y, mientras tanto, informa al usuario con precisión, por ejemplo, que no se pudo confirmar su finalización y que se está verificando el estado.

    Esta disciplina rige en cada etapa, no solo al momento del pago:

    • Cuando hay un error al buscar un pedido, la existencia del pedido es desconocida, no refutada.
  • Cuando la búsqueda de una póliza genera un error, la pregunta relacionada con la póliza permanece abierta en lugar de responderse como “ninguno”.
  • Un tiempo de espera no implica necesariamente que la operación haya fallado; su finalización no está confirmada.
  • Los sistemas fiables modelan estos casos como estados distintos en lugar de agruparlos bajo “fallido” o “no encontrado”.

    El hecho de que se devuelva una llamada a una herramienta no significa que todo esté listo

    Aquí la arquitectura va más allá de la simple orquestación. El hecho de que se reciba una respuesta de una herramienta no equivale a que el trabajo esté completado. Una respuesta de error indica que no lo está, al igual que un tiempo de espera. Si la aplicación no puede determinar si se realizó el reembolso, ciertamente aún no ha finalizado.

    Una definición más precisa de finalización es que el sistema pueda mostrar pruebas de que realmente se logró el resultado deseado. Eso cambia los requisitos del contrato. El objetivo no es este:

    call refund()
    

    El objetivo sí es este:

    establish that refund proposal #17
    for order #4821
    was successfully processed exactly once
    

    Esas son garantías muy diferentes, y esa diferencia es la razón por la cual los orquestadores y especialistas son solo el comienzo del diseño.

    Nueve preguntas antes de que un sistema multiagente pueda actuar

    Antes de conectar un flujo de trabajo de IA con operaciones que tengan consecuencias reales, responda a estas preguntas:

    1. ¿Cada agente tiene una responsabilidad definida que pueda expresarse en una sola oración?
    2. ¿Puede rastrearse una decisión importante hasta los documentos y registros que la sustentan?
    3. ¿La información faltante puede permanecer explícitamente desconocida, o la incertidumbre se convierte silenciosamente en una recomendación segura?
    4. ¿La acción propuesta se almacena como un estado estructurado en lugar de existir únicamente dentro de la conversación?
    5. ¿El usuario aprueba una propuesta específica, de modo que un “sí” autorice algo concreto?
  • ¿Se aplican los permisos en el código de la aplicación, o el modelo se limita a hacer recomendaciones?
  • ¿Se vuelve a validar la propuesta justo antes de su ejecución, ya que las condiciones podrían haber cambiado?
  • ¿La ejecución puede soportar intentos repetidos de forma segura, considerando la idempotencia como parte de la corrección?
  • ¿Se confirma el resultado antes de declarar que todo está completo, en lugar de considerar una llamada a la herramienta como prueba suficiente?
  • Incluso si algunas de estas preguntas no tienen respuesta clara, aún puede tener una demostración impresionante, pero probablemente no se trate aún de un sistema en el que se pueda confiar para actuar.

    Conclusión

    Vuelva a revisar la solicitud original: verifique el pedido n.º 4821 y prepare un reembolso para su aprobación. Cada elemento ahora tiene su lugar asignado. El coordinador dirige el proceso, el especialista recopila pruebas, las herramientas se conectan con la documentación y los registros en tiempo real, el estado específico almacena la propuesta, una persona aprueba una acción concreta, el código de la aplicación verifica los permisos y los vuelve a validar, el sistema de pagos ejecuta la transacción, y el resultado se almacena y confirma. En ese momento, el asistente puede dar una respuesta agradablemente sencilla: el reembolso de 79 euros del pedido n.º 4821 ya ha sido procesado, y se adjunta una confirmación. Esa frase es fiable gracias a todo lo que hay detrás de ella.

    A medida que el sistema crece, las mismas preguntas siguen siendo relevantes. ¿Cada capacidad está a cargo de alguien? ¿Se puede examinar la evidencia detrás de una decisión? ¿El flujo de trabajo mantiene la incertidumbre y se recupera cuando falla una dependencia? ¿Puede una persona ver exactamente lo que está aprobando, y puede el sistema demostrar que se produjo el resultado deseado? Las respuestas indican dónde un agente especializado aporta valor, dónde basta con una función simple y dónde se requiere un límite más estricto. La mejor experiencia con agentes puede terminar con una sola palabra común, “Hecho”, y la arquitectura es lo que la hace creíble.

    Lecturas relacionadas