El bucle de chat sin estado: Llamar a la API de OpenAI manualmente en Python
Cree un chat de varias turnos con el SDK de Python de OpenAI gestionando usted mismo el historial de mensajes, y descubra por qué el mismo bucle sirve como base para la memoria y los agentes de LangChain.
Frameworks como LangChain hacen que los modelos de chat parezcan objetos con memoria, pero la API subyacente no recuerda nada. Cada llamada es independiente, y la “conversación” es una lista de mensajes que tu código reconstruye y vuelve a enviar cada vez. Escribir ese bucle a mano una sola vez con el SDK de Python de OpenAI muestra exactamente lo que automatizan los frameworks de agentes, por qué aumentan los costos por token durante una conversación y qué errores se deben anticipar.
Qué es realmente un endpoint de modelo alojado
La API de OpenAI es una estructura sencilla: el proveedor ejecuta el modelo en sus GPUs y expone la inferencia a través de HTTPS. Tú envías texto, el modelo genera tokens en su habitual bucle de siguiente token, y pagas por cada token en ambas direcciones. De esto se derivan tres consecuencias:
- Es sin estado. No se mantiene nada de solicitudes anteriores, por lo que cada solicitud debe contener todo lo que el modelo debería saber.
Nunca coloque la clave de API en el código fuente. Las claves se filtran a través del historial de Git, capturas de pantalla y cuadernos compartidos, y una clave filtrada significa que otra persona gastará con su cuenta. El SDK lee automáticamente OPENAI_API_KEY del entorno, por lo que su script no necesita ningún código para manejar claves. El uso de la API se factura por separado de la suscripción a ChatGPT, y las cuentas nuevas suelen necesitar un pequeño saldo prepago.
Roles: el formato de mensaje compartido
Una solicitud contiene una lista de mensajes, cada uno con un rol:
systemalberga sus instrucciones, a las que el modelo da más importancia.usercontiene lo que escribió la persona.
assistant almacena las respuestas anteriores del modelo y, en los agentes, sus llamadas a herramientas.Este formato se utiliza en todo el ecosistema. Claude y Gemini emplean la misma idea con pequeñas diferencias, Ollama lo imita, y SystemMessage, HumanMessage y AIMessage de LangChain representan estos roles como clases. El proveedor plantea la lista en una sola secuencia de tokens antes de generar, por lo que los roles constituyen en realidad una ingeniería estructurada de prompts.
Una sola solicitud
El primer ejemplo crea un cliente que lee la clave del entorno, envía una instrucción de sistema junto con una pregunta y muestra la respuesta junto con los conteos de tokens del prompt y de la completación extraídos de usage:
from openai import OpenAI
client = OpenAI() # key from env
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": "You are a concise "
"Python assistant.",
},
{
"role": "user",
"content": "Why resend the whole "
"chat history each call?",
},
],
temperature=0,
)
print(resp.choices[0].message.content)
u = resp.usage
print(u.prompt_tokens, u.completion_tokens)
gpt-4o-mini es un modelo económico adecuado para el aprendizaje; pasar a uno más grande implica solo un cambio sencillo, aunque los nombres y precios de los modelos varían, así que consulte la lista actual. temperature=0 minimiza la aleatoriedad en el muestreo, siendo el valor predeterminado adecuado para responder preguntas y posteriormente para agentes que utilizan herramientas. Registre usage en cada llamada; ese es su medidor de costos.
Mantener una conversación por cuenta propia
Dado que el servidor olvida todo, su código es quien gestiona la historia: después de cada llamada, almacena la respuesta, agrega la siguiente pregunta y vuelve a enviar todo. La función auxiliar a continuación hace esto mediante una lista msgs a nivel de módulo que comienza con un mensaje del sistema:
msgs = [{
"role": "system",
"content": "You are a concise assistant.",
}]
def ask(text: str) -> str:
msgs.append(
{"role": "user", "content": text}
)
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=msgs, # full history
temperature=0,
)
reply = resp.choices[0].message.content
msgs.append({
"role": "assistant",
"content": reply,
})
return reply
print(ask("Define a context window."))
print(ask("Now for a five-year-old."))
print(ask("Which answer was shorter?"))
La tercera pregunta demuestra el punto. El modelo solo puede comparar las dos respuestas porque ambas se encuentran en msgs y se vuelven a enviar. Si elimina la línea que agrega la respuesta del asistente, este no tendrá idea de a qué se refiere.
Esta función ask() aparece de muchas maneras diferentes. La aplicación web de ChatGPT es, en esencia, la misma función con una interfaz de usuario. RunnableWithMessageHistory de LangChain es una versión gestionada de este proceso de agregar y reenviar datos. El bucle interno de un agente sigue el mismo patrón, pero con llamadas a herramientas y resultados adicionales. Tenga en cuenta el costo: tres reenvíos se convierten en uno o dos, por lo que los tokens de entrada aumentan con cada intercambio.
Ejecutar el ejemplo
Instale las dependencias, exporte la clave en su shell y ejecute el script. La clave mostrada es un marcador de posición; proporcione la suya a través del shell o de un gestor de secretos, nunca en un archivo subido al repositorio:
pip install -r requirements.txt
export OPENAI_API_KEY="sk-..."
python examples/part02_chat.py
El script realiza una llamada de un solo turno, luego ejecuta la conversación de tres turnos, y después de cada solicitud informa sobre el uso de tokens y un costo aproximado. Se detiene inmediatamente si falta la clave y se mantiene intencionalmente fuera de los entornos CI, ya que consume dinero real y necesita una clave real. Si desea pruebas para este tipo de código, simule el cliente.
Riesgos que deben considerarse desde el inicio
- Falta de clave: se produce un
AuthenticationErrorcon código HTTP 401, generalmente porque la variable no está definida en ese entorno, está mal escrita o contiene espacios en blanco pegados. Verifique al inicio del proceso y deténgase rápidamente si hay algún problema. - Límites de tasa: un
RateLimitErrorcon código HTTP 429 indica demasiadas solicitudes o un saldo prepago vacío. Los agentes que operan en bucle se toparán con esto, por lo que agregue intentos de reintentar con retroceso ahora mismo.
Misma estructura en todos los proveedores
Claude recibe una lista de mensajes del usuario y del asistente, con la instrucción del sistema trasladada a un parámetro de nivel superior separado. Gemini utiliza el mismo modelo basado en listas de conversaciones, con roles denominados user y model. Ollama ofrece un endpoint compatible con OpenAI, por lo que este código puede dirigirse a un modelo local cambiando la URL base y el nombre del modelo; consulte cómo llamar a Claude, GPT y Gemini mediante endpoints compatibles con OpenAI. Esa convergencia es lo que permite a LangChain ofrecer una única abstracción para múltiples proveedores.
Puntos clave
- La API de chat no tiene estado; su código es el responsable de gestionar y reenviar la conversación.
- La lista de mensajes etiquetados con roles constituye, en efecto, un estándar entre proveedores diferentes.
- Registre el
usoen cada llamada, ya que los tokens de entrada aumentan con cada turno.
Lecturas relacionadas
- Llamar a Claude, GPT y Gemini a través de puntos de acceso compatibles con OpenAI — Conozca qué funciones soportan las capas compatibles con OpenAI de Anthropic y Gemini, dónde descartan silenciosamente ciertas características, y cuándo tiene sentido enrutar a los tres servicios a través de una misma pasarela.
- Enrutamiento de preguntas entre herramientas SQL y búsqueda web con un agente Gemini — Cómo un agente de llamadas a herramientas de LangChain en Vertex AI elige entre tres herramientas SQLite de texto a SQL y la búsqueda web en tiempo real, además de los problemas relacionados con datos, dependencias y autenticación que es posible encontrar.
- Composición de pipelines de LangChain con LCEL: lineales, paralelos y ramificados. — Aprende a conectar prompts, modelos y analizadores en pipelines lineales, de múltiples etapas, paralelos y condicionales de LangChain mediante el operador pipe, RunnableParallel y RunnableBranch.