Llamar modelos desde código


En el capítulo anterior armaste una lista de mensajes. En este capítulo vas a enviar esa lista a un modelo real desde Python y a recibir una respuesta. Ese viaje de ida y vuelta es el ciclo completo más pequeño de trabajar con estos modelos, y casi toda funcionalidad que escribas más adelante se apoya en él.
Escribes un prompt, tu código lo envía a algún lado, y regresa texto. Da la sensación de que el modelo vive dentro de tu programa. No es así, y entender dónde se ejecuta realmente explica el costo, la velocidad y las rarezas que vas a encontrar.
Así que imagina lo que ocurre físicamente. El modelo es un enorme conjunto de parámetros que vive en los servidores del proveedor, en hardware que nunca vas a ver. Cuando lo llamas, tu código envía una solicitud web común a través de internet con tus mensajes dentro.
El proveedor corre el ciclo de predicción en su máquina y te devuelve los tokens generados. Llamar a un modelo es alquilar unos instantes en la computadora de otra persona. Ese marco explica muchas cosas: la latencia es el modelo generando tokens uno por uno del otro lado, el costo es lo que ellos cobran por ese cómputo, y todo el proceso es una llamada de red, con todo lo que eso implica.
Los ejemplos usan la librería oficial de OpenAI. Otros proveedores difieren en los detalles, pero la forma es casi igual en todos lados: envías una lista de mensajes, recibes un mensaje de vuelta. Esa uniformidad es tu seguro contra quedar atado a un proveedor, ya que el mismo código puede apuntar a un modelo abierto que hospedes tú o que alquiles en otro lugar, muchas veces con solo cambiar la URL base (Modelos abiertos y cerrados cubre esa decisión).
La solicitud
Instala la librería con pip install openai, crea un cliente, y llámalo. El cliente lee tu clave de API desde una variable de entorno, así que la clave nunca aparece en tu código.
from openai import OpenAI
client = OpenAI() # lee OPENAI_API_KEY del entorno
MODEL = "gpt-4o-mini" # la única línea que cambias para usar otro modelo
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": "You are a concise assistant. Answer in one sentence."},
{"role": "user", "content": "Why is the sky blue?"},
],
)Se requieren dos piezas: model, el nombre del modelo que quieres ejecutar, y messages, la lista de mensajes con rol del capítulo anterior. Todo lo demás tiene un valor por defecto.
Fíjate que el nombre del modelo vive en una sola constante. En un campo donde cada pocos meses aparece un modelo mejor y más barato, eso es deliberado. Mantén el nombre del modelo en un solo lugar para que cambiar de modelo sea una modificación de una sola línea. Esta es la regla del cambio constante en la práctica: mantén los detalles volátiles donde puedas encontrarlos, no dispersos por todo tu código.
model y messages. El nombre del modelo debe vivir en una sola constante, porque cada pocos meses aparece uno más barato o mejor y no quieres andar buscándolo. A mí me costó aprenderlo por las malas, después de que renombraron un modelo y lo encontré pegado en nueve archivos distintos. Leer la respuesta
La respuesta llega envuelta en cierta estructura. El texto que quieres es solo un camino dentro de esa estructura, pero vale la pena conocer el resto, porque te dice qué pasó.
choice = response.choices[0]
print(choice.message.content) # el texto de la respuesta
print(choice.finish_reason) # "stop" -> el modelo terminó por sí solo
print(response.usage) # Usage(prompt_tokens=24, completion_tokens=18, total_tokens=42)Tres cosas ahí importan. message.content es el texto, ubicado dentro de choices[0] porque la API puede devolver más de una opción, aunque casi siempre pides una sola y leas la primera.
finish_reason te dice por qué se detuvo el modelo. "stop" significa que terminó su idea, mientras que "length" significa que llegó a tu límite de tokens y se cortó a mitad de la respuesta. Revisar ese campo es cómo detectas una respuesta truncada en código en lugar de a simple vista.
Y usage reporta los tokens que gastó la llamada, separados en el prompt que enviaste y la respuesta que escribió. Ese objeto usage es tu factura, detallada. Cada llamada te dice exactamente cuánto costó en tokens, que es el número que hay que vigilar mientras construyes.
response.choices[0].message.content, pero hay dos campos vecinos que también importan. finish_reason te dice por qué se detuvo: "stop" significa que terminó, "length" significa que llegó a tu límite de tokens y se cortó. Y response.usage es tu factura en tokens, con el prompt y la respuesta separados, así que nunca tienes que adivinar cuánto costó una llamada. Los parámetros que vale la pena conocer
Más allá de model y messages, aparecen constantemente dos configuraciones opcionales.
temperature es el control de aleatoriedad de Cómo funcionan los LLM, hecho concreto. Valores bajos (cerca de 0) mantienen al modelo apegado a sus opciones más probables: enfocado y repetible, lo que quieres para extracción y clasificación. Valores más altos (entre 0.7 y 1) le permiten elegir tokens menos probables: más variado y creativo, y más propenso a desviarse. Solo estás ajustando cuán audaz es el muestreo del modelo, nada más.
max_tokens limita cuántos tokens puede tener la respuesta. Te protege de una respuesta descontrolada y de una factura descontrolada. Si fijas max_tokens demasiado bajo, el modelo se corta a mitad de camino, que es exactamente el motivo de finalización "length" de la sección anterior, así que deja margen real.
response = client.chat.completions.create(
model=MODEL,
messages=messages,
temperature=0.2, # enfocado y consistente
max_tokens=300, # limita la longitud de la respuesta
)temperature ajusta cuán audaz es el muestreo del modelo: bajo para respuestas enfocadas y repetibles, alto para respuestas variadas y creativas. max_tokens limita la longitud de la respuesta para proteger tu factura, pero si lo fijas demasiado bajo dispara el corte "length", así que deja margen. Para extracción y clasificación, elige siempre una temperatura baja. Streaming
Por defecto esperas a que termine de generarse toda la respuesta, y entonces llega de golpe. Como el modelo produce los tokens de uno en uno, una respuesta larga significa una espera larga mirando la pantalla sin nada.
Streaming te envía cada token en el momento en que se genera, así que el texto aparece de inmediato y se va completando en vivo, de la misma forma en que las apps de chat muestran una respuesta "escribiéndose sola".
stream = client.chat.completions.create(
model=MODEL,
messages=messages,
stream=True,
)
for chunk in stream:
piece = chunk.choices[0].delta.content
if piece:
print(piece, end="", flush=True)El tiempo total de generación es el mismo en ambos casos; el modelo no se vuelve más rápido. El streaming solo cambia cuándo ves los tokens, mostrándolos a medida que se producen en lugar de retenerlos hasta el final.
Lo manejas iterando sobre los fragmentos e imprimiendo cada delta de texto. El costo es un poco más de código a cambio de una espera mucho mejor. El streaming cambia cuándo ves los tokens, no qué tan rápido llegan.
delta. Cada llamada es independiente
Este es el comportamiento más importante del capítulo, y se deriva directamente de la lección de la ventana de contexto. La API no guarda estado: cada solicitud se sostiene completamente por sí sola. El servidor del proveedor no recuerda tu llamada anterior. Corre la predicción exactamente sobre los mensajes que enviaste, devuelve el resultado, y no guarda nada del intercambio para la próxima vez.
Así que la conversación no se almacena en ningún lado por parte del modelo ni de la API. Vive en tu código. Si quieres que el segundo turno sepa qué pasó en el primero, tú mismo mantienes la lista creciente de mensajes y la envías completa otra vez. Un ida y vuelta de chat es una ilusión que tú creas reenviando un historial cada vez más grande en cada llamada.
Esa es también la razón por la que una conversación larga cuesta más por mensaje con el tiempo: cada llamada reenvía todos los turnos anteriores como tokens de entrada, así que el prompt sigue creciendo aunque la pregunta nueva del usuario sea corta.
history = [
{"role": "system", "content": "You are a friendly travel assistant. Keep answers short."},
]
def chat(user_text):
history.append({"role": "user", "content": user_text})
# se envía TODO el historial cada vez; el servidor no recuerda nada
response = client.chat.completions.create(model=MODEL, messages=history)
reply = response.choices[0].message.content
history.append({"role": "assistant", "content": reply}) # guarda la respuesta para el siguiente turno
return reply
chat("Tengo un fin de semana en Lima. ¿Qué debería ver?")
chat("¿Cuál de esas opciones es mejor para niños?") # solo funciona porque history lleva el primer turnoLa segunda pregunta tiene sentido solo porque la primera pregunta y su respuesta siguen en history y se envían junto con ella. Tú eres la memoria. Esta idea, que armas y reenvías el contexto completo cada vez, es la base de la conversación, y más adelante también es la base del uso de herramientas, la recuperación de información y los agentes. Todos son variaciones sobre decidir qué entra en esa lista de mensajes antes de cada llamada independiente.
Cuando las cosas salen mal
Llamar a un modelo es una llamada de red a los servidores de otra persona, así que falla de todas las formas en que fallan las llamadas de red. Diséñalo así desde el principio.
- Errores y límites de tasa. Los proveedores limitan cuántas solicitudes puedes enviar en una ventana de tiempo. Si llegas al límite, la llamada falla con un error de límite de tasa. La respuesta estándar es esperar y reintentar, alargando la espera después de cada fallo (esto se llama backoff) para no golpear un servicio ya ocupado.
- Tiempos de espera agotados. Una llamada puede quedarse colgada. Configura un tiempo límite para que una solicitud lenta no congele tu aplicación.
- Las claves se quedan en el servidor. Tu clave de API es una contraseña que gasta tu dinero. Nunca pongas la clave de API en código de navegador. Cualquiera puede abrir las herramientas de desarrollador y leerla. El navegador habla con tu backend, y tu backend, que tiene la clave, habla con el modelo.
- Asume que cualquier respuesta puede estar equivocada. Incluso una llamada exitosa puede devolver una alucinación o una salida malformada, así que los siguientes capítulos tratan de convertir el resultado en algo en lo que tu código pueda confiar.
def ask_model(messages):
try:
response = client.chat.completions.create(
model=MODEL,
messages=messages,
timeout=20, # abandona después de 20 segundos
)
return response.choices[0].message.content
except Exception as err:
print("Model call failed:", err)
return "Sorry, something went wrong. Please try again."
