En este artículo, aprenderá cómo crear un agente de llamada de herramientas local que dé prioridad a la privacidad utilizando la familia de modelos Gemma 4 y Ollama.
Los temas que cubriremos incluyen:
Una descripción general de la familia de modelos Gemma 4 y sus capacidades. Cómo las llamadas a herramientas permiten que los modelos de lenguaje interactúen con funciones externas. Cómo implementar un sistema de llamada de herramientas local usando Python y Ollama.
Cómo implementar llamadas a herramientas con Gemma 4 y Python
Imagen del editor
Presentamos la familia Gemma 4
El ecosistema de modelos de pesos abiertos cambió recientemente con el lanzamiento de la familia de modelos Gemma 4. Creadas por Google, las variantes de Gemma 4 se crearon con la intención de proporcionar capacidades de nivel fronterizo bajo una licencia permisiva de Apache 2.0, permitiendo a los profesionales del aprendizaje automático un control total sobre su infraestructura y privacidad de datos.
El lanzamiento de Gemma 4 presenta modelos que van desde el 31B, denso en parámetros, y el 26B, estructuralmente complejo, Mixture of Experts (MoE) hasta variantes livianas y enfocadas en los bordes. Lo que es más importante para los ingenieros de IA es que la familia de modelos presenta soporte nativo para flujos de trabajo agentes. Se han ajustado para generar de manera confiable salidas JSON estructuradas e invocar de forma nativa llamadas a funciones basadas en instrucciones del sistema. Esto los transforma de motores de razonamiento con "dedos cruzados" a sistemas prácticos capaces de ejecutar flujos de trabajo y conversar con API externas localmente.
Llamada de herramientas en modelos de lenguaje
Los modelos lingüísticos comenzaron su vida como conversadores de circuito cerrado. Si le preguntaras a un modelo de lenguaje sobre la lectura de sensores en el mundo real o las tasas de mercado en vivo, en el mejor de los casos podría disculparse y, en el peor, alucinar una respuesta. La llamada a herramientas, también conocida como llamada a funciones, es el cambio de arquitectura fundamental necesario para solucionar esta brecha.
La llamada a herramientas sirve como puente que puede ayudar a transformar modelos estáticos en agentes autónomos dinámicos. Cuando la llamada a herramientas está habilitada, el modelo evalúa un mensaje de usuario frente a un registro proporcionado de herramientas programáticas disponibles (proporcionado a través del esquema JSON). En lugar de intentar adivinar la respuesta utilizando únicamente ponderaciones internas, el modelo detiene la inferencia, formatea una solicitud estructurada diseñada específicamente para activar una función externa y espera el resultado. Una vez que la aplicación host procesa el resultado y lo devuelve al modelo, el modelo sintetiza el contexto en vivo inyectado para formular una respuesta final fundamentada.
La preparación: Ollama y Gemma 4:E2B
Para construir un sistema de llamada de herramientas genuinamente local y privado, usaremos Ollama como nuestro corredor de inferencia local, junto con el modelo gemma4:e2b (parámetro Edge 2 mil millones).
El modelo gemma4:e2b está diseñado específicamente para dispositivos móviles y aplicaciones de IoT. Representa un cambio de paradigma en lo que es posible en el hardware de consumo, activando una huella efectiva de 2 mil millones de parámetros durante la inferencia. Esta optimización preserva la memoria del sistema y al mismo tiempo logra una ejecución con latencia cercana a cero. Al ejecutarse completamente fuera de línea, elimina los límites de velocidad y los costos de API al tiempo que preserva la estricta privacidad de los datos.
A pesar de este tamaño increíblemente pequeño, Google ha diseñado gemma4:e2b para heredar las propiedades multimodales y las capacidades nativas de llamada de funciones del modelo 31B más grande, lo que lo convierte en una base ideal para un agente de escritorio rápido y con capacidad de respuesta. También nos permite probar las capacidades de la nueva familia de modelos sin necesidad de una GPU.
El código: configurar el agente
Para orquestar el modelo de lenguaje y las interfaces de herramientas, nos basaremos en una filosofía de dependencia cero para nuestra implementación, aprovechando solo bibliotecas estándar de Python como urllib y json, garantizando la máxima portabilidad y transparencia y evitando al mismo tiempo la sobrecarga.
El código completo de este tutorial se puede encontrar en este repositorio de GitHub.
El flujo arquitectónico de nuestra aplicación opera de la siguiente manera:
Definir funciones locales de Python que actúan como nuestras herramientas Definir un esquema JSON estricto que explique al modelo de lenguaje exactamente qué hacen estas herramientas y qué parámetros esperan Pasar la consulta del usuario y el registro de la herramienta a la API de Ollama local Capture la respuesta del modelo, identifique si solicitó una llamada a la herramienta, ejecute el código local correspondiente y envíe la respuesta
Construyendo las herramientas: get_current_weather
Profundicemos en el código, teniendo en cuenta que la capacidad de nuestro agente depende de la calidad de sus funciones subyacentes. Nuestra primera función es get_current_weather, que se comunica con la API Open-Meteo de código abierto para resolver datos meteorológicos en tiempo real para una ubicación específica.
def get_current_weather(city: str, unit: str = “celsius”) -> str:
“””Gets the current temperature for a given city using open-meteo API.”””
try:
# Geocode the city to get latitude and longitude
geo_url = f”https://geocoding-api.open-meteo.com/v1/search?name={urllib.parse.quote(city)}&count=1″
geo_req = urllib.request.Request(geo_url, headers={‘User-Agent’: ‘Gemma4ToolCalling/1.0’})
with urllib.request.urlopen(geo_req) as response:
geo_data = json.loads(response.read().decode(‘utf-8’))
if “results” not in geo_data or not geo_data[“results”]:
return f”Could not find coordinates for city: {city}.”
location = geo_data[“results”][0]lat = ubicación["latitud"] lon = ubicación["longitud"] país = ubicación.get("país", "") # Obtener el clima temp_unit = "fahrenheit" if unit.lower() == "fahrenheit" else "celsius" weather_url = f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}¤t=temperature_2m,wind_speed_10m&temperature_unit={temp_unit}" weather_req = urllib.request.Request(weather_url, headers={'User-Agent': 'Gemma4ToolCalling/1.0'}) con urllib.request.urlopen(weather_req) como respuesta: Weather_data = json.loads(response.read().decode('utf-8')) si "actual" en Weather_data: actual = Weather_data["current"] temp = actual["temperature_2m"] viento = actual["wind_speed_10m"] temp_unit_str = Weather_data["current_units"]["temperature_2m"] wind_unit_str = Weather_data["current_units"]["wind_speed_10m"] return f"El clima actual en {city.title()} ({country}) es {temp}{temp_unit_str} con velocidades de viento de {wind}{wind_unit_str}." else: return f"Los datos meteorológicos de {ciudad} no están disponibles en la API". excepto excepción como e: return f"Error al obtener el clima para {ciudad}: {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
def get_current_weather ( ciudad : cadena , unidad : cadena = "grados centígrados" ) -> cadena :
"" "Obtiene la temperatura actual de una ciudad determinada utilizando la API open-meteo." ""
intentar :
# Geocodificar la ciudad para obtener latitud y longitud
geo_url = f "https://geocoding-api.open-meteo.com/v1/search?name={urllib.parse.quote(ciudad)}&count=1"
geo_req = URLlib . pedido . Solicitud ( geo_url , encabezados = { 'Usuario-Agente' : 'Gemma4ToolCalling/1.0' } )
con urllib . pedido . urlopen ( geo_req ) como respuesta :
geo_datos = json . cargas ( respuesta . leer ( ) . decodificar ( 'utf-8' ) )
si "resultados" no en geo_datos o no geo_data [ "resultados" ] :
devolver f "No se pudieron encontrar las coordenadas de la ciudad: {ciudad}."
ubicación = geo_data [ "resultados" ] [ 0 ]
latitud = ubicación [ "latitud" ]
lon = ubicación [ "longitud" ]
país = ubicación . obtener ( "país" , "" )
# Busca el clima
unidad_temperatura = "fahrenheit" si unidad . más bajo ( ) == "fahrenheit" demás "Celsius"
URL del tiempo = f "https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}¤t=temperature_2m,wind_speed_10m&temperature_unit={temp_unit}"
clima_req = URLlib . pedido . Solicitud ( meteo_url , encabezados = { 'Usuario-Agente' : 'Gemma4ToolCalling/1.0' } )
con urllib . pedido . urlopen ( weather_req ) como respuesta :
datos_meteorológicos = json . cargas ( respuesta . leer ( ) . decodificar ( 'utf-8' ) )
si "actual" en datos_meteorológicos :
actual = datos_meteorológicos [ "actual" ]
temperatura = actual [ "temperatura_2m" ]
viento = actual [ "wind_speed_10m" ]
temp_unit_str = datos_meteorológicos [ "unidades_actuales" ] [ "temperatura_2m" ]
cadena_unidad_viento = datos_meteorológicos [ "unidades_actuales" ] [ "velocidad_viento_10m" ]
devolver f "El clima actual en {city.title()} ({country}) es {temp}{temp_unit_str} con velocidades de viento de {wind}{wind_unit_str}."
demás :
devolver f "Los datos meteorológicos de {ciudad} no están disponibles en la API".
excepto excepción como mi :
devolver f "Error al obtener el clima para {ciudad}: {e}"
Esta función de Python implementa un patrón de resolución API de dos etapas. Debido a que las API meteorológicas estándar generalmente requieren coordenadas geográficas estrictas, nuestra función intercepta de forma transparente la cadena de ciudad proporcionada por el modelo y la geocodifica en coordenadas de latitud y longitud. Con las coordenadas formateadas, invoca el punto final del pronóstico del tiempo y construye una cadena concisa en lenguaje natural que representa el punto de telemetría.
Sin embargo, escribir la función en Python es sólo la mitad de la ejecución. El modelo necesita ser informado visualmente sobre esta herramienta. Hacemos esto asignando la función de Python a un diccionario de esquema JSON compatible con Ollama:
{
“type”: “function”,
“function”: {
“name”: “get_current_weather”,
“description”: “Gets the current temperature for a given city.”,
“parameters”: {
“type”: “object”,
“properties”: {
“city”: {
“type”: “string”,
“description”: “The city name, e.g. Tokyo”
},
“unit”: {
“type”: “string”,
“enum”: [“celsius”, “fahrenheit”]
}
},
“required”: [“city”]
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
“type” : “function” ,
“function” : {
“name” : “get_current_weather” ,
“description” : “Gets the current temperature for a given city.” ,
“parameters” : {
“type” : “object” ,
“properties” : {
“city” : {
“type” : “string” ,
“description” : “The city name, e.g. Tokyo”
} ,
“unit” : {
“type” : “string” ,
“enum” : [ “celsius” , “fahrenheit” ]
}
} ,
“required” : [ “city” ]
}
}
}
Este modelo estructural rígido es fundamental, ya que detalla explícitamente expectativas variables, enumeraciones de cadenas estrictas y parámetros requeridos, todo lo cual guía los pesos de gemma4:e2b para generar llamadas con sintaxis perfecta de manera confiable.
Llamada de herramientas bajo el capó
El núcleo del flujo de trabajo autónomo ocurre principalmente dentro del orquestador del bucle principal. Una vez que un usuario envía un mensaje, establecemos la carga útil JSON inicial para la API de Ollama, vinculando explícitamente gemma4:e2b y agregando la matriz global que contiene nuestro kit de herramientas analizado.
# Initial payload to the model
messages = [{“role”: “user”, “content”: user_query}]
payload = {
“model”: “gemma4:e2b”,
“messages”: messages,
“tools”: available_tools,
“stream”: False
}
try:
response_data = call_ollama(payload)
except Exception as e:
print(f”Error calling Ollama API: {e}”)
return
message = response_data.get(“message”, {})
# Initial payload to the model
messages = [ { “role” : “user” , “content” : user_query } ]
payload = {
“model” : “gemma4:e2b” ,
“messages” : messages ,
“tools” : available_tools ,
“stream” : False
}
try :
response_data = call_ollama ( payload )
except Exception as e :
print ( f “Error calling Ollama API: {e}” )
return
message = response_data . get ( “message” , { } )
Una vez que se resuelve la solicitud web inicial, es fundamental que evaluemos la arquitectura del bloque de mensaje devuelto. No asumimos ciegamente que exista texto aquí. El modelo, consciente de las herramientas activas, indicará el resultado deseado adjuntando un diccionario tool_calls.
Si existen tool_calls, pausamos el flujo de trabajo de síntesis estándar, analizamos el nombre de la función solicitada fuera del bloque del diccionario, ejecutamos la herramienta Python con los kwargs analizados dinámicamente e inyectamos los datos en vivo devueltos nuevamente en la matriz conversacional.
# Check if the model decided to call tools
if “tool_calls” in message and message[“tool_calls”]:
# Add the model’s tool calls to the chat history
messages.append(message)
# Execute each tool call
num_tools = len(message[“tool_calls”])
for i, tool_call in enumerate(message[“tool_calls”]):
function_name = tool_call[“function”][“name”]
arguments = tool_call[“function”][“arguments”]
if function_name in TOOL_FUNCTIONS:
func = TOOL_FUNCTIONS[function_name]
try:
# Execute the underlying Python function
result = func(**arguments)
# Add the tool response to messages history
messages.append({
“role”: “tool”,
“content”: str(result),
“name”: function_name
})
except TypeError as e:
print(f”Error calling function: {e}”)
else:
print(f”Unknown function: {function_name}”)
# Send the tool results back to the model to get the final answer
payload[“messages”] = messages
try:
final_response_data = call_ollama(payload)
print(“[RESPONSE]”)
print(final_response_data.get(“message”, {}).get(“content”, “”)+”n”)
except Exception as e:
print(f”Error calling Ollama API for final response: {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
# Check if the model decided to call tools
if “tool_calls” in message and message [ “tool_calls” ] :
# Add the model’s tool calls to the chat history
messages . append ( message )
# Execute each tool call
num_tools = len ( message [ “tool_calls” ] )
for i , tool_call in enumerate ( message [ “tool_calls” ] ) :
function_name = tool_call [ “function” ] [ “name” ]
arguments = tool_call [ “function” ] [ “arguments” ]
if function_name in TOOL_FUNCTIONS :
func = TOOL_FUNCTIONS [ function_name ]
try :
# Execute the underlying Python function
result = func ( * * arguments )
# Add the tool response to messages history
messages . append ( {
“role” : “tool” ,
“content” : str ( result ) ,
“name” : function _ name
} )
except TypeError as e :
print ( f “Error calling function: {e}” )
else :
print ( f “Unknown function: {function_name}” )
# Send the tool results back to the model to get the final answer
payload [ “messages” ] = messages
try :
final_response_data = call_ollama ( payload )
print ( “[RESPONSE]” )
imprimir ( final_response_data . get ( "mensaje" , { } ) . obtener ( "contenido" , "" ) + "n" )
excepto excepción como mi :
print ( f "Error al llamar a la API de Ollama para obtener la respuesta final: {e}" )
Observe la importante interacción secundaria: una vez que el resultado dinámico se agrega como función de "herramienta", agrupamos el historial de mensajes por segunda vez y activamos la API nuevamente. Este segundo paso es lo que permite al motor de razonamiento gemma4:e2b leer las cadenas de telemetría con las que previamente alucinó, cerrando la brecha final para generar los datos de manera lógica en términos humanos.
Más herramientas: ampliación de las capacidades de llamada de herramientas
Una vez completada la base arquitectónica, enriquecer nuestras capacidades no requiere más que agregar funciones modulares de Python. Utilizando la misma metodología descrita anteriormente, incorporamos tres herramientas en vivo adicionales:
get_current_news: utilizando puntos finales de NewsAPI, esta función analiza matrices de titulares globales en función de temas de palabras clave consultados que el modelo identifica como contextualmente relevantes. get_current_time: al hacer referencia a TimeAPI.io, esta función determinista une la lógica compleja de la zona horaria del mundo real y las compensa en cadenas de fecha y hora nativas y legibles. convert_currency: basándose en ExchangeRate-API en vivo, esta función permite el seguimiento matemático y los cálculos de conversión fraccionaria entre monedas fiduciarias
Cada capacidad se procesa a través del registro de esquema JSON, lo que amplía la utilidad del modelo de referencia sin requerir orquestación externa ni grandes dependencias.
Probando las herramientas
Y ahora probamos nuestra herramienta de llamadas.
Comencemos con la primera función que creamos, get_current_weather, con la siguiente consulta:
¿Cuál es el clima en Ottawa?
¿Cuál es el clima en Ottawa?
Puede ver que nuestra CLI UI nos proporciona:
Confirmación de las herramientas disponibles. El usuario solicita detalles sobre la ejecución de la herramienta, incluida la función utilizada, los argumentos enviados y la respuesta. La respuesta del modelo de lenguaje.
Parece que hemos tenido una primera ejecución exitosa.
A continuación, probemos otra de nuestras herramientas de forma independiente, concretamente convert_currency:
Dado el tipo de cambio actual, ¿cuánto son 1200 dólares canadienses en euros?
Dado el tipo de cambio actual, ¿cuánto son 1200 dólares canadienses en euros?
Más ganar.
Ahora, apilemos las solicitudes de llamadas a herramientas. También tengamos en cuenta que estamos utilizando un modelo de 4 mil millones de parámetros que tiene la mitad de sus parámetros activos en cualquier momento durante la inferencia:
La semana que viene voy a Francia. ¿Cuál es la hora actual en París? ¿Cuántos euros serían 1500 dólares canadienses? ¿Cuál es el clima actual allí? ¿Cuáles son las últimas noticias sobre París?
Me voy a Francia la semana que viene…
¿Podrías mirar eso? Las cuatro preguntas respondidas por cuatro funciones diferentes de las cuatro llamadas de herramientas separadas. Todo en un modelo de lenguaje local, privado e increíblemente pequeño atendido por Ollama.
Realicé consultas sobre esta configuración durante el fin de semana y ni una sola vez falló el razonamiento del modelo. Nunca una vez. Cientos de indicaciones. Es cierto que estaban en las mismas cuatro herramientas, pero independientemente de cuán vaga sea mi redacción, que de otro modo sería razonable, no pude dejarlo perplejo.
Gemma 4 ciertamente parece ser una potencia de un motor de razonamiento de modelo de lenguaje pequeño con capacidades de llamada de herramientas. A continuación centraré mi atención en construir un sistema totalmente agente, así que estad atentos.
Conclusión
La llegada del comportamiento de llamada de herramientas dentro de modelos de peso abierto es uno de los desarrollos más útiles y prácticos en la IA local en los últimos tiempos. Con el lanzamiento de Gemma 4, podemos operar de forma segura fuera de línea, creando sistemas complejos sin las restricciones de la nube y la API. Al integrar arquitectónicamente el acceso directo a la web, los sistemas de archivos locales, la lógica de procesamiento de datos sin procesar y las API localizadas, incluso los dispositivos de consumo de baja potencia pueden operar de forma autónoma en formas que antes estaban restringidas exclusivamente al hardware de nivel de nube.