As tecnologias GenAI são definitivamente um item de tendência em 2023 e 2024 e, como eu trabalho para a Tikalque publica seu próprio relatório anual radar e tendências tecnológicas relatório, o LLM e a genAI não escaparam da minha atenção. Como desenvolvedor, frequentemente consulto chatbots de IA generativa para me ajudar a resolver todos os tipos de erros de TypeScript e problemas misteriosos de linting, uso ferramentas de assistência genAI em meu IDE e para melhorar meus PRs. Essa tecnologia pode mudar sua vida.
Como pessoal técnico e, definitivamente, nós, desenvolvedores de software, essa nova tendência abre a oportunidade de integrar esses recursos a todos os projetos em que trabalhamos, e vejo meus amigos e colegas explorando essas opções, o que me levou à decisão: eu também deveria fazer isso!
E eu tinha exatamente o projeto:
Sou um dançarino amador que dança em uma trupe de dança amadora. Muitas vezes me pergunto como os artistas amadores podem explorar o vasto mundo dos eventos culturais locais e mundiais para poder entrar em contato e talvez receber o convite desejado para se apresentar. Não temos os recursos, as conexões e o conhecimento de tudo o que está disponível. É claro que existem mecanismos de pesquisa e sites especializados, mas é preciso saber o que e como pesquisar, por isso decidi usar o genAI para obter recomendações.
Etapa 1 - Isso pode ser feito?
A verificação da viabilidade de um mecanismo de recomendação usando um dos LLMs incluiu a abertura de contas em vários serviços de bate-papo da genAI e a realização da mesma pergunta:
Somos uma amador Dança folclórica israelense grupo, incluindo dançarinos em cadeiras de rodas. Estamos procurando por cultural e folclore eventos e festivais em Europa para entrar em contato sobre a possibilidade de recebermos um convite para nos apresentarmos, desde que cobrir nossas despesas. Você poderia recomendar alguns?
Os resultados no primeiro semestre de 2024 variaram entre os diferentes serviços de bate-papo:
-
- Me direcionou para sites dedicados que eu poderia consultar para obter resultados
- Me deu resultados reais
Dos que retornaram resultados, classifiquei a qualidade dos resultados de acordo com a relevância e a precisão, e cheguei a OpenAI GPT-3 como opção.
Etapa 2 - É suficiente?
Lembrando que até mesmo um dos assistentes de bate-papo na Etapa 1 sugeriu que eu verificasse outros sites, e se eu pudesse incorporar alguns desses dados nos resultados?
Considerando que também sou dependente de quem treinou o modelo e quando ele foi treinado, eu queria que minhas recomendações fossem baseadas em mais fontes de dados e sabia que isso poderia ser feito com o RAG. Antes de tudo, o que é o RAG?
Geração Aumentada de Recuperação (RAG)
RAG é o processo de enriquecimento e otimização dos resultados que você recebe do LLM adicionando dados "externos". Se eu puder adicionar resultados baseados na mesma pesquisa em fontes de dados externas (de sites dedicados), poderei expandir a variedade dos resultados que meu aplicativo fornecerá.
Para fazer isso, você precisará de:
-
- Fontes de dados externas - Para meu experimento, criei uma conta de avaliação para API de eventos do predictHQ
- Armazenar meus dados externos é um mecanismo que permite a pesquisa por similaridade e não uma correspondência exata
Tornar os dados acessíveis para o RAG
Depois de analisar os dados, sua aparência e os recursos que eles contêm, é hora de selecionar os recursos de dados que você gostaria de usar e torná-los utilizáveis para o RAG.
Para permitir uma pesquisa de similaridade, precisaríamos transformar nossos dados em um formato que seja pesquisável e comparável. Como não estamos procurando correspondências exatas, mas correspondências semelhantes, há duas técnicas muito comuns para isso:
Técnica RAG | Detalhes |
Pesquisa vetorial (também conhecida como RAG comum) | As informações e a pergunta são transformadas em vetores de números (pontos flutuantes).
Os cálculos matemáticos são usados para determinar a similaridade entre a pergunta e os dados |
GraphRAG | As informações e a pergunta são transformadas em gráfico vértices e bordas.
As relações do gráfico são comparadas quanto à similaridade |
O processo de criação da representação dos dados é chamado de incorporaçãoNeste artigo, vamos nos concentrar na pesquisa vetorial.
Métrica de similaridade
Há três opções comuns (em resumo):
-
- Produto de pontos: Cálculo da similaridade com base no produto dos valores em cada vetor
- Cosseno: Com base no ângulo entre os vetores
- L2_norm: Distância euclidiana entre os vetores, com base no ângulo entre os vetores e o comprimento de cada vetor
Leia mais sobre o opções de similaridade de vetores.
Etapa 3 - Como faço isso?
Antes de nos aprofundarmos em como vamos fazer isso e em alguns códigos e capturas de tela reais, vamos ver como essa arquitetura seria construída e como o Couchbase entra em cena:
O que isso significa na prática é:
-
- Ingestão app para:
- Obter dados da API externa
- Criar embeddings vetoriais
- Carregar dados em uma coleção do Couchbase
- Criar um índice de pesquisa vetorial no Couchbase
- Solicitação imediata para:
- Solicitar resultados aos dados do Couchbase
- Adicione os resultados da pesquisa de vetores ao prompt do LLM como contexto
- Devolver os resultados coesos aos usuários
- Ingestão app para:
Aplicação por ingestão
Esse processo foi provavelmente o mais longo, pois passei um tempo criando incorporações em diferentes campos e em diferentes formatos. Para simplificar, acabei optando por usar apenas as informações geográficas que coletei:
1 2 3 4 5 |
de langchain_openai importação Aberturas do OpenAIEmbeddings embeddings_model = Aberturas do OpenAIEmbeddings(modelo="text-embedding-3-small") texto = f"Geo Info: {row['geo_info']}" incorporação = embeddings_model.consulta_incorporada(texto) |
Para criar a incorporação, optei por usar as incorporações textuais como ponto de partida, ou seja, "comparar" a representação de texto com a representação de texto. A incorporação em si inclui cerca de 1.500 números (esse é o menor deles).
O código em si não é extremamente complexo, mas pode ser demorado. Criar uma incorporação para 5.000 eventos levou aproximadamente uma hora em meu MacBook pro M1 de 16 GB.
O código completo usando o pandas2 pode ser encontrado em este repositório.
Coleção e índice de pesquisa do Couchbase
Para poder pesquisar dados semelhantes entre a pergunta e os resultados que preparamos com base em uma API externa, vamos
-
- Criar uma coleção do Couchbase
- Faça upload dos dados preparados para uma coleção do Couchbase incluindo os embeddings
- Crie um índice de pesquisa nos campos de incorporação escolhendo o algoritmo de similaridade de vetores para comparar vetores
Nova coleção do Couchbase
Para meu aplicativo, optei por usar o serviço hospedado do Couchbase - Capella, cuja configuração é muito fácil. Eu me registrei, escolhi o serviço de nuvem e criei um novo projeto.
Clicando em meu projeto e navegando até a guia Ferramentas de dados, posso agora criar uma nova coleção para os dados que preparei:
Para carregar os dados que preparei, há várias opções: como o tamanho do arquivo era bastante grande, optei por usar o cbimport utilitário para fazer isso.
1 |
./cbimport json --agrupamento bases de sofá:// --username --password --bucket --scope-collection-exp "." --dataset for_collection.json --generate-key '%id%' --cacert --format lines |
Observe que escolhi a opção ID dos documentos JSON para ser o documento chave na coleção.
Lembre-se que, antes de fazer isso, você precisa:
-
- Criar usuário/senha de acesso ao banco de dados com privilégio de gravação, no mínimo
- Abra o cluster para chamadas de seu host
- Faça o download de um certificado para o cluster
O esquema de documento inferido mostra que o incorporação foi criado com o tipo de matriz de números:
Para permitir a pesquisa de similaridade de vetores, vamos criar o índice de pesquisa navegando até a guia Search (Pesquisa).
Obviamente, devemos selecionar o campo de incorporação para o índice de pesquisa, mas observe que há mais parâmetros a serem definidos:
Já discutimos qual é a métrica de similaridade, basta observar que o Couchbase suporta l2_norm (ou seja, distância euclidiana) e produto escalar.produto de pontos"o que pode ser mais vantajoso para meu sistema de recomendação.
A próxima etapa é escolher campos adicionais dos documentos que seriam retornados sempre que um vetor fosse dimerizado de forma semelhante à pergunta:
Se você não adicionar pelo menos um campo, seu aplicativo falhará porque não haverá nenhum dado retornado.
Aqui está, a seleção dos campos de índice:
Chegamos a um ponto crucial em nosso projeto, agora podemos começar a executar a pesquisa de similaridade nos dados que preparamos, mas talvez você não consiga fazer uma pesquisa de similaridade que funcione na primeira tentativa. Descreverei algumas dicas para obter resultados da sua pesquisa de similaridade ou para verificar por que não está obtendo resultados:
-
- Certifique-se de que sua técnica de incorporação, ao criar os dados e preparar uma pesquisa, seja idêntica
- Comece com um formato simples e previsível para as informações que você deseja comparar. Por exemplo, , ,
- Certifique-se de que não haja informações extras que sejam acidentalmente anexadas aos dados para os quais você está criando embeddings (por exemplo, eu tinha quebras de linha)
- Certifique-se de que a pesquisa de correspondência exata funcione:
- Pesquisa dos dados exatos para os quais você criou embeddings
- Compare o vetor de incorporação para garantir que sejam criadas incorporações idênticas na parte de geração e pesquisa (a depuração será útil aqui). Se houver alguma diferença, volte às etapas 1-3
Depois de ter uma busca por similaridade que funcione, adicione gradualmente mais campos, altere formatos, incorporações e tudo o mais que achar que está faltando.
Lembre-se de que qualquer alteração nos embeddings significa:
-
- Recriando os embeddings
- Carregamento dos dados de alterações em uma coleção truncada
- Alterar o índice de pesquisa, se necessário
- É necessário alterar o código
Essas etapas podem ser demoradas, especialmente a criação de incorporações, portanto, talvez você queira começar por elas:
-
- Uma pequena parte de seus documentos
- Uma técnica de incorporação pequena/rápida
Aplicação para LLM e RAG
O que nosso aplicativo precisa fazer é:
-
- Peça ao Couchbase para encontrar resultados semelhantes à pergunta do usuário
- Adicione os resultados ao contexto da pergunta do LLM
- Faça uma pergunta ao LLM
Para simplificar, criei esse código em Python como um notebook Jupyter, que você pode encontrar neste repositório. Usei as seguintes bibliotecas para fazer isso:
-
- Couchbase: Conectar e autenticar no meu cluster Capella
- LangChain: uma estrutura para o desenvolvimento de aplicativos alimentados por grandes modelos de linguagem (LLMs), para:
- Embeddings
- Usando o Couchbase como um armazenamento de vetores
- "Conversando" com a OpenAI
- LangGraph: Uma estrutura para criar aplicativos LLM com vários atores e com estado, para criar um fluxo do aplicativo LLM
Se você tem lido sobre, e até mesmo tentado criar seu próprio aplicativo LLM, provavelmente está familiarizado com o LangChain, que é um conjunto de bibliotecas que permite escrever, criar, implantar e monitorar um aplicativo. Ele tem muitos agentes e extensões que permitem integrar diferentes partes ao seu código, como uma API de terceiros, um banco de dados, uma pesquisa na Web e muito mais.
Ultimamente, também tomei conhecimento do LangGraph, da casa do LangChain, que permite que você, como desenvolvedor, crie topologias mais complexas do aplicativo LLM com condições, loops (o gráfico não precisa ser um DAG!), interação com o usuário e, talvez, o recurso mais procurado: Manter o estado.
Antes de examinarmos o código, vamos dar uma olhada no arquivo de ambiente (.env) para ver quais credenciais e outros dados confidenciais são necessários:
1 2 3 4 5 6 7 8 9 10 11 |
LANGSMITH_KEY=langsmithkey OPENAI_API_KEY=openaikey PROJETO LANGCHAIN=meu projeto COUCHBASE_CONNECTION_STRING=couchbase://mycluster.com USUÁRIO DA BASE DE DADOS=meu usuário COUCHBASE_PASS=mypass COUCHBASE_BUCKET=meu cesto ESCOPO DA BASE DE SOFÁ=miscópio COLEÇÃO DE SOFÁS=mycollection ÍNDICE DE PESQUISA DA BASE DE SOFÁ=mysearchindex LANGCHAIN_API_KEY=langchainapikey |
O estado de cada nó do gráfico é:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
de langógrafo.gráfico importação add_messages, StateGraph de typing_extensions importação TypedDict de digitação importação Anotado de langógrafo.ponto de controle.sqlite importação SqliteSaver classe Estado(TypedDict): # As mensagens têm o tipo "list". A função `add_messages # na anotação define como essa chave de estado deve ser atualizada # (nesse caso, ele acrescenta mensagens à lista, em vez de substituí-las) mensagens: Anotado[lista, add_messages] tipo_de_evento: str localização: str rótulos: str construtor de gráficos = StateGraph(Estado) |
É importante observar que, a menos que você defina um redutor, o estado será substituído entre cada nó do gráfico. O membro messages da classe state tem um redutor que anexará as novas mensagens à lista.
Para conectar-se ao Couchbase e usá-lo como um armazenamento de vetores para o aplicativo LLM, autenticamos o cluster e passamos a conexão do cluster para o objeto LangChain do armazenamento de vetores:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
de langchain_openai importação Aberturas do OpenAIEmbeddings importação os de couchbase.agrupamento importação Aglomerado de couchbase.opções importação ClusterOptions de couchbase.autenticação importação PasswordAuthenticator de langchain_couchbase importação CouchbaseVectorStore COUCHBASE_CONNECTION_STRING = os.ambiente["COUCHBASE_CONNECTION_STRING"] COUCH_USER = os.ambiente["COUCHBASE_USER"] COUCH_PASS = os.ambiente["COUCHBASE_PASS"] BUCKET_NAME = os.ambiente["COUCHBASE_BUCKET"] SCOPE_NAME = os.ambiente["COUCHBASE_SCOPE"] NOME_DA_COLEÇÃO = os.ambiente["COUCHBASE_COLLECTION" (COLEÇÃO DE BASES DE SOFÁ)] NOME_DO_ÍNDICE_DE_PESQUISA = os.ambiente["COUCHBASE_SEARCH_INDEX"] autenticação = PasswordAuthenticator(COUCH_USER, COUCH_PASS) opções = ClusterOptions(autenticação) agrupamento = Aglomerado(COUCHBASE_CONNECTION_STRING, opções) incorporação = Aberturas do OpenAIEmbeddings(modelo="text-embedding-3-small") vector_store = CouchbaseVectorStore( agrupamento=agrupamento, nome_do_balde=BUCKET_NAME, nome_do_escopo=SCOPE_NAME, nome_da_coleção=NOME_DA_COLEÇÃO, incorporação=incorporação, nome_do_índice=NOME_DO_ÍNDICE_DE_PESQUISA, ) |
Há dois detalhes importantes que devem ser levados em conta:
-
- A incorporação no aplicativo deve ser idêntico ao usado na parte de ingestão
- O nome padrão do campo de incorporação é "embedding"; se o nome do respectivo campo for diferente em seu índice de pesquisa, você precisará defini-lo durante a instanciação do CouchbaseVectorStore (embedding_key)
Neste momento, você está pronto para escrever seu aplicativo LangGraph e usar o Couchbase como armazenamento de vetores. Vamos montar tudo: cada gráfico precisa de nós, ponto inicial e bordas direcionadas.
Nosso gráfico buscará dados do armazenamento de vetores e continuará a adicionar essas informações ao contexto do prompt do LLM.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
de langchain_core.avisos importação ChatPromptTemplate de langchain_core.analisadores de saída importação StrOutputParser de langchain_openai importação ChatOpenAI lm = ChatOpenAI(modelo="gpt-3.5-turbo") modelo = """Você é um bot útil que tem a finalidade de encontrar eventos para artistas que procuram locais nos EUA. Se você não puder responder com base no contexto fornecido, responda com uma resposta genérica Resposta. Responda à pergunta da forma mais verdadeira possível usando o contexto abaixo: {context} Por favor, formate também o resultado no formato Markdown. Question: {question}""" imediato = ChatPromptTemplate.from_template(modelo) cadeia_de_geração = imediato | lm | StrOutputParser() def chatbot(estado: Estado): resposta = cadeia_de_geração.invocar({"contexto": estado['mensagens'], "pergunta": f"Somos um grupo amador de {state['event_type']} que está procurando {state['labels']} festivais em {state['location']}, você poderia recomendar alguns para entrarmos em contato?"}) estado['mensagens'].anexar(resposta) retorno estado def search_couchbase(estado: Estado): consulta = f"Geo Info: {state['location']}" retriever = vector_store.as_retriever() resultados = retriever.invocar(consulta) para resultado em resultados: texto = f"Title: {result.metadata['title']}/{result.metadata['alternate_titles_flat']} - {result.metadata['description']} from {result.metadata['start']} to {result.metadata['end']}, location {result.metadata['geo_info']}. Rótulos {result.metadata['labels_flat']}, categoria {result.metadata['category']}" estado['mensagens'].anexar(texto) retorno estado construtor de gráficos.add_node("vector_search", search_couchbase) construtor de gráficos.add_node("chatbot", chatbot) construtor de gráficos.set_entry_point("vector_search") construtor de gráficos.add_edge("vector_search", "chatbot") construtor de gráficos.set_finish_point("chatbot") memória = SqliteSaver.from_conn_string(":memória:") gráfico = construtor de gráficos.compilar(ponteiro de verificação=memória) |
No código acima, isso se traduz em dois nós:
-
- vector_search (ponto de entrada)
- chatbot (ponto de chegada)
Como uma imagem vale mais que mil palavras, usei o código a seguir para visualizar o gráfico para você:
1 2 3 4 5 6 7 8 9 10 |
de IPython.exibição importação Imagem, exibição de langchain_core.executáveis.gráfico importação CurveStyle, MermaidDrawMethod, NodeStyles exibição( Imagem( gráfico.get_graph().draw_mermaid_png( draw_method=MermaidDrawMethod.API, ) ) ) |
O resultado foi o seguinte desenho:
Para obter mais opções de visualização no langGraph, consulte este Notebook Jupyter da LangGraph.
Perguntar ao vector store significa pesquisar dados com localização semelhante. Você pode notar que o formato da consulta é o mesmo do texto incorporado; os resultados são adicionados ao estado para serem usados no próximo nó.
O nó do chatbot obtém as informações das mensagens e as incorpora à pergunta do prompt para o LLM.
Observe que o estado é mantido no banco de dados sqlite na memória. Para usar o gráfico, fique à vontade para usar o exemplo a seguir:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
de aleatório importação randint de IPython.núcleo.exibição importação Marcação session_id = randint(1, 10000) configuração = {"configurável": {"thread_id": session_id}} local_de_entrada = "kansas" categoria_de_entrada = "jaz" rótulos_de_entrada = "grange" # Stream the graph, cada saída será impressa quando estiver pronta para evento em gráfico.fluxo({"event_type": categoria_de_entrada, "localização": local_de_entrada, "rótulos": rótulos_de_entrada}, configuração): para valor em evento.valores(): se len(valor['mensagens']) > 0: exibição(Marcação(valor['mensagens'][-1])) |
E aí está, você criou um aplicativo LLM para recomendar eventos culturais para grupos amadores solicitarem convites.
Resumo
Começar a usar os aplicativos do LLM é empolgante e, na minha humilde opinião, é um aumento divertido, empolgante e factível devido à sua natureza rápida; no entanto, tornar nosso aplicativo melhor e mais robusto esconde mais desafios.
Neste artigo, concentrei-me no desafio de aproveitar o conhecimento do nosso modo com dados externos por meio da técnica ou RAG e em como você pode aproveitar o Couchbase para fazer isso.
É importante lembrar que a criação de embeddings que o aplicativo LLM encontrará na pesquisa de vetores pode não funcionar em sua primeira tentativa. Verifique a formatação, tente começar com embeddings simples e use a depuração o máximo possível.
Também demonstrei os recursos do LangGraph da LangChain, que permite criar decisões e fluxos complexos no aplicativo LLM.
Aproveite sua jornada com as inscrições para o LLM.