Email copiado — support@tuurt.com
Cargando experiencia
ia · 01 de octubre de 2026 · 7 min

Tool calling en agentes LLM de producción: qué registramos antes de darle acceso a una API real

Probar tool calling en un notebook es trivial; exponerlo contra una API que modifica datos reales no. Vemos qué registramos por cada llamada para auditar y revertir cuando el modelo elige mal.

Por Equipo Tuurt

Tool calling en agentes LLM de producción: qué registramos antes de darle acceso a una API real

Probar tool calling en un notebook es casi siempre tranquilizador. El modelo recibe un esquema de funciones, elige una, el notebook imprime los argumentos, uno los revisa a simple vista, y si algo se ve raro, se corrige el prompt y se vuelve a correr la celda. Todo el ciclo cabe en una pantalla y en la memoria de quien lo está probando. El problema aparece cuando ese mismo agente queda conectado, en producción, a una API que de verdad modifica algo: crea un pedido, cancela una suscripción, envía un correo a un cliente, borra un registro. En ese momento la pregunta deja de ser "¿el modelo elige bien la herramienta la mayoría de las veces?" y pasa a ser otra, mucho menos cómoda: "si elige mal una sola vez, ¿cómo nos enteramos, y cómo lo deshacemos?". Sin una respuesta a esa segunda pregunta, dar acceso real a un agente no es automatizar una tarea, es delegar una decisión que nadie va a poder auditar después.

Lo que cambia entre el notebook y producción

En el notebook hay una sola llamada a la vez, con argumentos que uno mismo construyó o revisó antes de ejecutar. En producción, un agente típico encadena varias llamadas a herramientas dentro de un mismo turno de conversación, decide por su cuenta el orden, y a veces repite una llamada porque interpretó que la anterior no tuvo efecto. Nadie está mirando la pantalla mientras eso ocurre. El único rastro de lo que pasó, si existe, va a ser el que alguien haya decidido guardar de antemano. Y la costumbre que trae la mayoría de los equipos al pasar de prototipo a producción es guardar el mensaje final que el agente le mostró al usuario, no cada paso intermedio que lo produjo. Esa costumbre es la que falla primero.

El registro mínimo por cada llamada a herramienta

La unidad que hay que registrar no es la conversación completa ni la respuesta final: es cada llamada a herramienta, de forma individual, con su propio identificador. Cada fila de ese registro necesita, como mínimo, un identificador de la llamada, un identificador de la sesión o traza a la que pertenece, el nombre de la herramienta, los argumentos exactos que el modelo generó, el resultado crudo que devolvió la API real, el estado final de esa llamada, y una marca de tiempo. Si el proveedor del modelo expone algún tipo de razonamiento o justificación asociada a la elección de la herramienta, también se guarda, pero nunca como sustituto de los argumentos y el resultado reales: el razonamiento explica la intención, no lo que realmente ocurrió contra la API.

def execute_tool(tool_name, arguments, call_fn, trace_id):
    call_id = str(uuid4())
    entry = {
        "call_id": call_id,
        "trace_id": trace_id,
        "tool": tool_name,
        "input": arguments,
        "timestamp": datetime.utcnow().isoformat(),
    }
    try:
        result = call_fn(**arguments)
        entry["status"] = "success"
        entry["output"] = result
    except Exception as exc:
        entry["status"] = "error"
        entry["error"] = str(exc)
        raise
    finally:
        append_log(entry)
    return result

La parte importante de este envoltorio no es la captura de errores, que es obvia. Es que el registro se escribe en el bloque finally, antes de que la excepción se propague o de que el resultado llegue de vuelta al agente. Si el registro se escribiera después de que el agente decide qué hacer con el resultado, una falla en ese paso posterior dejaría la llamada real sin ningún rastro.

Capturar el estado anterior, no solo el resultado

Guardar el resultado de una llamada que modifica datos no basta para poder revertirla. Si un agente cancela una suscripción, el registro de "cancelación exitosa" no dice cuál era el plan, la fecha de renovación o el método de pago antes de cancelar, y sin eso, revertir la operación significa reconstruir ese estado a mano, con la información incompleta que quede en otro sistema. Por eso, para cualquier herramienta que modifique estado, capturamos una fotografía del estado anterior como parte del mismo registro, antes de ejecutar la llamada:

