paint-brush
Worteinbettungen: Das Geheimnis, um Ihrem ChatBot einen Kontext für bessere Antworten zu gebenvon@tomfernblog
2,991 Lesungen
2,991 Lesungen

Worteinbettungen: Das Geheimnis, um Ihrem ChatBot einen Kontext für bessere Antworten zu geben

von Tomas Fernandez18m2023/07/26
Read on Terminal Reader
Read this story w/o Javascript

Zu lang; Lesen

Erfahren Sie, wie Sie mithilfe von Worteinbettungen und ChatGPT einen Experten-Bot erstellen. Nutzen Sie die Leistungsfähigkeit von Wortvektoren, um die Antworten Ihres Chatbots zu verbessern.
featured image - Worteinbettungen: Das Geheimnis, um Ihrem ChatBot einen Kontext für bessere Antworten zu geben
Tomas Fernandez HackerNoon profile picture
0-item
1-item

Es besteht kein Zweifel, dass ChatGPT von OpenAI außergewöhnlich intelligent ist – es hat die Anwaltsprüfung bestanden, verfügt über Kenntnisse wie ein Arzt und einige Tests haben seinen IQ auf 155 ermittelt . Allerdings tendiert es dazu, Informationen zu fabrizieren , anstatt Unwissenheit einzugestehen. Dieser Trend, gepaart mit der Tatsache, dass sein Wissen im Jahr 2021 eingestellt wird, stellt die Entwicklung spezialisierter Produkte mithilfe der GPT-API vor Herausforderungen.


Wie können wir diese Hindernisse überwinden? Wie können wir einem Modell wie GPT-3 neues Wissen vermitteln? Mein Ziel ist es, diese Fragen zu beantworten, indem ich einen Frage-Antwort-Bot konstruiere, der Python, die OpenAI-API und Worteinbettungen verwendet.

Was ich bauen werde

Ich beabsichtige, einen Bot zu erstellen, der aus einer Eingabeaufforderung kontinuierliche Integrationspipelines generiert, die, wie Sie vielleicht wissen, mit YAML in Semaphore CI/CD formatiert sind .


Hier ist ein Beispiel des Bots in Aktion:

Screenshot des laufenden Programms. Auf dem Bildschirm wird der Befehl ausgeführt: python query.py „Erstellen Sie eine CI-Pipeline, die ein Docker-Image erstellt und auf Docker Hub hochlädt“, und das Programm gibt YAML aus, das einer CI-Pipeline entspricht, die die angeforderte Aktion ausführt. Screenshot des laufenden Programms. Auf dem Bildschirm wird der Befehl python query.py "Create a CI pipeline that builds and uploads a Docker image to Docker Hub" ausgeführt und das Programm gibt YAML aus, das einer CI-Pipeline entspricht, die die angeforderte Aktion ausführt.


Im Geiste von Projekten wie DocsGPT , My AskAI und Libraria habe ich vor, dem GPT-3-Modell Semaphore und das Generieren von Pipeline-Konfigurationsdateien beizubringen. Ich werde dies erreichen, indem ich die vorhandene Dokumentation nutze.


Ich setze keine Vorkenntnisse in der Bot-Erstellung voraus und pflege sauberen Code, damit Sie ihn an Ihre Anforderungen anpassen können.

Voraussetzungen

Sie benötigen keine Erfahrung im Codieren eines Bots oder Kenntnisse über neuronale Netze, um diesem Tutorial folgen zu können. Sie benötigen jedoch:


Aber ChatGPT kann nicht lernen, oder?

ChatGPT, oder genauer gesagt GPT-3 und GPT-4, die ihnen zugrunde liegenden Large Language Models (LLMs), wurden anhand eines riesigen Datensatzes trainiert, mit einem Stichtag um September 2021.


Im Wesentlichen weiß GPT-3 sehr wenig über Ereignisse nach diesem Datum. Wir können dies mit einer einfachen Eingabeaufforderung überprüfen:

Screenshot von ChatGPT ChatGPT weiß nicht, wer die Weltmeisterschaft 2022 gewonnen hat.


Während einige OpenAI-Modelle einer Feinabstimmung unterzogen werden können, ist dies bei den fortgeschritteneren Modellen, an denen wir interessiert waren, nicht möglich. Wir können ihre Trainingsdaten nicht erweitern.


Wie können wir von GPT-3 über seine Trainingsdaten hinaus Antworten erhalten? Eine Methode besteht darin, seine Textverständnisfähigkeiten auszunutzen; Indem wir die Eingabeaufforderung mit relevantem Kontext ergänzen, können wir wahrscheinlich die richtige Antwort erhalten.


