Una persona pregunta: «¿Quedan dos plazas para Fotografía nocturna?». El modelo puede entender la pregunta, pero no conoce las plazas que guarda nuestra aplicación. Si responde sin consultarlas, puede inventarse una disponibilidad con una seguridad estupenda y bastante poco útil.
Podemos ofrecerle una operación para consultar ese dato. El modelo decide si la necesita y solicita su ejecución. Nuestra aplicación comprueba la solicitud, consulta las plazas y devuelve el resultado al modelo. Con esa información, el modelo redacta la respuesta.
Ese ciclo —decidir, ejecutar una herramienta y continuar con su resultado— es una forma pequeña de construir un agente. El modelo propone el siguiente paso dentro de las opciones disponibles; la aplicación conserva el control sobre qué operaciones ejecuta y cuándo termina.
Vamos a seguir una consulta completa y después abrir el código en Python. Utilizaremos un catálogo ficticio en memoria, una única herramienta de lectura y un límite de llamadas.
Una consulta y dos llamadas al modelo
La aplicación conoce estos datos:
| Taller | Plazas disponibles |
|---|---|
| Fotografía nocturna | 3 |
| Cerámica | 0 |
La persona pregunta por dos plazas para Fotografía nocturna. El recorrido que queremos permitir es este:
- La aplicación envía la pregunta y la descripción de
get_available_seats, una herramienta para consultar plazas por nombre de taller. - El modelo pide utilizarla con
workshop_nameigual a"Fotografía nocturna". - La aplicación consulta su catálogo y obtiene
3. Devuelve{"available_seats": 3}asociado a esa solicitud. - La aplicación llama otra vez al modelo, incluyendo lo anterior. Una respuesta adecuada sería: «Sí, hay tres plazas disponibles, suficientes para dos personas».
La solicitud de herramienta y la frase final son ilustrativas, no salidas medidas de un modelo. La consulta al catálogo sí la realiza el código que veremos. No se reservan plazas ni se reduce la disponibilidad.
Hay dos llamadas al modelo porque el primer intercambio permite pedir el dato y el segundo permite utilizarlo. El modelo recibe la descripción de la herramienta, no el diccionario completo del catálogo ni permiso para ejecutar código arbitrario.
Qué decide cada parte
Una herramienta es una operación que la aplicación ofrece mediante un nombre, una descripción y los argumentos admitidos. En nuestro caso, workshop_name es el argumento que identifica qué taller consultar; el valor concreto será Fotografía nocturna o Cerámica.
El modelo devuelve una solicitud con ese nombre y sus argumentos. No ejecuta la función Python en nuestro equipo. La aplicación lee la solicitud, comprueba que corresponde a su herramienta y llama al código asociado.
En un flujo completamente fijado podríamos consultar las plazas antes de llamar al modelo. Aquí dejamos que el modelo elija entre solicitar el dato o terminar con una respuesta. Esa elección dentro de un ciclo caracteriza este ejemplo de agente. La explicación de Anthropic sobre agentes y flujos distingue los recorridos prefijados de los que permiten al modelo dirigir los siguientes pasos.
El diagrama muestra el caso en que se consulta Fotografía nocturna. Cada columna conserva el mismo participante y el tiempo avanza hacia abajo. La herramienta se ejecuta dentro de nuestra aplicación; su columna separa esa responsabilidad para poder seguir la consulta.

