paint-brush
Incorporações de palavras: o molho secreto para dar contexto ao seu ChatBot para melhores respostaspor@tomfernblog
2,991 leituras
2,991 leituras

Incorporações de palavras: o molho secreto para dar contexto ao seu ChatBot para melhores respostas

por Tomas Fernandez18m2023/07/26
Read on Terminal Reader

Muito longo; Para ler

Aprenda a criar um bot especialista usando incorporação de palavras e ChatGPT. Aproveite o poder dos vetores de palavras para aprimorar as respostas do seu chatbot.
featured image - Incorporações de palavras: o molho secreto para dar contexto ao seu ChatBot para melhores respostas
Tomas Fernandez HackerNoon profile picture
0-item
1-item

Não há dúvida de que o ChatGPT da OpenAI é excepcionalmente inteligente - passou no teste de advogado , possui conhecimento semelhante ao de um médico e alguns testes marcaram seu QI em 155 . No entanto, tende a fabricar informações em vez de conceder ignorância. Essa tendência, aliada ao fato de seu conhecimento cessar em 2021, impõe desafios na construção de produtos especializados usando a API GPT.


Como podemos superar esses obstáculos? Como podemos transmitir novos conhecimentos a um modelo como o GPT-3? Meu objetivo é abordar essas questões construindo um bot de resposta a perguntas empregando Python, a API OpenAI e incorporação de palavras.

O que estarei construindo

Pretendo criar um bot que gere pipelines de integração contínua a partir de um prompt, que, como você deve saber, são formatados com YAML em Semaphore CI/CD.


Aqui está um exemplo do bot em ação:

Captura de tela do programa em execução. Na tela, o comando é executado: python query.py "Create a CI pipeline that builds and uploads a Docker image to Docker Hub", e o programa imprime o YAML correspondente a um CI pipeline que executa a ação solicitada. Captura de tela do programa em execução. Na tela, o comando python query.py "Create a CI pipeline that builds and uploads a Docker image to Docker Hub" é executado e o programa imprime o YAML correspondente a um CI pipeline que executa a ação solicitada.


No espírito de projetos como DocsGPT , My AskAI e Libraria , pretendo "ensinar" o modelo GPT-3 sobre Semaphore e como gerar arquivos de configuração de pipeline. Vou conseguir isso aproveitando a documentação existente .


Não assumirei conhecimento prévio de construção de bot e manterei o código limpo para que você possa adaptá-lo às suas necessidades.

Pré-requisitos

Você não precisa de experiência em codificar um bot ou conhecimento de redes neurais para seguir este tutorial. No entanto, você precisará de:


Mas o ChatGPT não pode aprender, pode?

O ChatGPT, ou mais precisamente, GPT-3 e GPT-4, os Large Language Models (LLMs) que os alimentam, foram treinados em um enorme conjunto de dados com uma data limite em setembro de 2021.


Em essência, o GPT-3 sabe muito pouco sobre eventos além dessa data. Podemos verificar isso com um prompt simples:

Captura de tela do ChatGPT ChatGPT não sabe quem ganhou a Copa do Mundo em 2022.


Enquanto alguns modelos OpenAI podem sofrer ajustes finos , os modelos mais avançados, como os que nos interessam, não podem; não podemos aumentar seus dados de treinamento.


Como podemos obter respostas do GPT-3 além de seus dados de treinamento? Um método envolve explorar suas habilidades de compreensão de texto; aprimorando o prompt com contexto relevante, provavelmente podemos obter a resposta correta.


No exemplo abaixo, forneço contexto do site oficial da FIFA e a resposta difere significativamente:

Segunda tentativa de resposta à pergunta Com o contexto fornecido, o ChatGPT pode responder com precisão.


Podemos deduzir que o modelo pode responder a qualquer solicitação se for dado um contexto relevante o suficiente. A questão permanece: como podemos saber o que é relevante dado um prompt arbitrário? Para resolver isso, precisamos explorar o que são incorporações de palavras .