Im folgenden Beispiel stelle ich Kontext von der offiziellen Website der FIFA bereit, und die Antwort unterscheidet sich erheblich:

Zweiter Versuch, auf die Frage zu antworten Mit dem bereitgestellten Kontext kann ChatGPT genau antworten.


Wir können daraus schließen, dass das Modell auf jede Eingabeaufforderung reagieren kann, wenn genügend relevanter Kontext vorhanden ist. Es bleibt die Frage: Wie können wir bei einer beliebigen Eingabeaufforderung wissen, was relevant ist? Um dieses Problem anzugehen, müssen wir untersuchen, was Worteinbettungen sind.

Was sind Worteinbettungen?

Im Kontext von Sprachmodellen ist eine Einbettung eine Möglichkeit, Wörter, Sätze oder ganze Dokumente als Vektoren oder Zahlenlisten darzustellen.


Um Einbettungen zu berechnen, benötigen wir ein neuronales Netzwerk wie word2vec oder text-embedding-ada-002 . Diese Netzwerke wurden anhand riesiger Textmengen trainiert und können Beziehungen zwischen Wörtern finden, indem sie die Häufigkeit analysieren, mit der bestimmte Muster in den Trainingsdaten auftreten.


Nehmen wir an, wir haben die folgenden Wörter:

  • Katze
  • Hund
  • Ball
  • Haus


Stellen Sie sich vor, wir verwenden eines dieser Einbettungsnetzwerke, um die Vektoren für jedes Wort zu berechnen. Zum Beispiel:

Wort

Vektor

Kontext

Katze

[0,1, 0,2, 0,3, 0,4, 0,5]

Tiere, Gegenstände, kleine Dinge

Hund

[0,6, 0,7, 0,8, 0,9, 1,0]

Tiere, Gegenstände, große Dinge

Ball

[0,2, 0,4, 0,6, 0,8, 1,0]

Gegenstände, Spielzeug, Kleinigkeiten

Haus

[0,3, 0,6, 0,9, 1,2, 1,5]

Gebäude, Häuser, große Dinge

Sobald wir die Vektoren für jedes Wort haben, können wir sie verwenden, um die Bedeutung des Textes darzustellen. Beispielsweise kann der Satz „Die Katze jagte den Ball“ als Vektor [0,1, 0,2, 0,3, 0,4, 0,5] + [0,2, 0,4, 0,6, 0,8, 1,0] = [0,3, 0,6, 0,9, 1,2] dargestellt werden , 1,5]. Dieser Vektor stellt einen Satz dar, in dem es um ein Tier geht, das einem Objekt nachjagt.


Worteinbettungen können als mehrdimensionale Räume visualisiert werden, in denen Wörter oder Sätze mit ähnlicher Bedeutung nahe beieinander liegen. Wir können den „Abstand“ zwischen Vektoren berechnen, um ähnliche Bedeutungen für jeden Eingabetext zu finden.

Dreidimensionale Darstellungen von Vektoren. Das erste ist mit „männlich-weiblich“ beschriftet und hat die Datenpunkte Mann-Frau und König-Königin, das zweite ist mit „Verb-Zeitform“ beschriftet und enthält Verben wie gehen-gehen-schwimmen-schwammen. Die letzte trägt die Bezeichnung „Landeshauptstadt“ und verfügt über mehrere Hauptstädte, die mit ihren Ländern verbunden sind 3D-Darstellung von Einbettungen als Vektorräume. In Wirklichkeit können diese Räume Hunderte oder Tausende von Dimensionen haben. Quelle: Lernen Sie das Multitool von AI kennen: Vector Embeddings


Die eigentliche Mathematik dahinter würde den Rahmen dieses Artikels sprengen. Die wichtigste Erkenntnis ist jedoch, dass wir mit Vektoroperationen die Bedeutung mithilfe der Mathematik manipulieren oder bestimmen können . Nehmen Sie den Vektor, der das Wort „Königin“ darstellt, subtrahieren Sie den Vektor „Frau“ davon und fügen Sie den Vektor „Mann“ hinzu. Das Ergebnis sollte ein Vektor in der Nähe von „König“ sein. Wenn wir „Sohn“ hinzufügen, sollten wir in die Nähe von „Prinz“ kommen.

Einbettung neuronaler Netze mit Tokens

Bisher haben wir die Einbettung neuronaler Netze diskutiert, die Wörter als Eingaben und Zahlen als Ausgaben verwenden. Viele moderne Netzwerke sind jedoch von der Verarbeitung von Wörtern zur Verarbeitung von Token übergegangen.


Ein Token ist die kleinste Texteinheit, die vom Modell verarbeitet werden kann. Token können Wörter, Zeichen, Satzzeichen, Symbole oder Wortteile sein.


