Startseite / Artikel / Der statelose Chat-Loop: Manuelle Aufrufe der OpenAI-API in Python

Der statelose Chat-Loop: Manuelle Aufrufe der OpenAI-API in Python

Erstellen Sie ein mehrteiliges Chat-Interface mit dem OpenAI Python SDK, indem Sie den Nachrichtenverlauf selbst verwalten, und erkennen Sie, warum derselbe Mechanismus die Speichermöglichkeiten sowie Agenten in LangChain ermöglicht.

1167 Wörter

Frameworke wie LangChain lassen Chat-Modelle so erscheinen, als hätten sie Gedächtnis, doch die darunterliegende API erinnert sich an nichts. Jeder Aufruf ist unabhängig, und die „Konversation“ besteht aus einer Liste von Nachrichten, die Ihr Code jedes Mal neu erstellt und erneut sendet. Wenn man diesen Loop einmal mit dem OpenAI Python SDK von Hand schreibt, wird genau gezeigt, was Agent-Frameworks automatisieren, warum die Tokenkosten während einer Konversation steigen und welche Fehler man berücksichtigen muss.

Was ein gehosteter Modell-Endpunkt wirklich ist

Die OpenAI-API ist eine einfache Struktur: Der Anbieter führt das Modell auf seinen GPUs aus und stellt die Inferenz über HTTPS zur Verfügung. Sie senden Text, das Modell erzeugt in seinem üblichen Next-Token-Loop Token, und Sie zahlen pro Token in beide Richtungen. Daraus ergeben sich drei Folgen:

  1. Es ist zustandslos. Nichts aus früheren Anfragen wird gespeichert, sodass jede Anfrage alles enthalten muss, was das Modell wissen sollte.
  • Es handelt sich um reines HTTP. Das SDK umhüllt eine POST-Anfrage, sodass Sie bei Fehlern den Rohdatenverkehr überprüfen können.
  • Sie kaufen Token, nicht Antworten. Eine ausführliche Anfrage kostet Geld bei jeder Aufruf, der sie enthält.
  • Fügen Sie niemals den API-Schlüssel in den Quellcode ein. Schlüssel können über Git-Historie, Screenshots und geteilte Notizbücher durchsickern, und ein durchgesickerter Schlüssel bedeutet, dass jemand anderes mit Ihrem Konto bezahlt. Das SDK liest automatisch OPENAI_API_KEY aus der Umgebung ab, sodass Ihr Skript überhaupt keinen Code zum Umgang mit Schlüsseln benötigt. Die API-Nutzung wird gesondert von einer ChatGPT-Abonnementgebühr abgerechnet, und neue Konten benötigen in der Regel einen kleinen Vorauszahlungsbetrag.

    Rollen: das gemeinsame Nachrichtenformat

    Eine Anfrage enthält eine Liste von Nachrichten, wobei jede eine Rolle hat:

    • system enthält Ihre Anweisungen, die vom Modell stärker berücksichtigt werden.
    • user enthält das, was die Person geschrieben hat.
  • assistant enthält die früheren Antworten des Modells und bei Agenten auch deren Tool-Aufrufe.
  • Dieses Format wird im gesamten Ökosystem verwendet. Claude und Gemini nutzen denselben Ansatz mit geringen Unterschieden, Ollama ahmt ihn nach, und LangChains SystemMessage, HumanMessage sowie AIMessage stellen diese Rollen als Klassen dar. Der Anbieter fasst die Liste vor der Generierung in eine einzige Tokenfolge zusammen, wodurch Rollen tatsächlich als strukturierte Prompt-Engineering-Elemente fungieren.

    Eine einzige Anfrage

    In dem ersten Beispiel wird ein Client erstellt, der den Schlüssel aus der Umgebung liest, eine Systemanweisung zusammen mit einer Frage sendet und die Antwort sowie die Tokenzahlen für Prompt und Ergänzung aus usage ausgibt:

    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 ist ein günstiges Modell, das sich zum Lernen eignet; der Wechsel zu einem größeren Modell erfordert nur eine einzige Änderung, wobei sich die Modellnamen und Preise ändern können, daher sollten Sie die aktuelle Liste überprüfen. temperature=0 minimiert die Zufälligkeit bei der Stichprobenziehung – das ist die richtige Standardeinstellung für Fragenbeantwortungen sowie später für Agenten, die Tools aufrufen. Protokollieren Sie Usage bei jedem Aufruf; das dient als Kostentracker.

    Selbst ein Gespräch führen

    Da der Server alles vergisst, liegt die Historie in Ihrem Code: Nach jedem Aufruf speichert er die Antwort, fügt die nächste Frage hinzu und sendet alles erneut ab. Der untenstehende Helper erledigt dies mithilfe einer auf Modulebene definierten msgs-Liste, die mit einer Systemnachricht beginnt:

    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?"))
    

    Die dritte Frage belegt den Punkt. Das Modell kann nur die beiden Antworten vergleichen, weil beide in msgs gespeichert sind und erneut gesendet werden. Entfernen Sie die Zeile, die die Antwort des Assistenten hinzufügt – dann hat das Modell keine Ahnung, was Sie meinen.

    Diese ask()-Funktion taucht in vielen Varianten auf. Die ChatGPT-Webanwendung ist im Grunde genommen genau das, nur mit einer Benutzeroberfläche. LangChains RunnableWithMessageHistory ist eine verwaltete Version von Hinzufügen und Erneutes Senden. Der innere Schleifenzyklus eines Agents folgt demselben Muster, wobei Toolaufrufe und Ergebnisse hinzugefügt werden. Beachten Sie die Kosten: Drei Erneutes-Senden-Vorgänge werden zu einem oder zwei, sodass die Eingabetoken mit jedem Austausch zunehmen.

    Ausführung des Beispiels

    Installieren Sie die Abhängigkeiten, exportieren Sie den Schlüssel in Ihrer Shell und führen Sie das Skript aus. Der angezeigte Schlüssel ist ein Platzhalter; geben Sie Ihren eigenen Schlüssel über die Shell oder einen Secrets-Manager ein – niemals in einer kommitierten Datei:

    pip install -r requirements.txt
    export OPENAI_API_KEY="sk-..."
    python examples/part02_chat.py
    

    Die Skript führt zunächst einen Anruf für eine einzige Runde durch, danach die Konversation über drei Runden. Nach jeder Anfrage gibt es einen Bericht über den Tokenverbrauch sowie eine ungefähre Kostenangabe. Es stoppt sofort, wenn der Schlüssel fehlt, und wird absichtlich aus dem CI ausgeschlossen, da es echtes Geld verbraucht und einen echten Schlüssel benötigt. Wenn Sie Tests für solchen Code erstellen möchten, mocken Sie den Client.

    Mögliche Probleme, die früh berücksichtigt werden müssen

    1. Fehlender Schlüssel: Ein AuthenticationError mit HTTP 401 tritt in der Regel auf, weil die Variable in dieser Umgebung nicht gesetzt ist, falsch geschrieben wurde oder Leerzeichen enthält. Überprüfen Sie dies bei Start und scheitern Sie schnell.
    2. Rate Limits: Ein RateLimitError mit HTTP 429 bedeutet zu viele Anfragen oder einen leeren Vorauszahlungsbetrag. Agenten, die in Schleifen arbeiten, stoßen darauf – fügen Sie daher bereits jetzt Wiederholungsversuche mit Verzögerung hinzu.
  • Die Historie versagt in beiden Fällen. Wenn man vergisst, sie hinzuzufügen, vergisst das Modell alles; wenn man sie ständig hinzufügt, wächst der Tokenverbrauch quadratisch, bis ein Kontextlängenfehler auftritt. Reale Systeme kürzen, fassen zusammen oder holen nur relevante Historieinformationen ab – und genau diese Idee ist das Herzstück von RAG.
  • Eine Temperatur von 0 sorgt für stabile, aber nicht notwendigerweise korrekte Ausgaben. Alles, was wichtig ist, muss überprüft werden.
  • Die gleiche Struktur bei allen Anbietern

    Claude nimmt eine Liste von Benutzer- und Assistenten-Nachrichten entgegen, wobei der Systemprompt in einen separaten obersten Parameter verschoben wird. Gemini verwendet dasselbe Modell, das Gespräche als Liste darstellt, wobei die Rollen user und model genannt werden. Ollama bietet einen mit OpenAI kompatiblen Endpunkt an, sodass dieser Code durch Änderung der Basis-URL und des Modellnamens auf ein lokales Modell ausgerichtet werden kann; siehe Calling Claude, GPT und Gemini über OpenAI-kompatible Endpunkte. Genau diese Konvergenz ermöglicht es LangChain, eine einzige Abstraktion für viele Anbieter zu bieten.

    Kernpunkte

    • Die Chat-API ist zustandslos; Ihr Code übernimmt die Kontrolle über das Gespräch und sendet es erneut.
    • Die mit Rollen versehene Nachrichtenliste ist im Grunde ein standardisierter Mechanismus für verschiedene Anbieter.
    • Führen Sie bei jedem Aufruf Usage-Protokollierung durch, da die Eingabetoken bei jeder Runde zunehmen.
  • Halten Sie Schlüssel im Umfeld und vermeiden Sie Live-API-Aufrufe in CI.
  • Behandeln Sie 401-, 429-Fehler sowie unbegrenzte Historien vor dem Erstellen von Agenten, da Agentenschleifen all diese Probleme verstärken.