Geração aumentada de recuperação (RAG, na sigla em inglês)

O Firebase Genkit fornece abstrações que ajudam a criar geração aumentada de recuperação (RAG), bem como plug-ins que fornecem integrações com ferramentas relacionadas.

O que é RAG?

A geração aumentada de recuperação é uma técnica usada para incorporar fontes de informação nas respostas de LLMs. É importante ser capaz de fazer porque, embora os LLMs sejam normalmente treinados com um amplo material, o uso prático de LLMs requer conhecimento de domínio específico (por por exemplo, talvez você queira usar um LLM para responder perguntas sobre sua produtos de uma empresa).

Uma solução é ajustar o modelo usando dados mais específicos. No entanto, pode ser caro tanto em termos de custo computacional quanto de esforço para preparar dados de treinamento adequados.

Em contraste, o RAG funciona incorporando fontes de dados externas em um comando hora em que é passado para o modelo. Por exemplo, é possível imaginar o comando, "Qual é a relação de Bart com Lisa?" pode ser expandida ("aumentado") por adicionando algumas informações relevantes, resultando no prompt, "Homer and Os filhos de Marge se chamam Bart, Lisa e Maggie. Qual é o relacionamento do Bart para Lisa?".

Essa abordagem tem várias vantagens:

  • Essa opção pode ser mais econômica porque não é necessário treinar novamente o modelo.
  • É possível atualizar continuamente sua fonte de dados, e o LLM pode fazer isso imediatamente o uso das informações atualizadas.
  • Agora você pode citar referências nas respostas do seu LLM.

Por outro lado, usar RAG naturalmente significa comandos mais longos e alguma API LLM de serviço cobram por cada token de entrada enviado. Por fim, você deve avaliar compensações de custos para seus aplicativos.

A RAG é uma área muito ampla e existem muitas técnicas diferentes usadas para atingir o RAG de melhor qualidade. O framework Genkit principal oferece duas abstrações principais para ajudar você a fazer a RAG:

  • Indexadores: adicionam documentos a um "índice".
  • Incorporadores: transforma documentos em uma representação vetorial.
  • Recuperadores: recuperam documentos de um "índice", de acordo com uma consulta.

Essas definições são amplas de propósito, porque o Genkit não opina sobre que "índice" ou como exatamente os documentos são recuperados dele. Somente genkit fornece um formato Document e todo o restante é definido pelo recuperador ou provedor de implementação do indexador.

Indexadores

O índice é responsável por monitorar seus documentos de maneira que você pode recuperar rapidamente documentos relevantes com base em uma consulta específica. Isso é mais geralmente é realizada com um banco de dados de vetores, que indexa documentos usando vetores multidimensionais chamados embeddings. Um embedding de texto (opaco) representa os conceitos expressos por um trecho de texto; eles são gerados usando modelos de ML para fins especiais. Ao indexar o texto usando seu embedding, um vetor é capaz de agrupar textos conceitualmente relacionados e recuperar documentos relacionadas a uma nova string de texto (a consulta).

Antes de recuperar documentos para geração, é necessário e processá-los no seu índice de documentos. Um fluxo de ingestão típico seguintes:

  1. Divida documentos grandes em documentos menores para que sejam relevantes são usadas para aumentar seus comandos. Isso é necessário porque muitos LLMs têm uma janela de contexto limitada, o que torna impraticável inclua documentos inteiros com um comando.

    O Genkit não fornece bibliotecas de agrupamento integradas. No entanto, há disponíveis e disponíveis que são compatíveis com o Genkit.

  2. Gerar embeddings para cada bloco. Dependendo do banco de dados usado, faça isso explicitamente com um modelo de geração de embedding ou pode usar o gerador de embedding fornecido pelo banco de dados.

  3. Adicione o bloco de texto e o índice dele ao banco de dados.

Você pode executar seu fluxo de ingestão com pouca frequência ou apenas uma vez se estiver trabalhando com uma fonte de dados estável. Por outro lado, se você trabalha com dados que muda com frequência, é possível executar continuamente o fluxo de ingestão (por exemplo, em um gatilho do Cloud Firestore, sempre que um documento é atualizado).

Incorporadores

Um embedder é uma função que usa conteúdo (texto, imagens, áudio etc.) e cria um vetor numérico que codifica o significado semântico do conteúdo original. Como mencionado acima, os embeddings são aproveitados como parte do processo de indexação, mas também podem ser usados de forma independente para criar embeddings sem um índice.

