Empezar aquí
Índice de la sección
Lección 20.3 · práctica

Manejo de errores e is_error

Qué hacer cuando una herramienta falla: devolver el error al modelo con is_error, escribir mensajes que le permitan corregirse y cortar los bucles de reintentos.

Publicado 40 min
Al terminar podrás
  • Distinguir errores de herramienta, resultados vacíos y errores de la API.
  • Implementar un ejecutor que siempre devuelve un tool_result, con is_error cuando falla.
  • Escribir mensajes de error que permitan al modelo corregir la llamada.
  • Limitar reintentos y pasos para evitar bucles.

Requiere: Definir herramientas con JSON Schema

Al terminar esta lección tu bucle de herramientas sobrevivirá a argumentos inválidos, servicios caídos y herramientas que el modelo se inventa, y en la mayoría de los casos el modelo se recuperará solo. La clave es una regla simple: un fallo de herramienta se le cuenta al modelo, no se esconde ni tumba el programa.

La idea

En el bucle de Function calling: el ciclo tool_use → tool_result, si buscar_pedido lanza una excepción, el programa se detiene. Y si la capturas y no devuelves nada, la siguiente llamada a la API falla, porque cada tool_use necesita su tool_result.

La solución es devolver un tool_result siempre, también cuando algo falla, marcándolo con "is_error": True y explicando qué ha pasado. El modelo lee el error y decide: corregir los argumentos y volver a intentarlo, probar otra herramienta, pedirle un dato al usuario o explicarle que no ha podido.

flowchart TD
  T[tool_use] --> E{¿Existe la herramienta?}
  E -- no --> R1[is_error: herramienta desconocida]
  E -- sí --> V{¿Argumentos válidos?}
  V -- no --> R2[is_error: qué campo y por qué]
  V -- sí --> X{¿Ejecuta sin fallo?}
  X -- no --> R3[is_error: qué falló y qué hacer]
  X -- sí --> R4[tool_result normal]

Tres tipos de problema, tres respuestas

Conviene separar tres situaciones que se suelen mezclar:

SituaciónEjemploQué devolver
Resultado vacíoEl cliente no tiene pedidos.Resultado normal: {"pedidos": []}. No es un error.
Error de herramientaPedido inexistente, argumento inválido, servicio caído.tool_result con is_error: True y un mensaje útil.
Error de la APILímite de uso, sobrecarga, clave inválida.Excepción del SDK en tu código; reintentar o abortar. El modelo no se entera.

La diferencia entre las dos primeras importa. Si marcas como error un resultado vacío, el modelo puede insistir pensando que algo ha ido mal. Si devuelves un error como si fuera un dato, el modelo puede responder al usuario con información falsa.

El ejecutor que nunca rompe el protocolo

Mete toda la ejecución en una función que recibe un bloque tool_use y siempre devuelve un tool_result:

import json
from pydantic import BaseModel, ValidationError

class ErrorHerramienta(Exception):
    """Error esperado, con un mensaje pensado para el modelo."""

class ArgsDevolucion(BaseModel):
    id_pedido: str
    motivo: str

PEDIDOS = {"4471": {"estado": "entregado", "dias_desde_entrega": 12}}

def solicitar_devolucion(id_pedido: str, motivo: str) -> dict:
    pedido = PEDIDOS.get(id_pedido)
    if pedido is None:
        raise ErrorHerramienta(f"No existe el pedido {id_pedido}. Pide al usuario que revise el número.")
    if pedido["dias_desde_entrega"] > 30:
        raise ErrorHerramienta("El plazo de devolución (30 días) ha terminado. No reintentes: explícaselo al usuario.")
    return {"devolucion": "creada", "id_pedido": id_pedido}

HERRAMIENTAS = {"solicitar_devolucion": (solicitar_devolucion, ArgsDevolucion)}

def ejecutar(bloque) -> dict:
    def resultado(contenido: str, error: bool = False) -> dict:
        return {"type": "tool_result", "tool_use_id": bloque.id, "content": contenido, "is_error": error}

    if bloque.name not in HERRAMIENTAS:
        return resultado(f"La herramienta {bloque.name} no existe. Disponibles: {', '.join(HERRAMIENTAS)}.", True)
    funcion, Args = HERRAMIENTAS[bloque.name]
    try:
        args = Args.model_validate(bloque.input)
    except ValidationError as e:
        return resultado(f"Argumentos inválidos: {e.errors(include_url=False)}", True)
    try:
        return resultado(json.dumps(funcion(**args.model_dump()), ensure_ascii=False))
    except ErrorHerramienta as e:
        return resultado(str(e), True)
    except Exception:
        # no filtres trazas ni datos internos al modelo; regístralos tú
        return resultado("Error interno al ejecutar la herramienta. Inténtalo más tarde o informa al usuario.", True)
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";

class ErrorHerramienta extends Error {}

