En una entrada entrada anterior estuvimos viendo cómo hacer llamadas a funciones en Javascript usando los modelos Gemini. Como no he hecho nada anteriormente con Anthropic en este blog, y recientemente han sacado el modelo Claude 3.5 Sonnet, vamos a aprovechar para probarlo haciendo algo similar a aquella entrada, y usando Python.
Para qué y cómo llamar a funciones
Las llamadas a funciones (también llamado uso de herramientas) permiten a los modelos de lenguaje suplir dos importantes limitaciones:
- sus conocimientos están limitados a sus datos de entrenamiento, o la información que directamente les facilitemos
- no pueden realizar acciones, o interactuar con nada externo a ellos.
Llamar a funciones permiten a los modelos suplir ambas deficiencias, y nos da la oportunidad de dotarles de herramientas que les da acceso a información no disponible en su entrenamiento (qué tiempo hace, estado de vuelos, cotizaciones de bolsa, …) o tomar acciones (reservar vuelos, comprar acciones, crear entradas en nuestro calendario).
La funcionalidad de llamar a funciones (pese a su nombre) no hace que el modelo pueda llamar a servicios directamente; el modelo lo que hará será indicar que una llamada a función debe producirse, para que nuestra aplicación haga la llamada. Viendo el intercambio más en detalle, el flujo sería:
- La aplicación inicia la interacción con el modelo de lenguaje. Mientras el modelo no necesite una llamada a función el, intercambio de mensajes entre usuario y modelo se produce con normalidad.
- Cuando el modelo determina que una llamada a función es requerida enviará un mensaje en un formato específico, especificando qué función se tiene que ejecutar y con qué parámetros.
- La aplicación controla que el mensaje recibido es una llamada a función y la ejecuta.
- La aplicación envía al modelo un mensaje de tipo “respuesta a función” con la respuesta de la función”.
- El modelo, teniendo ya la respuesta que necesitaba de la función, reanuda el intercambio de mensajes, con el beneficio de la información adicional obtenida de la función.

