Ir al contenido principal
Idempotencia en agentes IA: evita duplicar acciones

Idempotencia en agentes IA: evita duplicar acciones

Best Practices
••5 min read•Por Daily Miranda Pardo

Un timeout. El agente espera. No llega respuesta. El sistema de retry entra en acción.

Lo que el código no sabe: la primera llamada sí llegó al servidor de email. El email ya salió. El cliente lo recibió. Cuando el retry ejecuta la misma herramienta, el cliente recibe un segundo email idéntico. A veces, dos cobros.

No hay excepción en los logs. Todo aparece correcto.

Este es el problema de construir agentes IA en producción sin idempotencia en las tool calls.

Por qué los reintentos rompen agentes sin idempotencia

Cuando un agente llama a una herramienta y no recibe respuesta, no puede saber qué ocurrió:

  • La llamada llegó, se procesó y la respuesta se perdió en la red
  • La llamada no llegó porque el error ocurrió antes del envío
  • La llamada llegó pero el servidor murió antes de responder

Sin idempotencia, el retry duplica cualquiera de estos casos. Los retries son necesarios — ya cubrimos retry con backoff exponencial y circuit breaker en profundidad. Pero si las herramientas no son idempotentes, el mecanismo que protege tu agente de las interrupciones puede causar un problema mayor.

Las operaciones de lectura son naturalmente idempotentes: puedes ejecutar getCustomer() diez veces y el resultado siempre es el mismo. El problema son las escrituras con efectos en el mundo real:

  • Enviar un email transaccional
  • Procesar un pago con Stripe
  • Crear un registro en la base de datos
  • Enviar una notificación push
  • Publicar un mensaje en Slack o WhatsApp

Cada una puede ejecutarse dos veces con un solo timeout y su correspondiente retry.

El patrón de idempotency key

La solución es la misma que usan Stripe, Twilio y cualquier API de pagos seria: idempotency keys.

Antes de ejecutar la herramienta, se genera una clave única que identifica esa operación en ese run. Si la clave ya existe en el cache, se devuelve el resultado almacenado en lugar de volver a ejecutar:

import { createHash } from 'crypto';
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_KEY!
);

async function idempotentExecute<T>(
  key: string,
  operation: () => Promise<T>,
  ttlSeconds = 3600
): Promise<T> {
  const { data } = await supabase
    .from('idempotency_cache')
    .select('result')
    .eq('key', key)
    .gt('expires_at', new Date().toISOString())
    .maybeSingle();

  if (data?.result) {
    return JSON.parse(data.result) as T;
  }

  const result = await operation();

  const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
  await supabase.from('idempotency_cache').upsert({
    key,
    result: JSON.stringify(result),
    expires_at: expiresAt,
  });

  return result;
}

Cómo generar la clave correcta

La clave debe identificar de forma única esta operación en esta ejecución. La fórmula correcta combina el ID del run con un hash del input:

import { createHash } from 'crypto';

function getIdempotencyKey(
  runId: string,
  toolName: string,
  input: object
): string {
  const inputHash = createHash('sha256')
    .update(JSON.stringify(input))
    .digest('hex')
    .slice(0, 16);

  return `${runId}:${toolName}:${inputHash}`;
}

Si el agente envía el mismo email en dos runs distintos (porque la lógica de negocio lo requiere), eso son dos operaciones con dos runId distintos — ambas se ejecutan correctamente. Pero si el mismo run llama a send_email dos veces con el mismo input porque hubo un retry, la segunda llamada encuentra la clave en cache y devuelve el resultado original sin re-ejecutar.

Implementación práctica: envío de emails

interface SendEmailInput {
  to: string;
  subject: string;
  body: string;
}

const sendEmailTool = {
  name: 'send_email',
  description: 'Envía un email al cliente',

  async execute(
    input: SendEmailInput,
    runId: string
  ): Promise<{ messageId: string }> {
    const key = getIdempotencyKey(runId, 'send_email', input);

    return idempotentExecute(key, async () => {
      const messageId = await emailProvider.send(input);
      return { messageId };
    });
  },
};

La primera ejecución envía el email y almacena el messageId. Si el timeout ocurre después de que el email salió pero antes de que el resultado llegue al agente, el retry consulta la tabla y devuelve el messageId original — sin enviar un segundo email.

La tabla en base de datos

create table idempotency_cache (
  key        text primary key,
  result     jsonb not null,
  created_at timestamptz default now(),
  expires_at timestamptz not null
);

create index idx_idempotency_expires
  on idempotency_cache(expires_at);

Un job de limpieza o una función de Supabase elimina las claves expiradas:

delete from idempotency_cache
where expires_at < now();

Qué TTL usar por tipo de herramienta

El TTL debe ser mayor que el tiempo total máximo del run más sus reintentos:

Tipo de operaciónTTL recomendado
Pagos, emails transaccionales24 horas
Notificaciones, actualizaciones de registro1 hora
Generación de documentos, exportaciones7 días
Consultas de lecturano aplica

Cuándo NO es necesario

El patrón solo tiene sentido para operaciones con efectos secundarios en el mundo real:

  • Lecturas (getCustomer(), searchProducts()): naturalmente idempotentes, no necesitan cache
  • Logs y telemetría: un duplicado es inofensivo
  • Operaciones analíticas: no modifican estado externo

Añadir idempotencia a las lecturas consume recursos sin ningún beneficio.

Conclusión

Un agente sin idempotencia en sus herramientas de escritura es un agente que, estadísticamente, va a duplicar una acción real. No porque el código falle, sino porque el mundo falla: los timeouts, las caídas de red y los reinicios de servidor son eventos que ocurren en producción. El retry que previene una interrupción puede causar un problema mayor si las herramientas no están preparadas para él.

Si ya tienes retries configurados en tus agentes, la idempotencia en tool calls es el siguiente paso. Sin ella, cada retry es un riesgo.

¿Construyes agentes IA para producción y necesitas una arquitectura que resista timeouts y retries sin duplicar acciones? Escríbeme por WhatsApp y lo resolvemos juntos.

Compartir artículo

LinkedInXWhatsApp

¿Procesos repetitivos en tu empresa?

Descarga gratis el Mapa de Automatización IA — los 5 procesos que más tiempo roban y cómo resolverlos.

Sin spam. Solo el PDF. Puedes darte de baja cuando quieras.

Escrito por Daily Miranda Pardo

Ayudo a empresas a automatizar procesos, crear agentes IA y conectar sistemas inteligentes.