En este artículo, aprenderá cómo transformar un script básico de llamada de herramientas en un agente resistente que maneja con elegancia las fallas causadas por herramientas que se comportan mal, resultados de modelos con formato incorrecto y servicios no disponibles.
Los temas que cubriremos incluyen:
Cómo estructurar un bucle de agente iterativo con un límite de seguridad en el recuento de iteraciones. Las cuatro categorías distintas de fallas que encuentra un agente al llamar a las herramientas y cómo manejar cada una de ellas. Cómo diseñar mensajes de error de herramientas que le enseñen al modelo cómo recuperarse, reduciendo las iteraciones desperdiciadas.
Creación de un agente Gemma 4 multiherramienta con recuperación de errores
Introducción
En un artículo anterior, conectamos Gemma 4 a un puñado de funciones de Python utilizando la API de llamada de herramientas de Ollama. Eso nos dio un despachador de un solo turno que funciona: el modelo elige una herramienta, nuestro código la ejecuta, el modelo responde. Es un punto de partida útil, pero dista mucho de ser un agente.
Una de las cosas que convierte una demostración de llamada de herramientas en un agente real es cómo maneja las cosas que van mal. Las herramientas fallan. El modelo alucina el nombre de una función, pasa una cadena donde quería un número o pregunta sobre una ciudad de la que su tabla de búsqueda nunca ha oído hablar. Se agota el tiempo de espera de una API ascendente. Falta un argumento obligatorio. En el tutorial anterior, cualquiera de estos bloquearía el script o sería absorbido por un intento/excepto que imprimiera un mensaje y se diera por vencido. Eso está bien para una demostración de ruta única. No está bien para nada que quieras dejar funcionando.
Este artículo reconstruye el agente en torno a la suposición de que las cosas saldrán mal y muestra cómo recuperarse con gracia cuando esto sucede. El patrón es simple: detectar errores en el límite, convertirlos en mensajes que el modelo pueda leer, enviarlos de regreso al modelo y dejar que el modelo decida si reintentar, solucionar el problema o explicar la falla al usuario. También envolveremos todo en un bucle de agente iterativo adecuado con un límite de seguridad en el recuento de iteraciones.
El guión completo se puede encontrar aquí. Este artículo analiza las partes que importan.
Repensar el bucle de herramientas
El despachador original ejecutó una sola ronda: enviar la consulta del usuario, recopilar llamadas a herramientas, ejecutarlas, enviar los resultados e imprimir la respuesta del modelo. Esa es una interacción de una sola vez. Funciona bien cuando la primera respuesta del modelo responde correctamente a la pregunta del usuario, pero no tiene adónde ir cuando algo sale mal. Si una herramienta falla, el modelo tiene una oportunidad de reaccionar y listo. Si el modelo quiere llamar a otra herramienta después de ver el primer resultado, lástima; ya salimos.
Un bucle de agente adecuado es iterativo. La estructura es sencilla:
Envía el historial de mensajes actual al modelo. Si el modelo produce llamadas a herramientas, ejecute cada una, agregue cada resultado al historial y vuelva a realizar el bucle. Si el modelo produce una respuesta de texto sin formato, esa es la respuesta final. Devolver. Limite el bucle en MAX_ITERACIONES para que un modelo confuso no pueda consumir su CPU para siempre.
Este último punto no es negociable. Los modelos pequeños ocasionalmente se quedan atascados llamando repetidamente a la misma herramienta u oscilando entre dos herramientas, y no hay nada más desmoralizante que regresar a su terminal y encontrar los ventiladores de su computadora portátil gritando porque Gemma decidió buscar el clima en Londres treinta veces seguidas.
Aquí está el bucle:
def run_agent(user_query):
messages = [{“role”: “user”, “content”: user_query}]
for iteration in range(1, MAX_ITERATIONS + 1):
payload = {
“model”: MODEL_NAME,
“messages”: messages,
“tools”: available_tools,
“stream”: False,
}
print(f”[EXECUTION — iteration {iteration}]”)
print(” ● Querying model…n”)
try:
response_data = call_ollama(payload)
except Exception as e:
print(f” └─ [ERROR] Error calling Ollama API: {e}”)
print(f” └─ Make sure Ollama is running and {MODEL_NAME} is pulled.”)
return
message = response_data.get(“message”, {})
tool_calls = message.get(“tool_calls”) or[]# Rama A: el modelo quiere usar herramientas si tool_calls: print(f"[TOOL EXECUTION — {len(tool_calls)} call(s)]") message.append(message) tool_messages = print_tool_calls(tool_calls) message.extend(tool_messages) print() continue # Rama B: el modelo produjo una respuesta final print("[RESPUESTA]") print(message.get("content", "") + "n") return # Barandilla de seguridad: agotamos MAX_ITERACIONES sin una respuesta final print("[RESPUESTA]") print( f"Alcanza el límite de iteración {MAX_ITERACIONES} sin una respuesta final. " "Esto generalmente significa que el modelo está atrapado en un bucle de llamada de herramientas. " "Intenta simplificar la consulta.n" )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def run_agent ( consulta_usuario ) :
mensajes = [ { "role" : "usuario" , "contenido" : consulta_usuario } ]
para iteración en rango ( 1 , MAX_ITERACIONES + 1 ) :
carga útil = {
"modelo" : MODELO_NOMBRE ,
"mensajes" : mensajes ,
"herramientas" : herramientas_disponibles ,
"arroyo" : FALSO ,
}
print ( f "[EJECUCIÓN – iteración {iteración}]" )
print ( " ● Consultando modelo…n" )
intentar :
datos_respuesta = call_ollama ( carga útil )
excepto excepción como mi :
print ( f " └─ [ERROR] Error al llamar a la API de Ollama: {e}" )
print ( f " └─ Asegúrate de que Ollama se esté ejecutando y que {MODEL_NAME} esté extraído." )
devolver
mensaje = datos_respuesta . obtener ( "mensaje" , { } )
llamadas_herramientas = mensaje . obtener ( "llamadas a herramientas" ) o [ ]
# Rama A: el modelo quiere usar herramientas
si llamadas_herramientas :
print ( f "[EJECUCIÓN DE LA HERRAMIENTA — {len(tool_calls)} llamada(s)]" )
mensajes . agregar ( mensaje )
mensajes_herramientas = print_tool_calls ( llamadas_herramientas )
mensajes . extender ( mensajes_herramientas )
imprimir ( )
continuar
# Rama B: el modelo produjo una respuesta final
imprimir ( "[RESPUESTA]" )
imprimir ( mensaje . get ( "contenido" , "" ) + "n" )
devolver
# Barandilla de seguridad: agotamos MAX_ITERACIONES sin una respuesta final
imprimir ( "[RESPUESTA]" )
imprimir (
f "Alcanzar el límite de iteración {MAX_ITERATION} sin una respuesta final."
"Esto normalmente significa que el modelo está atrapado en un bucle de llamada de herramientas".
"Intenta simplificar la consulta.n"
)
Vale la pena memorizar el patrón porque aparece en cada marco de agente que jamás lea: el historial de mensajes es el estado. Para cada iteración, enviamos la conversación completa (la consulta del usuario original, la solicitud de llamada a la herramienta del modelo, los resultados de nuestra herramienta, cualquier mensaje de seguimiento del modelo) de regreso al modelo. El modelo es apátrida; la lista es la memoria del agente.
Esta estructura iterativa también es lo que hace posible la recuperación de errores. Cuando una herramienta falla y enviamos el error como un mensaje de herramienta, el modelo ve ese error y reacciona ante él en la siguiente iteración. Sin el bucle, no hay nada ante lo que reaccionar.
Creación del registro de herramientas
Aquí construimos nuestras cuatro herramientas, todas deterministas, todas fuera de línea. Sin claves API, sin llamadas de red, sin servicios externos inestables que depurar. El objetivo de este artículo es la arquitectura de manejo de errores, no las herramientas en sí, por lo que queremos que las herramientas se comporten de manera predecible para que podamos centrarnos en el marco que las rodea y así podamos activar deliberadamente cada modo de falla a voluntad.
Las herramientas son:
get_weather(city): busca una ciudad en un pequeño dictado de datos meteorológicos enlatados get_local_time(city): calcula la hora actual real en la zona horaria de esa ciudad usando info de zona convert_currency(amount, from_currency, to_currency): hace los cálculos con una tabla de tarifas anclada en USD codificada get_city_population(city): otra búsqueda con un pequeño dict
Los datos estáticos se encuentran en la parte superior del archivo:
CITY_DATA = {
“london”: {“timezone”: “Europe/London”, “population”: 8_982_000},
“tokyo”: {“timezone”: “Asia/Tokyo”, “population”: 13_960_000},
“sao paulo”: {“timezone”: “America/Sao_Paulo”, “population”: 12_330_000},
“paris”: {“timezone”: “Europe/Paris”, “population”: 2_161_000},
“new york”: {“timezone”: “America/New_York”, “population”: 8_336_000},
“sydney”: {“timezone”: “Australia/Sydney”, “population”: 5_312_000},
“mumbai”: {“timezone”: “Asia/Kolkata”, “population”: 20_410_000},
}
EXCHANGE_RATES = {
“USD”: 1.00, “EUR”: 0.92, “GBP”: 0.79, “JPY”: 156.40,
“BRL”: 5.12, “CAD”: 1.37, “AUD”: 1.51, “INR”: 83.20,
}
CITY_DATA = {
“london” : { “timezone” : “Europe/London” , “population” : 8_982_000 } ,
“tokyo” : { “timezone” : “Asia/Tokyo” , “population” : 13_960_000 } ,
“sao paulo” : { “timezone” : “America/Sao_Paulo” , “population” : 12_330_000 } ,
“paris” : { “timezone” : “Europe/Paris” , “population” : 2_161_000 } ,
“new york” : { “timezone” : “America/New_York” , “population” : 8_336_000 } ,
"Sídney" : { "zona horaria" : "Australia/Sídney" , "población" : 5_312_000 } ,
"mumbai" : { "zona horaria" : "Asia/Calcuta" , "población" : 20_410_000 } ,
}
TIPOS DE CAMBIO = {
"DÓLAR ESTADOUNIDENSE" : 1,00 , "euros" : 0,92 , "GBP" : 0,79 , "GUAY" : 156,40 ,
"BRL" : 5.12 , "CAD" : 1,37 , "AUD" : 1,51 , "INR" : 83,20 ,
}
Las funciones son deliberadamente simples, pero se activan ante una entrada incorrecta en lugar de devolver cadenas de error. Aquí está get_weather:
def get_weather(city: str) -> str:
“””Returns current weather conditions for a known city.”””
key = city.lower().strip()
if key not in WEATHER_DATA:
raise ValueError(
f”Unknown city: ‘{city}’. Known cities: {‘, ‘.join(sorted(WEATHER_DATA.keys()))}.”
)
data = WEATHER_DATA[key]
return f”The weather in {city.title()} is {data[‘conditions’]} with a temperature of {data[‘temp_c’]}°C.”
def get_weather ( city : str ) -> str :
“” “Returns current weather conditions for a known city.” “”
key = city . lower ( ) . strip ( )
if key not in WEATHER_DATA :
raise ValueError (
f “Unknown city: ‘{city}’. Known cities: {‘, ‘.join(sorted(WEATHER_DATA.keys()))}.”
)
data = WEATHER_DATA [ key ]
return f “The weather in {city.title()} is {data[‘conditions’]} with a temperature of {data[‘temp_c’]}°C.”
Dos cosas a destacar sobre ese mensaje de error. Primero, es específico: le dice a la persona que llama qué salió mal y cuáles son las opciones válidas. En segundo lugar, la herramienta genera un ValueError en lugar de devolver el error como una cadena. No detecte errores de formato de cadena dentro de la herramienta; en cambio, déjelos propagarse. Queremos que el despachador maneje todo tipo de fallas en un solo lugar y queremos que el mensaje que el modelo ve en una entrada incorrecta sea lo suficientemente informativo como para que el modelo pueda corregirse por sí solo.
get_local_time hace el único trabajo real (aritmética de fecha y hora real con reconocimiento de zona horaria) y esa es también la herramienta que usaremos más adelante para demostrar una degradación elegante frente a una falla ascendente simulada:
“””Returns the current local time for a city, with a cached fallback.”””
key = city.lower().strip()
# Simulate an upstream geocoding service that may fail unpredictably
if SIMULATE_GEOCODING_OUTAGE and random.random() < 0.6:
if key in TIMEZONE_FALLBACK_CACHE:
tz_name = TIMEZONE_FALLBACK_CACHE[key]
now = datetime.datetime.now(ZoneInfo(tz_name))
return (
f”[cached] The current local time in {city.title()} is “
f”{now.strftime(‘%H:%M on %A, %B %d, %Y’)} ({tz_name}). “
“Note: geocoding service is currently unavailable; this value is from the local cache.”
)
raise ToolUnavailableError(
f”Geocoding service is unavailable and ‘{city}’ is not in the local cache. “
“Please try again later or use a city from the cache: “
f”{‘, ‘.join(sorted(TIMEZONE_FALLBACK_CACHE.keys()))}.”
)
if key not in CITY_DATA:
raise ValueError(f”Unknown city: ‘{city}’. Known cities: {‘, ‘.join(sorted(CITY_DATA.keys()))}.”)
tz_name = CITY_DATA[key][“timezone”]
now = datetime.datetime.now(ZoneInfo(tz_name))
return f”The current local time in {city.title()} is {now.strftime(‘%H:%M on %A, %B %d, %Y’)} ({tz_name}).”
That SIMULATE_GEOCODING_OUTAGE flag lets us reproduce a real-world failure mode without needing real infrastructure to fail. We’ll come back to it.
The tool schemas are unchanged from the previous tutorial’s style: standard Ollama function-calling format, with clear descriptions of what each tool does and what arguments it expects.
The Four Error Recovery Patterns
Time to get serious. There are four distinct failure modes you’ll encounter when an agent talks to tools, and each one needs its own strategy. They’re handled in a single dispatcher function, but it’s worth understanding them as separate concepts.
Pattern 1: Tool Execution Errors
The first defense is the dispatcher itself. It wraps every tool call in a structured try/except block and converts every kind of failure into a (status, content) pair the agent loop can pass back to the model:
def dispatch_tool_call(tool_call):
function_name = tool_call[“function”][“name”]
arguments = tool_call[“function”][“arguments”] or {}
# Defense 1: validate the tool name against the registry
if function_name not in TOOL_FUNCTIONS:
return “error”, (
f”Unknown tool ‘{function_name}’. “
f”Valid tools are: {‘, ‘.join(TOOL_FUNCTIONS.keys())}.”
)
func = TOOL_FUNCTIONS[function_name]
# Defense 2: catch argument errors (wrong types, missing or extra args)
try:
result = func(**arguments)
return “ok”, str(result)
except TypeError as e:
return “error”, f”Bad arguments for {function_name}: {e}”
except ValueError as e:
return “error”, str(e)
except ToolUnavailableError as e:
return “error”, f”Tool temporarily unavailable: {e}”
except Exception as e:
return “error”, f”Unexpected error in {function_name}: {type(e).__name__}: {e}”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
def get_local_time(city: str) -> str:
“”“Returns the current local time for a city, with a cached fallback.”“”
key = city.lower().strip()
# Simulate an upstream geocoding service that may fail unpredictably
if SIMULATE_GEOCODING_OUTAGE and random.random() < 0.6:
if key in TIMEZONE_FALLBACK_CACHE:
tz_name = TIMEZONE_FALLBACK_CACHE[key]
now = datetime.datetime.now(ZoneInfo(tz_name))
return (
f“[cached] The current local time in {city.title()} is “
f“{now.strftime(‘%H:%M on %A, %B %d, %Y’)} ({tz_name}). “
“Note: geocoding service is currently unavailable; this value is from the local cache.”
)
raise ToolUnavailableError(
f“Geocoding service is unavailable and ‘{city}’ is not in the local cache. “
“Please try again later or use a city from the cache: “
f“{‘, ‘.join(sorted(TIMEZONE_FALLBACK_CACHE.keys()))}.”
)
if key not in CITY_DATA:
raise ValueError(f“Unknown city: ‘{city}’. Known cities: {‘, ‘.join(sorted(CITY_DATA.keys()))}.”)
tz_name = CITY_DATA[key][“timezone”]
now = datetime.datetime.now(ZoneInfo(tz_name))
devolver f "La hora local actual en {city.title()} es {now.strftime('%H:%M on %A, %B %d, %Y')} ({tz_name})."
Eso < código > SIMULATE_GEOCODING_OUTAGE < / código > La bandera nos deja reproducir. a modo de falla del mundo real sin necesidad de infraestructura real para fallar . Volveremos a ello.
< h2 > Los cuatro patrones de recuperación de errores < / h2 >
Es hora de ponte serio . Hay cuatro modos de falla distintos que encontrará cuando un agente habla con las herramientas, y cada uno necesita su propia estrategia. Son manejados en a función de despachador único , pero vale la pena entenderlos como conceptos separados.
Patrón 1: errores de ejecución de herramientas
La primera defensa es el propio despachador. Envuelve cada llamada a la herramienta en un bloque try/except estructurado y convierte cada tipo de falla en un par (estado, contenido) que el bucle del agente puede devolver al modelo:
nombre_función = llamada_herramienta["función"]["nombre"]
argumentos = llamada_herramienta["función"]["argumentos"] o {}
# Defensa 1: validar el nombre de la herramienta con el registro
si nombre_función no está en TOOL_FUNCTIONS:
devolver "error", (
f"Herramienta desconocida ' { nombre_función } '. "
f"Las herramientas válidas son: {' , ' . unirse ( TOOL_FUNCTIONS . claves ( ) ) } . "
)
func = TOOL_FUNCTIONS[nombre_función]
# Defensa 2: detectar errores de argumentos (tipos incorrectos, argumentos faltantes o adicionales)
intentar:
resultado = func(**argumentos)
devolver " ok ", str(resultado)
excepto TypeError como e:
devolver " error ", f " Malo argumentos para { nombre_función } : { mi } "
excepto ValueError como e:
devolver " error ", cadena (e)
excepto ToolUnavailableError como e:
return " error ", f " Herramienta no disponible temporalmente : { mi } "
excepto excepción como e:
devolver " error ", f" Inesperado error en { nombre_función } : { escriba ( e ) . __nombre__ } : { mi } "
La idea clave: devolver el error al modelo como resultado de la herramienta en lugar de devolverlo al bucle del agente. El modelo puede leer el error, ver que solicitó "Atlantis" y Atlantis no es una ciudad conocida, y pasar a una ciudad diferente o disculparse con el usuario. Si en lugar de eso subes, habrás despojado al modelo de la capacidad de recuperarse.
Observe los cuatro tipos diferentes de excepciones y el comodín en la parte inferior. Cada uno corresponde a una categoría real de falla: errores de dominio (ValueError), discrepancias de firmas (TypeError), interrupciones de la infraestructura (ToolUnavailableError) y las incógnitas de Don Rumsfeld (Exception). Separarlos genera mensajes de error más claros, lo que le da al modelo mejores señales para la recuperación.
El comodín es importante y quizás controvertido. Algunas guías de estilo le dirán que nunca detecte una excepción simple. En un despachador de agentes, la alternativa (dejar que una excepción inesperada cierre el ciclo) es peor. El modelo pierde la posibilidad de recuperarse, el usuario pierde la respuesta y usted pierde el historial de conversación que podría haber utilizado para depurar lo sucedido. Es mejor captar, registrar y entregar el mensaje al modelo.
Patrón 2: llamadas a herramientas con formato incorrecto desde el modelo
Ocasionalmente, el modelo alucina con el nombre de una herramienta que no existe o envía argumentos con claves incorrectas (pueblo en lugar de ciudad, por ejemplo). La primera defensa en el fragmento anterior maneja el primer caso: incluso antes de intentar enviar, verificamos el nombre con el registro y devolvemos un mensaje correctivo que enumera los nombres válidos.
El caso del argumento erróneo lo maneja la segunda defensa. El desempaquetado de **argumentos de Python genera TypeError si el modelo envía una palabra clave que la función no acepta u omite una requerida. Detectamos el TypeError, lo formateamos limpiamente y el modelo obtiene un error útil en la siguiente iteración:
[ERROR]: Bad arguments for get_weather: get_weather() got an unexpected keyword argument ‘town’
[ ERROR ] : Bad arguments for get_weather : get_weather ( ) got an unexpected keyword argument ‘town’
Ese mensaje contiene todo lo que el modelo necesita para corregirse: el nombre de la herramienta, el argumento ofensivo y una señal implícita de que el nombre correcto es otra cosa. En la práctica, el modelo suele fijar la decisión en su siguiente turno.
También hay un fallo más sutil relacionado con los argumentos: la deriva tipográfica. El modelo sabe que la cantidad debe ser un número, pero en conversaciones más largas ocasionalmente comienza a enviar "100" como una cadena. Dejar que convert_currency aumente eso obligaría a un giro adicional para que el modelo se corrija. Un mejor enfoque es la coerción defensiva en la propia herramienta:
def convert_currency(amount: float, from_currency: str, to_currency: str) -> str:
# Defensive type coercion: the model sometimes sends numbers as strings
try:
amount = float(amount)
except (TypeError, ValueError):
raise ValueError(f”‘amount’ must be a number, got: {amount!r}”)
# … rest of the function
def convert_currency ( amount : float , from_currency : str , to_currency : str ) -> str :
# Defensive type coercion: the model sometimes sends numbers as strings
try :
amount = float ( amount )
except ( TypeError , ValueError ) :
raise ValueError ( f “‘amount’ must be a number, got: {amount!r}” )
# … rest of the function
Esto soluciona silenciosamente el caso común ("100" se convierte en 100.0) y al mismo tiempo genera un error claro para el caso genuinamente roto ("cincuenta"). El principio: sé liberal en lo que aceptas del modelo y estricto en lo que te quejas.
Patrón 3: errores a nivel de dominio
Estos son los errores que genera la propia herramienta cuando las entradas están bien formadas pero la solicitud no se puede satisfacer, como preguntar por el clima en Atlantis o convertir desde una moneda que no está en la tabla de tasas. Estos deberían producir mensajes de error que le enseñen al modelo cómo recuperarse, no solo decir "fallido".
Compare estos dos mensajes de error:
Good: “Unknown city: ‘Atlantis’. Known cities: london, mumbai, new york, paris, sao paulo, sydney, tokyo.”
Good : “Unknown city: ‘Atlantis’. Known cities: london, mumbai, new york, paris, sao paulo, sydney, tokyo.”
La buena versión le da al modelo todo lo que necesita para volver a intentarlo con una entrada válida o explicar la limitación al usuario. La mala versión obliga al modelo a adivinar. Cada mensaje de error en las funciones de la herramienta sigue este patrón: diga qué salió mal y, cuando sea posible, enumere las alternativas válidas.
Esto no es sólo una sutileza de UX. Afecta directamente la cantidad de iteraciones que grabará el ciclo del agente antes de llegar a una buena respuesta. Un error vago puede costarle un viaje de ida y vuelta adicional mientras el modelo busca una solución. Un error específico generalmente se corrige en el siguiente turno o, cuando la entrada es realmente irrecuperable, permite que el modelo produzca una explicación limpia sin volver a intentarlo.
Patrón 4: Degradación elegante para herramientas no disponibles
El último patrón es para la situación en la que una herramienta no está rota, simplemente ha desaparecido: un servicio de geocodificación no funciona, una cuota de API está agotada, una base de datos está teniendo un mal día. Tiene tres opciones aquí, aproximadamente en orden de cuánto confía en el modelo para manejar la situación:
Devuelve un valor almacenado en caché o predeterminado y márcalo en el resultado. Mejor cuando la frescura de la herramienta no es crítica. Omita la herramienta por completo y envíe un mensaje claro sobre lo que no se pudo proporcionar. Deje que el modelo decida si desea volver a intentarlo o solucionarlo. Muestre la interrupción al usuario haciendo que el agente se detenga y solicite orientación.
get_local_time demuestra la opción 1. Cuando SIMULATE_GEOCODING_OUTAGE está activado y la verificación aleatoria se activa, la herramienta primero prueba el caché local:
if SIMULATE_GEOCODING_OUTAGE and random.random() < 0.6:
if key in TIMEZONE_FALLBACK_CACHE:
tz_name = TIMEZONE_FALLBACK_CACHE[key]
now = datetime.datetime.now(ZoneInfo(tz_name))
return (
f”[cached] The current local time in {city.title()} is “
f”{now.strftime(‘%H:%M on %A, %B %d, %Y’)} ({tz_name}). “
“Note: geocoding service is currently unavailable; this value is from the local cache.”
)
raise ToolUnavailableError(
f”Geocoding service is unavailable and ‘{city}’ is not in the local cache. “
“Please try again later or use a city from the cache: “
f”{‘, ‘.join(sorted(TIMEZONE_FALLBACK_CACHE.keys()))}.”
)
if SIMULATE_GEOCODING_OUTAGE and random . random ( ) < 0.6 :
if key in TIMEZONE_FALLBACK_CACHE :
nombre_tz = TIMEZONE_FALLBACK_CACHE [ tecla ]
ahora = fecha y hora . fecha y hora . ahora ( ZoneInfo ( tz_name ) )
devolver (
f "[en caché] La hora local actual en {city.title()} es "
f "{now.strftime('%H:%M en %A, %B %d, %Y')} ({tz_name}). "
"Nota: el servicio de codificación geográfica no está disponible actualmente; este valor proviene del caché local".
)
aumentar ToolUnavailableError (
f "El servicio de codificación geográfica no está disponible y '{ciudad}' no está en la caché local".
"Vuelva a intentarlo más tarde o utilice una ciudad del caché: "
f "{', '.join(ordenado(TIMEZONE_FALLBACK_CACHE.keys()))}."
)
Si la ciudad está en el caché, la herramienta devuelve un resultado exitoso etiquetado con [en caché] y una nota que explica que el servicio en vivo no está disponible. El modelo ve una respuesta perfectamente utilizable y una pequeña advertencia que puede optar por mencionarle al usuario. Si la ciudad no está en el caché, la herramienta pasa a la opción 2: genera ToolUnavailableError con un mensaje que enumera lo que está almacenado en caché.
Ese ToolUnavailableError es intencionalmente un tipo de excepción separado en lugar de un ValueError. El despachador le da su propio brazo de captura con un prefijo de error distintivo (“Herramienta no disponible temporalmente”) para que el modelo pueda distinguir entre “usted pidió algo que no tengo” y “el servicio no está disponible en este momento”. Esos dos fallos tienen respuestas apropiadas muy diferentes (volver a intentarlo más tarde en lugar de elegir una entrada diferente) y darle al modelo una señal clara le ayuda a elegir la correcta.
En producción, extendería este patrón con una política de reintento y retroceso antes de pasar al modo alternativo. La estructura sigue siendo la misma: el despachador distingue las fallas recuperables de las irrecuperables, y el modelo recibe información suficiente sobre cada una de ellas para tomar el siguiente paso sensato.
Poniéndolo todo junto
Es hora de ejecutar la cosa. Aquí hay una consulta que analiza todo: múltiples ciudades, múltiples herramientas y una entrada incorrecta intencional para desencadenar la recuperación de errores en vuelo:
python main.py “What’s the weather in London, Tokyo, and Atlantis right now? And convert 50 GBP to JPY.”
python main . py “What’s the weather in London, Tokyo, and Atlantis right now? And convert 50 GBP to JPY.”
El recuento exacto de iteraciones y el orden de las llamadas a herramientas variarán de una ejecución a otra dependiendo de cómo Gemma decida secuenciar el trabajo, pero aquí hay un rastro representativo, ligeramente recortado:
Mire lo que sucedió en la iteración 3. El modelo preguntó sobre Atlantis, la herramienta generó ValueError, el despachador lo convirtió en un mensaje de error que enumeraba las ciudades válidas y el modelo, en la iteración 5, combinó esa información en una respuesta limpia. No volvió a intentar Atlantis. No se estrelló. Se dio cuenta del fracaso, lo integró con los resultados exitosos y produjo una respuesta que reconocía la limitación. Ésa es toda la recompensa de la arquitectura de recuperación de errores en un solo rastro.
Para ver la degradación elegante en acción, cambie SIMULATE_GEOCODING_OUTAGE a Verdadero y ejecute una consulta que solicite la hora local:
python main.py “What’s the local time in London and Paris?”
python main . py “What’s the local time in London and Paris?”
Aproximadamente el 60 % de las veces verá el prefijo [en caché] en el resultado de la herramienta y el modelo mencionará la fuente almacenada en caché en su respuesta final. El resto del tiempo, la herramienta regresará correctamente y la ruta almacenada en caché no se activará. De cualquier manera, el ciclo se completa y el usuario obtiene una respuesta.
Conclusión
Construimos tres cosas sobre la base del primer tutorial: un bucle de agente iterativo con un límite de iteración estricto, un despachador en capas que detecta cada categoría de falla de la herramienta y funciones de herramienta cuyos mensajes de error le enseñan al modelo cómo recuperarse. Juntos, marcan la diferencia entre una demostración de llamada de herramientas y un agente que realmente querrías dejar funcionando sin supervisión.
Algunos próximos pasos naturales incluyen:
Memoria persistente entre sesiones, para que el agente pueda recordar lo que aprendió sobre usted la semana pasada. Políticas de reintento con retroceso para fallas transitorias en el flujo ascendente. Reincorporar las API externas en lugar de las tablas de búsqueda estáticas, lo que en su mayoría significa simplemente aceptar que los tiempos de espera y los límites de velocidad se conviertan en parte de la superficie de falla normal.
El guión completo está en GitHub. Clónelo, ejecútelo, divídalo deliberadamente para observar la recuperación en acción e incorpore los siguientes pasos anteriores.