Inicio / Artículos / El bucle de chat sin estado: Llamar a la API de OpenAI manualmente en Python

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.

1167 palabras

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:

  1. 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.
  • Se trata de HTTP puro. El SDK envuelve una solicitud POST, por lo que cuando algo falla se puede inspeccionar el tráfico en bruto.
  • Se compran tokens, no respuestas. Una solicitud larga cuesta dinero en cada llamada que la incluya.
  • 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:

    • system alberga sus instrucciones, a las que el modelo da más importancia.
    • user contiene 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

    1. Falta de clave: se produce un AuthenticationError con 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.
    2. Límites de tasa: un RateLimitError con 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.
  • La historia falla en ambos casos. Si se olvida de añadir nuevos datos, el modelo lo olvida todo; si se siguen añadiendo indefinidamente, el consumo de tokens crece de forma cuadrática hasta que se produce un error por longitud de contexto. Los sistemas reales eliminan, resumen o recuperan solo la historia relevante, y esa última idea es el núcleo de RAG.
  • Una temperatura de 0 hace que la salida sea estable, no correcta. Valide todo lo que sea importante.
  • 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 uso en cada llamada, ya que los tokens de entrada aumentan con cada turno.
  • Mantenga las claves en el entorno y evite las llamadas a APIs en tiempo real dentro del proceso CI.
  • Maneje los errores 401, 429 y el historial ilimitado antes de crear agentes, ya que los bucles de los agentes amplifican todos estos problemas.
  • Lecturas relacionadas