Llamar modelos desde código

El capítulo anterior construyó una lista de mensajes. Este capítulo envía esa lista a un modelo real desde Python y obtiene una respuesta. Ese viaje de ida y vuelta es el bucle completo más pequeño al construir con estos modelos, y casi cada función que escribirás se sitúa encima de él.
Escribes una indicación, tu código la envía a algún lugar, y vuelve texto. Parece que el modelo vive en tu programa. No es así, y ver dónde corre realmente explica el costo, la velocidad, y las particularidades que encontrarás.
Entonces imagina qué está sucediendo físicamente. El modelo es un enorme conjunto de parámetros sentados en los servidores del proveedor, en hardware que nunca verás. Cuando lo llamas, tu código envía una solicitud web ordinaria por internet llevando tus mensajes.
El proveedor ejecuta el bucle de predicción en su máquina y envía los tokens generados de vuelta a ti. Una llamada a modelo es alquilar algunos momentos en la computadora de otra persona. Ese encuadre tiene sentido para mucho: la latencia es el modelo generando tokens uno a uno en su extremo, el costo es que te cobren por ese cómputo, y todo es una llamada de red, con todo lo que eso implica.
Los ejemplos usan la librería oficial de OpenAI. Otros proveedores difieren en detalles, pero la forma es casi la misma en todos lados: envías una lista de mensajes, obtienes un mensaje de vuelta. Esa similitud es tu seguro contra el bloqueo de proveedor, ya que el mismo código puede apuntar a un modelo abierto que alojas o alquilas en otro lugar, a menudo con solo un cambio de URL base (Modelos abiertos y cerrados cubre esa opció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 línea que cambias para intercambiar modelos
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": "Eres un asistente conciso. Responde en una sola oración."},
{"role": "user", "content": "¿Por qué es azul el cielo?"},
],
)Dos piezas son requeridas: model, el nombre del modelo que quieres ejecutar, y messages, la lista de mensajes etiquetados por rol del capítulo anterior. Todo lo demás tiene un valor por defecto.
Nota que el nombre del modelo vive en una sola constante. En un campo donde un modelo mejor y más barato llega cada pocos meses, eso es deliberado. Mantén el nombre del modelo en un solo lugar así que intercambiar modelos es un cambio de una línea. Esta es la regla de cambio en la práctica: mantén los detalles volátiles específicos donde puedas encontrarlos, no dispersos por tu código.
model y messages. El nombre del modelo pertenece a una sola constante, porque un modelo más barato o mejor aparece lo suficientemente frecuente que codificarlo en todos lados convierte un intercambio en una búsqueda. Me tomó un tiempo aprender eso de la manera difícil, después de que un modelo fue renombrado y lo encontré pegado en nueve archivos. Leyendo la respuesta
La respuesta vuelve envuelta en alguna estructura. El texto que quieres está un camino dentro, pero el resto de la estructura vale la pena conocer, porque te dice qué sucedió.
choice = response.choices[0]
print(choice.message.content) # el texto de respuesta
print(choice.finish_reason) # "stop" -> el modelo terminó por su propia cuenta
print(response.usage) # Usage(prompt_tokens=24, completion_tokens=18, total_tokens=42)Tres cosas allí importan. message.content es el texto, sentado dentro de choices[0] porque la API puede devolver más de una opción, aunque casi siempre pides una y lees la primera.
finish_reason te dice por qué se detuvo el modelo. "stop" significa que terminó su pensamiento, mientras "length" significa que golpeó tu límite de tokens y fue cortado a mitad de respuesta. Verificar ese campo es cómo detectas una respuesta truncada en código en lugar de a simple vista.
Y usage reporta los tokens que la llamada gastó, dividida en el indicativo que enviaste y la conclusión que escribió. Ese objeto usage es tu factura, desglosada. Cada llamada te dice exactamente qué costó en tokens, que es el número a vigilar mientras construyes.
response.choices[0].message.content, pero dos vecinos importan también. finish_reason te dice por qué se detuvo: "stop" significa hecho, "length" significa que golpeó tu límite de tokens y fue cortado. Y response.usage es tu factura en tokens, indicativo y conclusión divididos, así que nunca tienes que adivinar qué costó una llamada. Los parámetros que valen la pena
Más allá de model y messages, dos configuraciones opcionales surgen constantemente.
temperature es el dial de aleatoriedad de Cómo funcionan los LLMs, hecho concreto. Valores bajos (cerca de 0) mantienen el modelo cerca de sus mejores selecciones: enfocado y repetible, lo que quieres para extracción y clasificación. Valores más altos (alrededor de 0.7 a 1) lo dejan alcanzar tokens menos probables: más variado y creativo, y más propenso a desviarse. Estás afinando qué tan atrevidamente el modelo muestrea, nada más.
max_tokens limita cuántos tokens la respuesta puede ejecutar. Te protege de una respuesta desbocada y una factura desbocada. Fija max_tokens demasiado bajo y el modelo se corta, que es exactamente la razón "length" del fin 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 respuesta
)temperature afina qué tan atrevidamente el modelo muestrea: bajo para respuestas enfocadas y repetibles, más alto para variadas y creativas. max_tokens limita la longitud de respuesta para proteger tu factura, pero fijado demasiado bajo dispara el truncamiento "length", así que deja margen. Para extracción y clasificación, alcanza siempre una temperatura baja. Streaming
Por defecto esperas a que toda la respuesta termine de generarse, luego llega a la vez. Como el modelo produce tokens uno a la vez, una respuesta larga significa una espera larga mirando nada.
El streaming envía cada token abajo a ti el momento en que es generado, así que el texto aparece de inmediato y se llena en vivo, la manera que aplicaciones de chat muestran una respuesta escribiéndose a sí misma.
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 de cualquier forma; el modelo no es más rápido. El streaming solo cambia cuándo ves los tokens, llevándolos a la superficie mientras son producidos en lugar de mantenerlos a raya hasta el fin.
Lo manejas iterando sobre chunks e imprimiendo cada delta de texto. El tradeoff es un poco más de código por una espera mucho mejor. El streaming cambia cuándo ves tokens, no qué tan rápido llegan.
delta. Cada llamada es independiente
Este es el comportamiento más importante en el capítulo, y sigue directamente de la lección de context window. La API es stateless: cada solicitud está completamente por sí sola. El servidor del proveedor no recuerda tu llamada anterior. Ejecuta la predicción en exactamente los mensajes que enviaste, devuelve el resultado, y mantiene nada sobre el intercambio para próxima vez.
Así que la conversación no se almacena en ningún lugar por el modelo o la API. Vive en tu código. Si quieres que el turno dos sepa qué pasó en el turno uno, mantienes la lista de mensajes en ejecución tú mismo y envías el todo de nuevo. Un diálogo ida y vuelta es una ilusión que creas al reenviar un historial siempre creciente en cada llamada.
Eso es también por qué una conversación larga cuesta más por mensaje a lo largo del tiempo: cada llamada reenvía todos los turnos anteriores como tokens de entrada, así que el indicativo mantiene creciendo incluso cuando la pregunta nueva del usuario es corta.
history = [
{"role": "system", "content": "Eres un asistente de viajes amigable. Mantén respuestas cortas."},
]
def chat(user_text):
history.append({"role": "user", "content": user_text})
# el HISTORIAL COMPLETO sube 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}) # mantén la respuesta para el próximo turno
return reply
chat("Tengo un fin de semana en Ciudad de México. ¿Qué debo ver?")
chat("¿Cuál de esos es mejor para niños?") # solo funciona porque el historial lleva el turno unoLa segunda pregunta tiene sentido solo porque la primera pregunta y su respuesta todavía están en history y son enviadas junto. Eres tú la memoria. Esta idea, que ensamblas y reenenvías el contexto completo cada vez, subyace conversación, y después subyace uso de herramientas, recuperación, y agentes también. Son todas variaciones en decidir qué va en esa lista de mensajes antes de cada llamada independiente.
Cuando las cosas van mal
Una llamada a modelo es una llamada de red a servidores de otra persona, así que falla en todas las formas que las llamadas de red fallan. Diseña para ello desde el inicio.
- Errores y límites de tasa. Los proveedores limitan cuántas solicitudes puedes enviar en una ventana. Golpea el límite y 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) así que no martilleas un servicio ocupado.
- Timeouts. Una llamada puede colgarse. Fija un timeout así que una solicitud lenta no congela 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 herramientas de desarrollador y leerla. El navegador habla a tu backend, y tu backend, teniendo la clave, habla al modelo.
- Asume que cualquier respuesta puede estar equivocada. Incluso una llamada exitosa puede devolver una alucinación o salida malformada, así que los próximos capítulos son sobre hacer el resultado 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, # rinde después de 20 segundos
)
return response.choices[0].message.content
except Exception as err:
print("Model call failed:", err)
return "Lo siento, algo salió mal. Por favor intenta de nuevo."