Olivia
Para ilustrar el uso de llamadas a funciones, vamos a construir a Olivia, nuestra ayudante personal de cocina. Olivia nos ayudará a decidir nuestro menú y a encontrar recetas nuevas, pero además hará las veces de archivista de recetas: será capaz de salvar recetas y consultar las que tenemos ya guardadas.
Librerías
Implementaremos nuestras funciones guardar_receta y listar_recetas usando SQLite, que nos permite persistir la base de datos de recetas en un archivo de forma sencilla. La mayoría de instalaciones de Python ya incluyen SQLite como librería estándar, pero si no es el caso deberemos instalarla, además la librería de Anthropic para Python:
pip install anthropic
pip install sqlite
Especificación de las funciones
Para poder usa las funciones, Claude necesita que previamente se las especifiquemos. Es importante que la especificación sea lo más clara y precisa posible en términos de descripción y parámetros de entrada, porque evitar que el modelo trate de usarlo cuando realmente no aplica, o que nos genere la llamada con parámetros incorrectos. En nuestro caso especificaremos nuestras dos funciones –guardar_receta y listar_recetas– de la siguiente manera:
declaracion_guardar_receta = {
"name": "guardar_receta",
"description": "Guarda una nueva receta",
"input_schema": {
"type": "object",
"properties": {
"nombre": {
"type": "string",
"description": "Nombre de la receta.",
},
"ingredientes": {
"type": "string",
"description": "Lista de ingredientes necesarios para preparar la receta, con sus cantidades.",
},
"preparacion": {
"type": "string",
"description": "Lista de pasos específicos necesarios para preparar la receta, incluyendo el orden, tiempos de cocción y cualquier criterio que sea necesario especificar para que una persona sin conocimiento previo pueda elaborarla con éxito.",
},
},
"required": ["nombre", "ingredientes", "preparacion"],
}
}
declaracion_listar_recetas = {
"name": "listar_recetas",
"description": "Lista las recetas guardadas.",
"input_schema": {
"type": "object",
"properties": {}
}
}
Implementación de las funciones
Las correspondientes implementaciones de ambos servicios serían:
tabla_existe = False
def listar_recetas():
# Obtiene la lista de recetas guardadas
conn = sqlite3.connect("recetas.db")
try:
recetas = [{"nombre":row[0], "ingredientes":row[1], "preparacion":row[2]} for row in conn.execute("select nombre, ingredientes, preparacion from receta")]
except:
recetas = []
conn.close()
return recetas
def guardar_receta(nombre, ingredientes, preparacion):
# Guarda una nueva receta
conn = sqlite3.connect("recetas.db")
cur = conn.cursor()
if not tabla_existe:
cur.execute("""CREATE TABLE if not exists receta (
[nombre] TEXT NOT NULL,
[ingredientes] TEXT,
[preparacion] TEXT)""")
cur.execute("insert into receta (nombre, ingredientes, preparacion) values (?,?,?)",
(nombre, ingredientes, preparacion))
conn.commit()
cur.close()
conn.close()
Mensaje de sistema
Para hacer que Olivia se comporte como queremos, es importante que le facilitemos un mensaje de sistema detallando lo que esperamos de ella. Cuando llamemos a Claude, enviaremos este mensaje cada vez en el campo system. El mensaje de sistema que usaremos es el siguiente:
# QUIEN ERES
Eres Olivia, una gran chef y archivista de recetas. Con tu conocimiento enciclopédico, eres capaz de proponer recetas, escribirlas a gusto del usuario y modificarlas para adaptarlas a sus preferencias.
# COMPORTAMIENTO
Habla con el usuario, averigua sus gustos, aconséjale sobre posibles recetas y modifícalas conforme a lo que te pida. Una vez que el usuario confirma que está satisfecho con la receta, pregúntale si desea guardarla, y en caso afirmativo, genera una llamada al servicio "guardar_receta".
Si el usuario quiere saber las recetas que ya tiene guardadas, debes obtener las recetas guardadas llamando al servicio "listar_recetas". Dicho servicio te devolverá una lista de objetos, cada uno de los cuales representa una receta, con la estructura que se explica a continuación.
# ESTRUCTURA DE UNA RECETA
Una receta se compone de tres partes:
- Título
- Ingredientes: lista de ingredientes necesarios para preparar la receta, con sus cantidades.
- Preparación: lista de pasos específicos necesarios para preparar la receta, incluyendo el orden, tiempos de cocción y cualquier criterio que sea necesario especificar para que una persona sin conocimiento previo pueda elaborarla con éxito.
El mensaje de sistema lo almacenaremos en un archivo de texto separado, el cual leeremos al inicio y usaremos a lo largo de toda la conversación.
Interacción con Claude
Toda la interacción con el modelo de lenguaje la encapsularemos dentro de una clase, que llamaremos ClienteClaude:
class ClienteClaude:
def __init__(self, modelo, mensaje_sistema):
self.modelo = modelo
self.mensaje_sistema = mensaje_sistema
self.anthropic = Anthropic()
self.mensajes = []
def __llama_funcion(self, llf):
if llf.name == "listar_recetas":
return json.dumps(listar_recetas())
elif llf.name == "guardar_receta":
return json.dumps(guardar_receta(llf.input["nombre"], llf.input["ingredientes"], llf.input["preparacion"]))
def chat(self, mensaje_usuario):
self.mensajes.append(
{
"role": "user",
"content": mensaje_usuario
}
)
respuesta = self.anthropic.messages.create(
max_tokens=4096,
system=self.mensaje_sistema,
messages=self.mensajes,
model = self.modelo,
tools=[declaracion_guardar_receta, declaracion_listar_recetas]
)
self.mensajes.append(
{
"role": "assistant",
"content": respuesta.content
}
)
if respuesta.stop_reason == "tool_use":
salida = ""
resultado_funcion = []
for c in respuesta.content:
if c.type == "text":
salida = c.text
elif c.type == "tool_use":
resultado_funcion.append(
{
"type": "tool_result",
"tool_use_id": c.id,
"content": self.__llama_funcion(c)
})
self.mensajes.append(
{
"role": "user",
"content": resultado_funcion
})
respuesta2 = respuesta = self.anthropic.messages.create(
max_tokens=4096,
system=self.mensaje_sistema,
messages=self.mensajes,
model = self.modelo,
tools=[declaracion_guardar_receta, declaracion_listar_recetas]
)
self.mensajes.append(
{
"role": "assistant",
"content": respuesta2.content
}
)
return salida+"\n"+respuesta2.content[0].text
return respuesta.content[0].text
La parte central del código está en el método chat, que es el encargado de obtener y devolver la respuesta al mensaje del usuario:
- Empieza llamando al servicio messages.create pasando la lista completa de mensajes de la conversación, el mensaje de sistema, y las declaraciones de los servicios que queremos que pueda usar (informada como lista en el parámetros tools). Si la respuesta del modelo no pide que se ejecute ninguna función guardará la respuesta y la devolverá para que se muestre por pantalla y la conversación continúe.
- Cuando Claude nos pida que llamemos a un servicio, nos lo hará saber informando en la respuesta el parámetro stop_reason con el valor tool_use. En ese caso, la lista del parámetro content contendrá al menos un elemento tipo tool_use; cada uno de estos elementos (puede haber más de uno) representa una llamada a funcion, que nuestra aplicación deberá ejecutar.
- El siguiente mensaje que la aplicación mande a Claude deberá contener un elemento de tipo tool_result por cada llamada a función realizada. Para distinguir adecuadamente las llamadas y las respuestas cada elemento tool_use contiene un parámetro id cuyo valor debe ir informado en el tool_result correspondiente.
- Por simplicidad, este código no contempla la posibilidad de que se Claude solicite la ejecución de una llamada a función inmediatamente después de otra llamada a función, pero es algo que teóricamente podría ocurrir.
Interfaz del chat
Por último, tan sólo resta el cuerpo del script, que recogerá el mensaje de sistema de un archivo, instanciará la clase ClienteClaude, y mostrará en bucle la interfaz del chat:
from anthropic import Anthropic
import sqlite3
import io
import json
with io.open("olivia.md", "r") as arch:
mensaje_sistema = "\n".join(arch.readlines())
cliente = ClienteClaude("claude-3-5-sonnet-20240620", mensaje_sistema)
while (texto_usuario := input("Qué le quieres decir a Olivia?\n> ").strip()) !="":
if len(texto_usuario)==0:
break
print(cliente.chat(texto_usuario))
¡Y con eso ya tenemos a Olivia terminada! Espero que resulte útil, pero sobre todo que haya quedado claro con este ejemplo cómo usar la funcionalidad de llamadas a funciones, que puede ampliar muchísimo lo que podremos hacer con nuestras aplicaciones basadas en grandes modelos de lenguaje.
Como siempre, el código completo se puede encontrar en GitHub.
