paint-brush
Embeddings de mots : la sauce secrète pour donner du contexte à votre chatbot pour de meilleures réponsespar@tomfernblog
2,991 lectures
2,991 lectures

Embeddings de mots : la sauce secrète pour donner du contexte à votre chatbot pour de meilleures réponses

par Tomas Fernandez18m2023/07/26
Read on Terminal Reader

Trop long; Pour lire

Apprenez à créer un bot expert à l'aide d'incorporations de mots et de ChatGPT. Tirez parti de la puissance des vecteurs de mots pour améliorer les réponses de votre chatbot.
featured image - Embeddings de mots : la sauce secrète pour donner du contexte à votre chatbot pour de meilleures réponses
Tomas Fernandez HackerNoon profile picture
0-item
1-item

Il ne fait aucun doute que le ChatGPT d'OpenAI est exceptionnellement intelligent - il a réussi le test du barreau de l'avocat , il possède des connaissances proches de celles d'un médecin et certains tests ont montré son QI à 155 . Cependant, il a tendance à fabriquer des informations au lieu de concéder l'ignorance. Cette tendance, associée au fait que ses connaissances cessent en 2021, pose des défis dans la création de produits spécialisés utilisant l'API GPT.


Comment pouvons-nous surmonter ces obstacles ? Comment pouvons-nous transmettre de nouvelles connaissances à un modèle comme GPT-3 ? Mon objectif est de répondre à ces questions en construisant un bot de réponse aux questions utilisant Python, l'API OpenAI et des incorporations de mots.

Ce que je vais construire

J'ai l'intention de créer un bot qui génère des pipelines d'intégration continue à partir d'une invite, qui, comme vous le savez peut-être, sont formatés avec YAML dans Semaphore CI/CD.


Voici un exemple du bot en action :

Capture d'écran du programme en cours d'exécution. A l'écran, la commande est exécutée : python query.py "Create a CI pipeline that builds and uploads a Docker image to Docker Hub", et le programme imprime YAML correspondant à un pipeline CI qui effectue l'action demandée. Capture d'écran du programme en cours d'exécution. Sur l'écran, la commande python query.py "Create a CI pipeline that builds and uploads a Docker image to Docker Hub" est exécutée, et le programme imprime YAML correspondant à un pipeline CI qui effectue l'action demandée.


Dans l'esprit de projets comme DocsGPT , My AskAI et Libraria , je prévois "d'enseigner" le modèle GPT-3 sur Semaphore et comment générer des fichiers de configuration de pipeline. J'y parviendrai en m'appuyant sur la documentation existante .


Je ne supposerai pas une connaissance préalable de la construction de bots et maintiendrai un code propre afin que vous puissiez l'adapter à vos besoins.

Conditions préalables

Vous n'avez pas besoin d'expérience dans le codage d'un bot ou de connaissances en réseaux de neurones pour suivre ce tutoriel. Cependant, vous aurez besoin de :


Mais ChatGPT ne peut pas apprendre, n'est-ce pas ?

ChatGPT, ou plus précisément, GPT-3 et GPT-4, les grands modèles de langage (LLM) qui les alimentent, ont été formés sur un ensemble de données massif avec une date limite autour de septembre 2021.


Essentiellement, GPT-3 en sait très peu sur les événements au-delà de cette date. Nous pouvons le vérifier avec une simple invite :

Capture d'écran de ChatGPT ChatGPT ne sait pas qui a remporté la Coupe du monde en 2022.


Alors que certains modèles OpenAI peuvent subir des ajustements , les modèles les plus avancés, tels que ceux qui nous intéressaient, ne le peuvent pas ; nous ne pouvons pas augmenter leurs données de formation.


Comment pouvons-nous obtenir des réponses de GPT-3 au-delà de ses données d'entraînement ? Une méthode consiste à exploiter ses capacités de compréhension de texte ; en améliorant l'invite avec un contexte pertinent, nous pouvons probablement obtenir la bonne réponse.


Dans l'exemple ci-dessous, je fournis le contexte du site officiel de la FIFA , et la réponse diffère considérablement :

Deuxième tentative de réponse à la question Avec le contexte fourni, ChatGPT peut répondre avec précision.


Nous pouvons en déduire que le modèle peut répondre à n'importe quelle invite s'il dispose d'un contexte suffisamment pertinent. La question demeure : comment pouvons-nous savoir ce qui est pertinent à partir d'une invite arbitraire ? Pour résoudre ce problème, nous devons explorer ce que sont les incorporations de mots .

Que sont les incorporations de mots ?

Dans le contexte des modèles de langage, une incorporation est un moyen de représenter des mots, des phrases ou des documents entiers sous forme de vecteurs ou de listes de nombres.


Pour calculer les intégrations, nous aurons besoin d'un réseau de neurones tel que word2vec ou text-embedding-ada-002 . Ces réseaux ont été formés sur des quantités massives de texte et peuvent trouver des relations entre les mots en analysant les fréquences avec lesquelles des modèles spécifiques apparaissent dans les données de formation.


Disons que nous avons les mots suivants :

  • Chat
  • Chien
  • Balle
  • Loger


Imaginez que nous utilisions l'un de ces réseaux d'intégration pour calculer les vecteurs de chaque mot. Par exemple:

Mot

Vecteur

Contexte

Chat

[0.1, 0.2, 0.3, 0.4, 0.5]

Animaux, objets, petites choses

Chien

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

Animaux, objets, grandes choses

Balle

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

Objets, jouets, petites choses

Loger

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

Bâtiments, maisons, grandes choses

Une fois que nous avons les vecteurs pour chaque mot, nous pouvons les utiliser pour représenter le sens du texte. Par exemple, la phrase "Le chat a chassé la balle" peut être représentée par le vecteur [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, 1,5]. Ce vecteur représente une phrase qui parle d'un animal poursuivant un objet.


Les incorporations de mots peuvent être visualisées comme des espaces multidimensionnels où des mots ou des phrases ayant des significations similaires sont proches les uns des autres. Nous pouvons calculer la "distance" entre les vecteurs pour trouver des significations similaires pour tout texte d'entrée.

Trois représentations tridimensionnelles de vecteurs. Le premier est étiqueté 'Male-Female' et a des points de données homme-femme et roi-reine, le second est étiqueté 'Verb-Tense' et a des verbes comme walking-walked swimming-swam. La dernière est labellisée 'Pays-Capitale' et possède plusieurs capitales liées à leurs pays Représentation 3D des plongements sous forme d'espaces vectoriels. En réalité, ces espaces peuvent avoir des centaines ou des milliers de dimensions. Source : Découvrez le multi-outil d'IA : intégrations vectorielles


Les mathématiques réelles derrière tout cela dépassent la portée de cet article. Cependant, le point clé à retenir est que les opérations vectorielles nous permettent de manipuler ou de déterminer le sens à l'aide des mathématiques . Prenez le vecteur qui représente le mot « reine », soustrayez-en le vecteur « femme » et ajoutez le vecteur « homme ». Le résultat devrait être un vecteur proche de "king". Si nous ajoutons "fils", nous devrions nous rapprocher de "prince".

Intégrer des réseaux de neurones avec des jetons

Jusqu'à présent, nous avons discuté de l'intégration de réseaux de neurones prenant des mots comme entrées et des nombres comme sorties. Cependant, de nombreux réseaux modernes sont passés du traitement des mots au traitement des jetons.


Un jeton est la plus petite unité de texte pouvant être traitée par le modèle. Les jetons peuvent être des mots, des caractères, des signes de ponctuation, des symboles ou des parties de mots.


Nous pouvons voir comment les mots sont convertis en jetons en expérimentant le tokenizer en ligne OpenAI , qui utilise le Byte-Pair Encoding (BPE) pour convertir le texte en jetons et représenter chacun par un nombre :

Capture d'écran du tokenizer OpenAI. Du texte a été saisi et chaque jeton est représenté par des couleurs différentes, ce qui nous permet de voir comment les mots sont mappés aux jetons. Le texte se lit comme suit : Derrière tout modèle d'intégration, il existe un réseau de neurones qui convertit le texte d'entrée en vecteurs. Chaque type de modèle d'intégration a des capacités et des vitesses différentes. Word2vec, par exemple, prend des mots et produit des vecteurs dans la plage de 100 à 300 dimensions. Il existe souvent une relation de 1 à 1 entre les jetons et les mots. La plupart des jetons incluent le mot et un espace au début. Cependant, il existe des cas particuliers tels que "embedding", qui se compose de deux jetons, "embed" et "ding", ou "capabilities", qui se compose de quatre jetons. Si vous cliquez sur "ID de jeton", vous pouvez voir la représentation numérique du modèle de chaque jeton.

Concevoir un bot plus intelligent à l'aide d'incorporations

Maintenant que nous comprenons ce que sont les intégrations, la question suivante est : comment peuvent-elles nous aider à créer un bot plus intelligent ?


Considérons d'abord ce qui se passe lorsque nous utilisons directement l'API GPT-3. L'utilisateur émet une invite et le modèle répond au mieux de ses capacités.

Diagramme montrant l'interaction entre l'utilisateur et GPT-3. L'utilisateur envoie une invite, le modèle répond.


Cependant, lorsque nous ajoutons du contexte à l'équation, les choses changent. Par exemple, lorsque j'ai interrogé ChatGPT sur le vainqueur de la Coupe du monde après avoir fourni le contexte, cela a fait toute la différence.


Ainsi, le plan pour construire un bot plus intelligent est le suivant :

  1. Intercepter l'invite de l'utilisateur.
  2. Calculez les plongements pour cette invite, ce qui donne un vecteur.
  3. Recherchez dans une base de données les documents proches du vecteur, car ils doivent être sémantiquement pertinents par rapport à l'invite initiale.
  4. Envoyez l'invite d'origine à GPT-3, ainsi que tout contexte pertinent.
  5. Transférez la réponse de GPT-3 à l'utilisateur.

Une implémentation plus complexe d'un bot. L'utilisateur envoie l'invite à une application chatbot, qui recherche une base de données contextuelle et l'utilise pour enrichir l'invite. L'invite est envoyée à GPT-3 et sa réponse est transmise à l'utilisateur.


Commençons comme la plupart des projets, par la conception de la base de données.

Création d'une base de connaissances avec des incorporations

Notre base de données de contexte doit inclure la documentation originale et leurs vecteurs respectifs. En principe, nous pouvons utiliser n'importe quel type de base de données pour cette tâche, mais une base de données vectorielle est l'outil optimal pour le travail.


Les bases de données vectorielles sont des bases de données spécialisées conçues pour stocker et récupérer des données vectorielles de grande dimension. Au lieu d'employer un langage de requête tel que SQL pour la recherche, nous fournissons un vecteur et demandons les N voisins les plus proches.


Pour générer les vecteurs, nous utiliserons text-embedding-ada-002 d'OpenAI, car c'est le modèle le plus rapide et le plus rentable qu'ils proposent. Le modèle convertit le texte d'entrée en jetons et utilise un mécanisme d'attention appelé Transformer pour apprendre leurs relations. La sortie de ce réseau de neurones est constituée de vecteurs représentant la signification du texte.

Schéma illustrant le processus de tokenisation. Un document est tokenisé puis envoyé à un réseau neuronal d'intégration. La sortie du réseau est un vecteur.


Pour créer une base de données de contexte, je vais :

  1. Rassemblez toute la documentation source.
  2. Filtrez les documents non pertinents.
  3. Calculez les plongements pour chaque document.
  4. Stockez les vecteurs, le texte original et toute autre métadonnée pertinente dans la base de données.

Schéma illustrant le processus de stockage des données dans la base de données contextuelle. Le document source est envoyé au réseau neuronal d'intégration. La base de données stocke le vecteur avec le texte d'origine.

Conversion de documents en vecteurs

Tout d'abord, je dois initialiser un fichier d'environnement avec la clé API OpenAI. Ce fichier ne doit jamais être validé pour le contrôle de version, car la clé API est privée et liée à votre compte.

 export OPENAI_API_KEY=YOUR_API_KEY

Ensuite, je vais créer un virtualenv pour mon application Python :

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

Et installez le package OpenAI :

 ```bash $ pip install openai numpy

Essayons de calculer l'intégration de la chaîne "Docker Container". Vous pouvez l'exécuter sur le REPL Python ou en tant que script Python :

 $ 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 } }

Comme vous pouvez le voir, le modèle d'OpenAI répond avec une liste embedding contenant 1536 éléments - la taille du vecteur pour text-embedding-ada-002.

Stockage des plongements dans Pinecone

Bien qu'il existe plusieurs moteurs de base de données vectorielles parmi lesquels choisir, comme Chroma qui est open-source, j'ai choisi Pinecone car c'est une base de données gérée avec un niveau gratuit, ce qui simplifie les choses. Leur plan Starter est plus que capable de gérer toutes les données dont j'aurai besoin.


Après avoir créé mon compte Pinecone et récupéré ma clé API et mon environnement, j'ajoute les deux valeurs à mon fichier .env .

Capture d'écran de génération de clé API Pinecone

Maintenant .env devrait contenir mes secrets Pinecone et OpenAI.

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

Ensuite, j'installe le client Pinecone pour Python :

 $ pip install pinecone-client

J'ai besoin d'initialiser une base de données ; voici le contenu du script db_create.py :

 # 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']} )

Le script peut prendre quelques minutes pour créer la base de données.

 $ python db_create.py

Ensuite, je vais installer le package tiktoken . Je vais l'utiliser pour calculer le nombre de jetons des documents source. Ceci est important car le modèle d'intégration ne peut gérer que jusqu'à 8191 jetons.

 $ pip install tiktoken

Lors de l'installation des packages, installons également tqdm pour produire une belle barre de progression.

 $ pip install tqdm

Maintenant, je dois télécharger les documents dans la base de données. Le script pour cela s'appellera index_docs.py . Commençons par importer les modules requis et définissons quelques constantes :

 # 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

Ensuite, nous aurons besoin d'une fonction pour compter les jetons. Il y a un exemple de compteur de jetons sur la page OpenAI :

 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

Enfin, j'aurai besoin de quelques fonctions de filtrage pour convertir le document original en exemples utilisables. La plupart des exemples de la documentation se situent entre les clôtures de code, je vais donc extraire tout le code YAML de chaque fichier :

 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

J'en ai fini avec les fonctions. Ensuite, cela va charger les fichiers en mémoire et extraire les exemples :

 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}]" })

À ce stade, tous les YAML doivent être stockés dans la liste new_data . La dernière étape consiste à télécharger les intégrations dans Pinecone.

 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)

À titre de référence, vous pouvez trouver le fichier index_docs.py complet dans le référentiel de démonstration

Exécutons le script d'index pour terminer la configuration de la base de données :

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

Tester la base de données

Le tableau de bord Pinecone doit afficher les vecteurs dans la base de données.

Capture d'écran du tableau de bord Pinecone montrant la base de données avec un total de 79 vecteurs

Nous pouvons interroger la base de données avec le code suivant, que vous pouvez exécuter en tant que script ou directement dans le REPL Python :

 $ 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': []}

Comme vous pouvez le voir, la première correspondance est le YAML pour un pipeline Semaphore qui extrait une image Docker et l'exécute. C'est un bon début car il est pertinent pour notre chaîne de recherche "Docker Containers".

Construire le bot

Nous avons les données et nous savons comment les interroger. Mettons-le au travail dans le bot.

Les étapes de traitement de l'invite sont :

  1. Prenez l'invite de l'utilisateur.
  2. Calculez son vecteur.
  3. Récupérer le contexte pertinent de la base de données.
  4. Envoyez l'invite de l'utilisateur avec le contexte à GPT-3.
  5. Transférez la réponse du modèle à l'utilisateur.

Schéma du flux de données pour le bot. Sur la gauche, l'invite de l'utilisateur entre, qui est traitée par le réseau neuronal d'intégration, puis envoyée à la base de données contextuelle. La recherche donne un texte pertinent qui est envoyé au modèle GPT-3. La sortie du modèle est envoyée à l'utilisateur comme réponse finale. Comme d'habitude, je commencerai par définir quelques constantes dans complete.py , le script principal du bot :

 # 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)

Ensuite, j'ajouterai des fonctions pour compter les jetons comme indiqué dans les exemples OpenAI . La première fonction compte les jetons dans une chaîne, tandis que la seconde compte les jetons dans les messages. Nous verrons les messages en détail dans un instant. Pour l'instant, disons simplement que c'est une structure qui garde en mémoire l'état de la conversation.

 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

La fonction suivante utilise l'invite d'origine et les chaînes de contexte pour renvoyer une invite enrichie pour GPT-3 :

 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:" )

La fonction get_message formate l'invite dans un format compatible avec l'API :

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

Il existe trois types de rôles qui affectent la façon dont le modèle réagit :

  • Utilisateur : pour l'invite d'origine de l'utilisateur.
  • Système : permet de définir le comportement de l'assistant. Bien qu'il existe une certaine controverse quant à son efficacité, il semble être plus efficace lorsqu'il est envoyé à la fin de la liste des messages.
  • Assistant : représente les réponses passées du modèle. L'API OpenAI n'a pas de "mémoire" ; à la place, nous devons renvoyer les réponses précédentes du modèle lors de chaque interaction pour maintenir la conversation.


Maintenant, pour la partie engageante. La fonction get_context prend l'invite, interroge la base de données et génère une chaîne de contexte jusqu'à ce que l'une de ces conditions soit remplie :

  • Le texte complet dépasse context_tokens_per_query , l'espace que j'ai réservé pour le contexte.
  • La fonction de recherche récupère toutes les correspondances demandées.
  • Les correspondances dont le score de similarité est inférieur match_min_score sont ignorées.
 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

La fonction suivante et finale, complete , envoie la requête API à OpenAI et renvoie la réponse du modèle.

 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()

C'est tout; maintenant, je n'ai plus qu'à gérer les arguments de la ligne de commande et appeler les fonctions dans le bon ordre :

 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))

Il est temps d'exécuter le script et de voir comment il se comporte :

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

Le résultat est:

 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'"

C'est le premier bon résultat. Le modèle a déduit la syntaxe des exemples de contexte que nous avons fournis.

Réflexions sur l'extension des capacités du bot

Rappelez-vous que j'ai commencé avec un objectif modeste : créer un assistant pour écrire des pipelines YAML. Avec un contenu plus riche dans ma base de données vectorielle, je peux généraliser le bot pour répondre à toute question sur Semaphore (ou tout autre produit — rappelez-vous de cloner les docs dans /tmp ?).


La clé pour obtenir de bonnes réponses est — sans surprise — un contexte de qualité. Il est peu probable que le simple téléchargement de chaque document dans la base de données vectorielle donne de bons résultats. La base de données contextuelle doit être organisée, étiquetée avec des métadonnées descriptives et être concise. Sinon, nous risquons de remplir le quota de jetons dans l'invite avec un contexte non pertinent.


Donc, dans un sens, il y a un art - et beaucoup d'essais et d'erreurs - impliqués dans le réglage fin du bot pour répondre à nos besoins. Nous pouvons expérimenter la limite de contexte, supprimer le contenu de mauvaise qualité, résumer et filtrer le contexte non pertinent en ajustant le score de similarité.

Mettre en place un bon chatbot

Vous avez peut-être remarqué que mon bot ne nous permet pas d'avoir une conversation réelle comme ChatGPT. Nous posons une question et obtenons une réponse.


Convertir le bot en un chatbot à part entière n'est, en principe, pas trop difficile. Nous pouvons maintenir la conversation en renvoyant les réponses précédentes au modèle avec chaque requête API. Les réponses GPT-3 précédentes sont renvoyées sous le rôle "assistant". Par exemple:

 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))

Malheureusement, cette implémentation est assez rudimentaire. Il ne prendra pas en charge les conversations prolongées car le nombre de jetons augmente à chaque interaction. Bientôt, nous atteindrons la limite de 4096 jetons pour GPT-3, empêchant la poursuite du dialogue.


Nous devons donc trouver un moyen de maintenir la demande dans les limites des jetons. Quelques stratégies suivent :

  • Supprimer les anciens messages. Bien qu'il s'agisse de la solution la plus simple, elle limite la "mémoire" de la conversation aux seuls messages les plus récents.
  • Résumez les messages précédents. Nous pouvons utiliser "Demandez au modèle" pour condenser les messages précédents et les remplacer par les questions et réponses d'origine. Bien que cette approche augmente le coût et le décalage entre les requêtes, elle peut produire des résultats supérieurs par rapport à la simple suppression des messages passés.
  • Fixez une limite stricte au nombre d'interactions.
  • Attendez la disponibilité générale de l'API GPT-4, qui est non seulement plus intelligente, mais a une double capacité de jeton.
  • Utilisez un modèle plus récent comme "gpt-3.5-turbo-16k" qui peut gérer jusqu'à 16k jetons .

Conclusion

Améliorer les réponses du bot est possible avec des incorporations de mots et une bonne base de données de contexte. Pour y parvenir, nous avons besoin d'une documentation de bonne qualité. Il y a une quantité importante d'essais et d'erreurs impliqués dans le développement d'un bot qui possède apparemment une compréhension du sujet.


J'espère que cette exploration approfondie des incorporations de mots et des grands modèles de langage vous aidera à créer un bot plus puissant, adapté à vos besoins.


Bonne construction !


Également publié ici .