def execute_mutating_tool(tool_name, arguments, call_fn, fetch_before_fn, trace_id):
    before = fetch_before_fn(arguments)
    entry = {"before_state": before, "tool": tool_name, "input": arguments}
    result = call_fn(**arguments)
    entry["after_result"] = result
    append_log(entry)
    return result

Con esa fotografía, revertir deja de ser una reconstrucción manual y se convierte en aplicar la operación inversa usando datos conocidos y exactos, no supuestos sobre cómo estaban las cosas antes.

Idempotencia: el modelo también reintenta

Un agente puede llamar a la misma herramienta dos veces con los mismos argumentos, no porque esté mal diseñado, sino porque no recibió confirmación a tiempo de la primera llamada —un timeout, una respuesta lenta— e interpretó que debía intentarlo de nuevo. Contra una herramienta de lectura eso no tiene consecuencias. Contra una que cobra, envía o borra, una llamada duplicada es un efecto real duplicado. La solución no es confiar en que el modelo no va a reintentar, porque va a reintentar: es exigir una clave de idempotencia determinista en cada llamada mutante, derivada de la sesión y del paso, para que la API de destino pueda reconocer el segundo intento como el mismo y no como uno nuevo.

idempotency_key = sha256(f"{trace_id}:{step_index}:{tool_name}".encode()).hexdigest()

Aprobación humana para las categorías de mayor riesgo

No todas las herramientas merecen el mismo tratamiento. Una herramienta de búsqueda o de consulta puede ejecutarse automáticamente porque un resultado equivocado no tiene costo real fuera de una mala respuesta. Una herramienta que reembolsa dinero, cancela un contrato o envía una comunicación a un tercero sí tiene costo, y para esas categorías no ejecutamos directamente: el agente registra la llamada propuesta, con sus argumentos y su justificación, en una cola de aprobación, y un humano decide antes de que la llamada real ocurra. La decisión de esa persona queda también registrada, enlazada al mismo call_id, de modo que el historial completo —qué propuso el modelo, quién lo aprobó o lo rechazó, y qué pasó después— vive en el mismo lugar.

Lo que no funcionó: loguear solo la respuesta final

La primera versión de este sistema, en un proyecto de un agente que gestionaba tickets de soporte, solo guardaba el mensaje final que el agente le devolvía al usuario, asumiendo que ese mensaje reflejaba fielmente lo que había pasado por dentro. Cuando un ticket terminó cerrado con el estado equivocado después de que el agente encadenara tres llamadas a herramientas distintas, no había manera de saber cuál de las tres había causado el problema: solo existía el texto final, que mencionaba el resultado en términos generales, no cada llamada con sus argumentos reales. Tuvimos que reconstruir el incidente leyendo logs de la API de tickets por separado y cruzando marcas de tiempo a mano, un trabajo que debería haber tomado minutos y tomó la mayor parte de una tarde.

El segundo supuesto que falló fue pensar que los logs del proveedor del modelo —el registro de qué prompt se envió y qué completación se recibió— servían como registro de auditoría de las herramientas. Esos logs capturan la conversación con el modelo, no el efecto real contra nuestra API: son dos sistemas distintos, y confundir uno con el otro deja un hueco exactamente donde más importa, que es el momento en que el agente deja de hablar y empieza a actuar.

Qué revisamos antes de dar acceso a una API real

Antes de conectar un agente a una herramienta que modifica datos, verificamos que cada llamada quede en su propia fila de registro, con identificador propio, argumentos exactos y resultado crudo, independiente del mensaje final que el usuario vea; que toda llamada mutante capture el estado anterior en el mismo registro, para que revertir sea aplicar una operación inversa con datos reales y no una reconstrucción manual; que cada llamada mutante lleve una clave de idempotencia determinista que absorba los reintentos del modelo sin duplicar el efecto; y que las categorías de mayor riesgo pasen por una cola de aprobación humana antes de ejecutarse, no después. Cuando esas cuatro condiciones se cumplen, que el modelo elija mal una herramienta deja de ser un incidente sin explicación, y se convierte en una fila de log que dice exactamente qué pasó y qué hacer para deshacerlo.

llm agentes observabilidad ia
← Volver al blog