Ver el diagrama a tamaño completo.
La respuesta final utiliza tanto las dos plazas mencionadas por la persona como las tres devueltas por la herramienta. Para relacionarlas, el segundo envío tiene que conservar el intercambio anterior.
Preparar la herramienta en Python
Empezamos con el catálogo y la función que lo consulta. Los nombres deben coincidir con sus claves; un taller desconocido produce un error explícito.
import json
import os
from anthropic import Anthropic
from anthropic.types import MessageParam, ToolParam
catalog = {
"Fotografía nocturna": 3,
"Cerámica": 0,
}
def get_available_seats(workshop_name: str) -> int:
"""Read available seats from the in-memory catalog."""
if workshop_name not in catalog:
raise ValueError("Unknown workshop.")
return catalog[workshop_name]
La función devuelve cero para Cerámica. Cero significa que el taller existe y no tiene plazas disponibles; es distinto de no conocer el taller.
En el mismo módulo describimos la operación para el proveedor. El SDK de anthropic utiliza input_schema para indicar los argumentos. La lista enum contiene los nombres admitidos del catálogo:
tool: ToolParam = {
"name": "get_available_seats",
"description": "Read available seats for a workshop. Does not reserve.",
"input_schema": {
"type": "object",
"properties": {
"workshop_name": {"type": "string", "enum": list(catalog)},
},
"required": ["workshop_name"],
"additionalProperties": False,
},
}
La descripción no ejecuta nada: comunica al modelo qué puede solicitar. La comprobación local seguirá siendo necesaria antes de usar sus argumentos.
Conservar los mensajes y ejecutar el ciclo
Guardaremos los intercambios en messages. Comienza con la pregunta de la persona y después incorporará las solicitudes del modelo, los resultados de herramientas y la respuesta final.
messages: list[MessageParam] = [
{
"role": "user",
"content": "¿Quedan dos plazas para Fotografía nocturna?",
},
]
SYSTEM_PROMPT = """Answer in Spanish.
Use get_available_seats for questions about workshop availability.
Use the returned count and the requested number of seats in your answer.
If the workshop is unknown, ask for a supported workshop name.
Never claim that seats have been reserved."""
La clave del proveedor se configura en ANTHROPIC_API_KEY; ANTHROPIC_MODEL indica el modelo compatible con herramientas que utilicemos. El siguiente bloque hace como máximo tres llamadas y permite una solicitud de herramienta por respuesta:
with Anthropic(timeout=20.0, max_retries=0) as client:
for step in range(3):
response = client.messages.create(
model=os.environ["ANTHROPIC_MODEL"],
system=SYSTEM_PROMPT,
messages=messages,
tools=[tool],
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
max_tokens=512,
)
messages.append(
{
"role": "assistant",
"content": [
block.model_dump(exclude_none=True)
for block in response.content
],
}
)
if response.stop_reason == "end_turn":
text = "\n".join(
block.text
for block in response.content
if block.type == "text"
)
if not text:
raise RuntimeError("The response contains no answer text.")
print(text)
break
calls = [b for b in response.content if b.type == "tool_use"]
if response.stop_reason != "tool_use" or len(calls) != 1:
raise RuntimeError("Unexpected model response.")
if step == 2:
raise RuntimeError("The model call limit was reached.")
call = calls[0]
if call.name != "get_available_seats":
raise ValueError("Unknown tool.")
if set(call.input) != {"workshop_name"}:
raise ValueError("Invalid tool arguments.")
workshop_name = call.input["workshop_name"]
if not isinstance(workshop_name, str):
raise ValueError("The workshop name must be text.")
seats = get_available_seats(workshop_name)
messages.append(
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": call.id,
"content": json.dumps({"available_seats": seats}),
}
],
}
)
Cuando stop_reason vale tool_use, el modelo está pidiendo una operación. La aplicación comprueba el nombre y los argumentos, consulta el catálogo y añade un bloque tool_result. tool_use_id enlaza ese resultado con la solicitud que lo originó. La documentación del ciclo de herramientas explica ese emparejamiento.
Ese bloque viaja en un mensaje con rol user por el formato de esta API. Lo escribe nuestra aplicación, no la persona. En la siguiente vuelta se envían también la pregunta y la respuesta anterior del modelo: el resultado no llega aislado.
Cuando el modelo termina con end_turn, mostramos su texto y salimos del ciclo. Las respuestas incompletas o inesperadas producen un error. Si intenta seguir pidiendo herramientas al alcanzar la tercera llamada, detenemos el recorrido antes de ejecutar otra consulta. El bloque with cierra las conexiones.
Historial, memoria y límites
La lista messages conserva el contexto de este recorrido: qué se preguntó, qué herramienta se solicitó y qué devolvió. Esa es una forma de memoria de corto plazo de la aplicación. No modifica los pesos del modelo ni queda guardada automáticamente para otra ejecución del programa.
En la Messages API utilizada aquí, la aplicación vuelve a enviar el historial necesario en cada llamada. La documentación de conversaciones explica ese funcionamiento sin estado conversacional implícito entre peticiones. Para continuar con «¿Y para tres personas?», tendríamos que añadir la nueva pregunta al historial y ejecutar otro turno conservando el contexto pertinente.
Persistir conversaciones o guardar preferencias entre sesiones sería otra responsabilidad. Un registro de que antes había tres plazas tampoco sustituye una nueva consulta cuando la disponibilidad puede cambiar.
El ejemplo permite consultar y responder. Si quisiéramos reservar, necesitaríamos una operación distinta con sus propias comprobaciones y confirmación del resultado. Incluso en esta lectura, el modelo puede interpretar mal una pregunta o redactar una respuesta incorrecta: el ciclo hace posible consultar datos, pero la calidad de sus decisiones y de su explicación se comprueba mediante evaluaciones.
En el recorrido mostrado, la información decisiva sale del catálogo: hay tres plazas y se han preguntado por dos. La herramienta obtiene ese dato; el historial permite relacionarlo con la petición; la respuesta explica el resultado sin convertir una consulta en una reserva.