Recuperadores

Um recuperador é um conceito que encapsula a lógica relacionada a qualquer tipo de documento recuperação. Os casos de recuperação mais populares normalmente incluem a recuperação de repositórios de vetores. No entanto, no Genkit um recuperador pode ser qualquer função que retorne dados.

Para criar um recuperador, você pode usar uma das implementações fornecidas ou criar o seu próprio.

Indexadores, recuperados e embedders compatíveis

O Genkit oferece suporte ao indexador e ao recuperador por meio do sistema de plug-in. O a seguir plug-ins são oficialmente suportados:

  • Banco de dados de vetores de nuvem Pinecone

Além disso, o Genkit suporta os seguintes armazenamentos de vetores por meio de padrões modelos de código, que podem ser personalizados para a configuração do banco de dados e esquema:

O suporte a modelos de embedding é fornecido pelos seguintes plug-ins:

Plug-in Modelos
IA generativa do Google Incorporação de texto da Gecko
Vertex AI do Google Incorporação de texto da Gecko

Como definir um fluxo RAG

Os exemplos a seguir mostram como ingerir uma coleção de documentos PDF do cardápio de um restaurante em um banco de dados de vetores e recuperá-los para uso em um fluxo que determina quais alimentos estão disponíveis.

Instalar dependências

Neste exemplo, usaremos a biblioteca textsplitter da langchaingo e da biblioteca de análise de PDF ledongthuc/pdf:

go get github.com/tmc/langchaingo/textsplitter
go get github.com/ledongthuc/pdf

Definir um indexador

O exemplo a seguir mostra como criar um indexador para processar uma coleção de documentos PDF e armazená-las em um banco de dados de vetores local.

Ele usa o recuperador de similaridade vetorial baseado em arquivos local que o Genkit oferece pronto para uso para testes e prototipagem simples (não para uso na produção)

Criar o indexador

// Import Genkit's file-based vector retriever, (Don't use in production.)
import "github.com/firebase/genkit/go/plugins/localvec"

// Vertex AI provides the text-embedding-004 embedder model.
import "github.com/firebase/genkit/go/plugins/vertexai"
ctx := context.Background()

err := vertexai.Init(ctx, &vertexai.Config{})
if err != nil {
    log.Fatal(err)
}
err = localvec.Init()
if err != nil {
    log.Fatal(err)
}

menuPDFIndexer, _, err := localvec.DefineIndexerAndRetriever(
    "menuQA",
    localvec.Config{
        Embedder: vertexai.Embedder("text-embedding-004"),
    },
)
if err != nil {
    log.Fatal(err)
}

Criar configuração de divisão

Este exemplo usa a biblioteca textsplitter, que fornece um texto simples divisor para dividir documentos em segmentos que podem ser vetorizados.

A definição a seguir configura a função de divisão para retornar o documento segmentos de 200 caracteres, com uma sobreposição entre blocos de 20 caracteres.

splitter := textsplitter.NewRecursiveCharacter(
    textsplitter.WithChunkSize(200),
    textsplitter.WithChunkOverlap(20),
)

Mais opções de divisão para esta biblioteca podem ser encontradas no Documentação do langchaingo.

Definir o fluxo do indexador

genkit.DefineFlow(
    "indexMenu",
    func(ctx context.Context, path string) (any, error) {
        // Extract plain text from the PDF. Wrap the logic in Run so it
        // appears as a step in your traces.
        pdfText, err := genkit.Run(ctx, "extract", func() (string, error) {
            return readPDF(path)
        })
        if err != nil {
            return nil, err
        }

        // Split the text into chunks. Wrap the logic in Run so it
        // appears as a step in your traces.
        docs, err := genkit.Run(ctx, "chunk", func() ([]*ai.Document, error) {
            chunks, err := splitter.SplitText(pdfText)
            if err != nil {
                return nil, err
            }

            var docs []*ai.Document
            for _, chunk := range chunks {
                docs = append(docs, ai.DocumentFromText(chunk, nil))
            }
            return docs, nil
        })
        if err != nil {
            return nil, err
        }

        // Add chunks to the index.
        err = menuPDFIndexer.Index(ctx, &ai.IndexerRequest{Documents: docs})
        return nil, err
    },
)
// Helper function to extract plain text from a PDF. Excerpted from
// https://github.com/ledongthuc/pdf
func readPDF(path string) (string, error) {
    f, r, err := pdf.Open(path)
    if f != nil {
        defer f.Close()
    }
    if err != nil {
        return "", err
    }

    reader, err := r.GetPlainText()
    if err != nil {
        return "", err
    }

    bytes, err := io.ReadAll(reader)
    if err != nil {
        return "", err
    }
    return string(bytes), nil
}