Wir können sehen, wie Wörter in Token umgewandelt werden, indem wir mit dem OpenAI-Online-Tokenizer experimentieren, der Byte-Pair-Encoding (BPE) verwendet, um Text in Token umzuwandeln und jeden einzelnen durch eine Zahl darzustellen:

Screenshot des OpenAI-Tokenizers. Es wurde etwas Text eingegeben und jedes Token wird durch unterschiedliche Farben dargestellt, sodass wir sehen können, wie Wörter Token zugeordnet werden. Der Text lautet: Hinter jedem Einbettungsmodell steht ein neuronales Netzwerk, das den Eingabetext in Vektoren umwandelt. Jede Art von Einbettungsmodell verfügt über unterschiedliche Fähigkeiten und Geschwindigkeiten. Word2vec beispielsweise nimmt Wörter und erzeugt Vektoren im Bereich von 100 bis 300 Dimensionen. Zwischen Token und Wörtern besteht häufig eine 1:1-Beziehung. Die meisten Token enthalten das Wort und ein führendes Leerzeichen. Es gibt jedoch Sonderfälle wie „embedding“, das aus zwei Token besteht, „embed“ und „ding“, oder „capabilities“, das aus vier Token besteht. Wenn Sie auf „Token-IDs“ klicken, können Sie die numerische Darstellung jedes Tokens im Modell sehen.

Entwerfen eines intelligenteren Bots mithilfe von Einbettungen

Nachdem wir nun verstanden haben, was Einbettungen sind, lautet die nächste Frage: Wie können sie uns dabei helfen, einen intelligenteren Bot zu entwickeln?


Betrachten wir zunächst, was passiert, wenn wir die GPT-3-API direkt verwenden. Der Benutzer gibt eine Eingabeaufforderung aus und das Modell reagiert nach besten Kräften.

Diagramm, das die Interaktion zwischen Benutzer und GPT-3 zeigt. Der Benutzer sendet eine Eingabeaufforderung, das Modell antwortet.


Wenn wir der Gleichung jedoch Kontext hinzufügen, ändern sich die Dinge. Als ich beispielsweise ChatGPT nach dem Gewinner der Weltmeisterschaft fragte, nachdem ich den Kontext angegeben hatte, machte das den Unterschied.


Der Plan zum Aufbau eines intelligenteren Bots sieht also wie folgt aus:

  1. Fangen Sie die Eingabeaufforderung des Benutzers ab.
  2. Berechnen Sie die Einbettungen für diese Eingabeaufforderung und erhalten Sie einen Vektor.
  3. Durchsuchen Sie eine Datenbank nach Dokumenten in der Nähe des Vektors, da diese für die erste Eingabeaufforderung semantisch relevant sein sollten.
  4. Senden Sie die ursprüngliche Eingabeaufforderung zusammen mit dem relevanten Kontext an GPT-3.
  5. Leiten Sie die Antwort von GPT-3 an den Benutzer weiter.

Eine komplexere Implementierung eines Bots. Der Benutzer sendet die Eingabeaufforderung an eine Chatbot-App, die eine Kontextdatenbank durchsucht und diese zur Anreicherung der Eingabeaufforderung verwendet. Die Eingabeaufforderung wird an GPT-3 gesendet und die Antwort wird an den Benutzer weitergeleitet.


Beginnen wir wie bei den meisten Projekten mit dem Entwurf der Datenbank.

Erstellen einer Wissensdatenbank mit Einbettungen

Unsere Kontextdatenbank muss die Originaldokumentation und ihre jeweiligen Vektoren enthalten. Im Prinzip können wir für diese Aufgabe jede Art von Datenbank verwenden, eine Vektordatenbank ist jedoch das optimale Werkzeug für diese Aufgabe.


Vektordatenbanken sind spezialisierte Datenbanken zum Speichern und Abrufen hochdimensionaler Vektordaten. Anstatt eine Abfragesprache wie SQL für die Suche zu verwenden, stellen wir einen Vektor bereit und fragen die N nächsten Nachbarn ab.


Zur Generierung der Vektoren verwenden wir text-embedding-ada-002 von OpenAI, da es das schnellste und kostengünstigste Modell ist, das sie anbieten. Das Modell wandelt den Eingabetext in Token um und nutzt einen Aufmerksamkeitsmechanismus namens Transformer , um deren Beziehungen zu lernen. Die Ausgabe dieses neuronalen Netzwerks sind Vektoren, die die Bedeutung des Textes darstellen.

Diagramm, das den Tokenisierungsprozess veranschaulicht. Ein Dokument wird tokenisiert und dann an ein eingebettetes neuronales Netzwerk gesendet. Die Ausgabe des Netzwerks ist ein Vektor.


