volver al blog

Errores de herramienta escritos para el modelo, no para el log

agentesia-generativatool-callingarquitectura

El error que nadie diseñó

Tu herramienta buscar_pedido falla y devuelve esto: KeyError: 'customer_id'. Para ti es una pista clara. Para el modelo es ruido. No sabe si el fallo es suyo, de la base de datos o del universo, y tiene que decidir entre reintentar, cambiar de argumentos, probar otra herramienta o rendirse y contarle algo al usuario.

Decide, como siempre, con lo único que tiene: esa cadena. Si la cadena no dice qué ha ido mal, quién puede arreglarlo y qué conviene hacer ahora, el agente rellena el hueco con una suposición. A veces acierta. Cuando no, reintenta en bucle, inventa un parámetro o responde con seguridad algo que no ha comprobado.

La respuesta de una herramienta tiene dos lectores: el código que la llama y el modelo que la interpreta. Casi todos los errores están escritos para un tercero, el log, que nadie mira a las tres de la madrugada.

Qué necesita saber el modelo

Un mensaje de error útil para un agente responde a tres preguntas, en este orden:

  1. ¿Se puede arreglar desde aquí? Un argumento mal formado se corrige; una base de datos caída no.
  2. ¿Merece la pena reintentar? Un timeout, quizá. Un 404, nunca.
  3. ¿Qué hago ahora? La acción concreta: corregir el campo X, esperar, pedir un dato al usuario o escalar.

No necesita el nombre de la excepción ni la línea del fichero. Necesita una clasificación y una instrucción.

Tipo de falloQué hace el modelo con un error crudoQué debería leer
Argumento inválidoReintenta igual o inventa otro valorQué campo falla, qué formato se espera y un ejemplo
Recurso inexistenteReintenta con variaciones al azar”No existe el pedido 8812. Verifica el identificador con listar_pedidos”
Fallo transitorioAbandona demasiado pronto o machaca el servicio”Servicio ocupado. Reintenta en 5 s; máximo dos veces”
Fallo permanenteInsiste hasta agotar el presupuesto”Esta acción no está disponible. No reintentes; informa al usuario”
PermisosPrueba rutas alternativas peligrosas”Sin permiso para esta operación. No hay forma de resolverlo desde el agente”

La última fila importa más de lo que parece. Un agente al que le deniegan un permiso y no se le dice que es definitivo tiende a buscar un rodeo. Un error claro es también un límite de seguridad.

Un envoltorio que lo hace por defecto

No conviene escribir el mensaje a mano en cada herramienta. Se centraliza: la herramienta lanza excepciones de dominio y un envoltorio las traduce a un formato estable.

from dataclasses import dataclass, asdict
from typing import Literal

# Cuatro acciones posibles. El modelo no necesita más matices.
Accion = Literal["corregir_argumentos", "reintentar", "pedir_dato", "abandonar"]

@dataclass
class ErrorHerramienta:
    ok: bool
    codigo: str          # estable y corto: "pedido_no_existe"
    mensaje: str         # una frase en lenguaje llano
    accion: Accion       # lo que debería hacer el agente
    reintentable: bool
    detalle: dict | None = None  # campo, formato esperado, ejemplo...

class PedidoNoExiste(Exception):
    def __init__(self, pedido_id: str):
        self.pedido_id = pedido_id

def traducir(exc: Exception) -> ErrorHerramienta:
    # Cada excepción de dominio se convierte en un contrato legible.
    if isinstance(exc, PedidoNoExiste):
        return ErrorHerramienta(
            ok=False,
            codigo="pedido_no_existe",
            mensaje=f"No existe el pedido {exc.pedido_id}.",
            accion="corregir_argumentos",
            reintentable=False,
            detalle={"campo": "pedido_id", "ejemplo": "PED-20481"},
        )
    if isinstance(exc, TimeoutError):
        return ErrorHerramienta(
            ok=False, codigo="servicio_lento",
            mensaje="El servicio no respondió a tiempo.",
            accion="reintentar", reintentable=True,
        )
    # Lo desconocido se declara como tal: sin stack trace, sin inventar causa.
    return ErrorHerramienta(
        ok=False, codigo="error_interno",
        mensaje="Fallo interno de la herramienta. No es un problema de tus argumentos.",
        accion="abandonar", reintentable=False,
    )

def ejecutar(herramienta, **kwargs) -> dict:
    try:
        return {"ok": True, "datos": herramienta(**kwargs)}
    except Exception as exc:
        # El traceback completo va al log, con el id de la llamada.
        # Al modelo solo llega el contrato.
        return asdict(traducir(exc))

Dos decisiones merecen explicación. El campo accion tiene un vocabulario cerrado, así que puedes medir con qué frecuencia el agente la obedece. Y el caso desconocido dice expresamente que el problema no son los argumentos: sin esa frase, el modelo suele culparse y probar variaciones inútiles.

Tres errores de diseño frecuentes

Devolver el traceback entero. Cuesta tokens, expone rutas internas y a veces secretos. Y no mejora la decisión: el modelo lee “línea 214” y no puede hacer nada con ella.

Un mensaje demasiado amable. “Parece que algo salió mal, ¿quizá quieras intentarlo de nuevo?” invita a reintentar incluso cuando no sirve. Los mensajes de error son una de las pocas partes de un prompt donde la ambigüedad sale cara.

Éxito parcial disfrazado de éxito. Una herramienta que procesa 8 de 10 elementos y devuelve ok: true produce el peor de los resultados: el agente informa de que terminó. Si falla algo, el campo ok no puede ser true; añade la lista de lo que falló y de lo que se completó.

Cómo comprobar que funciona

Los errores se testean como cualquier otro contrato. Fuerza cada fallo con un doble de la herramienta y mira qué hace el agente. Tres métricas bastan:

  • Reintentos inútiles: llamadas repetidas con los mismos argumentos tras un error no reintentable.
  • Obediencia a accion: porcentaje de turnos donde el siguiente paso coincide con la acción indicada.
  • Rodeos: intentos de usar otra herramienta para esquivar una denegación.

Si los reintentos inútiles bajan y la obediencia sube, el cambio vale. Si no, el problema suele estar en que el mensaje sigue sin decir qué hacer.

Qué hacer el lunes

Coge la herramienta que más falla en tus trazas y lee sus últimos veinte errores como si fueras el modelo, sin contexto. Si no sabrías qué hacer con alguno, tampoco lo sabe él. Reescribe esos mensajes con una causa, una clasificación y una acción, y compara los reintentos de la semana siguiente.

Esto se parece a la idempotencia en herramientas de agentes: la herramienta es la frontera correcta para defender al agente, no el prompt. Un prompt que dice “no reintentes si el error es permanente” funciona peor que una herramienta que dice con claridad que lo es.