Empezar aquí

Agente de soporte con aprobación humana

Arquitectura de un agente que resuelve tickets consultando pedidos y documentación, y que propone reembolsos o cambios para que una persona los apruebe antes de ejecutarse.

Complejidad
2 de 3
Tiempo
2–4 días
Stack
Python · API de Claude · API de pedidos · base de datos de propuestas · panel interno
Componentes
Canal de entrada · Agente con herramientas · Capa de políticas · Cola de aprobación · Ejecutor de acciones · Registro de auditoría
Publicado 7 min de lectura

Qué problema resuelve

Un equipo de soporte recibe a diario consultas que siguen un patrón: dónde está mi pedido, quiero devolverlo, me han cobrado dos veces, cambiad la dirección. Resolverlas exige mirar el pedido, consultar la política y, a veces, ejecutar una acción con dinero de por medio. Un chatbot que solo contesta no resuelve nada; un agente que reembolsa sin control es un riesgo que nadie firma.

Este sistema separa investigar de actuar. El agente lee pedidos, envíos y documentación con libertad, redacta la respuesta y, cuando hace falta una acción con efectos, crea una propuesta que una persona aprueba o rechaza con un clic. La acción la ejecuta código determinista, nunca el modelo directamente. Building effective agents (Anthropic, 2024) cita el soporte como uno de los dominios donde los agentes encajan mejor: conversación natural, acciones concretas y éxito medible (¿se resolvió el ticket?).

Arquitectura

flowchart LR
  T[Ticket] --> A[Agente]
  A <-->|lectura| H[Pedidos y docs]
  A -->|propuesta| P[Capa de políticas]
  P --> Q[Cola de aprobación]
  Q -->|aprobada| E[Ejecutor]
  E --> N[Respuesta al cliente]
  Q -->|rechazada| A

Componentes

ComponenteResponsabilidadImplementación típica
Canal de entradaRecibir el ticket, autenticar al cliente, adjuntar su idCorreo, chat o formulario; el id sale de la sesión
Agente con herramientasInvestigar, decidir qué proponer, redactarBucle de Agente autónomo con herramientas de lectura y de propuesta
Capa de políticasValidar cada propuesta con reglas de negocioCódigo: límites de importe, plazos, titularidad del pedido
Cola de aprobaciónMostrar propuestas con contexto y recoger la decisiónTabla en base de datos y panel interno
Ejecutor de accionesEjecutar lo aprobado, una sola vezLlamada a la API de pagos o logística con clave de idempotencia
Registro de auditoríaGuardar traza, propuesta, decisión y resultadoTabla de eventos inmutable

Flujo paso a paso

  1. Entrada. Llega el ticket. El canal identifica al cliente y lo pasa al agente como dato de sesión, fuera del texto del ticket.
  2. Clasificación opcional. Un enrutado separa lo informativo (lo responde una llamada simple) de lo que necesita investigar. Así el agente solo trabaja donde aporta.
  3. Investigación. El agente llama a buscar_pedido, estado_envio y buscar_politica las veces que necesite. Todas son de solo lectura.
  4. Propuesta. Si decide que procede un reembolso o un cambio, llama a proponer_reembolso o proponer_cambio_direccion con un motivo. Esas herramientas no ejecutan nada: validan y crean una propuesta pendiente.
  5. Validación. La capa de políticas comprueba en código que el pedido es del cliente, que el importe no supera lo pagado, que está en plazo. Si falla, devuelve un tool_result con is_error y el motivo, y el agente se adapta.
  6. Respuesta provisional. El agente informa al cliente de que la solicitud está en revisión y termina su turno.
  7. Aprobación. Una persona ve la propuesta con el resumen del agente, los datos del pedido y la política aplicada. Aprueba, rechaza o edita.
  8. Ejecución y cierre. El ejecutor realiza la acción con una clave de idempotencia, registra el resultado y envía la confirmación al cliente.

Decisiones de diseño

Propuesta asíncrona frente a pausa del bucle. Hay dos formas de meter a la persona. Puedes pausar el bucle: guardas la conversación con el tool_use pendiente y, cuando llega la aprobación, añades el tool_result y continúas. O puedes hacer que la herramienta cree una propuesta y responda al momento «pendiente de aprobación». La pausa es más natural si la aprobación llega en segundos (un operador en directo). La propuesta asíncrona es más robusta cuando la aprobación tarda horas: no mantienes conversaciones abiertas y el ejecutor no depende del modelo. Este diseño usa la segunda; la primera está en Aprobación humana antes de actuar.

