Chatbot DIY: So erstellst Du mit OpenAI einen selbsttrainierten Chatbot für Deine Webseite.

So erstellst Du mit OpenAI einen selbsttrainierten Chatbot für deine Webseite.

Dieser Beitrag dreht sich darum, wie Du mithilfe der OpenAI API Deinen eigenen Chatbot trainieren und Deinen Kunden bereitstellen kannst.

Im Gegensatz zum „normalen“ ChatGPT soll dieser Chatbot in der Lage sein, verschiedene ganz spezifische Fragen zu deinem Angebot zu beantworten – bspw. was Deine Dienstleistung ausmacht, welche spezifischen Merkmale Deine Produkte aufweisen, wo der Unterschied zwischen Produkt A und Produkt B liegt, wie der Bezahlvorgang abläuft etc.

Was brauchst Du dafür? Nichts weiter als Grundkenntnisse in Python und einen OpenAI Account.

Inhalt

1. Lässt sich ChatGPT überhaupt mit eigenen Daten trainieren?

Derzeit wird viel über ChatGPT berichtet und Du stellst dir vielleicht die Frage:
Wie kann ich dieses Tool nutzen, um damit einen eigenen Chatbot zu erstellen, der meine User:innen im Onboarding oder als Support-Kontakt auf unserer Webseite unterstützt?

Denn: ChatGPT wurde mit Hilfe von Daten trainiert, die bis ins Jahr 2021 hinein erfasst wurden, und kennt vermutlich nicht im Detail Deine Webseite, Dein Produkt und die typischen Servicefragen Deiner Kund:innen. Wie kannst Du also ChatGPT dieses Wissen beibringen?

Um das zu verstehen, hier nochmal ein kurzer Überblick über die OpenAI Produkte, die Du auch auf der OpenAI-Plattform findest:

  • Wenn Du Dich auf die Chat.OpenAI Webseite begibst, dann chattest Du mit dem ChatGPT-3.5 Modell von OpenAI. Es gibt mittlerweile auch die Version 4 im Rahmen der kostenpflichtigen Plus-Subscription. Die ChatGPT Sprachmodelle sind darauf ausgerichtet, natürlich-sprachliche, menschlich wirkende Chat Konversationen zu führen – daher der Name. Verknüpfbar per API ist bspw. die Variante gpt-3.5-turbo.
  • Auf der GPT Architektur basieren jedoch auch andere Sprachmodelle neben ChatGPT. Diese findest Du bspw. wenn du Playground betrittst. Hier siehst Du die Sprachmodelle der GPT-3 Familie mit solchen Modellen wie Curie oder Text-Davinci. Dabei handelt es sich um Modelle, die im Hinblick auf natürlich-sprachliche Konversation weniger mächtig sind als GPT-4, jedoch andere Vorteile bieten. So sind die Modelle per API zugänglich, teilweise schneller, preislich günstiger und – und das ist für uns in diesem Use Case zentral – mittels des sogenannten „Fine Tuning“ trainierbar.

Daher werden wir letztlich zur Familie von GPT-3 greifen.

2. Was bedeutet Fine-Tuning von Modellen?

Wir benötigen einen Chatbot, der detaillierte Fragen zu unseren Angeboten beantworten kann. Dieses Spezialwissen müssen wir dem Bot zunächst irgendwie vermitteln.

Als erstes könnte man auf den Gedanken kommen, das Spezialwissen, auf das sich das Modell beziehen soll, einfach in natürlicher Sprache mitzuteilen. So können wir bspw. einfach schreiben, was für eine Webseite wir betreiben, welche Produkte dort zu finden sind usw.
Ein Beispiel aus dem Playground soll das verdeutlichen:

Beispielhafter Kontext für unseren Chatbot am Beispiel von Text-Davinci.

In diesem Beispiel teilen wir dem Sprachmodell – hier im Beispiel übrigens text-davinci-003 – zunächst den Kontext mit, nämlich dass wir eine Webseite betreiben und dort drei Produkte zu unterschiedlichen Preisen anbieten. Der letzte Teil unserer Eingabe umfasst dann die fiktive Kundenfrage, nämlich:

„Hallo, was kosten bei euch die Wanderschuhe?“

