Pętla czatu bez stanu: ręczne wywoływanie API OpenAI w Pythonie
Stwórz rozmowę wieloetapową za pomocą SDK Python OpenAI, samodzielnie zarządzając historią wiadomości, i zobacz, dlaczego ten sam mechanizm leży u podstaw pamięci i agentów LangChain.
Bramki takie jak LangChain sprawiają wrażenie, że modele do rozmów są obiektami posiadającymi pamięć, ale API pod spodem nic nie pamięta. Każda wywołanie jest niezależne, a „rozmowa” to lista wiadomości, którą twój kod odbudowuje i ponownie wysyła za każdym razem. Napisanie tego pętla ręcznie raz przy użyciu SDK Python od OpenAI pokazuje dokładnie to, co automatyzują ramki agentów, dlaczego koszty tokenów rosną podczas rozmowy oraz jakie błędy należy przewidzieć.
Czym naprawdę jest punkt końcowy modelu hostowanego
API OpenAI to prosta struktura: dostawca uruchamia model na swoich kartach graficznych GPU i udostępnia możliwość przetwarzania danych przez HTTPS. Wysyłasz tekst, model generuje tokeny w swojej standardowej pętli wybierania następnego tokena, a ty płacisz za każdy token w obu kierunkach. Wynikają z tego trzy konsekwencje:
- Jest to rozwiązanie bezstanowe. Nic z wcześniejszych żądań nie jest przechowywane, więc każde żądanie musi zawierać wszystko, co model powinien znać.
Nigdy nie umieszczaj klucza API w kodzie źródłowym. Klucze mogą wyciec przez historię Git, zrzuty ekranu i udostępniane notatniki, a wyciek klucza oznacza, że ktoś inny będzie korzystać z twojego konta. SDK automatycznie odczytuje OPENAI_API_KEY z środowiska, więc twój skrypt w ogóle nie potrzebuje kodu do obsługi klucza. Korzystanie z API jest rozliczane oddzielnie od subskrypcji ChatGPT, a nowe konta zazwyczaj wymagają niewielkiego zadatku.
Rolę: format wspólnych wiadomości
Żądanie zawiera listę wiadomości, z których każda ma określoną rolę:
systemzawiera twoje instrukcje, które model traktuje z większą wagą.userzawiera to, co napisała osoba.
assistant przechowuje wcześniejsze odpowiedzi modelu, a w przypadku agentów – ich wywołania narzędzi.Ten format jest używany we całym ekosystemie. Claude i Gemini stosują tę samą koncepcję z niewielkimi różnicami, Ollama ją naśladuje, a SystemMessage, HumanMessage i AIMessage z LangChain reprezentują te role jako klasy. Dostawca spłaszcza listę do jednej sekwencji tokenów przed generowaniem, więc role stanowią w rzeczywistości strukturyzowaną inżynierię promptów.
Jedna prośba
Pierwszy przykład tworzy klienta, który odczytuje klucz z środowiska, wysyła instrukcję systemu wraz z jednym pytaniem i wyświetla odpowiedź wraz z liczbami tokenów promptu i uzupełnienia z pola 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 to tani model odpowiedni do nauki; przejście na większy model wymaga tylko jednej zmiany, chociaż nazwy modeli i ceny się zmieniają, więc sprawdź aktualną listę. Ustawienie temperature=0 minimalizuje losowość w procesie wybierania odpowiedzi – to odpowiednia wartość domyślna dla odpowiadania na pytania, a później także dla agentów używających narzędzi. Rejestruj usage przy każdym wywołaniu; to twój wskaźnik kosztów.
Prowadzenie rozmowy samodzielnie
Ponieważ serwer zapomina o wszystkim, historia rozmowy znajduje się w twoim kodzie: po każdym wywołaniu zapisuje on odpowiedź, dodaje kolejne pytanie i ponownie wysyła wszystko. Poniższy helper realizuje to za pomocą listy msgs na poziomie modułu, która zaczyna się od wiadomości systemowej:
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?"))
Trzecie pytanie potwierdza ten fakt. Model może porównać tylko te dwa odpowiedzi, ponieważ obie znajdują się w msgs i są przesyłane ponownie. Usuń wiersz, który dodaje odpowiedź asystenta – wtedy model nie będzie miał pojęcia, co masz na myśli.
Funkcja ask() pojawia się w wielu różnych formach. Aplikacja internetowa ChatGPT jest w istocie jej wersją z interfejsem użytkownika. Klasa RunnableWithMessageHistory z LangChain to zarządzana wersja operacji dodawania i ponownego wysyłania danych. Wewnętrzny pętla agenta opiera się na tym samym schemacie, z dodatkowymi wywołaniami narzędzi i ich wynikami. Zwróć uwagę na koszt: trzy przesyłania zamieniają się w jedno lub dwa, więc liczba tokenów wejściowych rośnie z każdą wymianą.
Rozpoczęcie działania przykładu
Zainstaluj wymagane zależności, wyeksportuj klucz w swoim shellu i uruchom skrypt. Pokazany klucz jest tylko zamiennikiem; użyj swojego klucza poprzez shell lub menedżer tajemnic, nigdy nie przechowuj go w pliku komitowanym:
pip install -r requirements.txt
export OPENAI_API_KEY="sk-..."
python examples/part02_chat.py
Skrypt wykonuje jedno połączenie typu „jedna tura”, a następnie uruchamia rozmowę trwającą trzy tury; po każdej prośbie pokazuje ilość użytych tokenów oraz przybliżoną cenę. Natychmiast przerywa działanie, jeśli brakuje klucza, i celowo nie jest używany w środowiskach CI, ponieważ wymaga rzeczywistych pieniędzy oraz prawdziwego klucza. Jeśli chcesz napisać testy dla takiego kodu, stwórz mock klienta.
Potencjalne problemy, które należy uwzględnić od samego początku
- Brakujący klucz: błąd
AuthenticationErrorz kodem HTTP 401, zwykle spowodowany tym, że zmienna nie została ustawiona w tym środowisku, jest błędnie zapisana lub zawiera zbędne spacje. Sprawdź to przy uruchamianiu i szybko przerwij działanie w przypadku problemu. - Ograniczenia szybkości: błąd
RateLimitErrorz kodem HTTP 429 oznacza zbyt wiele żądań lub pusty stan konta przedpłaconego. Agenty działające w pętlach napotkają ten problem, dlatego dodaj już teraz możliwość ponownych prób z opóźnieniem.
Ta sama struktura u wszystkich dostawców
Claude przyjmuje list wiadomości od użytkownika i asystenta, przy czym instrukcje systemu są przenoszone do oddzielnego parametru najwyższego poziomu. Gemini wykorzystuje ten sam model konwersacji w formie listy, z rolami o nazwach user i model. Ollama oferuje endpoint kompatybilny z OpenAI, dzięki czemu ten kod może być skierowany na lokalny model poprzez zmianę podstawowej adresu URL i nazwy modelu; zobacz jak korzystać z Claude, GPT i Gemini za pośrednictwem endpointów kompatybilnych z OpenAI. To połączenie umożliwia LangChain oferowanie jednej abstrakcji dla wielu dostawców.
Główne wnioski
- API do czatowania jest bezstanowe; twój kod zarządza konwersacją i ją ponownie wysyła.
- Lista wiadomości z tagami ról stanowi w praktyce standard między dostawcami.
- Zapisuj informacje o
użyciuprzy każdym wywołaniu, ponieważ liczba tokenów wejściowych rośnie z każdą turą.