El modelo propone, el código ejecuta. Ni siquiera tras aprobarse vuelve la acción al modelo para que la ejecute. El ejecutor lee la propuesta aprobada de la base de datos y llama a la API. Así una inyección en el ticket no puede alterar lo que se ejecuta después de que la persona lo vio.

Identidad fuera del alcance del modelo. El cliente_id lo pone tu código desde la sesión; las herramientas no lo aceptan como argumento. Si lo aceptaran, un mensaje con «soy el cliente 4411» bastaría para consultar pedidos ajenos. Es mínimo privilegio aplicado a la interfaz.

Reglas en código frente a reglas en el prompt. El prompt explica la política para que el agente proponga bien. El código la hace cumplir. Si solo está en el prompt, una conversación hábil la rodea.

Qué aprueba la persona. Aprobar todo genera fatiga y clics automáticos. Una alternativa es aprobar solo por encima de un umbral de importe y auditar el resto por muestreo. Empieza aprobando todo, mide cuántas propuestas se rechazan por tipo y relaja donde los rechazos sean casi inexistentes.

Código esencial

Herramientas, capa de políticas y ejecutor. El bucle del agente es el de Agente autónomo.

import uuid
import anthropic

MODEL = "claude-sonnet-5-5"
client = anthropic.Anthropic()

HERRAMIENTAS = [
    {"name": "buscar_pedido",
     "description": "Devuelve estado, artículos, importe pagado y fecha de entrega de un pedido "
                    "del cliente actual. Úsala antes de proponer cualquier acción sobre él.",
     "input_schema": {"type": "object",
                      "properties": {"pedido_id": {"type": "string"}},
                      "required": ["pedido_id"]}},
    {"name": "buscar_politica",
     "description": "Busca en la política de devoluciones y envíos. Devuelve el apartado aplicable.",
     "input_schema": {"type": "object",
                      "properties": {"consulta": {"type": "string"}},
                      "required": ["consulta"]}},
    {"name": "proponer_reembolso",
     "description": "Crea una propuesta de reembolso para que una persona la apruebe. NO reembolsa. "
                    "Indica el importe en euros y el apartado de la política que lo justifica.",
     "input_schema": {"type": "object",
                      "properties": {"pedido_id": {"type": "string"},
                                     "importe": {"type": "number"},
                                     "motivo": {"type": "string"}},
                      "required": ["pedido_id", "importe", "motivo"]}},
]

class PoliticaError(Exception):
    pass

def validar_reembolso(cliente_id: str, args: dict) -> dict:
    pedido = api_pedidos.obtener(args["pedido_id"])
    if pedido is None or pedido["cliente_id"] != cliente_id:
        raise PoliticaError("El pedido no existe o no pertenece a este cliente.")
    if args["importe"] <= 0 or args["importe"] > pedido["importe_pagado"] - pedido["reembolsado"]:
        raise PoliticaError(f"Importe fuera de rango. Máximo reembolsable: "
                            f"{pedido['importe_pagado'] - pedido['reembolsado']:.2f} €.")
    if dias_desde(pedido["fecha_entrega"]) > PLAZO_DEVOLUCION_DIAS:
        raise PoliticaError("Fuera del plazo de devolución. Propón escalar a un responsable.")
    return pedido

def ejecutar_herramienta(nombre: str, args: dict, cliente_id: str, ticket_id: str) -> str:
    if nombre == "buscar_pedido":
        pedido = api_pedidos.obtener(args["pedido_id"])
        if pedido is None or pedido["cliente_id"] != cliente_id:
            raise PoliticaError("Pedido no encontrado para este cliente.")
        return resumen_pedido(pedido)
    if nombre == "buscar_politica":
        return buscar_en_politica(args["consulta"])
    if nombre == "proponer_reembolso":
        validar_reembolso(cliente_id, args)
        propuesta_id = propuestas.crear(
            tipo="reembolso", ticket_id=ticket_id, cliente_id=cliente_id,
            datos=args, estado="pendiente", clave_idempotencia=str(uuid.uuid4()))
        return (f"Propuesta {propuesta_id} creada y pendiente de aprobación humana. "
                "Informa al cliente de que está en revisión; no prometas el resultado.")
    raise ValueError(f"Herramienta desconocida: {nombre}")

