Creación de un agente Gemma 4 multiherramienta con recuperación de errores

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:

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:

Las funciones son deliberadamente simples, pero se activan ante una entrada incorrecta en lugar de devolver cadenas de error. Aquí está get_weather:

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:

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:

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:

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:

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:

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:

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:

Salida de código

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:

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.