Wie man im Beispiel sieht, antwortet das Modell korrekt und gut verständlich. Man bezeichnet diesen Vorgang als „Few Shots Learning“. Dabei teilen wir dem Modell als Kontext diejenigen Fakten mit, die es zur Beantwortung der Frage benutzen soll. Auf diese Weise lässt sich auch ChatGPT trainieren. Dieses Vorgehen ist sehr einfach und produziert hochwertige Resultate, weist jedoch folgende entscheidende Nachteile auf:

  1. Wir müssen den Kontext mit jeder einzelnen Anfrage immer wieder mitsenden. Dieses Vorgehen stößt offensichtlich schnell an seine Grenzen, wenn wir Support für unsere Webseite und ein komplexes Angebot anbieten möchten, zu dem wir Dutzende oder mehr Seiten an Kontext liefern müssen.
  2. OpenAI begrenzt die Verarbeitung von Anfragen von vorneherein auf eine maximale Länge von Tokens, je nach Modell z.B. 2.000 oder 4.000. Dieser Begriff aus der Sprachverarbeitung bezeichnet Bestandteile von Wörtern. Ohne Details zu nennen, kann man im Englischen bspw. erwarten, dass 4.000 Tokens ungefähr 3.000 Wörtern entsprechen (https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them). Dieses Limit gilt zusammengerechnet für die Anfrage und die Antwort, so dass ein umfangreiches Training mittels Few Shots Learning nicht möglich ist.
  3. Das Versenden des Kontextes treibt Laufzeiten und Kosten in die Höhe, denn: Bei OpenAI zahlen wir i.d.R. für jeweils 1.000 Tokens einen festen Betrag. Wenn wir mit jeder Anfrage einen langen Text als Kontext mitsenden, treibt dies die Kosten unnötig in die Höhe.

Aus den oben genannten Gründen werden wir einen anderen Ansatz benutzen und ein Sprachmodell mit Hilfe des sogenannten „Fine Tuning“ anpassen. Dabei benutzen wir ein Set an Trainingsdaten, um das Modell zu trainieren, das wir anschließend über die API ansteuern.

3. Auswahl & Kosten eines passenden Sprachmodells

Die Möglichkeit des Fine Tuning ist längst nicht bei allen Sprachmodellen möglich. Aus diesem Grund müssen wir uns auf die GPT-3 Familie beschränken. Das bedeutet jedoch immer noch, dass wir die Auswahl aus verschiedenen spezialisierten Modellen haben: https://platform.openai.com/docs/models/gpt-3.

Die Modelle unterscheiden sich anhand der Aufgaben, auf die sie ausgerichtet sind, ihrer Geschwindigkeit, teilweise der maximalen Anzahl der Tokens sowie auch der Kosten.

Kosten entstehen übrigens zweifach: Zum einen fällt eine Gebühr für das Training des eigenen Modells an, zum anderen für jede Anfrage, die wir später an das Modell stellen, jeweils wieder gemessen an 1.000 angefangenen Tokens. Diese befinden sich jedoch im niedrigen Cent-Bereich, wodurch für unser Testing zunächst keine nennenswerten Ausgaben anfallen.

Das Training der Modelle geschieht immer auf die gleiche Weise. Wir werden am Beispiel des Modells Curie testen. Schließlich wird es beschrieben als „Very capable, but faster and lower cost than Davinci”. Die Kosten belaufen sich auf 0.003 $ für je 1.000 Tokens beim Training sowie 0.012 $ per 1.000 Tokens bei der Verarbeitung von Anfragen. Eine Übersicht der Kosten findet sich hier: https://openai.com/pricing.

4. Vorbereitung: API Key und Zahlungsdaten

Bevor wir unser Modell trainieren, benötigen wir einen API Key und müssen, sofern wir kein Testguthaben mehr besitzen, noch Zahlungsdaten hinterlegen, damit wir die kostenpflichtige API nutzen können. Dieser Prozess ist jedoch maximal einfach:

  • Sofern noch nicht geschehen, leg Dir einen Account auf OpenAI an oder logge Dich ein unter https://platform.openai.com
  • Öffne die API Keys unter https://platform.openai.com/account/api-keys und klicke auf „Create new secret key“. Kopiere den API Key für später.
  • Klicke links im Menü auf „Billing“. Hier kannst Du Deine Zahlungsdaten ändern und ein Limit bei der API Nutzung setzen (z.B. 50,00 $ pro Monat).
  • Ebenfalls links im Menü findest Du den Punkt Usage, unter dem Du die aktuelle Nutzung und die Kosten überprüfen kannst.