O que são incorporações do Word?

No contexto dos modelos de linguagem, uma incorporação é uma forma de representar palavras, sentenças ou documentos inteiros como vetores ou listas de números.


Para calcular os embeddings, precisaremos de uma rede neural como word2vec ou text-embedding-ada-002 . Essas redes foram treinadas em grandes quantidades de texto e podem encontrar relações entre palavras analisando as frequências com as quais padrões específicos aparecem nos dados de treinamento.


Digamos que temos as seguintes palavras:

  • Gato
  • Cachorro
  • Bola
  • Casa


Imagine que usamos uma dessas redes de incorporação para calcular os vetores para cada palavra. Por exemplo:

Palavra

Vetor

Contexto

Gato

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

Animais, objetos, pequenas coisas

Cachorro

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

Animais, objetos, coisas grandes

Bola

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

Objetos, brinquedos, pequenas coisas

Casa

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

Prédios, casas, coisas grandes

Assim que tivermos os vetores para cada palavra, podemos usá-los para representar o significado do texto. Por exemplo, a frase “O gato perseguiu a bola” pode ser representada como o vetor [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 vetor representa uma frase sobre um animal perseguindo um objeto.


Embeddings de palavras podem ser visualizados como espaços multidimensionais onde palavras ou frases com significados semelhantes estão próximas. Podemos calcular a "distância" entre os vetores para encontrar significados semelhantes para qualquer texto de entrada.

Representações tridimensionais de vetores. O primeiro é rotulado como 'Male-Female' e tem pontos de dados homem-mulher e rei-rainha, o segundo é rotulado como 'Verb-Tense' e tem verbos como caminhar-caminhar-nadar. O último é rotulado como 'Country-Capital' e possui várias capitais conectadas a seus países Representação 3D de incorporações como espaços vetoriais. Na realidade, esses espaços podem ter centenas ou milhares de dimensões. Fonte: Conheça a multiferramenta da AI: Embeddings vetoriais


A matemática real por trás de tudo isso está além do escopo deste artigo. No entanto, a principal conclusão é que as operações vetoriais nos permitem manipular ou determinar o significado usando a matemática . Pegue o vetor que representa a palavra “rainha”, subtraia o vetor “mulher” dele e adicione o vetor “homem”. O resultado deve ser um vetor próximo a “rei”. Se adicionarmos “filho”, devemos chegar perto de “príncipe”.

Incorporando redes neurais com tokens

Até agora, discutimos a incorporação de redes neurais tomando palavras como entradas e números como saídas. No entanto, muitas redes modernas passaram do processamento de palavras para o processamento de tokens.


Um token é a menor unidade de texto que pode ser processada pelo modelo. Os tokens podem ser palavras, caracteres, sinais de pontuação, símbolos ou partes de palavras.


Podemos ver como as palavras são convertidas em tokens experimentando o tokenizador on-line OpenAI , que usa codificação de pares de bytes (BPE) para converter texto em tokens e representar cada um com um número:

Captura de tela do tokenizador OpenAI. Algum texto foi inserido e cada token é representado por cores diferentes, permitindo-nos ver como as palavras são mapeadas para os tokens. O texto diz: Atrás de qualquer modelo de incorporação, existe uma rede neural que converte o texto de entrada em vetores. Cada tipo de modelo de incorporação tem capacidades e velocidades diferentes. Word2vec, por exemplo, pega palavras e produz vetores na faixa de 100 a 300 dimensões. Muitas vezes, há uma relação de 1 para 1 entre tokens e palavras. A maioria dos tokens inclui a palavra e um espaço à esquerda. No entanto, existem casos especiais como "embedding", que consiste em dois tokens, "embed" e "ding" ou "capacities", que consistem em quatro tokens. Se você clicar em "IDs de token", poderá ver a representação numérica do modelo de cada token.

Projetando um bot mais inteligente usando incorporações

Agora que entendemos o que são os embeddings, a próxima pergunta é: como eles podem nos ajudar a criar um bot mais inteligente?


Primeiro, vamos considerar o que acontece quando usamos a API GPT-3 diretamente. O usuário emite um prompt e o modelo responde da melhor maneira possível.

Diagrama mostrando a interação entre o usuário e o GPT-3. O usuário envia um prompt, o modelo responde.


No entanto, quando adicionamos contexto à equação, as coisas mudam. Por exemplo, quando perguntei ao ChatGPT sobre o vencedor da Copa do Mundo depois de contextualizar, isso fez toda a diferença.


Então, o plano para construir um bot mais inteligente é o seguinte:

  1. Intercepte o prompt do usuário.
  2. Calcule os embeddings para esse prompt, gerando um vetor.
  3. Pesquise em um banco de dados por documentos próximos ao vetor, pois eles devem ser semanticamente relevantes para o prompt inicial.
  4. Envie o prompt original para GPT-3, juntamente com qualquer contexto relevante.
  5. Encaminhe a resposta do GPT-3 para o usuário.

Uma implementação mais complexa de um bot. O usuário envia o prompt para um aplicativo de chatbot, que pesquisa um banco de dados de contexto e o usa para enriquecer o prompt. O prompt é enviado ao GPT-3 e sua resposta é encaminhada ao usuário.


Vamos começar como a maioria dos projetos, projetando o banco de dados.

Criando um banco de dados de conhecimento com incorporações

Nosso banco de dados de contexto deve incluir a documentação original e seus respectivos vetores. Em princípio, podemos empregar qualquer tipo de banco de dados para esta tarefa, mas um banco de dados vetorial é a ferramenta ideal para o trabalho.


Bancos de dados vetoriais são bancos de dados especializados projetados para armazenar e recuperar dados vetoriais de alta dimensão. Em vez de empregar uma linguagem de consulta como SQL para pesquisa, fornecemos um vetor e solicitamos os N vizinhos mais próximos.


Para gerar os vetores, usaremos o text-embedding-ada-002 da OpenAI, pois é o modelo mais rápido e econômico que eles oferecem. O modelo converte o texto de entrada em tokens e usa um mecanismo de atenção conhecido como Transformer para aprender seus relacionamentos. A saída dessa rede neural são vetores que representam o significado do texto.

Diagrama ilustrando o processo de tokenização. Um documento é tokenizado e, em seguida, enviado para uma rede neural incorporada. A saída da rede é um vetor.


Para criar um banco de dados de contexto, irei:

  1. Colete toda a documentação de origem.
  2. Filtre documentos irrelevantes.
  3. Calcule os embeddings para cada documento.
  4. Armazene os vetores, o texto original e quaisquer outros metadados relevantes no banco de dados.

Diagrama ilustrando o processo de armazenamento de dados no banco de dados de contexto. O documento de origem é enviado para a rede neural de incorporação. O banco de dados armazena o vetor junto com o texto original.

Convertendo Documentos em Vetores

Primeiro, devo inicializar um arquivo de ambiente com a chave da API OpenAI. Este arquivo nunca deve ser submetido ao controle de versão, pois a chave da API é privada e vinculada à sua conta.

 export OPENAI_API_KEY=YOUR_API_KEY

Em seguida, criarei um virtualenv para meu aplicativo Python:

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

E instale o pacote OpenAI:

 ```bash $ pip install openai numpy

Vamos tentar calcular a incorporação da string "Docker Container". Você pode executar isso no Python REPL ou como um 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 } }

Como você pode ver, o modelo do OpenAI responde com uma lista embedding contendo 1536 itens — o tamanho do vetor para text-embedding-ada-002.

Armazenando as incorporações no Pinecone

Embora existam vários mecanismos de banco de dados vetoriais para escolher, como o Chroma , que é de código aberto, escolhi o Pinecone porque é um banco de dados gerenciado com um nível gratuito, o que torna as coisas mais simples. O plano inicial deles é mais do que capaz de lidar com todos os dados de que preciso.


Depois de criar minha conta Pinecone e recuperar minha chave de API e ambiente, adiciono os dois valores ao meu arquivo .env .

Captura de tela da geração da chave da API Pinecone

Agora .env deve conter meus segredos Pinecone e OpenAI.

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

Em seguida, instalo o cliente Pinecone para Python:

 $ pip install pinecone-client

Preciso inicializar um banco de dados; estes são os conteúdos do 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']} )

O script pode levar alguns minutos para criar o banco de dados.

 $ python db_create.py

Em seguida, instalarei o pacote tiktoken . Vou usá-lo para calcular quantos tokens os documentos de origem têm. Isso é importante porque o modelo de incorporação só pode lidar com até 8191 tokens.

 $ pip install tiktoken

Ao instalar os pacotes, vamos também instalar tqdm para produzir uma barra de progresso bonita.

 $ pip install tqdm

Agora eu preciso fazer o upload dos documentos para o banco de dados. O script para isso será chamado index_docs.py . Vamos começar importando os módulos necessários e definindo algumas 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

Em seguida, precisaremos de uma função para contar tokens. Há um exemplo de contador de token na página 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

Por fim, precisarei de algumas funções de filtragem para converter o documento original em exemplos utilizáveis. A maioria dos exemplos na documentação está entre cercas de código, então vou apenas extrair todo o código YAML de cada arquivo:

 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

Acabei com as funções. Em seguida, isso carregará os arquivos na memória e extrairá os exemplos:

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

Neste ponto, todos os YAMLs devem ser armazenados na lista new_data . A etapa final é carregar as incorporações no 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 referência, você pode encontrar o arquivo index_docs.py completo no repositório de demonstração

Vamos executar o script index para finalizar a configuração do banco de dados:

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

Testando o banco de dados

O painel Pinecone deve mostrar vetores no banco de dados.

Captura de tela do painel Pinecone mostrando o banco de dados com um total de 79 vetores

Podemos consultar o banco de dados com o seguinte código, que você pode executar como um script ou diretamente no 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 você pode ver, a primeira correspondência é o YAML para um pipeline do Semaphore que extrai uma imagem do Docker e a executa. É um bom começo, pois é relevante para nossa string de pesquisa "Docker Containers".

Construindo o bot

Temos os dados e sabemos como consultá-los. Vamos colocá-lo para trabalhar no bot.

As etapas para processar o prompt são:

  1. Pegue o prompt do usuário.
  2. Calcule seu vetor.
  3. Recupere o contexto relevante do banco de dados.
  4. Envie o prompt do usuário junto com o contexto para GPT-3.
  5. Encaminhe a resposta do modelo para o usuário.

Diagrama do fluxo de dados para o bot. À esquerda, entra o prompt do usuário, que é processado pela rede neural incorporada e enviado para o banco de dados de contexto. A pesquisa produz texto relevante que é enviado para o modelo GPT-3. A saída do modelo é enviada ao usuário como a resposta final. Como de costume, começarei definindo algumas constantes em complete.py , o script principal do 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)

Em seguida, adicionarei funções para contar tokens, conforme mostrado nos exemplos do OpenAI . A primeira função conta tokens em uma string, enquanto a segunda conta tokens em mensagens. Veremos as mensagens em detalhes daqui a pouco. Por enquanto, digamos apenas que é uma estrutura que mantém o estado da conversa na memória.

 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

A função a seguir usa o prompt original e as strings de contexto para retornar um prompt 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:" )

A função get_message formata o prompt em um formato compatível com a API:

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

Existem três tipos de papéis que afetam como o modelo reage:

  • User : para o prompt original do usuário.
  • Sistema : ajuda a definir o comportamento do assistente. Embora haja alguma controvérsia quanto à sua eficácia, parece ser mais eficaz quando enviado no final da lista de mensagens.
  • Assistente : representa as respostas passadas do modelo. A API OpenAI não possui uma "memória"; em vez disso, devemos enviar as respostas anteriores do modelo durante cada interação para manter a conversa.


Agora, para a parte envolvente. A função get_context pega o prompt, consulta o banco de dados e gera uma string de contexto até que uma destas condições seja atendida:

  • O texto completo excede context_tokens_per_query , o espaço que reservei para context.
  • A função de pesquisa recupera todas as correspondências solicitadas.
  • As correspondências com pontuação de similaridade abaixo match_min_score são ignoradas.
 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

A próxima e última função, complete , emite a solicitação da API para OpenAI e retorna a resposta do 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()

Isso é tudo; agora só tenho que lidar com os argumentos da linha de comando e chamar as funções na ordem correta:

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

É hora de executar o script e ver como ele se sai:

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

O resultado é:

 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 é o primeiro bom resultado. O modelo inferiu a sintaxe dos exemplos de contexto que fornecemos.

Considerações sobre como expandir os recursos do bot

Lembre-se que comecei com um objetivo modesto: criar um assistente para escrever pipelines YAML. Com um conteúdo mais rico em meu banco de dados de vetores, posso generalizar o bot para responder a qualquer pergunta sobre o Semaphore (ou qualquer produto — lembre-se de clonar os documentos em /tmp ?).


A chave para obter boas respostas é - sem surpresa - contexto de qualidade. Simplesmente carregar todos os documentos no banco de dados de vetores provavelmente não produzirá bons resultados. O banco de dados de contexto deve ser curado, marcado com metadados descritivos e ser conciso. Caso contrário, corremos o risco de preencher a cota de token no prompt com contexto irrelevante.


Então, de certa forma, há uma arte – e muita tentativa e erro – envolvida em ajustar o bot para atender às nossas necessidades. Podemos experimentar o limite de contexto, remover conteúdo de baixa qualidade, resumir e filtrar o contexto irrelevante ajustando a pontuação de similaridade.

Implementando um Chatbot Adequado

Você deve ter notado que meu bot não nos permite ter uma conversa real como o ChatGPT. Fazemos uma pergunta e obtemos uma resposta.


Converter o bot em um chatbot completo não é, em princípio, muito desafiador. Podemos manter a conversa reenviando respostas anteriores ao modelo com cada solicitação de API. As respostas GPT-3 anteriores são enviadas de volta sob a função de "assistente". Por exemplo:

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

Infelizmente, esta implementação é bastante rudimentar. Ele não suportará conversas estendidas, pois a contagem de tokens aumenta a cada interação. Em breve, atingiremos o limite de 4096 tokens para GPT-3, impedindo mais diálogos.


Portanto, temos que encontrar uma maneira de manter a solicitação dentro dos limites do token. Seguem algumas estratégias:

  • Apague as mensagens mais antigas. Embora seja a solução mais simples, ela limita a "memória" da conversa apenas às mensagens mais recentes.
  • Resuma as mensagens anteriores. Podemos utilizar "Pergunte ao modelo" para condensar as mensagens anteriores e substituí-las pelas perguntas e respostas originais. Embora essa abordagem aumente o custo e o atraso entre as consultas, ela pode produzir resultados superiores em comparação com a simples exclusão de mensagens anteriores.
  • Defina um limite estrito para o número de interações.
  • Aguarde a disponibilidade geral da API GPT-4, que não é apenas mais inteligente, mas tem capacidade de token dupla.
  • Use um modelo mais recente como "gpt-3.5-turbo-16k" que pode lidar com até 16k tokens .

Conclusão

Aprimorar as respostas do bot é possível com incorporação de palavras e um bom banco de dados de contexto. Para conseguir isso, precisamos de documentação de boa qualidade. Há uma quantidade substancial de tentativa e erro envolvida no desenvolvimento de um bot que aparentemente possui uma compreensão do assunto.


Espero que esta exploração aprofundada de incorporações de palavras e grandes modelos de linguagem ajude você a criar um bot mais potente, personalizado de acordo com suas necessidades.


Edifício feliz!


Publicado também aqui .