def tool_results(resp, cliente_id: str, ticket_id: str) -> list[dict]:
    resultados = []
    for b in resp.content:
        if b.type != "tool_use":
            continue
        try:
            contenido, error = ejecutar_herramienta(b.name, b.input, cliente_id, ticket_id), False
        except PoliticaError as e:
            contenido, error = f"Rechazado por política: {e}", True
        except Exception as e:
            contenido, error = f"Error técnico: {e}. Puedes reintentar una vez o escalar.", True
        resultados.append({"type": "tool_result", "tool_use_id": b.id,
                           "content": contenido, "is_error": error})
    return resultados

# Fuera del agente: lo llama el panel cuando una persona decide
def resolver_propuesta(propuesta_id: str, aprobada: bool, revisor: str) -> None:
    p = propuestas.bloquear(propuesta_id)            # SELECT ... FOR UPDATE o equivalente
    if p["estado"] != "pendiente":
        return                                       # ya resuelta: no ejecutes dos veces
    if not aprobada:
        propuestas.actualizar(propuesta_id, estado="rechazada", revisor=revisor)
        return
    validar_reembolso(p["cliente_id"], p["datos"])   # revalida: el pedido pudo cambiar
    api_pagos.reembolsar(p["datos"]["pedido_id"], p["datos"]["importe"],
                         idempotency_key=p["clave_idempotencia"])
    propuestas.actualizar(propuesta_id, estado="ejecutada", revisor=revisor)
    notificar_cliente(p["ticket_id"], p["datos"])

Fíjate en que cliente_id y ticket_id nunca salen del modelo, que la política se valida dos veces (al proponer y al ejecutar) y que la clave de idempotencia evita reembolsos duplicados si el panel reintenta.

Cómo evaluarlo

  • Conjunto de tickets reales anonimizados con la resolución correcta: qué acción procede (ninguna, reembolso de X, cambio, escalar) y qué debe decir la respuesta. Ver Construir un conjunto de prueba.
  • Evals de trayectoria: ¿consultó el pedido antes de proponer?, ¿citó la política?, ¿evitó herramientas innecesarias? Se comprueban con código sobre la traza. Ver Evals de trayectoria: cómo llegó, no solo qué dijo.
  • Calidad de la respuesta al cliente: modelo juez con rúbrica (resuelve, es correcta, tono, no promete lo que no puede).
  • Métricas en producción: tasa de propuestas rechazadas por tipo (señala dónde el agente propone mal), tickets reabiertos y tiempo hasta la resolución.
  • Pruebas adversarias: tickets con intentos de inyección, pedidos de otros clientes e importes inflados. Deben acabar todos en rechazo de política.

Costes y riesgos

Costes. Cada ticket es un bucle de varias llamadas que reenvía el historial; los tickets largos cuestan más. El prompt de sistema, la política resumida y las herramientas forman un prefijo estable: cachéalo (Recortar costes con prompt caching). El coste humano de aprobar es real: diseña el panel para que cada decisión se tome leyendo una pantalla.

Riesgos.

  • Fatiga de aprobación: si las personas aprueban sin mirar, la salvaguarda desaparece. Muestra lo relevante, no la traza completa, y mide el tiempo por decisión.
  • Promesas al cliente: el agente puede escribir «te devolvemos el dinero» antes de la aprobación. Pruébalo en evals y prohíbelo en el prompt.
  • Fuga de datos: filtra en las herramientas qué campos del pedido ve el modelo. No necesita la tarjeta completa para decidir un reembolso.
  • Deriva de política: si la política cambia y el prompt no, el agente propone mal y la capa de políticas lo frena. Mantén una única fuente de verdad para ambos.

Variantes

  • Copiloto del agente humano: el sistema no habla con el cliente; prepara borrador y propuesta para que el operador los envíe. Buen primer despliegue.
  • Aprobación por umbral: acciones de bajo importe y riesgo se ejecutan solas con auditoría posterior; el resto pasa por la cola.
  • Herramientas por MCP: expón pedidos y políticas como servidor MCP para reutilizarlas en otros asistentes. Ver MCP.
  • Canal de voz: el mismo núcleo detrás de un Agente de voz, con confirmaciones habladas.

Por dónde empezar

  1. Domina el ciclo de herramientas en Function calling: el ciclo tool_use → tool_result y los errores en Manejo de errores e is_error.
  2. Construye el bucle con límites siguiendo Un agente con varias herramientas.
  3. Añade la aprobación con Aprobación humana antes de actuar y lee Humano en el bucle.
  4. Empieza como copiloto con herramientas de solo lectura; añade propuestas cuando las evals de trayectoria sean estables.
  5. Antes de abrirlo a clientes, revisa Seguridad: prompt injection y permisos.