Sobald Du bereit bist, geht es an die Programmierung.

5. Trainieren Deines eigenen Modells

Wir werden unser Modell in Python trainieren. Als erstes installieren wir dazu die vorgesehenen Library mittels pip install openai.

Als nächstes benötigen wir die Trainingsdaten. Diese erwartet OpenAI im sogenannten JSON Lines Format. Wir legen daher eine Datei namens input.jsonl im Ordner input an. Die Trainingsdaten geben wir dort Zeile für Zeile in einer Form wie dieser ein:

{„prompt“:“Was ist Lyncronize? ->“,“completion“:“ Lyncronize ist eine Plattform zur Vermittlung von IT Projekten.\n“}

{„prompt“:“Was macht Lyncronize? ->“,“completion“:“ Lyncronize hilft, IT Projekte auszuschreiben oder zu besetzen.\n“}

Jeder Eintrag umfasst ein prompt, in unserem Beispiel also eine separate Anfrage einer Kund:in an unseren Chatbot, sowie die zugehörige Antwort, die completion. Daneben benötigt das Modell klare Angaben, ab wann wir eine Textausgabe von ihm erwarten und wann es damit wieder aufhören soll. Im Klartext: Wir markieren das Ende jedes Prompts und jeder Completion mit einer eindeutigen Zeichenfolge, dem Indicator String bzw. der Stop Sequence. Diese beiden Zeichenfolgen können prinzipiell beliebig sein, dürfen aber nicht im Prompt- oder Completion-Text selbst vorkommen und müssen konsistent verwendet werden. OpenAI empfiehlt für das prompt die Zeichenfolge „->“ oder „\n###\n“ und für die Completion „\n“ (vgl. https://help.openai.com/en/articles/6811186-how-do-i-format-my-fine-tuning-data). Zudem soll jede Completion mit einem Whitespace starten.

Wichtig: Wenn Du das Modell später verwendest, um Anfragen zu beantworten, musst Du dieselben Suffixe benutzen, also jede Kund:innenanfrage bspw. mit einem „ ->“ abschließen.

Mit den oben geschilderten zwei Trainingsdaten können wir bereits loslegen. OpenAI selbst empfiehlt jedoch ein Trainingsset von mindestens 300 Daten. Laut eigener Aussage ergibt sich zudem eine lineare Verbesserung der Output-Qualität mit jeden weiteren 100 zusätzlichen Trainingsdaten.

Zudem kann man die Trainingsdaten vorab nochmal auf korrektes Format prüfen. Dies geschieht über die Kommandozeile über Eingabe von:

openai tools fine_tunes.prepare_data -f <LOCAL_FILE>

Im folgenden Beispiel erkennt das Tool ein Duplikat in den Trainingsdaten und schlägt uns vor, dieses zu entfernen. Wenn wir zustimmen, wird dadurch eine neue, korrigierte Datei erzeugt:

Das eigentliche Training erfordert lediglich einige wenige Schritte:

  • Wir laden die Trainingsdatei über die OpenAI API hoch und erhalten aus der Antwort die ID der hochgeladenen Datei
  • Wir starten den Trainingsprozess unter Angabe eines Basismodells wie Curie sowie der ID der hochgeladenen Trainings-Datei

Der notwendige Code sieht so aus:

import openai

openai.api_key = "DEIN-API-KEY"

# Wir wählen Curie als Grundlage für unser trainiertes Modell
model_engine = "curie"

# Hochladen der Trainingsdatei
upload_response = openai.File.create(
file=open("input/input.jsonl", "rb"),
purpose='fine-tune'
)
file_id = upload_response.id

# Starten des Fine Tunings/Trainings
fine_tune_response = openai.FineTune.create(training_file=file_id, model=model_engine)

