Índice de la sección
Humano en el bucle
Pausa el agente antes de acciones irreversibles, guarda su estado, enseña a una persona exactamente qué va a pasar y reanuda con su decisión.
Roadmap- Clasificar las herramientas de un agente según su reversibilidad e impacto para decidir cuáles requieren aprobación.
- Implementar la pausa del bucle con estado persistido y la reanudación con un tool_result que refleja la decisión humana.
- Diseñar la petición de aprobación con los argumentos reales, el contexto necesario y un plazo de caducidad.
- Evitar ejecuciones dobles con claves de idempotencia y registrar cada decisión.
Requiere: Seguridad: prompt injection y permisos
Al terminar esta lección tu agente se detendrá antes de reembolsar, borrar o enviar algo, guardará dónde estaba, pedirá a una persona que decida con la información justa y continuará con esa decisión, aunque pasen horas. Sabrás también qué acciones merecen esa pausa y cuáles no.
La idea
Humano en el bucle significa que una persona revisa y decide en un punto concreto del flujo del agente. El lugar más importante es antes de una acción irreversible o de alto impacto: mover dinero, borrar datos, enviar algo a un tercero, cambiar permisos. Es la última capa de defensa cuando todo lo demás falla, incluida una inyección de prompt que superó tus otras defensas (Seguridad: prompt injection y permisos).
Técnicamente, la pausa encaja de forma natural en el ciclo de herramientas. Cuando el modelo emite un tool_use para una herramienta sensible, tu código no la ejecuta. Guarda la conversación, avisa a una persona y termina la petición. Cuando la persona decide, cargas la conversación, construyes el Tool result con el resultado de la decisión y sigues el bucle.
flowchart LR M[Modelo] -- tool_use reembolsar --> P[Pausa: guardar estado] P --> H[Persona revisa] H -- aprueba --> E[Ejecutar] H -- rechaza --> R[tool_result con is_error] E --> C[Reanudar bucle] R --> C C --> M
Qué acciones necesitan aprobación
Pedir aprobación para todo es casi tan malo como no pedirla: la gente aprende a pulsar «Aprobar» sin leer. Clasifica tus herramientas con dos preguntas: ¿se puede deshacer? y ¿cuánto daño hace si sale mal?
| Impacto bajo | Impacto alto | |
|---|---|---|
| Reversible | Ejecuta y registra (crear un borrador, etiquetar un ticket) | Ejecuta, registra y permite deshacer (cambiar una dirección de envío) |
| Irreversible | Aprobación por umbral (reembolso bajo un importe) | Aprobación siempre (reembolso alto, borrar una cuenta, enviar un correo a un cliente) |
Los umbrales se definen en código, no en el prompt. Revísalos con datos: si el 100 % de las aprobaciones de una categoría se aceptan sin cambios durante meses, quizá ese umbral pueda subir. Si se rechazan muchas, el agente tiene un problema que hay que arreglar antes.
La pausa en código
El estado tiene que sobrevivir entre peticiones: la persona puede tardar minutos o un día. Guarda en tu base de datos el historial de mensajes, los tool_use pendientes y los resultados de las herramientas que sí se ejecutaron en ese mismo turno.
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
import anthropic
MODEL = "claude-sonnet-5-5"
client = anthropic.Anthropic()
SENSIBLES = {"reembolsar", "borrar_cuenta", "enviar_correo"}
@dataclass
class Pendiente:
id: str
mensajes: list # historial hasta la respuesta con el tool_use
resultados: list # tool_result ya resueltos de ese mismo turno
acciones: list # [{"tool_use_id", "nombre", "entrada", "clave"}]
caduca: str = field(default_factory=lambda: (
datetime.now(timezone.utc) + timedelta(hours=24)).isoformat())
def avanzar(sesion, mensajes: list, max_pasos: int = 10) -> dict:
for _ in range(max_pasos):
resp = client.messages.create(model=MODEL, max_tokens=1024, system=SYSTEM,
tools=TOOLS, messages=mensajes)
mensajes.append({"role": "assistant", "content": [b.model_dump(exclude_none=True) for b in resp.content]})
if resp.stop_reason != "tool_use":
return {"estado": "respondido",
"texto": "".join(b.text for b in resp.content if b.type == "text")}
resultados, acciones = [], []
for b in resp.content:
if b.type != "tool_use":
continue
if b.name in SENSIBLES:
acciones.append({"tool_use_id": b.id, "nombre": b.name, "entrada": b.input,
"clave": uuid.uuid4().hex}) # clave de idempotencia
else:
salida, error = ejecutar(sesion, b.name, b.input)
resultados.append({"type": "tool_result", "tool_use_id": b.id,
"content": salida, "is_error": error})
if acciones:
p = Pendiente(uuid.uuid4().hex, mensajes, resultados, acciones)
guardar_pendiente(p) # tu base de datos
notificar_revisor(p) # cola, correo, panel interno
return {"estado": "pendiente", "id": p.id}
mensajes.append({"role": "user", "content": resultados})
return {"estado": "limite_de_pasos"}Guardar el contenido con model_dump(exclude_none=True) convierte los bloques en diccionarios que puedes serializar a JSON y volver a enviar tal cual.
Cuando la persona decide, se completa el turno: un tool_result por cada tool_use, los de las herramientas ya ejecutadas y los de las sensibles, todos en el mismo mensaje.
def resolver(sesion, pendiente_id: str, decisiones: dict[str, dict]) -> dict:
p = cargar_pendiente(pendiente_id)
if datetime.fromisoformat(p.caduca) < datetime.now(timezone.utc):
decisiones = {a["tool_use_id"]: {"aprobada": False, "motivo": "caducada"} for a in p.acciones}
resultados = list(p.resultados)
for a in p.acciones:
d = decisiones[a["tool_use_id"]]
if d["aprobada"]:
salida, error = ejecutar(sesion, a["nombre"], a["entrada"], clave=a["clave"])
else:
salida, error = f"Rechazada por revisión humana: {d.get('motivo', 'sin motivo')}", True
registrar_decision(p.id, a, d, revisor=d.get("revisor"))
resultados.append({"type": "tool_result", "tool_use_id": a["tool_use_id"],
"content": salida, "is_error": error})
p.mensajes.append({"role": "user", "content": resultados})
marcar_resuelto(p.id)
return avanzar(sesion, p.mensajes)Tres detalles de robustez:
- Idempotencia. La clave se genera al pausar y se pasa a la herramienta. Si el revisor pulsa dos veces o tu servicio reintenta, la API de pagos reconoce la clave y no cobra ni reembolsa dos veces.
- Caducidad. Una aprobación de hace una semana puede estar basada en datos que ya no son ciertos. Al caducar, se rechaza y el agente lo explica.
- Rechazo como resultado. Un
tool_resultconis_error: Truey el motivo permite que el modelo siga: explicar al usuario, ofrecer una alternativa o escalar.
Diseñar la petición de aprobación
La pausa solo protege si la persona puede decidir bien en poco tiempo. Una buena petición de aprobación incluye:
- Qué: la acción con sus argumentos exactos. «Reembolsar 89,90 € al pedido 4512 a la tarjeta terminada en 1234.»
- Por qué: lo que pidió el usuario, en sus palabras, y los datos que usó el agente (el pedido, la política aplicada).
- Consecuencias: si se puede deshacer y cómo.
- Opciones: aprobar, rechazar con motivo y, si tiene sentido, editar un argumento dentro de límites (por ejemplo, un importe menor).
- Plazo: cuándo caduca.
Evita la fatiga: agrupa en una sola revisión varias acciones del mismo caso, pide aprobación solo donde la tabla de arriba lo indica y mide cuánto tardan las revisiones y cuántas se rechazan. Los patrones de interfaz para estas pantallas están en UX de agentes y la práctica del día a día en Humano en el bucle.
Errores frecuentes
- Pedir confirmación al modelo («¿Estás seguro?») en lugar de a una persona.
- Ejecutar la acción y pedir aprobación después.
- Guardar el estado solo en memoria y perderlo al reiniciar el servicio.
- Olvidar los
tool_resultde las herramientas no sensibles del mismo turno al reanudar. - No tener claves de idempotencia y ejecutar dos veces una acción aprobada.
- Pedir aprobación para todo y entrenar a los revisores a no leer.
Ejercicio
EjercicioPistas
Para la idempotencia en tu stub, guarda en un conjunto las claves ya usadas y devuelve el resultado anterior si llega una repetida. Para la caducidad, crea el Pendiente con una fecha ya pasada.
Resumen
- Pide aprobación antes de lo irreversible o de alto impacto, según una tabla de reversibilidad e impacto en código.
- La pausa es no ejecutar el
tool_use, guardar el estado y reanudar con untool_resultpor cadatool_use. - Usa claves de idempotencia, caducidad y registro de cada decisión.
- Enseña al revisor los argumentos reales, el porqué y las consecuencias.
- Pide pocas aprobaciones y buenas para evitar la fatiga.