Dies ist der erste Teil einer mehrteiligen Serie zum Erstellen von Agenten mit der Assistant-API von OpenAI unter Verwendung des Python SDK.
Meiner Meinung nach ist ein Agent eigentlich nur eine Software, die ein LLM (Large Language Model) nutzt und versucht, menschliches Verhalten nachzuahmen. Das heißt, es kann sich nicht nur unterhalten und Sprache verstehen, sondern auch Handlungen ausführen, die Auswirkungen auf die reale Welt haben. Diese Aktionen werden normalerweise als Tools bezeichnet.
In diesem Blogbeitrag werden wir untersuchen, wie man mithilfe der Assistant-API von OpenAI und dem Python SDK einen Agenten erstellt. Teil 1 wird nur das Skelett des Assistenten sein. Das heißt, nur der Konversationsteil.
Ich habe mich bewusst dafür entschieden, eine CLI-App zu erstellen, um Framework-unabhängig zu sein. Wir werden unsere Implementierung absichtlich als Agent bezeichnen und die OpenAI SDK-Implementierung als Assistent bezeichnen, um die beiden leicht unterscheiden zu können.
Ich verwende die Begriffe Tools und Funktionen synonym, wenn es um Funktionen geht, die der Agent aufrufen kann. In Teil 2 wird der Funktionsaufruf ausführlicher behandelt.
Um diesem Tutorial folgen zu können, benötigen Sie Folgendes:
Assistent : Ein Assistent in der Assistenten-API ist eine Entität, die so konfiguriert ist, dass sie auf Benutzernachrichten reagiert. Es nutzt Anweisungen, ein ausgewähltes Modell und Werkzeuge, um mit Funktionen zu interagieren und Antworten zu geben.
Thread : Ein Thread stellt eine Konversation oder einen Dialog in der Assistants-API dar. Es wird für jede Benutzerinteraktion erstellt und kann mehrere Nachrichten enthalten und als Container für die laufende Konversation dienen.
Nachricht : Eine Nachricht ist eine Kommunikationseinheit in einem Thread. Es enthält Text (und möglicherweise zukünftige Dateien) und wird verwendet, um Benutzeranfragen oder Assistentenantworten innerhalb eines Threads zu übermitteln.
Run : Ein Run ist eine Instanz des Assistenten, der einen Thread verarbeitet. Dazu gehört das Lesen des Threads, die Entscheidung, ob Tools aufgerufen werden sollen, und das Generieren von Antworten basierend auf der Interpretation der Thread-Nachrichten durch das Modell.
Der erste Schritt besteht darin, mit venv
eine virtuelle Umgebung zu erstellen und diese zu aktivieren. Dadurch wird sichergestellt, dass unsere Abhängigkeiten von der System-Python-Installation isoliert sind:
python3 -m venv venv source venv/bin/activate
Installieren wir unsere einzige Abhängigkeit: das openai
Paket:
pip install openai
Erstellen Sie eine main.py
Datei. Lassen Sie uns einige grundlegende Laufzeitlogik für unsere CLI-App auffüllen:
while True: user_input = input("User: ") if user_input.lower() == 'exit': print("Exiting the assistant...") break print(f"Assistant: You said {user_input}")
Probieren Sie es aus, indem Sie python3 main.py
ausführen:
python3 main.py User: hi Assistant: You said hi
Wie Sie sehen können, akzeptiert die CLI eine Benutzernachricht als Eingabe und unser genialer Assistent hat noch kein Gehirn 🧠, also wiederholt er die Nachricht einfach gleich. Noch nicht so schlau.
Jetzt beginnt der Spaß 😁 (oder die Kopfschmerzen 🤕). Ich werde jetzt alle für die letzte Klasse benötigten Importe bereitstellen, damit Sie sich nicht den Kopf darüber zerbrechen müssen, woher die Dinge kommen, da ich der Kürze halber Importe aus den Codebeispielen herausgehalten habe. Beginnen wir mit dem Erstellen einer Agent
Klasse in einer neuen Datei agent.py
:
import time import openai from openai.types.beta.threads.run import Run class Agent: def __init__(self, name: str, personality: str): self.name = name self.personality = personality self.client = openai.OpenAI(api_key="sk-*****") self.assistant = self.client.beta.assistants.create( name=self.name, model="gpt-4-turbo-preview" )
Im Klassenkonstruktor initialisieren wir den OpenAI-Client als Klasseneigenschaft, indem wir unseren OpenAI-API-Schlüssel übergeben. Als Nächstes erstellen wir eine Eigenschaft assistant
Klasse, die unserem neu erstellten Assistant zugeordnet ist. Wir speichern name
und personality
als Klasseneigenschaften zur späteren Verwendung.
Das name
, das wir an die Methode „create“ übergeben, dient lediglich der Identifizierung des Assistenten im OpenAI-Dashboard, und die KI ist sich dessen zu diesem Zeitpunkt noch nicht wirklich bewusst. Sie müssen den Namen tatsächlich an die instructions
übergeben, die wir später sehen werden.
Sie könnten bereits beim Erstellen des Assistenten instructions
festlegen, aber dadurch wird Ihr Assistent tatsächlich weniger flexibel gegenüber dynamischen Änderungen.
Sie können einen Assistenten aktualisieren, indem Sie client.beta.assistants.update
aufrufen, aber es gibt einen besseren Ort, um dynamische Werte zu übergeben, die wir sehen werden, wenn wir zu „Runs“ kommen.
Beachten Sie, dass die instructions
des Assistenten durch die instructions
des Laufs überschrieben werden, wenn Sie beim Erstellen eines Laufs hier und dann wieder instructions
übergeben. Sie ergänzen einander nicht. Wählen Sie daher je nach Bedarf eine aus: Assistentenebene für statische Anweisungen oder Ausführungsebene für dynamische Anweisungen.
Als Modell habe ich das gpt-4-turbo-preview
Modell ausgewählt, damit wir in Teil 2 dieser Serie Funktionsaufrufe hinzufügen können. Sie könnten gpt-3.5-turbo
verwenden, wenn Sie ein paar Bruchteile eines Cents sparen möchten und sich gleichzeitig eine Migräne purer Frustration bescheren möchten, wenn wir Tools implementieren.
GPT 3.5 ist beim Aufrufen von Tools schrecklich; Die Stunden, die ich damit verloren habe, damit umzugehen, erlauben es mir, das zu sagen. 😝 Ich belasse es dabei, dazu später mehr.
Nachdem wir einen Agenten erstellt haben, müssen wir einen Konversationsthread starten.
class Agent: # ... (rest of code) def create_thread(self): self.thread = self.client.beta.threads.create()
Und wir suchen nach einer Möglichkeit, diesem Thread Nachrichten hinzuzufügen:
class Agent: # ... (rest of code) def add_message(self, message): self.client.beta.threads.messages.create( thread_id=self.thread.id, role="user", content=message )
Beachten Sie, dass das Hinzufügen von Nachrichten derzeit nur mit der Rolle user
möglich ist. Ich glaube, OpenAI plant, dies in einer zukünftigen Version zu ändern, da dies ziemlich einschränkend ist.
Jetzt können wir die letzte Nachricht im Thread erhalten:
class Agent: # ... (rest of code) def get_last_message(self): return self.client.beta.threads.messages.list( thread_id=self.thread.id ).data[0].content[0].text.value
Als Nächstes erstellen wir eine run_agent
Einstiegspunktmethode, um zu testen, was wir bisher haben. Derzeit gibt die run_agent
Methode nur die letzte Nachricht im Thread zurück. Es führt eigentlich keinen Lauf durch. Es ist immer noch hirnlos.
class Agent: # ... (rest of code) def run_agent(self): message = self.get_last_message() return message
Zurück in main.py
erstellen wir den Agenten und unseren ersten Thread. Wir fügen dem Thread eine Nachricht hinzu. Senden Sie dann dieselbe Nachricht an den Benutzer zurück, diesmal jedoch aus diesem Live-Thread.
from agent import Agent agent = Agent(name="Bilbo Baggins", personality="You are the accomplished and renowned adventurer from The Hobbit. You act like you are a bit of a homebody, but you are always up for an adventure. You worry a bit too much about breakfast.") agent.create_thread() while True: user_input = input("User: ") if user_input.lower() == 'exit': print("Exiting the agent...") break agent.add_message(user_input) answer = agent.run_agent() print(f"Assistant: {answer}")
Lassen Sie es uns ausführen:
python3 main.py User: hi Assistant: hi
Immer noch nicht sehr schlau. Eher einem Papagei 🦜 als einem Hobbit. Im nächsten Abschnitt beginnt der wahre Spaß.
Wenn Sie einen Lauf erstellen, müssen Sie das Run
Objekt regelmäßig abrufen, um den Status des Laufs zu überprüfen. Das nennt man Polling und es ist scheiße. Sie müssen eine Umfrage durchführen, um festzustellen, was Ihr Agent als Nächstes tun soll. OpenAI plant, Unterstützung für Streaming hinzuzufügen, um dies einfacher zu machen. In der Zwischenzeit werde ich Ihnen im nächsten Abschnitt zeigen, wie Sie die Umfrage einrichten.
Beachten Sie das _
in den folgenden Methodennamen, das der Standard in Python ist und anzeigt, dass die Methode für den internen Gebrauch gedacht ist und nicht direkt durch externen Code aufgerufen werden sollte.
Erstellen wir zunächst eine Hilfsmethode _create_run
zum Erstellen eines Run
und aktualisieren wir run_agent
, um diese Methode aufzurufen:
class Agent: # ... (rest of code) def get_breakfast_count_from_db(self): return 1 def _create_run(self): count = self.get_breakfast_count_from_db() return self.client.beta.threads.runs.create( thread_id=self.thread.id, assistant_id=self.assistant.id, instructions=f""" Your name is: {self.name} Your personality is: {self.personality} Metadata related to this conversation: {{ "breakfast_count": {count} }} """, ) def run_agent(self): run = self._create_run() # add this line message = self.get_last_message() return message
Beachten Sie, wie wir thread.id
und assistant.id
übergeben, um einen Lauf zu erstellen.
Erinnern Sie sich, wie ich am Anfang sagte, dass es einen besseren Ort für die Weitergabe dynamischer Anweisungen und Daten gibt? Das wäre der instructions
beim Erstellen des Laufs. In unserem Fall könnten wir die count
der Frühstücke aus einer Datenbank abrufen lassen. Auf diese Weise können Sie jedes Mal, wenn Sie eine Antwort auslösen möchten, problemlos verschiedene relevante dynamische Daten übergeben.
Jetzt ist sich Ihr Agent bewusst, dass sich die Welt um ihn herum verändert, und kann entsprechend handeln. Ich möchte in meinen Anweisungen ein Metadaten-JSON-Objekt haben, das den relevanten dynamischen Kontext beibehält. Dadurch kann ich Daten mit weniger Ausführlichkeit und in einem Format weitergeben, das der LLM wirklich gut versteht.
Führen Sie dies noch nicht aus; Es wird nicht funktionieren, weil wir nicht auf den Abschluss des Laufs warten, wenn wir die letzte Nachricht erhalten, es also immer noch die letzte Benutzernachricht sein wird.
Lassen Sie uns dieses Problem lösen, indem wir unseren Umfragemechanismus ausbauen. Zuerst benötigen wir eine Möglichkeit, einen Lauf wiederholt und einfach abzurufen. Fügen wir also eine _retrieve_run
-Methode hinzu:
class Agent: # ... (rest of code) def _retrieve_run(self, run: Run): return self.client.beta.threads.runs.retrieve( run_id=run.id, thread_id=self.thread.id)
Beachten Sie, dass wir sowohl run.id
als auch thread.id
übergeben müssen, um einen bestimmten Lauf zu finden.
Fügen Sie unserer Agent-Klasse eine _poll_run
Methode hinzu:
class Agent: # ... (rest of code) def _cancel_run(self, run: Run): self.client.beta.threads.runs.cancel( run_id=run.id, thread_id=self.thread.id) def _poll_run(self, run: Run): status = run.status start_time = time.time() while status != "completed": if status == 'failed': raise Exception(f"Run failed with error: {run.last_error}") if status == 'expired': raise Exception("Run expired.") time.sleep(1) run = self._retrieve_run(run) status = run.status elapsed_time = time.time() - start_time if elapsed_time > 120: # 2 minutes self._cancel_run(run) raise Exception("Run took longer than 2 minutes.")
🥵 Puh, das ist eine ganze Menge... Packen wir es mal aus.
_poll_run
empfängt ein Run
Objekt als Argument und extrahiert den aktuellen Run- status
. Alle verfügbaren Status finden Sie in den OpenAI- Dokumenten . Wir werden nur einige verwenden, die unserem aktuellen Zweck entsprechen.
Wir führen nun eine While-Schleife aus, um den Status „Abgeschlossen“ zu prüfen und gleichzeitig einige Fehlerszenarien zu behandeln. Die tatsächliche Abrechnung der Assistant API ist etwas unklar, daher habe ich mich sicherheitshalber dafür entschieden, meine Läufe nach 2 Minuten abzubrechen.
Auch wenn es einen expired
Status gibt, wenn OpenAI Ausführungen nach 10 Minuten abbricht. Wenn ein Lauf länger als 2 Minuten dauert, liegt wahrscheinlich sowieso ein Problem vor.
Da ich auch nicht alle paar Millisekunden abfragen möchte, drossle ich meine Anfrage, indem ich nur alle 1 Sekunde abfrage, bis ich die 2-Minuten-Marke erreiche und meinen Lauf abbreche. Sie können dies nach Ihren Wünschen anpassen.
Bei jeder Iteration nach der Verzögerung rufen wir den Ausführungsstatus erneut ab.
Fügen wir das alles nun in unsere run_agent
Methode ein. Sie werden feststellen, dass wir zuerst den Lauf mit _create_run
erstellen und dann mit _poll_run
abfragen, bis wir eine Antwort erhalten oder ein Fehler ausgegeben wird. Wenn die Abfrage abgeschlossen ist, rufen wir schließlich die letzte Nachricht aus dem Thread ab, die nun vom Agenten stammt.
Anschließend geben wir die Nachricht an unsere Laufzeitschleife zurück, damit sie an den Benutzer zurückgesendet werden kann.
class Agent: # ... (rest of code) def run_agent(self): run = self._create_run() self._poll_run(run) # add this line message = self.get_last_message() return message
Voilà, wenn Sie jetzt Ihren Agenten erneut ausführen, erhalten Sie eine Antwort von unserem freundlichen Agenten:
python3 main.py User: hi Assistant: Hello there! What adventure can we embark on today? Or perhaps, before we set out, we should think about breakfast. Have you had yours yet? I've had mine, of course – can't start the day without a proper breakfast, you know. User: how many breakfasts have you had? Assistant: Ah, well, I've had just 1 breakfast today. But the day is still young, and there's always room for a second, isn't there? What about you? How can I assist you on this fine day?
In Teil 2 werden wir unserem Agenten die Möglichkeit hinzufügen, Tools aufzurufen.
Den vollständigen Code finden Sie auf meinem GitHub .
Vielen Dank für Ihre Lektüre. Ich freue mich über alle Gedanken und Rückmeldungen in den Kommentaren. Folgen Sie mir auf Linkedin für weitere Inhalte wie diesen: https://www.linkedin.com/in/jean-marie-dalmasso-1b5473141/