Modelos de idiomas grandesA tecnologia de inteligência artificial, popularmente conhecida como LLMs, é um dos tópicos mais debatidos no setor de IA. Todos nós estamos cientes das possibilidades e dos recursos do ChatGPT da OpenAI. Eventualmente, o uso desses LLMs em nosso benefício descobre muitas novas possibilidades usando os dados.
Mas não se pode esperar tudo dos LLMs devido às limitações de seu projeto e a muitos outros fatores. Mas usando o conceito de Pesquisa de vetores nos oferece um novo tipo de aplicativo chamado Aplicativos RAG (Retrieval Augmented Generation). Então, vamos ver o que são aplicativos RAG, como eles podem ser usados para resolver novos problemas, como podem ser desenvolvidos usando o Couchbase e ter uma visão geral detalhada de como o Vector Search ajuda a criar esses aplicativos.
Antes de entrarmos no histórico do aplicativo, aqui está um diagrama arquitetônico do que estamos construindo e como o LangChain está ligado a ele:
Geração aumentada de recuperação (RAG)
O RAG fornece uma maneira de aumentar o resultado de um LLM com informações direcionadas sem modificar o modelo subjacente em si, de modo que as informações direcionadas podem ser mais atualizadas do que o LLM, bem como específicas para uma determinada organização e setor. Isso significa que o IA generativa O sistema pode fornecer respostas contextualmente mais adequadas aos prompts, além de basear essas respostas em dados extremamente atuais. Vamos entender esse conceito usando um exemplo da vida real.
Digamos que você pertença à organização X, que tem uma tonelada de dados armazenados em seu banco de dados, e esteja encarregado de desenvolver um aplicativo que solicite a entrada do usuário e forneça a saída com base nos dados presentes em seu banco de dados.
Inicialmente, você pode pensar que isso parece fácil, certo? Se você conhece os LLMs e sabe como aproveitá-los de acordo com suas necessidades, então é uma tarefa simples. Basta escolher os LLMs da OpenAI ou os modelos Llama e Mistral, se quiser ser econômico, e simplesmente enviar as perguntas dos usuários para o LLM e obter os resultados.
Mas há um grande problema aqui...
Por exemplo, vamos supor que você esteja usando o Llama 2 LLM 8B tipo.
Agora, esse modelo é treinado com quase todos os dados presentes na Internet pública. Você faz qualquer pergunta, até mesmo perguntas sobre sua organização X, e ele lhe dá a resposta correta mais próxima.
Agora vamos fazer uma pequena alteração na declaração de seu problema. A tonelada de dados que está presente em seu banco de dados não é mais pública, mas privada. Isso significa que o Llama 2 não tem conhecimento dos seus dados e você não dará mais as respostas corretas.
Tendo o cenário acima em mente, considere a pergunta do usuário, "Quais são as atualizações do componente C na organização X?"
Então, como resolver isso?
Você pode pensar: por que não passamos os dados completos presentes no banco de dados, juntamente com o prompt, para que o LLM possa usar os dados como contexto e responder à pergunta? Mas aqui está o grande problema: todos os LLMs têm uma restrição chamada limite de token. Sem entrar em detalhes sobre o que são tokens, etc., por enquanto considere 1 token == 1 palavra.
Infelizmente, o limite de tamanho de token do Llama 2 é de 4096 tokens (palavras). Suponha que todos os dados presentes em seu banco de dados tenham 10 milhões de palavras, então será impossível passar todos os dados para fins de contexto.
A solução para o problema acima é chamada de RAG. No RAG, selecionamos uma proporção de dados presentes no seu banco de dados que está intimamente relacionada à consulta do usuário. O tamanho da proporção é tal que:
Tamanho da proporção < limite de token
Agora, passamos os dados extraídos como contexto junto com a consulta e obtemos bons resultados. Isso é o RAG. Mas como obtemos a proporção de dados que está intimamente relacionada à consulta do usuário e, ao mesmo tempo, o tamanho não excede o tamanho do token? Isso é resolvido usando o conceito de Pesquisa de vetores.
O que é pesquisa vetorial?
A pesquisa vetorial aproveita o aprendizado de máquina (ML) para capturar o significado e o contexto de dados não estruturados, incluindo texto e imagens, transformando-os em uma representação numérica. Frequentemente usada para pesquisa semântica, a pesquisa vetorial encontra dados semelhantes usando algoritmos de vizinho mais próximo aproximado (ANN).
O Couchbase versão 7.6.0 e superior vem com isso Recurso de pesquisa de vetores. O importante aqui é que não são necessárias bibliotecas, módulos e configurações externas. Basta ter pelo menos 1 pesquisa O nó faz o trabalho.
O Couchbase usa internamente o Estrutura FAISS fornecido pelo Facebook para realizar a pesquisa de vetores.
Criação de um aplicativo RAG
Agora vamos ao que realmente interessa: desenvolver um aplicativo RAG de ponta a ponta usando o Funcionalidade de pesquisa vetorial do Couchbase.
Neste passo a passo, vamos desenvolver um converse com seus PDFs aplicação.
Antes de prosseguir, há várias maneiras de criar o aplicativo. Uma dessas maneiras é usar a estrutura LangChain, que usaremos para desenvolver o aplicativo RAG.
Aplicativo 1: construção usando a estrutura LangChain
Etapa 1: Configuração de um banco de dados Couchbase
Você pode configurar o servidor Couchbase no EC2, na máquina virtual, em sua máquina local, etc.
Siga este link para configurar o cluster do Couchbase. Certifique-se de que esses serviços estejam ativados; os outros são opcionais:
-
- Dados
- Pesquisa
Observação: Certifique-se de instalar Servidor Couchbase versão 7.6.0 ou superior para realizar a pesquisa vetorial. Além disso, desenvolveremos esse aplicativo usando Python no ambiente Mac OS.
Quando o cluster estiver em funcionamento, crie um novo projeto <project_name> e criar uma nova chamada de arquivo Python app.py.
Agora, no terminal do projeto, execute o comando abaixo:
1 |
tubulação instalar --atualização --silencioso langchain langchain-openai langchain-couchbase sentença-transformadores |
Agora, vá para a interface do usuário e crie um bucket chamado projeto. Para este passo a passo, usaremos o padrão escopo e coleção.
-
- Saiba mais sobre o balde, escopo e coleções nos documentos.
Agora, há diferentes maneiras de gerar embeddings vetoriais. A mais popular delas é a OpenAI, que usaremos para gerar embeddings de vetores.
Copie o código abaixo para app.py:
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 |
importação getpass importação os de langchain_couchbase.lojas de vetores importação CouchbaseVectorStore de langchain_openai importação Aberturas do OpenAIEmbeddings de data e hora importação timedelta de couchbase.autenticação importação PasswordAuthenticator de couchbase.agrupamento importação Aglomerado de couchbase.opções importação ClusterOptions os.ambiente["OPENAI_API_KEY"] = getpass.getpass("Chave da API da OpenAI:") COUCHBASE_CONNECTION_STRING = "couchbase://localhost" NOME DE USUÁRIO DO BANCO = "Administrador" DB_PASSWORD = "Senha" BUCKET_NAME = "projeto" SCOPE_NAME = "_default" NOME_DA_COLEÇÃO = "_default" NOME_DO_ÍNDICE_DE_PESQUISA = "vector-index" autenticação = PasswordAuthenticator(NOME DE USUÁRIO DO BANCO, DB_PASSWORD) opções = ClusterOptions(autenticação) agrupamento = Aglomerado(COUCHBASE_CONNECTION_STRING, opções) agrupamento.wait_until_ready(timedelta(segundos=5)) incorporações = Aberturas do OpenAIEmbeddings() |
Se você hospedou o Couchbase em uma VM, certifique-se de substituir a palavra localhost para o ip público da VM.
Etapa 2: Importar o índice de pesquisa
O recurso de pesquisa vetorial no Couchbase requer um índice de pesquisa para ser executado. Há várias maneiras de criar o índice, mas para facilitar e agilizar as coisas, abaixo está o JSON do índice. Copie o código abaixo e cole-o:
- IU > Pesquisa > Adicionar índice (canto superior direito) > Importação
Index.json
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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
{ "name" (nome): "vector-index", "tipo": "fulltext-index", "params": { "doc_config": { "docid_prefix_delim": "", "docid_regexp": "", "mode" (modo): "type_field", "type_field": "tipo" }, "mapeamento": { "default_analyzer": "padrão", "default_datetime_parser": "dateTimeOptional", "default_field": "_all", "default_mapping": { "dinâmico": verdadeiro, "habilitado": verdadeiro, "propriedades": { "metadata": { "dinâmico": verdadeiro, "habilitado": verdadeiro }, "embedding" (incorporação): { "habilitado": verdadeiro, "dinâmico": falso, "campos": [ { "dims": 1536, "índice": verdadeiro, "name" (nome): "embedding" (incorporação), "similaridade": "dot_product", "tipo": "vetor", "vector_index_optimized_for": "recall" } ] }, "texto": { "habilitado": verdadeiro, "dinâmico": falso, "campos": [ { "índice": verdadeiro, "name" (nome): "texto", "loja": verdadeiro, "tipo": "texto" } ] } } }, "default_type": "_default", "docvalues_dynamic": falso, "index_dynamic": verdadeiro, "store_dynamic": verdadeiro, "type_field": "_type" }, "loja": { "indexType": "scorch" (queimar), "segmentVersion": 16 } }, "sourceType": "gocbcore", "sourceName": "projeto", "sourceParams": {}, "planParams": { "maxPartitionsPerPIndex": 103, "indexPartitions": 10, "numReplicas": 0 } } |
Etapa 3: Carregando os dados
Agora é hora de armazenar todos os dados dos PDFs como partes, juntamente com suas incorporações vetoriais no banco de dados.
Observação: Leia isto blog detalhado sobre fragmentação, coleta de dados, etc.. É altamente recomendável ler o blog para ter uma compreensão clara do que será abordado nas etapas posteriores.
Há bibliotecas para diferentes tipos de documentos que você deseja carregar. Por exemplo, se seus dados de origem estiverem em .txt e, em seguida, adicione o seguinte código ao seu formato app.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
de langchain_community.carregadores de documentos importação TextLoader de langchain_text_splitters importação CharacterTextSplitter carregador = TextLoader("caminho de seu arquivo de texto") documentos = carregador.carregar() divisor de texto = CharacterTextSplitter(tamanho do bloco=500, chunk_overlap=0) documentos = divisor de texto.split_documents(documentos) vector_store = CouchbaseVectorStore.from_documents( documentos=documentos, incorporação=incorporações, agrupamento=agrupamento, nome_do_balde=BUCKET_NAME, nome_do_escopo=SCOPE_NAME, nome_da_coleção=NOME_DA_COLEÇÃO, nome_do_índice=NOME_DO_ÍNDICE_DE_PESQUISA, ) |
Mas suponha que seu tipo de fonte seja PDF:
1 |
tubulação instalar pypdf |
1 2 3 4 |
de langchain_community.carregadores de documentos importação PyPDFLoader carregador = PyPDFLoader("caminho para seu arquivo PDF") páginas = carregador.load_and_split() |
O LangChain oferece suporte não apenas para PDFs, mas também para vários tipos, como, por exemplo, PDFs:
-
- CSV
- HTML
- JSON
- Markdown e muito mais
Saiba mais sobre Carregadores de documentos Langchain.
Etapa 4: Inferindo resultados
Agora estamos prontos para enviar consultas ao nosso aplicativo:
1 2 3 |
consulta = "" resultados = vector_store.similarity_search(consulta) impressão(resultados[0]) |
Aplicativo 2: Criando o aplicativo do zero
Antes de começar, conforme descrito na seção anterior, gire o cluster com um bucket chamado projeto. Além disso, siga a Etapa 2 da seção anterior, certificando-se de importar o índice de pesquisa.
Etapa 1: Configuração do Couchbase
Se você estiver usando os padrões, então seu app.py deve ter a seguinte aparência:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
de couchbase.autenticação importação PasswordAuthenticator de couchbase.agrupamento importação Aglomerado de couchbase.opções importação (ClusterOptions, ClusterTimeoutOptions,Opções de consulta) de data e hora importação timedelta nome de usuário = "Administrador" senha = "senha" nome_do_balde = "projeto" nome_do_escopo = "_default" nome_da_coleção = "_default" autenticação = PasswordAuthenticator( nome de usuário, senha, ) agrupamento = Aglomerado(f'couchbase://localhost}', ClusterOptions(autenticação)) agrupamento.wait_until_ready(timedelta(segundos=5)) cb = agrupamento.balde(nome_do_balde) cb_coll = cb.escopo(nome_do_escopo).coleção(nome_da_coleção) |
Agora que os links da coleção do Couchbase e o índice de pesquisa estão prontos, vamos passar para a parte de carregamento de dados.
Etapa 2: Carregamento de dados
Para manter as coisas modularizadas, crie um novo arquivo Python chamado load.py.
Há várias maneiras de extrair dados de PDFs. Para facilitar, vamos usar o pypdf do pacote Langchain:
load.py
1 2 3 4 |
de langchain_community.carregadores de documentos importação PyPDFLoader carregador = PyPDFLoader("caminho para seu pdf") páginas = carregador.load_and_split() |
Agora isso páginas é uma lista de blocos de texto extraídos do pdf. Vamos mesclar todo o conteúdo em uma única variável:
1 2 3 |
texto = "" para página em páginas: texto += página['page_content'] |
Antes de fazer o chunking, precisamos configurar o modelo de incorporação. Neste caso, vamos usar o modelo sentence-transformers/paraphrase-distilroberta-base-v1 de abraçar o rosto.
Esse modelo fornece incorporação de vetores de 768 dimensões.
load.py
1 2 |
de sentence_transformers importação SentenceTransformer modelo = SentenceTransformer('sentence-transformers/paraphrase-distilroberta-base-v1') |
Agora nosso modelo está pronto. Vamos enviar os documentos. Podemos usar o pacote divisor de texto de caracteres recursivo de Langchain.
Esse pacote fornece pedaços do tamanho fornecido e, em seguida, encontraremos a incorporação do vetor para cada pedaço usando o modelo acima e enviaremos o documento para o banco de dados.
Portanto, o documento terá dois campos:
1 2 3 4 |
{ "Dados" : <pedaço>, "Vetor_dados" : <vetor incorporação de o pedaço> } |
load.py
1 2 3 4 5 6 7 8 9 |
de langchain_text_splitters importação RecursiveCharacterTextSplitter divisor de texto = RecursiveCharacterTextSplitter( tamanho do bloco=500, chunk_overlap=20, função_de_comprimento=len, is_separator_regex=Falso, ) pedaços = divisor de texto.criar_documentos(texto) |
Agora que nossos blocos estão prontos, podemos encontrar os embeddings para cada bloco e enviá-los para o banco de dados.
load.py
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 |
de json importação Codificador JSONE de FlagEmbedding importação BGEM3FlagModel de sentence_transformers importação SentenceTransformer importação numérico como np importação json importação ast classe NumpyEncoder(Codificador JSONE): def padrão(autônomo, obj): se isinstância(obj, np.ndarray): retorno obj.Lista de usuários() retorno Codificador JSONE.padrão(autônomo, obj) docid_contador = 1 para sentença em pedaços: emb = modelo.codificar(str(sentença.conteúdo_da_página)) incorporação = np.matriz(emb) np.set_printoptions(suprimir=Verdadeiro) json_dump = json.lixeiras(incorporação, cls=NumpyEncoder) documento = { "dados":str(sentença.conteúdo_da_página), "vector_data":ast.literal_eval(json_dump) } docid = 'docid:' + str(nome) + str(contador docid) contador docid+=1 tentar: resultado = cb_coll.upsert(docid, documento) impressão(resultado.cas) exceto Exceção como e: impressão(e) |
Onde será que o cb_coll
veio? É o conector de coleção que criamos em app.py. Para passar, vamos embrulhar tudo isso em load.py para uma função que aceita cb_coll
como parâmetro.
Então, finalmente, seu load.py deve ter a seguinte aparência:
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 39 40 41 42 43 44 45 46 47 48 49 50 51 |
de langchain_community.carregadores de documentos importação PyPDFLoader de langchain_text_splitters importação RecursiveCharacterTextSplitter de json importação Codificador JSONE de FlagEmbedding importação BGEM3FlagModel de sentence_transformers importação SentenceTransformer importação numérico como np importação json importação ast classe NumpyEncoder(Codificador JSONE): def padrão(autônomo, obj): se isinstância(obj, np.ndarray): retorno obj.Lista de usuários() retorno Codificador JSONE.padrão(autônomo, obj) def Carregar_dados(cb_coll): carregador = PyPDFLoader("caminho para seu pdf") páginas = carregador.load_and_split() texto = "" para página em páginas: texto += página['page_content'] divisor de texto = RecursiveCharacterTextSplitter( tamanho do bloco=500, chunk_overlap=20, função_de_comprimento=len, is_separator_regex=Falso, ) pedaços = divisor de texto.criar_documentos(texto) docid_contador = 1 para sentença em pedaços: emb = modelo.codificar(str(sentença.conteúdo_da_página)) incorporação = np.matriz(emb) np.set_printoptions(suprimir=Verdadeiro) json_dump = json.lixeiras(incorporação, cls=NumpyEncoder) documento = { "dados":str(sentença.conteúdo_da_página), "vector_data":ast.literal_eval(json_dump) } docid = 'docid:' + str(nome) + str(contador docid) contador docid+=1 tentar: resultado = cb_coll.upsert(docid, documento) impressão(resultado.cas) exceto Exceção como e: impressão(e) |
Agora vamos para app.pyImporte isso e chame o load_data
função.
app.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
de couchbase.autenticação importação PasswordAuthenticator de couchbase.agrupamento importação Aglomerado de couchbase.opções importação (ClusterOptions, ClusterTimeoutOptions,Opções de consulta) de data e hora importação timedelta de ./carregar importação Carregar_dados nome de usuário = "Administrador" senha = "senha" nome_do_balde = "projeto" nome_do_escopo = "_default" nome_da_coleção = "_default" autenticação = PasswordAuthenticator( nome de usuário, senha, ) agrupamento = Aglomerado(f'couchbase://localhost}', ClusterOptions(autenticação)) agrupamento.wait_until_ready(timedelta(segundos=5)) cb = agrupamento.balde(nome_do_balde) cb_coll = cb.escopo(nome_do_escopo).coleção(nome_da_coleção) Carregar_dados(cb_coll) |
Isso enviará os documentos no formato necessário e nosso índice de pesquisa também sofrerá alterações. Agora é hora de fazer a pesquisa vetorial.
Etapa 3: Pesquisa de vetores
No Couchbase, você tem várias maneiras de fazer isso, sendo uma delas o método Curl.
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 |
importação subprocesso importação json de configuração importação COUCHBASE_URL def vector_search_chat1(nome_do_índice, search_vector, k, cb_coll,consulta): impressão("consulta :",consulta) argumentos = {"nome_do_índice":nome_do_índice, "search_vector":search_vector,"k":k,"query" (consulta):consulta,"couchbase_url":COUCHBASE_URL} comando curl_ = """ curl -XPOST -H "Content-Type: application/json" -u Administrator:password \ http://{couchbase_url}:8094/api/bucket/project/scope/_default/index/{index_name}/query \ -d '{{ "query": {{ "match_none":{} }}, "size": 6, "knn": [ {{ "campo": "vector_data", "k": {k}, "vector":{search_vector} }} ], "from": 0 }}' """.formato(**argumentos) tentar: resultado = subprocesso.executar(comando curl_, casca=Verdadeiro, verificar=Verdadeiro,saída=subprocesso.TUBO) retorno resultado.saída |
O resultado.stdout
contém o k mais próximo IDs de documentos. Você pode estender o script para executar uma solicitação get em todos os IDs retornados e combinar os resultados para obter o contexto final. Em seguida, passamos esse contexto junto com o prompt para o LLM para obter os resultados desejados.
Muito bom!