Hallo zusammen, hier ist Nina, zurück aus meinem digitalen Labor (das, seien wir ehrlich, hauptsächlich mein Küchentisch mit viel lauwarmem Kaffee ist). Heute möchte ich über etwas sprechen, das in meinen Slack-Kanälen für Aufregung sorgt und meine nächtlichen Codiersessions heimsucht: die manchmal frustrierende, oft brillante Welt der KI-SDKs, besonders wenn Sie versuchen, einen Chatbot zu erstellen, der wirklich wie ein Gespräch wirkt, nicht nur wie ein glorifizierter FAQ-Bot.
Meine besondere Besessenheit in letzter Zeit war, die Assistants API von OpenAI zu integrieren. Ich weiß, was Sie denken: „Nina, die Assistants API? Das ist alte Geschichte!“ Und ja, Sie haben nicht Unrecht. Sie ist schon eine Weile verfügbar. Aber hören Sie mir zu: Ich bin nicht hier, um Ihnen ein einfaches Tutorial dazu zu geben, „wie man client.beta.assistants.create() aufruft“. Das haben wir alle schon gesehen. Mein Ziel heute ist es, über ein spezifisches, kniffliges Problem zu sprechen, dem ich begegnet bin, und wie ich mit dem SDK gekämpft habe, damit es das tut, was ich wollte: mehrere Runden mit Zustand geführte Gespräche mit benutzerdefinierten Werkzeugen zu verwalten, ohne dabei den Kopf (oder den Kontext meines Benutzers) zu verlieren. Denken Sie über eine einfache FAQ hinaus. Denken Sie an komplexe Workflows, Folgefragen und dynamische Werkzeugaufrufe, die sich auf die Entwicklung der Benutzerintention stützen.
Mein Mental Health Status: Warum die Assistants API (weiterhin) wichtig ist
Vor der Assistants API schien der Aufbau von etwas Zustandsbezogenem mit den Modellen von OpenAI oft wie das Jonglieren mit zu vielen Bällen in der Luft. Sie waren verantwortlich dafür, den Nachrichtenverlauf zu verwalten, die Eingaben für den Kontext zu gestalten und die Werkzeugaufrufe alleine zu orchestrieren. Es war… ermüdend. Die Assistants API versprach, viel von dieser Last zu erleichtern, indem sie Threads, Nachrichten und sogar die Orchestrierung von Werkzeugen verwaltete. Und für einfache Anwendungsfälle funktioniert sie großartig.
Mein aktuelles Projekt besteht darin, einen „intelligenten“ persönlichen Finanzassistenten zu bauen. Nicht nur einen, der Ihnen Ihr Guthaben sagt, sondern einen, der Ihnen hilft, Budgets zu planen, Investitionsstrategien basierend auf Ihrer Risikotoleranz vorzuschlagen und sogar zukünftige finanzielle Szenarien zu simulieren. Das erfordert viele Hin- und Herbewegungen, das Erinnern an frühere Aussagen („Ich möchte für ein Haus sparen“, „Mein Einkommen beträgt X“, „Was wäre, wenn ich in Y investieren würde?“), und vor allem das rechtzeitige Aufrufen spezifischer Funktionen (wie z. B. das Abrufen von Echtzeit-Börsendaten oder das Ausführen eines Budgetprojektionsalgorithmus).
Mein erster Versuch endete in einem gloriosen Desaster. Ich habe den gesamten Nachrichtenverlauf hin- und hergeschoben, versucht, komplexe Anweisungen in die Systemaufforderung einzufügen, und manuell überprüft, ob ein Werkzeugaufruf erforderlich war. Es hat funktioniert, aber es war fragil, langsam und ein echtes Albtraum beim Debuggen. In diesem Moment habe ich beschlossen, mich wirklich auf die Assistants API und ihr SDK zu stützen.
Die Umarmung des SDK: Threads, Nachrichten und der flüchtige `run`
Im Herzen der Assistants API steht das Konzept von `Threads` und `Messages`. Sie erstellen einen `Thread` für ein Gespräch, fügen `Messages` hinzu und „führen“ dann den `Assistant` in diesem `Thread` aus. Der `Assistant` bearbeitet dann die Nachrichten, entscheidet, ob er ein Werkzeug aufrufen muss, und generiert eine Antwort. Ziemlich einfach, oder?
Hier wird es interessant. Mein Finanzassistent muss Dinge tun wie:
- Ziele verfolgen: „Ich möchte 50.000 $ für eine Anzahlung sparen.“
- Informationen sammeln: „Wie hoch ist Ihr aktuelles monatliches Einkommen?“
- Berechnungen durchführen: „Wie lange würde es dauern, wenn ich 1.000 $ pro Monat spare?“
- Ratschläge geben: „Erwägen Sie, Ihr Portfolio mit kostengünstigen Indexfonds zu diversifizieren.“
Jede dieser Aufgaben erfordert oft einen benutzerdefinierten Werkzeugaufruf. Zum Beispiel `calculate_savings_timeline(goal_amount, monthly_income, monthly_savings)`. Die Herausforderung besteht nicht nur darin, das Werkzeug zu definieren; es geht darum, den Assistant dazu zu bringen:
- Zu erkennen, dass er das Werkzeug benötigt.
- Alle erforderlichen Parameter zu identifizieren (auch wenn sie über mehrere Benutzer-Nachrichten verteilt sind).
- Den Benutzer behutsam nach den fehlenden Parametern zu fragen.
- Das Werkzeug auszuführen und die Ergebnisse zu integrieren.
Das Parameterproblem: Wenn der Assistant mehr fragt
Angenommen, ein Benutzer sagt: „Wie lange werde ich brauchen, um für ein Haus zu sparen?“ Mein Werkzeug `calculate_savings_timeline` benötigt `goal_amount`, `monthly_income` und `monthly_savings`. Der Assistant, der intelligent ist, wird oft erkennen, dass Informationen fehlen. Das SDK wird Ihnen über das `run`-Objekt mitteilen, dass es `requires_action` benötigt.
Mein ursprünglicher Ansatz bestand einfach darin, auf den Status `requires_action` zu warten und dem Benutzer eine generische Frage „Welche Informationen benötigen Sie?“ zu stellen. Das war unbeholfen. Der Benutzer wusste oft nicht, welche Parameter für das Werkzeug benötigt wurden. Ein besserer Ansatz, auf den ich schließlich gestoßen bin, bestand darin, die `tool_calls` im Status `requires_action` zu inspizieren.
def submit_tool_outputs(client, thread_id, run_id, tool_outputs):
return client.beta.threads.runs.submit_tool_outputs(
thread_id=thread_id,
run_id=run_id,
tool_outputs=tool_outputs
)
# ... in Ihrer Konversationsschleife ...
if run.status == 'requires_action':
tool_outputs = []
missing_params = {}
for tool_call in run.required_action.submit_tool_outputs.tool_calls:
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
# Hier ist der Trick: Überprüfen, ob ein erforderlicher Parameter fehlt
# Wir nehmen an, dass Ihre Werkzeuge mit einem Schema definiert sind
# Um es zu vereinfachen, sagen wir, dass wir wissen, dass 'monthly_savings' immer benötigt wird
if function_name == "calculate_savings_timeline" and "monthly_savings" not in arguments:
missing_params['monthly_savings'] = 'Wie viel planen Sie, jeden Monat zu sparen?'
# ... hier eine ausgefeiltere Prüfung der Parameter ...
if missing_params:
# Den Benutzer darüber informieren, was fehlt
user_prompt = "Ich benötige ein paar mehr Informationen, um Ihnen zu helfen. "
for param, question in missing_params.items():
user_prompt += f"{question} "
return {"status": "waiting_for_user_input", "prompt": user_prompt}
else:
# Alle Parameter sind vorhanden, Werkzeug ausführen
output = execute_tool_function(function_name, arguments) # Ihre Funktion zur Ausführung der Logik des Werkzeugs
tool_outputs.append({
"tool_call_id": tool_call.id,
"output": json.dumps(output)
})
if tool_outputs:
run = submit_tool_outputs(client, thread.id, run.id, tool_outputs)
# Fahren Sie fort, den run zu überprüfen, bis er abgeschlossen ist oder eine weitere Aktion benötigt
else:
# Dieser Fall behandelt, ob wir fehlende Parameter festgestellt und den Benutzer gefragt haben
pass
Dieser Code ist eine vereinfachte Version, aber die Hauptidee besteht darin, `requires_action` abzufangen, zu sehen, was der Assistant will, und wenn Argumente fehlen, den Benutzer spezifisch zu fragen. Dadurch wird das Gespräch viel natürlicher, wie ein Mensch, der klärende Fragen stellt, anstatt ein generisches „Fehler“.
Der ausweichende Zustand: Den Überblick zwischen den Runden behalten
Einer der größten Kopfschmerzen war sicherzustellen, dass, wenn ein Benutzer eine Information bereitstellte („Mein Einkommen beträgt 5.000 $ pro Monat“), der Assistant sich daran für weitere Werkzeugaufrufe innerhalb desselben Gesprächs erinnerte, selbst wenn sie nicht sofort verwendet wurde. Die Assistants API handhabt dies ziemlich gut innerhalb eines `Thread`, indem sie den Nachrichtenverlauf speichert. Manchmal möchte man sie jedoch ausdrücklich führen oder Informationen geben, die nicht Teil einer direkten Benutzer-Nachricht sind.
Ich stellte fest, dass ich `metadata` auf den `Thread`- und `Message`-Objekten mehr verwendete, als ich anfangs dachte. Zum Beispiel, wenn der Benutzer ausdrücklich seine Risikotoleranz angibt, könnte ich dies in den Metadaten des Threads speichern. Obwohl der Assistant diese Metadaten nicht direkt in seinem Denkprozess „liest“, sind sie für meine Anwendungslogik von unschätzbarem Wert, wenn ich Argumente für Werkzeugaufrufe vorab ausfüllen oder Entscheidungen über die Werkzeuge treffen muss, die überhaupt relevant sind.
Ein weiteres Modell, das ich übernommen habe, bestand darin, „systemähnliche“ Nachrichten in den Verlauf zu injizieren, falls ich den Assistenten explizit an bestimmte Fakten erinnern musste, die aus einem Toolaufruf oder einem internen Prozess abgeleitet wurden. Zum Beispiel, nachdem `calculate_savings_timeline` zurückgegeben hat, dass es 10 Jahre dauern würde, könnte ich eine Nachricht hinzufügen wie: `{“role”: “user”, “content”: “Der Sparplan des Nutzers sieht eine Dauer von 10 Jahren vor, um sein Ziel zu erreichen.”}`. Dies stammt nicht direkt vom Nutzer, hilft jedoch, den Kontext für die folgenden Antworten des Assistenten zu stärken.
Verwaltung der Toolausgabe und nachfolgender Aktionen
Wenn ein Tool ausgeführt wird, erhalten Sie eine Ausgabe. Der Assistent nimmt diese Ausgabe und generiert eine Antwort. Aber was passiert, wenn die Ausgabe des Tools selbst eine neue Reihe von Fragen oder einen neuen Toolaufruf auslöst? Zum Beispiel könnte das Tool `calculate_savings_timeline` zurückgeben, dass das Ziel mit den aktuellen Ersparnissen unerreichbar ist. Der Assistent sollte idealerweise vorschlagen: „Vielleicht sollten wir Möglichkeiten in Betracht ziehen, Ihre monatlichen Ersparnisse zu erhöhen oder Ihren Zielbetrag zu reduzieren.“ Es geht dabei nicht nur um Textausgaben; es geht darum, logische Schritte zu verknüpfen.
Das SDK der Assistants API verwaltet dies hervorragend, indem es das Objekt `run` aktiv hält. Nachdem Sie `submit_tool_outputs` ausgeführt haben, wechselt der `run` oft in den Status `in_progress` und kann dann ein neues `requires_action` (für einen weiteren Toolaufruf) oder `completed` generieren. Meine Hauptschleife zur Verwaltung des Gesprächsflusses sieht etwa so aus:
def get_assistant_response(client, thread_id, assistant_id, user_message):
client.beta.threads.messages.create(
thread_id=thread_id,
role="user",
content=user_message
)
run = client.beta.threads.runs.create(
thread_id=thread_id,
assistant_id=assistant_id
)
while run.status in ['queued', 'in_progress', 'cancelling']:
time.sleep(1) # Seien Sie ein guter Bürger, überlasten Sie die API nicht
run = client.beta.threads.runs.retrieve(thread_id=thread_id, run_id=run.id)
if run.status == 'completed':
messages = client.beta.threads.messages.list(thread_id=thread_id, order="desc")
for msg in messages.data:
if msg.role == "assistant":
# Finde die letzte Nachricht des Assistenten, oft die erste
return msg.content[0].text.value # Vorausgesetzt, der Inhalt ist textuell
return "Keine Antwort vom Assistenten."
elif run.status == 'requires_action':
tool_outputs = []
for tool_call in run.required_action.submit_tool_outputs.tool_calls:
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
# Hier Ihre benutzerdefinierte Logik zur Verwaltung fehlender Parameter oder Ausführen von Tools
# Für dieses vereinfachte Beispiel nehmen wir an, dass alle Parameter vorhanden sind und wir ausführen
print(f"Der Assistent möchte {function_name} mit args aufrufen: {arguments}")
output = execute_tool_function(function_name, arguments) # Ihre Logik zur Ausführung des Tools
tool_outputs.append({
"tool_call_id": tool_call.id,
"output": json.dumps(output)
})
# Toolausgaben einreichen und die Ausführung fortsetzen
run = client.beta.threads.runs.submit_tool_outputs(
thread_id=thread_id,
run_id=run.id,
tool_outputs=tool_outputs
)
# Rufen Sie diese Funktion rekursiv auf, um die aktualisierte Ausführung zu bearbeiten
# Achten Sie auf die Rekursionstiefe in der Produktion, Sie benötigen möglicherweise eine Schleife
return get_assistant_response(client, thread_id, assistant_id, "") # Leere Nachricht übergeben
elif run.status == 'failed':
return f"Die Ausführung des Assistenten ist fehlgeschlagen: {run.last_error.message}"
else:
return f"Unerwarteter Ausführungsstatus: {run.status}"
Diese Funktion `get_assistant_response`, obwohl vereinfacht, zeigt die zentrale Schleife. Das Wesentliche ist, dass `requires_action` keine Sackgasse ist. Es ist eine Gelegenheit für Ihre Anwendung, einzugreifen, das angeforderte Tool auszuführen und dann die Ergebnisse an den Assistenten zurückzusenden, um sein Denken fortzusetzen. Dieser Feedback-Prozess in geschlossenen Schleifen ermöglicht wirklich dynamische und anpassungsfähige Gespräche.
Wichtige Punkte für Ihr nächstes KI-Assistenten-Projekt
- Adoptieren Sie den Lebenszyklus `run.status`: Überprüfen Sie nicht einfach, ob es `completed` ist. `requires_action` ist Ihr Freund, keine Fehler. Erstellen Sie robuste Handler für jeden relevanten Status.
- Untersuchen Sie `tool_calls` auf fehlende Parameter: Statt einfacher Hinweise, tauchen Sie in `run.required_action.submit_tool_outputs.tool_calls` ein, um *genau* zu verstehen, was der Assistent benötigt. Das verbessert Ihre Benutzererfahrung.
- Strategische Nutzung von Systemnachrichten: Wenn Ihre interne Anwendungslogik neue Fakten ableitet oder bestimmte Kontexte betonen muss, ziehen Sie in Betracht, „Benutzernachrichten“ zu injizieren, die diese Fakten darstellen. Der Assistent behandelt sie als Teil des Verlaufs.
- Metadaten für den Anwendungsstatus: Obwohl der Assistent die Metadaten von Threads oder Nachrichten nicht direkt für sein Denken „lesen“ kann, ist dies ein leistungsfähiger Ort für Ihre Anwendung, um relevante Zustände für das Gespräch zu speichern und abzurufen. Denken Sie an Benutzerpräferenzen, aktuelle Sitzungsvariablen usw.
- Testen Sie Sonderfälle für die Orchestrierung von Tools: Was passiert, wenn ein Tool fehlschlägt? Was passiert, wenn der Nutzer seine Meinung während eines Toolaufrufs ändert? Entwerfen Sie Ihre `execute_tool_function` und verwalten Sie Fehler sorgfältig in der `requires_action`-Schleife.
Denken Sie daran, dass der Bau von wirklich intelligenten KI-gesteuerten Gesprächspartnern nicht nur darin besteht, das neueste Modell auszuwählen. Es geht darum, die bereitgestellten SDKs geschickt zu nutzen, um die Komplexitäten menschlicher Interaktionen zu bewältigen. Die API der OpenAI Assistants, mit ein wenig durchdachter Ingenieursarbeit bezüglich der Zustandsverwaltung und der Orchestrierung von Tools, kann Ihre Gesprächserfahrung wirklich über einen einfachen Austausch hinaus verbessern. Es ist noch ein Weg, und ich lerne immer noch neue Tricks, aber ich hoffe, diese Ideen ersparen Ihnen einige schlaflose Nächte und kalte Tassen Kaffee!
Verwandte Artikel
- Arting AI: Entfalten Sie Ihr kreatives Potenzial mit KI-Kunstgeneratoren
- Die besten API-Dokumentationstools für Entwickler
- Entwicklerproduktivitätswerkzeuge, die tatsächlich geändert haben, wie ich Code veröffentliche
🕒 Published: