paint-brush
Incrustaciones de palabras: la salsa secreta para darle contexto a su ChatBot para obtener mejores respuestaspor@tomfernblog
2,991 lecturas
2,991 lecturas

Incrustaciones de palabras: la salsa secreta para darle contexto a su ChatBot para obtener mejores respuestas

por Tomas Fernandez18m2023/07/26
Read on Terminal Reader

Demasiado Largo; Para Leer

Aprenda a crear un bot experto mediante incrustaciones de palabras y ChatGPT. Aproveche el poder de los vectores de palabras para mejorar las respuestas de su chatbot.
featured image - Incrustaciones de palabras: la salsa secreta para darle contexto a su ChatBot para obtener mejores respuestas
Tomas Fernandez HackerNoon profile picture
0-item
1-item

No hay duda de que ChatGPT de OpenAI es excepcionalmente inteligente: ha superado la prueba de la barra de abogados , posee conocimientos similares a los de un médico y algunas pruebas han registrado su coeficiente intelectual en 155 . Sin embargo, tiende a fabricar información en lugar de conceder ignorancia. Esta tendencia, junto con el hecho de que su conocimiento cesa en 2021, plantea desafíos en la creación de productos especializados utilizando la API de GPT.


¿Cómo podemos superar estos obstáculos? ¿Cómo podemos impartir nuevos conocimientos a un modelo como GPT-3? Mi objetivo es abordar estas preguntas mediante la construcción de un bot de respuesta a preguntas que emplee Python, la API de OpenAI e incrustaciones de palabras.

Lo que estaré construyendo

Tengo la intención de crear un bot que genere canalizaciones de integración continua a partir de un indicador que, como sabrá, están formateadas con YAML en Semaphore CI/CD.


Aquí hay un ejemplo del bot en acción:

Captura de pantalla del programa en ejecución. En la pantalla, se ejecuta el comando: python query.py "Crear una tubería de CI que cree y cargue una imagen de Docker en Docker Hub", y el programa imprime YAML correspondiente a una tubería de CI que realiza la acción solicitada. Captura de pantalla del programa en ejecución. En la pantalla, se ejecuta el comando python query.py "Create a CI pipeline that builds and uploads a Docker image to Docker Hub" y el programa imprime YAML correspondiente a una canalización de CI que realiza la acción solicitada.


En el espíritu de proyectos como DocsGPT , My AskAI y Libraria , planeo "enseñar" al modelo GPT-3 sobre Semaphore y cómo generar archivos de configuración de canalización. Lo lograré aprovechando la documentación existente .


No asumiré un conocimiento previo de la creación de bots y mantendré un código limpio para que pueda adaptarlo a sus requisitos.

requisitos previos

No necesita experiencia en la codificación de un bot o conocimiento de redes neuronales para seguir este tutorial. Sin embargo, necesitará:


Pero ChatGPT no puede aprender, ¿verdad?

ChatGPT, o más exactamente, GPT-3 y GPT-4, los modelos de lenguaje grande (LLM) que los impulsan, se entrenaron en un conjunto de datos masivo con una fecha límite alrededor de septiembre de 2021.


En esencia, GPT-3 sabe muy poco sobre eventos posteriores a esa fecha. Podemos verificar esto con un simple aviso:

Captura de pantalla de ChatGPT ChatGPT no sabe quién ganó la Copa del Mundo en 2022.


Si bien algunos modelos de OpenAI pueden someterse a ajustes finos , los modelos más avanzados, como los que estaban interesados, no pueden; no podemos aumentar sus datos de entrenamiento.


¿Cómo podemos obtener respuestas de GPT-3 más allá de sus datos de entrenamiento? Un método consiste en explotar sus habilidades de comprensión de texto; al mejorar el mensaje con el contexto relevante, es probable que podamos obtener la respuesta correcta.


En el siguiente ejemplo, proporciono el contexto del sitio oficial de la FIFA y la respuesta difiere significativamente:

Segundo intento de respuesta a la pregunta. Con el contexto proporcionado, ChatGPT puede responder con precisión.


Podemos deducir que el modelo puede responder a cualquier mensaje si se le da suficiente contexto relevante. La pregunta sigue siendo: ¿cómo podemos saber qué es relevante dada una indicación arbitraria? Para abordar esto, necesitamos explorar qué son las incrustaciones de palabras .

¿Qué son las incrustaciones de palabras?

En el contexto de los modelos de lenguaje, una incrustación es una forma de representar palabras, oraciones o documentos completos como vectores o listas de números.


Para calcular las incrustaciones, necesitaremos una red neuronal como word2vec o text-embedding-ada-002 . Estas redes han sido entrenadas con cantidades masivas de texto y pueden encontrar relaciones entre palabras analizando las frecuencias con las que aparecen patrones específicos en los datos de entrenamiento.


Digamos que tenemos las siguientes palabras:

  • Gato
  • Perro
  • Pelota
  • Casa


Imagina que usamos una de estas redes de incrustación para calcular los vectores de cada palabra. Por ejemplo:

Palabra

Vector

Contexto

Gato

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

Animales, objetos, cosas pequeñas.

Perro

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

Animales, objetos, cosas grandes.

Pelota

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

Objetos, juguetes, cosas pequeñas.

Casa

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

Edificios, casas, cosas grandes.

Una vez que tenemos los vectores para cada palabra, podemos usarlos para representar el significado del texto. Por ejemplo, la oración "El gato persiguió la pelota" se puede representar como el vector [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]. Este vector representa una oración que trata sobre un animal que persigue un objeto.


Las incrustaciones de palabras se pueden visualizar como espacios multidimensionales donde las palabras u oraciones con significados similares están juntas. Podemos calcular la "distancia" entre vectores para encontrar significados similares para cualquier texto de entrada.

Tres representaciones tridimensionales de vectores. El primero está etiquetado como 'Masculino-Femenino' y tiene puntos de datos hombre-mujer y rey-reina, el segundo está etiquetado como 'Verbo-Tiempo' y tiene verbos como caminar-caminar nadar-nadar. El último está etiquetado como 'País-Capital' y tiene varias capitales conectadas a sus países. Representación 3D de incrustaciones como espacios vectoriales. En realidad, estos espacios pueden tener cientos o miles de dimensiones. Fuente: Conozca la multiherramienta de AI: incrustaciones de vectores


Las matemáticas reales detrás de todo esto están más allá del alcance de este artículo. Sin embargo, la conclusión clave es que las operaciones vectoriales nos permiten manipular o determinar el significado usando las matemáticas . Tome el vector que representa la palabra "reina", réstele el vector "mujer" y agregue el vector "hombre". El resultado debería ser un vector en la vecindad de "rey". Si agregamos "hijo", deberíamos acercarnos a "príncipe".

Incrustación de redes neuronales con tokens

Hasta ahora, hemos discutido la incorporación de redes neuronales que toman palabras como entradas y números como salidas. Sin embargo, muchas redes modernas han pasado de procesar palabras a procesar tokens.


Un token es la unidad de texto más pequeña que puede procesar el modelo. Los tokens pueden ser palabras, caracteres, signos de puntuación, símbolos o partes de palabras.


Podemos ver cómo las palabras se convierten en tokens experimentando con el tokenizador en línea de OpenAI , que utiliza la codificación de pares de bytes (BPE) para convertir texto en tokens y representar cada uno con un número:

Captura de pantalla del tokenizador de OpenAI. Se ha ingresado algo de texto y cada token está representado por diferentes colores, lo que nos permite ver cómo se asignan las palabras a los tokens. El texto dice: Detrás de cualquier modelo de incrustación, hay una red neuronal que convierte el texto de entrada en vectores. Cada tipo de modelo de incrustación tiene diferentes capacidades y velocidades. Word2vec, por ejemplo, toma palabras y produce vectores en el rango de 100 a 300 dimensiones. A menudo hay una relación de 1 a 1 entre tokens y palabras. La mayoría de las fichas incluyen la palabra y un espacio inicial. Sin embargo, hay casos especiales como "incrustación", que consta de dos tokens, "incrustación" y "ding", o "capacidades", que consta de cuatro tokens. Si hace clic en "ID de token", puede ver la representación numérica del modelo de cada token.

Diseño de un bot más inteligente mediante incrustaciones

Ahora que entendemos qué son las incrustaciones, la siguiente pregunta es: ¿cómo pueden ayudarnos a construir un bot más inteligente?


Primero, consideremos qué sucede cuando usamos la API GPT-3 directamente. El usuario emite un aviso y el modelo responde lo mejor que puede.

Diagrama que muestra la interacción entre el usuario y GPT-3. El usuario envía un aviso, el modelo responde.


Sin embargo, cuando agregamos contexto a la ecuación, las cosas cambian. Por ejemplo, cuando le pregunté a ChatGPT sobre el ganador de la Copa del Mundo después de proporcionar contexto, marcó la diferencia.


Entonces, el plan para construir un bot más inteligente es el siguiente:

  1. Interceptar la indicación del usuario.
  2. Calcule las incrustaciones para ese aviso, produciendo un vector.
  3. Busque en una base de datos documentos cerca del vector, ya que deberían ser semánticamente relevantes para el aviso inicial.
  4. Envíe el aviso original a GPT-3, junto con cualquier contexto relevante.
  5. Reenviar la respuesta de GPT-3 al usuario.

Una implementación más compleja de un bot. El usuario envía el mensaje a una aplicación de chatbot, que busca en una base de datos de contexto y la usa para enriquecer el mensaje. El mensaje se envía a GPT-3 y su respuesta se reenvía al usuario.


Comencemos como la mayoría de los proyectos, diseñando la base de datos.

Creación de una base de datos de conocimientos con incrustaciones

Nuestra base de datos de contexto debe incluir la documentación original y sus respectivos vectores. En principio, podemos emplear cualquier tipo de base de datos para esta tarea, pero una base de datos vectorial es la herramienta óptima para el trabajo.


Las bases de datos vectoriales son bases de datos especializadas diseñadas para almacenar y recuperar datos vectoriales de alta dimensión. En lugar de emplear un lenguaje de consulta como SQL para buscar, proporcionamos un vector y solicitamos los N vecinos más cercanos.


Para generar los vectores, utilizaremos text-embedding-ada-002 de OpenAI, ya que es el modelo más rápido y rentable que ofrecen. El modelo convierte el texto de entrada en fichas y utiliza un mecanismo de atención conocido como Transformador para aprender sus relaciones. La salida de esta red neuronal son vectores que representan el significado del texto.

Diagrama que ilustra el proceso de tokenización. Un documento se tokeniza y luego se envía a una red neuronal integrada. La salida de la red es un vector.


Para crear una base de datos de contexto, haré lo siguiente:

  1. Recoger toda la documentación fuente.
  2. Filtre los documentos irrelevantes.
  3. Calcular las incrustaciones para cada documento.
  4. Almacene los vectores, el texto original y cualquier otro metadato relevante en la base de datos.

Diagrama que ilustra el proceso de almacenamiento de datos en la base de datos de contexto. El documento de origen se envía a la red neuronal integrada. La base de datos almacena el vector junto con el texto original.

Conversión de documentos en vectores

Primero, debo inicializar un archivo de entorno con la clave API de OpenAI. Este archivo nunca debe comprometerse con el control de versiones, ya que la clave API es privada y está vinculada a su cuenta.

 export OPENAI_API_KEY=YOUR_API_KEY

A continuación, crearé un virtualenv para mi aplicación de Python:

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