const ArgsDevolucion = z.object({ id_pedido: z.string(), motivo: z.string() });
const PEDIDOS: Record<string, { estado: string; dias_desde_entrega: number }> = {
  "4471": { estado: "entregado", dias_desde_entrega: 12 },
};

function solicitarDevolucion({ id_pedido }: z.infer<typeof ArgsDevolucion>) {
  const pedido = PEDIDOS[id_pedido];
  if (!pedido) throw new ErrorHerramienta(`No existe el pedido ${id_pedido}. Pide al usuario que revise el número.`);
  if (pedido.dias_desde_entrega > 30)
    throw new ErrorHerramienta("El plazo de devolución (30 días) ha terminado. No reintentes: explícaselo al usuario.");
  return { devolucion: "creada", id_pedido };
}

const HERRAMIENTAS: Record<string, { fn: (a: any) => object; args: z.ZodTypeAny }> = {
  solicitar_devolucion: { fn: solicitarDevolucion, args: ArgsDevolucion },
};

export function ejecutar(b: Anthropic.ToolUseBlock): Anthropic.ToolResultBlockParam {
  const res = (content: string, is_error = false) => ({ type: "tool_result" as const, tool_use_id: b.id, content, is_error });
  const h = HERRAMIENTAS[b.name];
  if (!h) return res(`La herramienta ${b.name} no existe. Disponibles: ${Object.keys(HERRAMIENTAS).join(", ")}.`, true);
  const args = h.args.safeParse(b.input);
  if (!args.success) return res(`Argumentos inválidos: ${args.error.message}`, true);
  try {
    return res(JSON.stringify(h.fn(args.data)));
  } catch (e) {
    if (e instanceof ErrorHerramienta) return res(e.message, true);
    return res("Error interno al ejecutar la herramienta. Inténtalo más tarde o informa al usuario.", true);
  }
}

En el bucle, sustituye la ejecución directa por resultados.append(ejecutar(bloque)). A partir de aquí, ninguna herramienta puede romper la conversación.

Mensajes de error para un modelo

El mensaje de error es lo único que el modelo tiene para decidir el siguiente paso. Escríbelo como si fuera para una persona que va a reintentar:

  • Qué ha fallado, con el dato concreto: «No existe el pedido 4417».
  • Qué puede hacer: «Pide al usuario que revise el número», «Usa el formato AAAA-MM-DD».
  • Si tiene sentido reintentar: «No reintentes: el plazo ha terminado».

Compara "Error 404" con "No existe el pedido 4417. Pide al usuario que revise el número.". Con el primero, el modelo puede reintentar lo mismo o inventarse una explicación. Con el segundo, hace lo correcto.

Cortar los bucles

Un modelo que recibe errores puede reintentar la misma llamada una y otra vez. Pon dos límites:

  1. Pasos totales (MAX_PASOS), que ya tienes. Cuando se agotan, el programa termina con un mensaje claro.
  2. Errores consecutivos: si una herramienta falla varias veces seguidas con los mismos argumentos, deja de ejecutarla y devuelve un error que lo diga.
import json
from collections import Counter
from ejecutor import ejecutar

fallos = Counter()

def ejecutar_con_limite(bloque, max_fallos: int = 2) -> dict:
    clave = (bloque.name, json.dumps(bloque.input, sort_keys=True))
    if fallos[clave] >= max_fallos:
        return {"type": "tool_result", "tool_use_id": bloque.id, "is_error": True,
                "content": "Esta llamada ya ha fallado varias veces. No la repitas; informa al usuario."}
    r = ejecutar(bloque)
    if r["is_error"]:
        fallos[clave] += 1
    return r

Los reintentos por fallos transitorios (un servicio que tarda o da un error puntual) es mejor hacerlos dentro de la herramienta, con espera entre intentos, antes de devolver nada al modelo. Así no gastas un paso entero de conversación en algo que tu código puede resolver. La receta Reintentos y límites de tasa lo cubre, junto con los reintentos de la propia API.

Errores frecuentes

  • Dejar que una excepción de la herramienta tumbe el bucle.
  • Capturar el error y no devolver tool_result, lo que rompe la siguiente petición.
  • Marcar como error un resultado vacío legítimo.
  • Mensajes de error crípticos («Error», «400») que no dicen qué hacer.
  • Pasar trazas completas o datos internos al modelo.
  • Reintentar sin límite.

Ejercicio

EjercicioPistas

Para provocar la excepción, añade un raise RuntimeError("conexión rechazada a db-interna:5432") en una herramienta y comprueba que ese texto aparece en tu log y no en la conversación.

Resumen

  • Devuelve siempre un tool_result; si algo falla, con is_error: True.
  • Distingue resultado vacío, error de herramienta y error de la API.
  • Escribe errores que digan qué falló, qué hacer y si reintentar.
  • No pases trazas ni datos internos al modelo.
  • Limita pasos totales y fallos repetidos; reintenta lo transitorio dentro de la herramienta.

Para seguir