Desde que o ChatGPT capturou a imaginação do público no início de 2023, houve uma explosão de interesse na comercialização de aplicativos baseados em modelos de linguagem de grande porte. Uma das aplicações mais interessantes tem sido a criação de sistemas de chat especializados que podem responder a consultas em linguagem natural a partir de um banco de dados de conhecimento proprietário.
Uma das técnicas mais populares neste espaço é [geração aumentada de recuperação](https://retrieval aws de geração aumentada), ou RAG, que usa embeddings de documentos para encontrar itens relevantes para a consulta do usuário antes de usar um grande modelo de linguagem para gerar uma resposta.
A técnica é extremamente poderosa, pois permite pesquisas extremamente baratas e rápidas, fornece extrema flexibilidade para a base de conhecimento mudar e evoluir ao longo do tempo e respostas altamente informadas e precisas que reduzem enormemente alucinações e erros.
Para uma análise mais aprofundada dos sistemas RAG e como implementá-los, você pode ler meu artigo anterior aqui .
Por mais poderosos que os sistemas RAG possam ser, existem algumas limitações severas na arquitetura. Exploramos algumas das limitações em meu artigo anterior e propusemos maneiras de melhorar a arquitetura.
Hoje, exploraremos outra limitação da arquitetura baseada em incorporação e proporemos uma maneira de contornar as limitações da arquitetura.
Suponha que sejamos uma publicação que deseja criar uma interface de chat que permita aos leitores e clientes fazer perguntas.
Claro, poderemos responder perguntas como “Qual a sua opinião sobre X?” ou “O que você disse sobre Y?” com uma implementação RAG simples, mas uma arquitetura RAG realmente começa a ter dificuldades quando você lida com questões como “O que você disse sobre o X em 2021?” ou “Como mudou a sua cobertura de Y entre 2021 e 2023?”
Um dos desafios de um RAG baseado em embeddings é que os modelos de embeddings geralmente são incapazes de codificar metadados de maneira sistemática e, portanto, qualquer pesquisa que exija conhecimento de coisas como data de publicação ou nome do autor causará alguns problemas ao seu sistema RAG. .
Podemos resolver esse problema aproveitando um dos recursos mais interessantes dos grandes modelos de linguagem – a geração de código. Analisaremos uma publicação do mundo real, projetaremos um algoritmo baseado em LLM que melhore a arquitetura RAG e construiremos um chatbot baseado no algoritmo.
Hoje, daremos uma olhada no CB Insights Newsletter, um boletim informativo diário popular que cobre startups e tecnologia. Como ex-desenvolvedor full-stack da CB Insights, muitas vezes ansiava pela marca única de inteligência e visão do fundador no final do dia de trabalho.
Hoje, usaremos o arquivo do boletim informativo CB Insights como dados de base para construir um chatbot que pode responder a consultas de linguagem natural baseadas em metadados de uma forma que seja superior a uma implementação RAG baseada em vanilla Embeddings.
Especificamente, queremos que o chatbot seja capaz de responder a perguntas como:
Vamos lá!
Para realizar esta tarefa, utilizaremos as seguintes tecnologias:
Se você acompanhou meus outros artigos , não será surpresa que usarei Python na maior parte do código deste artigo. Python tem excelente web scraping, processamento de dados e integração OpenAI, todos os quais aproveitaremos em nosso projeto hoje.
SQL é a linguagem de consulta que permite aos usuários interagir com vários bancos de dados relacionais importantes, incluindo SQLite, MySQL, PostgreSQL e SQL Server. A linguagem é um conjunto de instruções para o banco de dados sobre como recuperar, combinar e manipular dados antes de devolvê-los ao usuário.
A geração de código LLM é uma técnica que tem recebido muita atenção nos últimos meses, já que vários modelos básicos, incluindo GPT 3.5, GPT 4 e LLaMa 2, demonstraram a capacidade de gerar código de complexidade surpreendente em resposta a consultas em linguagem natural.
Sistemas especialmente treinados e ajustados, como o Copilot do GitHub, são capazes de escrever código extremamente inteligente usando modelos projetados especificamente para geração de código, mas um modelo GPT de uso geral devidamente solicitado já possui recursos excepcionais quando se trata de escrever código.
A incorporação semântica é a espinha dorsal da maioria das implementações RAG. Usando uma série de técnicas de linguagem natural, podemos transformar o texto em linguagem natural em um vetor de números que representa o conteúdo do texto dentro de um espaço vetorial semântico.
Podemos então usar álgebra vetorial para manipular esses embeddings, permitindo-nos determinar a relação entre dois corpora de texto usando métodos matemáticos.
Com 1,7 trilhão de parâmetros, o GPT-4 é simplesmente o modelo de linguagem grande baseado em transformador mais poderoso disponível no mercado atualmente. O GPT-4 é capaz de compreender grandes volumes de texto, raciocínio complexo e gerar respostas longas e convincentes em resposta a solicitações difíceis.
GPT-3.5, primo muito menor do GPT-4, é o modelo que impulsionou o ChatGPT quando conquistou o mundo. Ele é capaz de processar solicitações incrivelmente complexas e o que falta em pura capacidade de raciocínio é compensado em velocidade e economia de custos.
Para tarefas mais simples, o GPT3.5 atinge o equilíbrio entre desempenho e precisão.
Antes de construirmos nossa IA, precisamos obter os dados. Para fazer isso, podemos usar a página de arquivo de boletins informativos da CB Insights [ https://www.cbinsights.com/newsletter/ ] que contém uma coleção de boletins informativos anteriores.
Para obter todos os links, podemos usar as solicitações do Python e a bela biblioteca de sopas da seguinte forma:
import requests from bs4 import BeautifulSoup res = requests.get('https://www.cbinsights.com/newsletter/') soup = BeautifulSoup(res.text) article_links = [[i.text, i['href']] for i in soup.find_all('a') if 'campaign-archive' in i['href'] ]
Assim que tivermos os links, podemos acessar cada um deles e baixar o HTML do artigo. Com a compreensão de lista do Python, podemos fazer isso em uma linha:
article_soups = [BeautifulSoup(requests.get(link[1]).text) for link in article_links]
Isso levará um tempo, mas, eventualmente, todos os links deverão ser removidos.
Agora podemos usar BeautifulSoup para extrair as seções relevantes:
import re # SEO optimizations cause some articles to appear twice so we dedupe them. # We also remove some multiple newlines and unicode characters. def get_deduped_article_tables(article_table): new_article_tables = [] for i in article_table: text_content = re.sub(r'\n{2,}', '\n', i.replace('\xa0', '').strip()) if text_content not in new_article_tables or text_content == '': new_article_tables.append(text_content) return new_article_tables result_json = {} for soup_meta, soup_art in zip(article_links, article_soups): article_tables = [] cur_article = [] for table in soup_art.find_all('table'): if table.attrs.get('mc:variant') == 'Section_Divider': article_tables.append(get_deduped_article_tables(cur_article)) cur_article = [] else: cur_article.append(table.text) article_tables.append(get_deduped_article_tables(cur_article)) result_json[soup_meta[0]] = article_tables
Vamos fazer mais alguns processamentos e transformá-lo em um DataFrame:
import pandas as pd result_rows = [] for article_name, article_json in result_json.items(): article_date = article_json[0][1] for idx, tbl in enumerate(article_json[1:]): txt = '\n'.join(tbl).strip() if txt != '': result_rows.append({ 'article_name': article_name, 'article_date': article_date, 'idx': idx, 'text': txt, }) df = apd.DataFrame(result_rows)
Se você inspecionar o dataframe, deverá ver algo como abaixo:
Enquanto temos os dados, vamos gerar também os embeddings para os artigos. Com o modelo de incorporação ada da OpenAI, isso é bastante fácil.
import openai EMBEDDING_MODEL = "text-embedding-ada-002" openai.api_key = [YOUR KEY] df['embedding'] = df['text'].map(lambda txt: openai.Embedding.create(model=EMBEDDING_MODEL, input=[txt])['data'][0]['embedding'])
Agora que temos os dados que estamos usando neste exercício, vamos carregá-los em um banco de dados. Para este exercício, usaremos SQLite, que é um sistema de banco de dados leve e independente que vem empacotado com Python.
Observe que em um ambiente de produção, você provavelmente desejará usar uma instância de banco de dados adequada, como MySQL ou PostgreSQL, com pequenos ajustes no SQL que estamos usando aqui, mas a técnica geral permanecerá a mesma.
Para instanciar e carregar o banco de dados, basta executar o seguinte em Python. Note que para além do texto do artigo e dos embeddings, estamos também a guardar alguns metadados, nomeadamente a data de publicação.
Observe também que o SQLite3, diferentemente da maioria dos outros bancos de dados SQL, usa um sistema de digitação dinâmico, portanto não precisamos especificar os tipos de dados na consulta de criação.
import sqlite3 import json con = sqlite3.connect("./cbi_article.db") cur = con.cursor() cur.execute("CREATE TABLE article(name, date, idx, content, embedding_json)") con.commit() rows = [] for _, row in df.iterrows(): rows.append([row['article_name'], row['article_date'], row['idx'], row['text'], json.dumps(row['embedding'])]) cur.executemany("INSERT INTO article VALUES (?, ?, ?, ?, ?)", rows) con.commit()
E vamos tentar consultar os dados.
res = cur.execute(""" SELECT name, date, idx FROM article WHERE date >= DATE('now', '-2 years'); """) res.fetchall()
Deve render algo como:
Parece muito bom!
Agora que temos os dados carregados no banco de dados SQLite, podemos passar para a próxima etapa. Lembre-se de que um dos desafios da implementação de RAG somente para incorporações é a falta de recursos flexíveis de pesquisa de metadados.
No entanto, agora que carregamos os metadados em um banco de dados SQL, podemos usar os recursos de geração de código do GPT para realizar pesquisas flexíveis de metadados.
Para gerar código SQL, podemos usar alguma engenharia simples de prompt.
response = openai.ChatCompletion.create( model="gpt-4", messages=[ {"role": "system", "content": "You are a SQL query writer that can construct queries based on incoming questions. Answer with only the SQL query."}, {"role": "user", "content": """ Suppose we have the SQLite database table called "article" with the following columns, which contains newsletter articles from a publication: name, date, idx, content, embedding_json Write a question that would retrieve the rows necessary to answer the following user question. Only filter on date. Do not filter on any other column. Make sure the query returns every row in the table by name. Reply only with the SQL query. User question: What did you say about the future of the fintech industry in summer of 2022? """}, ] )
Observe a seguinte engenharia de prompt: 1) fornecemos o esquema do banco de dados, mas o mantemos simples. 2) Especificamos as colunas de retorno. 3) Especificamos as colunas que estão disponíveis para filtragem. 4) Especificamos o tipo SQL. Este prompt deve gerar um código SQL como o mostrado abaixo:
SELECT * FROM article WHERE date BETWEEN '2022-06-01' AND '2022-08-31'
Agora, como o valor de retorno não é determinístico, às vezes você acabará com idiossincrasias no código gerado. Para lidar com essas condições, podemos simplesmente ter um loop try-catch para tentar regenerar os dados. Não queremos fazer isso infinitamente, é claro, então se não conseguirmos gerar um SQL adequado em três tentativas, simplesmente sairemos e voltaremos ao RAG básico.
Podemos implementar o filtro assim:
res = [] for i in range(3): response = openai.ChatCompletion.create( model="gpt-4", messages=[ {"role": "system", "content": "You are a SQL query writer that can construct queries based on incoming questions. Answer with only the SQL query."}, {"role": "user", "content": """ Suppose we have the SQLite database table called "article" with the following columns, which contains newsletter articles from a publication: name, date, idx, content, embedding_json Write a question that would retrieve the rows necessary to answer the following user question. Only filter on date. Do not filter on any other column. Make sure the query returns every row in the table by name. Reply only with the SQL query. User question: What did you say about the future of the fintech industry in summer of 2022? """}, ] ) generated_query = response.choices[0].message['content'] is_query_safe = True for no_use_word in {'DELETE', 'UPDATE', 'DROP'}: if no_use_word in generated_query.upper(): is_query_safe = False if not is_query_safe: break # the user input is likely malicious. Try to answer the question with vanilla RAG res = cur.execute(generated_query).fetchall() if len(res) > 0: break if len(res) == 0: # vanilla RAG in memory. Use a vector DB in production please. res = cur.execute('''SELECT * FROM articles''').fetchall()
Este é um filtro relativamente rudimentar, portanto, em casos de uso de produção, você provavelmente desejará executar mais verificações de relevância e correção de SQL, mas isso é suficiente para nosso exemplo.
Uma observação rápida sobre segurança de IA : devemos ter cuidado ao executar código retornado de uma IA, especialmente se a entrada do usuário foi usada como parte do prompt.
Se não higienizarmos a saída, ficaremos vulneráveis a ataques de engenharia imediata, onde o usuário tenta manipular a IA para gerar instruções de atualização ou exclusão.
Portanto, devemos sempre verificar se a saída é a esperada antes de executar o código em nosso computador.
Execute o seguinte código para ver o resultado recuperado:
df = pd.DataFrame([{c[0]: v for c, v in zip(cur.description, row)} for row in res])
E agora você deverá ver o seguinte resultado:
Agora que temos o resultado da pesquisa de metadados, o resto é simples. Calculamos a similaridade de cosseno para todos os resultados recuperados da seguinte forma:
from openai.embeddings_utils import cosine_similarity q_embed = openai.Embedding.create(model=EMBEDDING_MODEL, input=[user_question])['data'][0]['embedding'] df['cosine_similarity'] = df['embedding_json'].map(lambda js: cosine_similarity(json.loads(js), q_embed))
E agora podemos pegar os 10 principais boletins informativos e usar a engenharia imediata para responder à pergunta. Escolhemos 10 aqui porque cada trecho do boletim informativo é relativamente curto.
Se estiver trabalhando com artigos mais longos, você provavelmente desejará usar menos artigos ou usar a técnica abordada na seção de bônus.
answer_prompt = ''' Consider the following newsletter excerpts from the following dates: ''' for _, row in df.sort_values('cosine_similarity', ascending=False).iloc[:10].iterrows(): answer_prompt += """ ======= Date: %s ==== %s ===================== """ % (row['date'], row['content']) answer_prompt += """ Answer the following question: %s """ % user_question response = openai.ChatCompletion.create( model="gpt-4", messages=[ {"role": "system", "content": "You are a tech analyst that can summarize the content of newsletters"}, {"role": "user", "content": answer_prompt}, ] )
Isso deve fornecer um resultado semelhante ao seguinte:
O futuro da Fintech foi discutido em vários boletins informativos durante o verão de 2022. Houve uma desaceleração notável na Fintech à medida que o financiamento do segundo trimestre de 22 despencou para os níveis de 2020, após uma alta em 2021. O relatório do segundo trimestre de 22 destacou o declínio nos investimentos globais em fintech .
No entanto, o futuro da fintech parecia promissor, uma vez que foi observada uma mudança significativa em direção a startups em fase inicial, particularmente no espaço de pagamentos. O investimento global no setor de pagamentos caiu 43% em relação ao primeiro trimestre de 2022, para US$ 5,1 bilhões no segundo trimestre de 2022, devido ao retorno do financiamento ao normal após os picos de 2021.
Os novos participantes neste domínio atraíram uma percentagem maior de negócios (63%) até agora em 2022, marcando o interesse dos investidores em start-ups. Também foi relatada a crescente concorrência que a Fintech estava a causar aos bancos de retalho, forçando-os a dar prioridade à digitalização dos serviços essenciais.
O setor bancário respondeu concentrando-se na melhoria da experiência do cliente, especialmente no Mobile Banking, utilizando tecnologias como chatbots e plataformas de análise de clientes. Tudo isto aponta para uma indústria FinTech vibrante e competitiva pela frente.
O que é muito bom! Se você verificar a resposta olhando o prompt de resposta, notará que todas as estatísticas vêm do material de origem. Você também notará que há alguma formatação idiossincrática no material de origem que o GPT4 foi capaz de ignorar. Isto mostra a flexibilidade dos LLMs em sistemas de sumarização de dados.
Um dos problemas que você pode encontrar nesse processo é que, quando o corpus é extremamente grande, o prompt final pode ser muito grande. Isso pode custar caro se você estiver usando GPT-4, mas um prompt muito longo também pode confundir o modelo.
Para resolver isso, podemos pré-processar os artigos individuais com GPT-3.5, compactando o prompt final que enviaremos ao GPT-4 na etapa final.
summarization_prompt = ''' Summarize the following passage and extract only portions that are relevant to answering the user question. Passage: ======= %s ======= User Questions: %s ''' (row['content'], user_question) response = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[ {"role": "system", "content": "You are a summarizer of tech industry reports"}, {"role": "user", "content": summarization_prompt}, ] )
Podemos então colocar os resumos no prompt, com uma economia significativa em relação à colocação do artigo puro no prompt final.
Agora que escrevemos o código Python, vamos empacotá-lo como uma aplicação web simples.
É relativamente simples empacotar o código como uma API de back-end com Flask. Simplesmente crie uma função e vincule-a ao Flask assim:
import requests from bs4 import BeautifulSoup import re import pandas as pd import sqlite3 import json import openai from openai.embeddings_utils import cosine_similarity from flask import Flask, request, jsonify from flask_cors import CORS app = Flask(__name__) CORS(app) EMBEDDING_MODEL = "text-embedding-ada-002" openai.api_key = [Your OpenAI Key] db_location = [Location of your SQLite DB] def process_user_query(user_question): con = sqlite3.connect(db_location) cur = con.cursor() user_question = 'What did you say about the future of the fintech industry in summer of 2022?' res = [] for i in range(3): response = openai.ChatCompletion.create( model="gpt-4", messages=[ {"role": "system", "content": "You are a SQL query writer that can construct queries based on incoming questions. Answer with only the SQL query."}, {"role": "user", "content": """ Suppose we have the SQLite database table called "article" with the following columns, which contains newsletter articles from a publication: name, date, idx, content, embedding_json Write a question that would retrieve the rows necessary to answer the following user question. Only filter on date. Do not filter on any other column. Make sure the query returns every row in the table by name. Reply only with the SQL query. User question: What did you say about the future of the fintech industry in summer of 2022? """}, ] ) generated_query = response.choices[0].message['content'] is_query_safe = True for no_use_word in {'DELETE', 'UPDATE', 'DROP'}: if no_use_word in generated_query.upper(): is_query_safe = False if not is_query_safe: break # the user input is likely malicious. Try to answer the question with vanilla RAG res = cur.execute(generated_query).fetchall() if len(res) > 0: break if len(res) == 0: # vanilla RAG in memory. Use a vector DB in production please. res = cur.execute('''SELECT * FROM articles''').fetchall() df = pd.DataFrame([{c[0]: v for c, v in zip(cur.description, row)} for row in res]) q_embed = openai.Embedding.create(model=EMBEDDING_MODEL, input=[user_question])['data'][0]['embedding'] df['cosine_similarity'] = df['embedding_json'].map(lambda js: cosine_similarity(json.loads(js), q_embed)) answer_prompt = ''' Consider the following newsletter excerpts from the following dates: ''' for _, row in df.sort_values('cosine_similarity', ascending=False).iloc[:10].iterrows(): answer_prompt += """ ======= Date: %s ==== %s ===================== """ % (row['date'], row['content']) answer_prompt += """ Answer the following question: %s """ % user_question response = openai.ChatCompletion.create( model="gpt-4", messages=[ {"role": "system", "content": "You are a tech analyst that can summarize the content of newsletters"}, {"role": "user", "content": answer_prompt}, ] ) return response.choices[0].message['content'] @app.route('/process_user_question', methods=["POST"]) def process_user_question(): return jsonify({ 'status': 'success', 'result': process_user_query(request.json['user_question']) }) app.run()
E isso é tudo que precisamos fazer para o back-end!
Como temos apenas um endpoint e não precisamos de muito estado em nossa aplicação, o código frontend deve ser bem simples. Lembre-se de que em um artigo anterior configuramos um aplicativo React com roteamento que nos permite renderizar componentes em rotas específicas.
Basta seguir as instruções desse artigo para configurar um projeto React.JS e adicionar o seguinte componente em uma rota de sua escolha:
import React, {useState, useEffect} from 'react'; import axios from 'axios'; const HNArticle = () => { const [result, setResult] = useState(''); const [message, setMessage] = useState(''); const [question, setQuestion] = useState(''); const askQuestion = () => { axios.post("http://127.0.0.1:5000/process_user_question", {user_question: question}) .then(r => r.data) .then(d => { console.log(d); setResult(d.result); }); } return <div className="row" style={{marginTop: '15px'}}> <div className="col-md-12" style={{marginBottom: '15px'}}> <center> <h5>Hackernoon CB Insights Demo</h5> </center> </div> <div className="col-md-10 offset-md-1 col-sm-12 col-lg-8 offset-lg-2" style={{marginBottom: '15px'}}> <ul className="list-group"> <li className="list-group-item"> <h6>Your Question</h6> <p><input className="form-control" placeholder="Question" value={question} onChange={e => setQuestion(e.target.value)} /></p> <p>{message}</p> <p> <button className="btn btn-primary" onClick={askQuestion}>Ask</button> </p> </li> {result? <li className="list-group-item"> <h6>Response</h6> {result.split("\n").map((p, i) => <p key={i}>{p}</p>)} </li>: ''} </ul> </div> </div>; } export default HNArticle;
Execute o código e você verá uma interface como esta:
Faça uma pergunta e, depois de um tempo, você verá o resultado:
E pronto! Construímos com sucesso um chatbot com recursos de consulta avançados além de um sistema RAG básico!
No artigo de hoje, construímos um chatbot com poderosos recursos de geração de código. Este é um exemplo de uma nova classe de aplicações LLM que estão sendo construídas por muitos pioneiros de IA, que podem aproveitar dados, linguagens de programação e compreensão de linguagem natural para construir sistemas de IA generativos com conhecimento especializado.
Esses sistemas especializados são a chave para desbloquear a viabilidade comercial de aplicações LLM que buscam fornecer valor além do que está diretamente disponível em fornecedores de plataformas como OpenAI e Anthropic.
A geração de código é apenas uma das técnicas que se tornou possível graças à recente geração de grandes modelos de linguagem comercialmente disponíveis.
Se você tiver ideias sobre como LLMs podem ser comercializados ou gostaria de conversar sobre IA, não hesite em entrar em contato pelo LinkedIn ou GitHub . Tive muitas conversas esclarecedoras com leitores ao longo do ano passado e espero muitas mais!