E instale el paquete OpenAI:

 ```bash $ pip install openai numpy

Intentemos calcular la incrustación de la cadena "Docker Container". Puede ejecutar esto en Python REPL o como un script de 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 } }

Como puede ver, el modelo de OpenAI responde con una lista embedding que contiene 1536 elementos, el tamaño del vector para text-incrustar-ada-002.

Almacenamiento de las incrustaciones en Pinecone

Si bien hay varios motores de bases de datos vectoriales para elegir, como Chroma , que es de código abierto, elegí Pinecone porque es una base de datos administrada con un nivel gratuito, lo que simplifica las cosas. Su plan de inicio es más que capaz de manejar todos los datos que necesitaré.


Después de crear mi cuenta de Pinecone y recuperar mi clave de API y mi entorno, agrego ambos valores a mi archivo .env .

Captura de pantalla de generación de claves API de Pinecone

Ahora .env debería contener mis secretos de Pinecone y OpenAI.

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

Luego, instalo el cliente Pinecone para Python:

 $ pip install pinecone-client

Necesito inicializar una base de datos; estos son los contenidos del 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']} )

El script puede tardar unos minutos en crear la base de datos.

 $ python db_create.py

A continuación, instalaré el paquete tiktoken . Lo usaré para calcular cuántos tokens tienen los documentos de origen. Esto es importante porque el modelo de incrustación solo puede manejar hasta 8191 tokens.

 $ pip install tiktoken

Mientras instalamos paquetes, instalemos también tqdm para producir una barra de progreso atractiva.

 $ pip install tqdm

Ahora necesito subir los documentos a la base de datos. El script para esto se llamará index_docs.py . Comencemos importando los módulos requeridos y definiendo algunas 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

A continuación, necesitaremos una función para contar tokens. Hay un ejemplo de contador de fichas en la página de 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

Finalmente, necesitaré algunas funciones de filtrado para convertir el documento original en ejemplos utilizables. La mayoría de los ejemplos en la documentación están entre cercas de código, así que solo extraeré todo el código YAML de cada archivo:

 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

He terminado con las funciones. A continuación, esto cargará los archivos en la memoria y extraerá los ejemplos:

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

En este punto, todos los YAML deben almacenarse en la lista new_data . El paso final es cargar las incrustaciones en 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)

Como referencia, puede encontrar el archivo index_docs.py completo en el repositorio de demostración

Ejecutemos el script de índice para terminar con la configuración de la base de datos:

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

Probando la base de datos

El tablero de Pinecone debe mostrar vectores en la base de datos.

Captura de pantalla del tablero de Pinecone que muestra la base de datos con un total de 79 vectores

Podemos consultar la base de datos con el siguiente código, que puede ejecutar como un script o directamente en Python REPL:

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

Como puede ver, la primera coincidencia es el YAML para una canalización de Semaphore que extrae una imagen de Docker y la ejecuta. Es un buen comienzo ya que es relevante para nuestra cadena de búsqueda "Contenedores Docker".

Construyendo el bot

Tenemos los datos y sabemos cómo consultarlos. Pongámoslo a trabajar en el bot.

Los pasos para procesar el aviso son:

  1. Tome la indicación del usuario.
  2. Calcula su vector.
  3. Recuperar contexto relevante de la base de datos.
  4. Envíe el aviso del usuario junto con el contexto a GPT-3.
  5. Reenviar la respuesta del modelo al usuario.

Diagrama del flujo de datos para el bot. A la izquierda, ingresa el indicador del usuario, que es procesado por la red neuronal integrada y luego enviado a la base de datos de contexto. La búsqueda produce texto relevante que se envía al modelo GPT-3. La salida del modelo se envía al usuario como respuesta final. Como de costumbre, comenzaré definiendo algunas constantes en complete.py , el script principal del 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)

A continuación, agregaré funciones para contar tokens como se muestra en los ejemplos de OpenAI . La primera función cuenta tokens en una cadena, mientras que la segunda cuenta tokens en mensajes. Veremos los mensajes en detalle en un momento. Por ahora, digamos que es una estructura que mantiene el estado de la conversación en la memoria.

 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 siguiente función toma el mensaje original y las cadenas de contexto para devolver un mensaje enriquecido para 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 función get_message formatea el indicador en un formato compatible con la API:

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

Hay tres tipos de roles que afectan la forma en que reacciona el modelo:

  • Usuario : para el aviso original del usuario.
  • Sistema : ayuda a configurar el comportamiento del asistente. Si bien existe cierta controversia con respecto a su efectividad, parece ser más efectivo cuando se envía al final de la lista de mensajes.
  • Asistente : representa las respuestas pasadas del modelo. La API de OpenAI no tiene "memoria"; en cambio, debemos devolver las respuestas anteriores del modelo durante cada interacción para mantener la conversación.


Ahora para la parte atractiva. La función get_context toma el indicador, consulta la base de datos y genera una cadena de contexto hasta que se cumple una de estas condiciones:

  • El texto completo excede context_tokens_per_query , el espacio que reservé para context.
  • La función de búsqueda recupera todas las coincidencias solicitadas.
  • Las coincidencias que tienen una puntuación de similitud por debajo de match_min_score se ignoran.
 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 función siguiente y final, complete , emite la solicitud de API a OpenAI y devuelve la respuesta del modelo.

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

Eso es todo; ahora solo tengo que lidiar con los argumentos de la línea de comando y llamar a las funciones en el orden correcto:

 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 hora de ejecutar el script y ver cómo le va:

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

El resultado es:

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

Este es el primer buen resultado. El modelo ha inferido la sintaxis de los ejemplos de contexto que proporcionamos.

Reflexiones sobre la expansión de las capacidades del bot

Recuerde que comencé con un objetivo modesto: crear un asistente para escribir canalizaciones YAML. Con un contenido más rico en mi base de datos de vectores, puedo generalizar el bot para responder a cualquier pregunta sobre Semaphore (o cualquier producto; ¿recuerda haber clonado los documentos en /tmp ?).


La clave para obtener buenas respuestas es, como era de esperar, el contexto de calidad. Es poco probable que simplemente cargar cada documento en la base de datos de vectores produzca buenos resultados. La base de datos de contexto debe ser curada, etiquetada con metadatos descriptivos y ser concisa. De lo contrario, corremos el riesgo de llenar la cuota de tokens en el aviso con un contexto irrelevante.


Entonces, en cierto sentido, hay un arte, y una gran cantidad de prueba y error, involucrados en ajustar el bot para satisfacer nuestras necesidades. Podemos experimentar con el límite de contexto, eliminar contenido de baja calidad, resumir y filtrar el contexto irrelevante ajustando el puntaje de similitud.

Implementando un chatbot adecuado

Es posible que haya notado que mi bot no nos permite tener una conversación real como ChatGPT. Hacemos una pregunta y obtenemos una respuesta.


Convertir el bot en un chatbot completo no es, en principio, demasiado desafiante. Podemos mantener la conversación reenviando respuestas anteriores al modelo con cada solicitud de API. Las respuestas anteriores de GPT-3 se devuelven bajo el rol de "asistente". Por ejemplo:

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

Desafortunadamente, esta implementación es bastante rudimentaria. No admitirá conversaciones extendidas ya que el recuento de tokens aumenta con cada interacción. Muy pronto, alcanzaremos el límite de 4096 tokens para GPT-3, lo que evitará más diálogo.


Entonces, tenemos que encontrar alguna forma de mantener la solicitud dentro de los límites del token. Siguen algunas estrategias:

  • Eliminar mensajes antiguos. Si bien esta es la solución más simple, limita la "memoria" de la conversación solo a los mensajes más recientes.
  • Resumir mensajes anteriores. Podemos utilizar "Preguntar al modelo" para condensar mensajes anteriores y sustituirlos por las preguntas y respuestas originales. Aunque este enfoque aumenta el costo y el retraso entre las consultas, puede producir mejores resultados en comparación con la simple eliminación de mensajes anteriores.
  • Establezca un límite estricto en el número de interacciones.
  • Espere la disponibilidad general de la API GPT-4, que no solo es más inteligente sino que tiene doble capacidad de token.
  • Use un modelo más nuevo como "gpt-3.5-turbo-16k" que puede manejar hasta 16k tokens .

Conclusión

Es posible mejorar las respuestas del bot con incrustaciones de palabras y una buena base de datos de contexto. Para lograr esto, necesitamos documentación de buena calidad. Hay una cantidad sustancial de prueba y error involucrada en el desarrollo de un bot que aparentemente posee una comprensión del tema.


Espero que esta exploración en profundidad de las incrustaciones de palabras y los grandes modelos de lenguaje lo ayude a crear un bot más potente, personalizado según sus requisitos.


¡Feliz edificio!


También publicado aquí .