Human in the loop para agentes de IA: aprobaciones reanudables y auditables
Diseña aprobaciones humanas que sobrevivan a reintentos: persiste la intención normalizada, muestra el impacto real, valida sujeto, expiración y hash, y reanuda el mismo run sin convertir aprobar en un booleano del navegador.
Human in the loop para agentes de IA no significa pintar un botón de aprobar. Significa detener una ejecución antes de una acción sensible, guardar una intención normalizada y reanudarla después con identidad, política, expiración y hash comprobados.
Intención durable y estado
Normaliza antes de presentar: operación, recurso, tenant, importe o alcance, efecto esperado, versión de policy, creador, run_id, idempotency_key y referencias seguras a los datos que el revisor necesita. Excluye secretos y redacta PII; el reviewer debe entender el impacto, no recibir el prompt completo.
Guarda estados explícitos: `proposed`, `pending_review`, `approved`, `rejected`, `expired`, `executing`, `succeeded` y `unknown`. La aprobación incluye `approval_id`, `intent_hash`, `reviewer_id`, `decision_at`, `expires_at`, `policy_version`, `reason` y una versión para compare-and-set.
Calcula el hash sobre representación canónica, no sobre el HTML ni sobre JSON con orden accidental. Aprobar transferir 100 euros a A no autoriza transferir 1.000 a B aunque el modelo reutilice el mismo mensaje.
El flujo es propuesta → intención durable y policy → revisión humana → ejecución, reanudación y auditoría. El worker crea la intención y queda suspendido; la aplicación de revisión lee un resumen derivado; el backend autoriza la decisión y publica una señal para reanudar el mismo `run_id`.
¿Te está sirviendo? Hay una dosis cada semana
Te resumo herramientas de IA para devs, agentes, MCP, seguridad y workflows en un email de 5 minutos. En español y sin ruido.
Suscribirme gratisTypeScript seguro: hash, expiración y CAS
Este ejemplo muestra los invariantes. El almacenamiento real usa una transacción o CAS; `resumeRun` reanuda el checkpoint existente, no crea una segunda ejecución.
Puntos a revisar
Lo que conviene comprobar
type Decision = "approved" | "rejected";
type Intent = { runId:string; tenantId:string; actorId:string; action:string; resource:string; args:Record<string, unknown>; policyVersion:string; idempotencyKey:string; };
const canonical = (i:Intent) => JSON.stringify(i, Object.keys(i).sort());
const digest = (i:Intent) => createHash("sha256").update(canonical(i)).digest("hex");
async function decide(id:string, reviewerId:string, decision:Decision, expectedHash:string) {
const a = await db.approvals.get(id);
if (!a || a.status !== "pending" || a.expiresAt < Date.now()) throw Error("approval_not_active");
if (!await policy.canReview(reviewerId, a.intent)) throw Error("reviewer_forbidden");
if (a.intentHash !== expectedHash || digest(a.intent) !== expectedHash) throw Error("intent_changed");
if (!await db.approvals.compareAndSet(id, a.version, {status:decision, reviewerId, decidedAt:Date.now()})) throw Error("already_consumed");
await audit({id, runId:a.intent.runId, decision, reviewerId, intentHash:expectedHash});
if (decision === "approved") await resumeRun(a.intent.runId, {approvalId:id, intentHash:expectedHash});
}Añade comprobación de tenant y de que el run sigue pausado, una clave de idempotencia para el consumidor y una segunda autorización justo antes de mutar. El hash protege la identidad de la intención; no sustituye ACL, policy ni validación de argumentos.
Timeout, rechazo y cambio de argumentos
Un timeout de revisión termina en `expired`, nunca en aprobación implícita. Un rechazo conserva motivo y deja el run reanudable solo mediante una nueva intención. Si el reviewer pide cambios, genera nueva versión, hash y revisión.
Si el worker pierde conexión después de ejecutar, marca `unknown` y reconcilia por `idempotencyKey` antes de reintentar. Si policy o permiso cambió mientras esperaba, vuelve a evaluar y rechaza la decisión antigua. Para alto impacto, la ventana debe ser corta y visible.
Los tests de UI son insuficientes: el backend debe rechazar aunque la persona pulse aprobar. Evalúa claridad del resumen: destinatario, importe, entorno y efecto irreversible deben poder detectarse.
Antipatrones
- Enviar `approved=true` al modelo y confiar en que llamará la tool correcta.
- Guardar aprobación en sesión del navegador o memoria del worker.
- Aprobar `payments.write` sin ligar recurso, importe y tenant.
- Modificar argumentos después de aprobar para completar datos faltantes.
- Reintentar una mutación tras timeout sin reconciliar el resultado.
- Usar aprobación humana para saltarse autorización del backend.
Preguntas frecuentes
¿Hay que revisar cada llamada?
No. Aplica una matriz de riesgo: lecturas con autorización, reversibles con controles proporcionales y alto impacto con aprobación durable.
¿Puede el modelo aprobar su propuesta?
No es aprobación humana. El modelo prepara el resumen; el sujeto autorizado decide fuera del modelo.
¿Qué pasa si cambia el importe en pantalla?
El hash falla y se requiere una nueva intención.
¿Un framework resuelve la auditoría?
Aporta checkpoint, pero policy, identidad, CAS y autorización pertenecen al dominio.
¿Cuánto dura una aprobación?
Lo mínimo que permita revisar. Para dinero, permisos o producción, una ventana corta y explícita suele ser más segura.
Cómo implementar una aprobación humana reanudable para una tool mutante
- Clasificar la acción. separa lectura, reversible y alto impacto por efecto y datos.
- Persistir la intención. normaliza argumentos, tenant, recurso, policy, run e idempotency key.
- Presentar impacto. muestra destinatario, alcance, cambios, riesgo y expiración.
- Autorizar al reviewer. comprueba identidad, tenant, rol y separación de funciones.
- Consumir con CAS. valida hash y TTL y permite una sola transición.
- Reanudar y reconciliar. continúa el mismo run; ante timeout consulta por idempotency key.
- Observar y evaluar. mide estados, concurrencia, inyecciones y evidencia de auditoría.
Cómo encaja con tu runtime
Un checkpoint del orquestador resuelve dónde queda dormido el worker, pero no quién puede aprobar ni qué efecto está autorizado. En LangGraph, `interrupt()` puede pausar y reanudar con un identificador de thread; en otro runtime usarás una cola o una tabla de jobs. En todos los casos, guarda la intención y la decisión en tu propio contrato de dominio para que un cambio de framework no cambie la autoridad.
Separa la bandeja de revisión del ejecutor. La bandeja puede aplicar filtros, accesibilidad y rotación de reviewers; el ejecutor consume una decisión verificable y llama al backend con scopes mínimos. Esa separación permite que el mismo flujo funcione desde una consola, un webhook interno o una revisión por lotes sin convertir un endpoint de UI en un camino privilegiado.
Para operaciones largas, emite eventos de estado y conserva el historial de versiones. El reviewer debe ver si la intención sigue pendiente, si el recurso cambió o si la policy se actualizó. Una notificación tardía no debe reactivar automáticamente una operación cuyo TTL terminó.
Documenta además quién opera la cola, cómo se revoca a un reviewer y qué equipo responde ante estados desconocidos. La continuidad operativa forma parte de la seguridad de la aprobación.
Fuentes y referencias
También te puede interesar
LangGraph: agentes Python con estadoIdempotencia en agentes de IA y reintentos segurosn8n y agentes de IA: workflows fiablesPull requests y gobernanza humana para agentesPrompt injection en agentes de IA: defensas y evalsRecibe una lectura semanal de herramientas IA para devs
Cada semana te resumo herramientas de IA para devs, agentes, MCP, seguridad y workflows en un email de 5 minutos. En español y sin ruido.
Suscribirme gratis