Executar o fluxo do indexador

genkit flow:run indexMenu "'menu.pdf'"

Depois de executar o fluxo indexMenu, o banco de dados de vetores será propagado com e prontos para serem usados em fluxos Genkit com etapas de recuperação.

Definir um fluxo com recuperação

O exemplo a seguir mostra como você pode usar um recuperador em um fluxo RAG. Gostei exemplo do indexador, este exemplo usa o recuperador de vetor baseado em arquivo do Genkit, o que não deve ser usado na produção.

    ctx := context.Background()

    err := vertexai.Init(ctx, &vertexai.Config{})
    if err != nil {
        log.Fatal(err)
    }
    err = localvec.Init()
    if err != nil {
        log.Fatal(err)
    }

    model := vertexai.Model("gemini-1.5-pro")

    _, menuPdfRetriever, err := localvec.DefineIndexerAndRetriever(
        "menuQA",
        localvec.Config{
            Embedder: vertexai.Embedder("text-embedding-004"),
        },
    )
    if err != nil {
        log.Fatal(err)
    }

    genkit.DefineFlow(
        "menuQA",
        func(ctx context.Context, question string) (string, error) {
            // Retrieve text relevant to the user's question.
            docs, err := menuPdfRetriever.Retrieve(ctx, &ai.RetrieverRequest{
                Document: ai.DocumentFromText(question, nil),
            })
            if err != nil {
                return "", err
            }

            // Construct a system message containing the menu excerpts you just
            // retrieved.
            menuInfo := ai.NewSystemTextMessage("Here's the menu context:")
            for _, doc := range docs.Documents {
                menuInfo.Content = append(menuInfo.Content, doc.Content...)
            }

            // Call Generate, including the menu information in your prompt.
            resp, err := model.Generate(ctx, &ai.GenerateRequest{
                Messages: []*ai.Message{
                    ai.NewSystemTextMessage(`
You are acting as a helpful AI assistant that can answer questions about the
food available on the menu at Genkit Grub Pub.
Use only the context provided to answer the question. If you don't know, do not
make up an answer. Do not add or change items on the menu.`),
                    menuInfo,
                    ai.NewUserTextMessage(question),
                },
            }, nil)
            if err != nil {
                return "", err
            }

            return resp.Text()
        })

Escrever seus próprios indexadores e retrievers

Também é possível criar o seu próprio recuperador. Isso é útil se sua documentos são gerenciados em um repositório de documentos que não é compatível com o Genkit (por exemplo, MySQL, Google Drive etc.). O SDK Genkit fornece métodos flexíveis que permitem você fornece um código personalizado para buscar documentos.

Você também pode definir recuperars personalizados que se baseiam nos recuperadores existentes. no Genkit e aplicar técnicas avançadas de RAG (como reclassificação ou comando ) na parte superior.

Por exemplo, suponha que você queira usar uma função de reclassificação personalizada. O exemplo a seguir define um recuperador personalizado que aplica sua função ao recuperar o menu definido anteriormente:

type CustomMenuRetrieverOptions struct {
    K          int
    PreRerankK int
}
advancedMenuRetriever := ai.DefineRetriever(
    "custom",
    "advancedMenuRetriever",
    func(ctx context.Context, req *ai.RetrieverRequest) (*ai.RetrieverResponse, error) {
        // Handle options passed using our custom type.
        opts, _ := req.Options.(CustomMenuRetrieverOptions)
        // Set fields to default values when either the field was undefined
        // or when req.Options is not a CustomMenuRetrieverOptions.
        if opts.K == 0 {
            opts.K = 3
        }
        if opts.PreRerankK == 0 {
            opts.PreRerankK = 10
        }

        // Call the retriever as in the simple case.
        response, err := menuPDFRetriever.Retrieve(ctx, &ai.RetrieverRequest{
            Document: req.Document,
            Options:  localvec.RetrieverOptions{K: opts.PreRerankK},
        })
        if err != nil {
            return nil, err
        }

        // Re-rank the returned documents using your custom function.
        rerankedDocs := rerank(response.Documents)
        response.Documents = rerankedDocs[:opts.K]

        return response, nil
    },
)