Um eine Kontextdatenbank zu erstellen, werde ich:

  1. Sammeln Sie die gesamte Quelldokumentation.
  2. Filtern Sie irrelevante Dokumente heraus.
  3. Berechnen Sie die Einbettungen für jedes Dokument.
  4. Speichern Sie die Vektoren, den Originaltext und alle anderen relevanten Metadaten in der Datenbank.

Diagramm, das den Prozess der Datenspeicherung in der Kontextdatenbank veranschaulicht. Das Quelldokument wird an das einbettende neuronale Netzwerk gesendet. Die Datenbank speichert den Vektor zusammen mit dem Originaltext.

Konvertieren von Dokumenten in Vektoren

Zuerst muss ich eine Umgebungsdatei mit dem OpenAI-API-Schlüssel initialisieren. Diese Datei sollte niemals der Versionskontrolle unterliegen, da der API-Schlüssel privat und an Ihr Konto gebunden ist.

 export OPENAI_API_KEY=YOUR_API_KEY

Als Nächstes erstelle ich eine virtuelle Umgebung für meine Python-Anwendung:

 $ virtualenv venv $ source venv/bin/activate $ source .env

Und installieren Sie das OpenAI-Paket:

 ```bash $ pip install openai numpy

Versuchen wir, die Einbettung für die Zeichenfolge „Docker Container“ zu berechnen. Sie können dies auf der Python REPL oder als Python-Skript ausführen:

 $ python >>> import openai >>> embeddings = openai.Embedding.create(input="Docker Containers", engine="text-embedding-ada-002") >>> embeddings JSON: { "data": [ { "embedding": [ -0.00530336843803525, 0.0013223182177171111, ... 1533 more items ..., -0.015645816922187805 ], "index": 0, "object": "embedding" } ], "model": "text-embedding-ada-002-v2", "object": "list", "usage": { "prompt_tokens": 2, "total_tokens": 2 } }

Wie Sie sehen können, antwortet das Modell von OpenAI mit einer embedding mit 1536 Elementen – der Vektorgröße für text-embedding-ada-002.

Lagerung der Einbettungen in Pinecone

Es stehen zwar mehrere Vektordatenbank-Engines zur Auswahl, wie zum Beispiel Chroma , das Open Source ist, aber ich habe mich für Pinecone entschieden, weil es eine verwaltete Datenbank mit einem kostenlosen Kontingent ist, was die Sache einfacher macht. Ihr Starter-Plan ist mehr als in der Lage, alle Daten zu verarbeiten, die ich benötige.


Nachdem ich mein Pinecone-Konto erstellt und meinen API-Schlüssel und meine Umgebung abgerufen habe, füge ich beide Werte zu meiner .env Datei hinzu.

Screenshot der Pinecone-API-Schlüsselgenerierung

Jetzt sollte .env meine Pinecone- und OpenAI-Geheimnisse enthalten.

 export OPENAI_API_KEY=YOUR_API_KEY # Pinecone secrets export PINECONE_API_KEY=YOUR_API_KEY export PINECONE_ENVIRONMENT=YOUR_PINECONE_DATACENTER

Dann installiere ich den Pinecone-Client für Python:

 $ pip install pinecone-client

Ich muss eine Datenbank initialisieren; Dies sind die Inhalte des db_create.py -Skripts:

 # db_create.py import pinecone import openai import os index_name = "semaphore" embed_model = "text-embedding-ada-002" api_key = os.getenv("PINECONE_API_KEY") env = os.getenv("PINECONE_ENVIRONMENT") pinecone.init(api_key=api_key, environment=env) embedding = openai.Embedding.create( input=[ "Sample document text goes here", "there will be several phrases in each batch" ], engine=embed_model ) if index_name not in pinecone.list_indexes(): print("Creating pinecone index: " + index_name) pinecone.create_index( index_name, dimension=len(embedding['data'][0]['embedding']), metric='cosine', metadata_config={'indexed': ['source', 'id']} )

Das Erstellen der Datenbank durch das Skript kann einige Minuten dauern.

 $ python db_create.py

Als nächstes werde ich das Tiktoken- Paket installieren. Ich werde damit berechnen, wie viele Token die Quelldokumente haben. Dies ist wichtig, da das Einbettungsmodell nur bis zu 8191 Token verarbeiten kann.

 $ pip install tiktoken

Während wir Pakete installieren, installieren wir auch tqdm , um einen gut aussehenden Fortschrittsbalken zu erstellen.

 $ pip install tqdm

Jetzt muss ich die Dokumente in die Datenbank hochladen. Das Skript dafür heißt index_docs.py . Beginnen wir mit dem Importieren der erforderlichen Module und der Definition einiger Konstanten:

 # index_docs.py # Pinecone db name and upload batch size index_name = 'semaphore' upsert_batch_size = 20 # OpenAI embedding and tokenizer models embed_model = "text-embedding-ada-002" encoding_model = "cl100k_base" max_tokens_model = 8191

Als nächstes benötigen wir eine Funktion zum Zählen von Token. Auf der OpenAI-Seite gibt es ein Beispiel für einen Token-Zähler :

 import tiktoken def num_tokens_from_string(string: str) -> int: """Returns the number of tokens in a text string.""" encoding = tiktoken.get_encoding(encoding_model) num_tokens = len(encoding.encode(string)) return num_tokens

Abschließend benötige ich einige Filterfunktionen, um das Originaldokument in brauchbare Beispiele umzuwandeln. Die meisten Beispiele in der Dokumentation liegen zwischen Code-Fences, daher extrahiere ich einfach den gesamten YAML-Code aus jeder Datei:

 import re def extract_yaml(text: str) -> str: """Returns list with all the YAML code blocks found in text.""" matches = [m.group(1) for m in re.finditer("```yaml([\w\W]*?)```", text)] return matches

Ich bin mit den Funktionen fertig. Als nächstes werden die Dateien in den Speicher geladen und die Beispiele extrahiert:

 from tqdm import tqdm import sys import os import pathlib repo_path = sys.argv[1] repo_path = os.path.abspath(repo_path) repo = pathlib.Path(repo_path) markdown_files = list(repo.glob("**/*.md")) + list( repo.glob("**/*.mdx") ) print(f"Extracting YAML from Markdown files in {repo_path}") new_data = [] for i in tqdm(range(0, len(markdown_files))): markdown_file = markdown_files[i] with open(markdown_file, "r") as f: relative_path = markdown_file.relative_to(repo_path) text = str(f.read()) if text == '': continue yamls = extract_yaml(text) j = 0 for y in yamls: j = j+1 new_data.append({ "source": str(relative_path), "text": y, "id": f"github.com/semaphore/docs/{relative_path}[{j}]" })

Zu diesem Zeitpunkt sollten alle YAMLs in der Liste new_data gespeichert sein. Der letzte Schritt besteht darin, die Einbettungen in Pinecone hochzuladen.

 import pinecone import openai api_key = os.getenv("PINECONE_API_KEY") env = os.getenv("PINECONE_ENVIRONMENT") pinecone.init(api_key=api_key, enviroment=env) index = pinecone.Index(index_name) print(f"Creating embeddings and uploading vectors to database") for i in tqdm(range(0, len(new_data), upsert_batch_size)): i_end = min(len(new_data), i+upsert_batch_size) meta_batch = new_data[i:i_end] ids_batch = [x['id'] for x in meta_batch] texts = [x['text'] for x in meta_batch] embedding = openai.Embedding.create(input=texts, engine=embed_model) embeds = [record['embedding'] for record in embedding['data']] # clean metadata before upserting meta_batch = [{ 'id': x['id'], 'text': x['text'], 'source': x['source'] } for x in meta_batch] to_upsert = list(zip(ids_batch, embeds, meta_batch)) index.upsert(vectors=to_upsert)

Als Referenz finden Sie die vollständige Datei index_docs.py im Demo-Repository

Lassen Sie uns das Indexskript ausführen, um die Datenbankeinrichtung abzuschließen:

 $ git clone https://github.com/semaphoreci/docs.git /tmp/docs $ source .env $ python index_docs.py /tmp/docs

Testen der Datenbank

Das Pinecone-Dashboard sollte Vektoren in der Datenbank anzeigen.

Screenshot des Pinecone-Dashboards, das die Datenbank mit insgesamt 79 Vektoren zeigt

Wir können die Datenbank mit dem folgenden Code abfragen, den Sie als Skript oder direkt in der Python REPL ausführen können:

 $ python >>> import os >>> import pinecone >>> import openai # Compute embeddings for string "Docker Container" >>> embeddings = openai.Embedding.create(input="Docker Containers", engine="text-embedding-ada-002") # Connect to database >>> index_name = "semaphore" >>> api_key = os.getenv("PINECONE_API_KEY") >>> env = os.getenv("PINECONE_ENVIRONMENT") >>> pinecone.init(api_key=api_key, environment=env) >>> index = pinecone.Index(index_name) # Query database >>> matches = index.query(embeddings['data'][0]['embedding'], top_k=1, include_metadata=True) >>> matches['matches'][0] {'id': 'github.com/semaphore/docs/docs/ci-cd-environment/docker-authentication.md[3]', 'metadata': {'id': 'github.com/semaphore/docs/docs/ci-cd-environment/docker-authentication.md[3]', 'source': 'docs/ci-cd-environment/docker-authentication.md', 'text': '\n' '# .semaphore/semaphore.yml\n' 'version: v1.0\n' 'name: Using a Docker image\n' 'agent:\n' ' machine:\n' ' type: e1-standard-2\n' ' os_image: ubuntu1804\n' '\n' 'blocks:\n' ' - name: Run container from Docker Hub\n' ' task:\n' ' jobs:\n' ' - name: Authenticate docker pull\n' ' commands:\n' ' - checkout\n' ' - echo $DOCKERHUB_PASSWORD | docker login ' '--username "$DOCKERHUB_USERNAME" --password-stdin\n' ' - docker pull /\n' ' - docker images\n' ' - docker run /\n' ' secrets:\n' ' - name: docker-hub\n'}, 'score': 0.796259582, 'values': []}

Wie Sie sehen können, handelt es sich bei der ersten Übereinstimmung um die YAML für eine Semaphore-Pipeline, die ein Docker-Image abruft und ausführt. Das ist ein guter Anfang, da es für unsere Suchzeichenfolge „Docker Containers“ relevant ist.

Den Bot bauen

Wir haben die Daten und wissen, wie man sie abfragt. Lassen Sie es uns im Bot umsetzen.

Die Schritte zur Verarbeitung der Eingabeaufforderung sind:

  1. Befolgen Sie die Aufforderung des Benutzers.
  2. Berechnen Sie seinen Vektor.
  3. Rufen Sie relevanten Kontext aus der Datenbank ab.
  4. Senden Sie die Eingabeaufforderung des Benutzers zusammen mit dem Kontext an GPT-3.
  5. Leiten Sie die Antwort des Modells an den Benutzer weiter.

Diagramm des Datenflusses für den Bot. Auf der linken Seite wird die Eingabeaufforderung des Benutzers eingegeben, die vom einbettenden neuronalen Netzwerk verarbeitet und dann an die Kontextdatenbank gesendet wird. Die Suche ergibt relevanten Text, der an das GPT-3-Modell gesendet wird. Die Ausgabe des Modells wird als endgültige Antwort an den Benutzer gesendet. Wie üblich definiere ich zunächst einige Konstanten in complete.py , dem Hauptskript des Bots:

 # complete.py # Pinecone database name, number of matched to retrieve # cutoff similarity score, and how much tokens as context index_name = 'semaphore' context_cap_per_query = 30 match_min_score = 0.75 context_tokens_per_query = 3000 # OpenAI LLM model parameters chat_engine_model = "gpt-3.5-turbo" max_tokens_model = 4096 temperature = 0.2 embed_model = "text-embedding-ada-002" encoding_model_messages = "gpt-3.5-turbo-0301" encoding_model_strings = "cl100k_base" import pinecone import os # Connect with Pinecone db and index api_key = os.getenv("PINECONE_API_KEY") env = os.getenv("PINECONE_ENVIRONMENT") pinecone.init(api_key=api_key, environment=env) index = pinecone.Index(index_name)

Als Nächstes füge ich Funktionen zum Zählen von Token hinzu, wie in den OpenAI-Beispielen gezeigt. Die erste Funktion zählt Token in einer Zeichenfolge, während die zweite Funktion Token in Nachrichten zählt. Wir werden die Nachrichten gleich im Detail sehen. Sagen wir zunächst einfach, dass es sich um eine Struktur handelt, die den Stand des Gesprächs im Gedächtnis behält.

 import tiktoken def num_tokens_from_string(string: str) -> int: """Returns the number of tokens in a text string.""" encoding = tiktoken.get_encoding(encoding_model_strings) num_tokens = len(encoding.encode(string)) return num_tokens def num_tokens_from_messages(messages): """Returns the number of tokens used by a list of messages. Compatible with model """ try: encoding = tiktoken.encoding_for_model(encoding_model_messages) except KeyError: encoding = tiktoken.get_encoding(encoding_model_strings) num_tokens = 0 for message in messages: num_tokens += 4 # every message follows {role/name}\n{content}\n for key, value in message.items(): num_tokens += len(encoding.encode(value)) if key == "name": # if there's a name, the role is omitted num_tokens += -1 # role is always required and always 1 token num_tokens += 2 # every reply is primed with assistant return num_tokens

Die folgende Funktion verwendet die ursprüngliche Eingabeaufforderung und die Kontextzeichenfolgen, um eine erweiterte Eingabeaufforderung für GPT-3 zurückzugeben:

 def get_prompt(query: str, context: str) -> str: """Return the prompt with query and context.""" return ( f"Create the continuous integration pipeline YAML code to fullfil the requested task.\n" + f"Below you will find some context that may help. Ignore it if it seems irrelevant.\n\n" + f"Context:\n{context}" + f"\n\nTask: {query}\n\nYAML Code:" )

Die Funktion get_message formatiert die Eingabeaufforderung in einem mit der API kompatiblen Format:

 def get_message(role: str, content: str) -> dict: """Generate a message for OpenAI API completion.""" return {"role": role, "content": content}

Es gibt drei Arten von Rollen, die die Reaktion des Modells beeinflussen:

  • Benutzer : für die ursprüngliche Eingabeaufforderung des Benutzers.
  • System : hilft beim Festlegen des Verhaltens des Assistenten. Obwohl es einige Kontroversen hinsichtlich seiner Wirksamkeit gibt, scheint es effektiver zu sein, wenn es am Ende der Nachrichtenliste gesendet wird.
  • Assistent : Stellt frühere Antworten des Modells dar. Die OpenAI-API verfügt über kein „Gedächtnis“; Stattdessen müssen wir bei jeder Interaktion die vorherigen Antworten des Modells zurücksenden, um die Konversation aufrechtzuerhalten.


Nun zum spannenden Teil. Die Funktion get_context nimmt die Eingabeaufforderung, fragt die Datenbank ab und generiert eine Kontextzeichenfolge, bis eine dieser Bedingungen erfüllt ist:

  • Der vollständige Text überschreitet context_tokens_per_query , den Platz, den ich für den Kontext reserviert habe.
  • Die Suchfunktion ruft alle angeforderten Übereinstimmungen ab.
  • Übereinstimmungen mit einem Ähnlichkeitswert unter match_min_score werden ignoriert.
 import openai def get_context(query: str, max_tokens: int) -> list: """Generate message for OpenAI model. Add context until hitting `context_token_limit` limit. Returns prompt string.""" embeddings = openai.Embedding.create( input=[query], engine=embed_model ) # search the database vectors = embeddings['data'][0]['embedding'] embeddings = index.query(vectors, top_k=context_cap_per_query, include_metadata=True) matches = embeddings['matches'] # filter and aggregate context usable_context = "" context_count = 0 for i in range(0, len(matches)): source = matches[i]['metadata']['source'] if matches[i]['score'] < match_min_score: # skip context with low similarity score continue context = matches[i]['metadata']['text'] token_count = num_tokens_from_string(usable_context + '\n---\n' + context) if token_count < context_tokens_per_query: usable_context = usable_context + '\n---\n' + context context_count = context_count + 1 print(f"Found {context_count} contexts for your query") return usable_context

Die nächste und letzte Funktion, complete , gibt die API-Anfrage an OpenAI aus und gibt die Antwort des Modells zurück.

 def complete(messages): """Query the OpenAI model. Returns the first answer. """ res = openai.ChatCompletion.create( model=chat_engine_model, messages=messages, temperature=temperature ) return res.choices[0].message.content.strip()

Das ist alles; Jetzt muss ich mich nur noch mit den Kommandozeilenargumenten befassen und die Funktionen in der richtigen Reihenfolge aufrufen:

 import sys query = sys.argv[1] context = get_context(query, context_tokens_per_query) prompt = get_prompt(query, context) # initialize messages list to send to OpenAI API messages = [] messages.append(get_message('user', prompt)) messages.append(get_message('system', 'You are a helpful assistant that writes YAML code for Semaphore continuous integration pipelines and explains them. Return YAML code inside code fences.')) if num_tokens_from_messages(messages) >= max_tokens_model: raise Exception('Model token size limit reached') print("Working on your query... ") answer = complete(messages) print("Answer:\n") print(answer) messages.append(get_message('assistant', answer))

Es ist Zeit, das Skript auszuführen und zu sehen, wie es funktioniert:

 $ python complete.py "Create a CI pipeline that builds and uploads a Docker image to Docker Hub"

Das Ergebnis ist:

 version: v1.0 name: Docker Build and Push agent: machine: type: e1-standard-2 os_image: ubuntu1804 blocks: - name: "Build and Push Docker Image" task: jobs: - name: "Docker Build and Push" commands: - checkout - docker build -t /: . - echo "$DOCKERHUB_PASSWORD" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin - docker push /: promotions: - name: Deploy to production pipeline_file: deploy-production.yml auto_promote: when: "result = 'passed' and branch = 'master'"

Das ist das erste gute Ergebnis. Das Modell hat die Syntax aus den von uns bereitgestellten Kontextbeispielen abgeleitet.

Gedanken zur Erweiterung der Fähigkeiten des Bots

Denken Sie daran, dass ich mit einem bescheidenen Ziel begonnen habe: einen Assistenten zum Schreiben von YAML-Pipelines zu erstellen. Mit umfangreicheren Inhalten in meiner Vektordatenbank kann ich den Bot verallgemeinern, um jede Frage zu Semaphore (oder einem anderen Produkt – erinnern Sie sich daran, die Dokumente in /tmp geklont zu haben?) zu beantworten.


Der Schlüssel zu guten Antworten ist – wenig überraschend – ein qualitativ hochwertiger Kontext. Allein das Hochladen jedes Dokuments in die Vektordatenbank dürfte keine guten Ergebnisse bringen. Die Kontextdatenbank sollte kuratiert, mit beschreibenden Metadaten versehen und prägnant sein. Andernfalls besteht die Gefahr, dass das Token-Kontingent in der Eingabeaufforderung mit irrelevantem Kontext gefüllt wird.


In gewisser Weise ist es also eine Kunst – und eine Menge Versuch und Irrtum –, den Bot an unsere Bedürfnisse anzupassen. Wir können mit der Kontextbeschränkung experimentieren, minderwertige Inhalte entfernen, zusammenfassen und irrelevanten Kontext herausfiltern, indem wir den Ähnlichkeitswert anpassen.

Implementierung eines richtigen Chatbots

Sie haben vielleicht bemerkt, dass mein Bot uns keine echte Konversation wie ChatGPT ermöglicht. Wir stellen eine Frage und bekommen eine Antwort.


Den Bot in einen vollwertigen Chatbot umzuwandeln, ist im Prinzip keine allzu große Herausforderung. Wir können die Konversation aufrechterhalten, indem wir bei jeder API-Anfrage frühere Antworten erneut an das Modell senden. Frühere GPT-3-Antworten werden unter der Rolle „Assistent“ zurückgesendet. Zum Beispiel:

 messages = [] while True: query = input('Type your prompt:\n') context = get_context(query, context_tokens_per_query) prompt = get_prompt(query, context) messages.append(get_message('user', prompt)) messages.append(get_message('system', 'You are a helpful assistant that writes YAML code for Semaphore continuous integration pipelines and explains them. Return YAML code inside code fences.')) if num_tokens_from_messages(messages) >= max_tokens_model: raise Exception('Model token size limit reached') print("Working on your query... ") answer = complete(messages) print("Answer:\n") print(answer) # remove system message and append model's answer messages.pop() messages.append(get_message('assistant', answer))

Leider ist diese Implementierung eher rudimentär. Es werden keine erweiterten Konversationen unterstützt, da die Tokenanzahl mit jeder Interaktion steigt. Schon bald werden wir das 4096-Token-Limit für GPT-3 erreichen, was einen weiteren Dialog verhindert.


Wir müssen also eine Möglichkeit finden, die Anfrage innerhalb der Token-Grenzen zu halten. Es folgen einige Strategien:

  • Ältere Nachrichten löschen. Dies ist zwar die einfachste Lösung, beschränkt jedoch den „Speicher“ der Konversation auf die aktuellsten Nachrichten.
  • Fassen Sie frühere Nachrichten zusammen. Wir können „Ask the model“ verwenden, um frühere Nachrichten zu verdichten und sie durch die ursprünglichen Fragen und Antworten zu ersetzen. Obwohl dieser Ansatz die Kosten und die Verzögerung zwischen Abfragen erhöht, kann er zu besseren Ergebnissen führen als das einfache Löschen früherer Nachrichten.
  • Legen Sie eine strenge Grenze für die Anzahl der Interaktionen fest.
  • Warten Sie auf die allgemeine Verfügbarkeit der GPT-4-API, die nicht nur intelligenter ist, sondern auch über die doppelte Token-Kapazität verfügt.
  • Verwenden Sie ein neueres Modell wie „gpt-3.5-turbo-16k“, das bis zu 16.000 Token verarbeiten kann .

Abschluss

Die Antworten des Bots können durch Worteinbettungen und eine gute Kontextdatenbank verbessert werden. Um dies zu erreichen, benötigen wir eine qualitativ hochwertige Dokumentation. Die Entwicklung eines Bots, der scheinbar ein Verständnis für die Thematik besitzt, ist mit einem erheblichen Aufwand an Versuch und Irrtum verbunden.


Ich hoffe, dass diese eingehende Untersuchung der Worteinbettungen und großen Sprachmodelle Ihnen dabei hilft, einen leistungsfähigeren Bot zu erstellen, der auf Ihre Anforderungen zugeschnitten ist.


Viel Spaß beim Bauen!


Auch hier veröffentlicht.