Beachte, dass nur die Base Models für Fine Tuning zur Verfügung stehen, d.h. ada, babbage, curie und davinci.

Obiger Code stößt bereits das Training Deines eigenen Modells auf Basis des Sprachmodells Curie an, erzeugt jedoch keinerlei spezielle Ausgabe. Stattdessen arbeitet nun OpenAI für Dich und Du musst nun erstmal abwarten, bis Dein Modell zur Verfügung steht. Das kann durchaus mehrere Minuten oder länger dauern.

Damit wir erfahren, wann das Training abgeschlossen ist und das Modell bereit steht, erweitern wir das Skript entsprechend (vgl. auch https://community.openai.com/t/waiting-for-a-model-to-complete-training/137156).
Sobald dies der Fall ist, holen wir uns das neu trainierte Modell und stellen eine Anfrage.

Der Code dafür sieht so aus:

import openai
import time

openai.api_key = "Dein-API-KEY"

# Wir wählen Curie als Grundlage für unser trainiertes Modell
model_engine = "curie"

# Hochladen der Trainingsdatei
upload_response = openai.File.create(
file=open("input/input.jsonl", "rb"),
purpose='fine-tune'
)
file_id = upload_response.id

# Starten des Fine Tunings/Trainings
fine_tune_response = openai.FineTune.create(training_file=file_id, model=model_engine)
new_model_id = fine_tune_response.id

print("Starte Training ...")
status = openai.FineTune.retrieve(new_model_id).status

# Regelmäßig prüfen, ob das Training abgeschlossen wurde
while status == "pending" or status == "running":
status = openai.FineTune.retrieve(new_model_id).status
time.sleep(20)
print("Training läuft...")
print("Training abgeschlossen")

# Abfragen des trainierten Modells
fine_tune_response = openai.FineTune.retrieve(id=new_model_id)
fine_tuned_model = fine_tune_response.fine_tuned_model

# Durchführen einer testweisen Anfrage
print("Test: ========================================================")
prompt = "Was ist Lyncronize? ->"

completion = openai.Completion.create(
model=fine_tuned_model,
prompt=prompt,
max_tokens=1000,
temperature=0,
stop="\n"
)

print("Prompt:", prompt)
print("Completion:", completion['choices'][0]['text'])

Was passiert nun?

Sobald wir die Anfrage zum Fine-Tuning versenden, erhalten wir eine ID, welche unser neues, gerade in Berechnung befindliches Modell kennzeichnet. Wir prüfen nun in einer Schleife alle 20 Sekunden, ob das Training für dieses Modell noch läuft oder bereits abgeschlossen wurde. Sobald dies der Fall ist, holen wir uns das Modell über „openai.FineTune.retrieve“ und stellen eine Anfrage. Letzteres geschieht über „openai.Completion.create“. Dieser Anfrage fügen wir folgende Parameter hinzu:

  • Model: Das Modell, das die Anfrage beantworten soll, in diesem Fall unser neu trainiertes Curie Modell
  • Prompt: Die Anfrage selbst. Achte darauf, dass jede Anfrage denselben Indicator String verwendet, mit dem das Modell trainiert wurde, in unserem Fall also „ ->“.
  • Max_Tokens: Die maximale Anzahl Tokens. Damit kannst Du die Länge von Antworten durch das Modell begrenzen. Da wir ohnehin eher kurze knackige Antworten erwarten, kommen wir mit der Angabe von 1.000 Tokens mehr als aus.
  • Temperature: Diesert Wert zwischen 0 und 1.0 gibt an, wie sehr sich das Modell bei der Completion an die Trainingsdaten hält oder improvisiert. Ein Wert von 0 bedeutet, dass das Modell fast exakt mit den gelernten completions antwortet. Tendenziell wählen wir für unseren Bot einen konservativ niedrigen Wert, hier im Beispiel 0.
  • Stop: Die Zeichenfolge, ab der die API damit aufhört, weiteren Text zu generieren. Wir verwenden hier dieselbe Zeichenfolge, die auch das Ende jeder completion in unseren Trainingsdaten kennzeichnet, nämlich „\n“.

Wenn alles klappt, dann hast Du nun Dein erstes Modell trainiert und damit eine Anfrage beantwortet – Glückwunsch! Damit weißt Du bereits das Wichtigste. Als nächstes schauen wir uns an, wie wir Modelle löschen und genauer testen können.

Die komplette Anleitung von OpenAI zum Fine Tuning findest Du übrigens hier: https://platform.openai.com/docs/guides/fine-tuning

Die API Reference ist hier zu finden: https://platform.openai.com/docs/api-reference

6. Modelle prüfen und löschen

Jedes Mal, wenn Du wie oben beschrieben ein Trainings startest, wird dazu ein neues Modell erzeugt. Die ID eines solchen Modells sieht bspw. so aus: curie:ft-lyncronize-2023-04-13-13-08-00

Daran erkennst Du, dass das Modell auf Curie basiert und für meine Organisation lyncronize erzeugt wurde am 13.04.2023.

Um eine Liste aller Modelle zu erhalten, benötigst Du folgenden Code:

models = openai.Model.list()
print(models)

Du erhältst ein JSON mit allen zur Verfügung stehenden Modellen. Das schließt sowohl die ursprünglichen Modelle von OpenAI ein, also z.B. curie oder text-davinci-003, als auch die von Dir erzeugten Modelle – Du erkennst diese an dem Value von „owned_by“.

Möchtest Du nur eine Übersicht Deiner trainierten Modelle, dann verwende:

models = openai.FineTune.list()

Um eines Deiner eigenen Modelle zu löschen, benutze den folgenden Code mit der entsprechenden ID des Modells:

openai.Model.delete("curie:ft-lyncronize-2023-03-30-13-55-30")

7. Testen und Anpassen Deines Modells

Neben der Erweiterung der Trainingsdaten bieten sich folgende Punkte für Dein weiteres Testing an:

Praktisch ist, dass Du angelegte Modelle auch direkt über die GUI im Playground testen kannst. So können bspw. auch Deine Kolleg:innen aus dem Service oder Vertrieb ein Modell testen, ohne dass Du es bereits über einen eigenen Server zugänglich machen musst. Wähle dazu einfach im Playground rechts das Modell aus. Außerdem kannst Du so gut die Parameter wie die Temperature variieren um ein Gefühl für die Ergebnisse zu bekommen:

Testen und Anpassen Deines Sprachmodells über die GUI im Playground

Hier noch ein Hinweis zu Stop-Sequenz: Falls Du diese nicht angibst, „springt“ das Modell bei der Beantwortung von Anfragen zwischen den Trainingsdaten. Das sieht dann so aus, als würde das Modell selbst weitere Fragen stellen und beantworten (s.u.). Für den Produktivbetrieb solltest Du also darauf achten, die korrekte Stop Sequenz zu setzen.

Korrekte Stoppfrequenz in Playground setzen

8. Dein Modell per REST Call einbinden

Die bisherigen Zugriffe auf unser Modell habe ich in Python geschrieben. Du kannst Deine Modelle jedoch auch mit einem einfachen REST Call erreichen und so bspw. den Bot auf Deiner Webseite einbinden.

In Insomnia sieht das bspw. so aus:

Dein Modell per REST Call einbinden

Die Completion selbst ist ein POST Call auf https://api.openai.com/v1/completions

Im JSON Body geben wir wie gehabt das Model, Prompt, Temperature, Stop sequence und maximale Anzahl Tokens mit. Als Token setzt Du wiederum Deinen API Key ein. Solltest Du mit mehreren Keys arbeiten (z.B. privat und für Dein Unternehmen), setze hier den Key ein, mit dem das Modell erzeugt wurde und auf das Du die Zugriffsrechte hast.

9. Beispiele

Ich nutze nun Playground, um Dir einige Beispiele für Prompts und zugehörige Completions durch unsere trainierten Modelle zu zeigen. Dazu habe ich unser Modell zunächst mit rund insgesamt 50 Prompts und Completions zu unserer Plattform Lyncronize trainiert.

Zunächst frage ich das trainierte Curie Modell. Dabei verwende ich eine Temperatur von 0 und maximal 1.000 Tokens. Zudem wähle ich eine New Line als Stop Sequenz, damit das Modell nach einer Completion aufhört, weiteren Text zu generieren. Dazu drücke ich rechts im Textfeld unter Stop sequences einmal die Eingabetaste und füge dies als Kennzeichnung der Stop Sequenz hinzu, damit dadurch die New Line („\n“) korrekt erkannt wird:

Die Anfrage lautet: Warum sollte ich mich als Dienstleister registrieren? ->

Wie in den Beispielen zuvor verwende ich „ ->“ als Indicator String. Die Completion durch unser trainiertes Curie Modell sieht wie folgt aus:

Warum sollte ich mich als Dienstleister registrieren? -> Damit du als Dienstleister auf unserer Webseite präsent bist und unsere Dienstleister-Suche erweitert wird.

Und nun der Test mit Davinci:

Warum sollte ich mich als Dienstleister registrieren? -> Damit du deine Dienstleistungen in unserem Branchenindex präsentieren kannst und potenzielle Auftraggeber dich finden können.

Die Antworten sind ähnlich und beide noch nicht perfekt. Ich teste weiter mit einer Temperature von 0.7 im Davinci Modell:

Warum sollte ich mich als Dienstleister registrieren? -> Damit du deine Dienstleistungen im Graphenetwork einsehen und aktualisieren kannst.

Diese Antwort ergibt für uns gar keinen Sinn. Daher nun lieber ein weiterer Test mit Temperature 0 in Davinci:

Welche Daten benötigt ein Unternehmensprofil? -> Unternehmensprofile benötigen Angaben zu Branche, Stundensatz, Schwerpunkten sowie Projektreferenzen.

Diese Antwort ist knapp, aber korrekt. Mit einer Temperatur von 1.0 dagegen:

Welche Daten benötigt ein Unternehmensprofil? -> Unternehmensprofile benötigen die komplette Unternehmensinformation sowie die Projektinformationen. Für die Projektinformationen sind einzelne Projektdetails wie etwa der verantwortliche Projektleiter oder die Anzahl der durchschnittlichen Projektdauer erforderlich.

Naja. Wir sehen: Die Antworten gehen mit niedriger Temperature in die richtige Richtung und können durch mehre Trainingsdaten vermutlich noch verbessert werden. Ebenfalls zu überlegen ist, die Prompts jeweils um einen bestimmten Kontext zu ergänzen, der die Ergebnisqualität steigern kann.

Ein Beispiel: Eine User:in hat gerade einen neuen Unternehmensaccount bei uns angelegt. Nach dem Onboardingprozess stellt sie dem Chatbot die folgende Frage und erhält diese Antwort:

Was soll ich als nächstes tun? -> Bitte sende deine komplette Bewerbung an unseren Personaldienstleister und wir nehmen Kontakt mit dir auf.

Diese Antwort ergibt gar keinen Sinn. Wenn wir aber wissen, dass der Account gerade erst erstellt wurde, könnten wir dies als Kontext mitliefern, und das Prompt entsprechend erweitern:

Ich habe gerade einen Unternehmens-Account auf Lyncronize erstellt. Was soll ich als nächstes tun? -> Du solltest deinen Account so schnell wie möglich aktivieren, indem du dem Dienstleister-Support eine Mail schickst. Dann kannst du Unternehmen anschreiben und kommst in den Pool für Projekte.

Diese Antwort ist auch nicht sonderlich gut, aber schon deutlich sinnvoller als die vorige.

Du siehst: Schon mit relativ geringem Aufwand kann man loslegen und eigene Modelle trainieren und testen. Ich werde nun als nächstes weitere Trainingsdaten hinzufügen und mit Anpassungen an den Prompts experimentieren. Über meine weiteren Erfahrungen und die Umsetzung des Bots in unserem Produkt halte ich Dich gerne in meinem nächsten Beitrag auf dem Laufenden.

Rene Wegener

Rene Wegener

René ist Mitgründer und CTO von Lyncronize und als solcher mitverantwortlich für die Integration neuer Technologien in unsere Software. In seiner Freizeit treibt er gerne Sport, organisiert Film-Festivals und interessiert sich für Retro-Gaming.
Original veröffentlicht am 21. April 2023,
geändert am 30. Mai 2023.